mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
refactor: subagent usage and related tools
This commit is contained in:
@@ -346,6 +346,7 @@ CREATE TABLE IF NOT EXISTS subagents (
|
||||
messages JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
skills JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
usage JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
CONSTRAINT subagents_name_unique UNIQUE (bot_id, name)
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 0012_subagent_usage (rollback)
|
||||
-- Remove usage column from subagents table.
|
||||
|
||||
ALTER TABLE subagents DROP COLUMN IF EXISTS usage;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- 0012_subagent_usage
|
||||
-- Add usage JSONB column to subagents table for tracking cumulative token usage.
|
||||
|
||||
ALTER TABLE subagents ADD COLUMN IF NOT EXISTS usage JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
-- name: CreateSubagent :one
|
||||
INSERT INTO subagents (name, description, bot_id, messages, metadata, skills)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills;
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage;
|
||||
|
||||
-- name: GetSubagentByID :one
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
FROM subagents
|
||||
WHERE id = $1 AND deleted = false;
|
||||
|
||||
-- name: GetSubagentByBotAndName :one
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
FROM subagents
|
||||
WHERE bot_id = $1 AND name = $2 AND deleted = false;
|
||||
|
||||
-- name: ListSubagentsByBot :many
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
FROM subagents
|
||||
WHERE bot_id = $1 AND deleted = false
|
||||
ORDER BY created_at DESC;
|
||||
@@ -21,21 +26,29 @@ SET name = $2,
|
||||
metadata = $4,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted = false
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills;
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage;
|
||||
|
||||
-- name: UpdateSubagentMessages :one
|
||||
UPDATE subagents
|
||||
SET messages = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted = false
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills;
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage;
|
||||
|
||||
-- name: UpdateSubagentMessagesAndUsage :one
|
||||
UPDATE subagents
|
||||
SET messages = $2,
|
||||
usage = $3,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted = false
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage;
|
||||
|
||||
-- name: UpdateSubagentSkills :one
|
||||
UPDATE subagents
|
||||
SET skills = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted = false
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills;
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage;
|
||||
|
||||
-- name: SoftDeleteSubagent :exec
|
||||
UPDATE subagents
|
||||
@@ -43,4 +56,3 @@ SET deleted = true,
|
||||
deleted_at = now(),
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted = false;
|
||||
|
||||
|
||||
@@ -291,6 +291,7 @@ type Subagent struct {
|
||||
Messages []byte `json:"messages"`
|
||||
Metadata []byte `json:"metadata"`
|
||||
Skills []byte `json:"skills"`
|
||||
Usage []byte `json:"usage"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
const createSubagent = `-- name: CreateSubagent :one
|
||||
INSERT INTO subagents (name, description, bot_id, messages, metadata, skills)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
`
|
||||
|
||||
type CreateSubagentParams struct {
|
||||
@@ -48,12 +48,44 @@ func (q *Queries) CreateSubagent(ctx context.Context, arg CreateSubagentParams)
|
||||
&i.Messages,
|
||||
&i.Metadata,
|
||||
&i.Skills,
|
||||
&i.Usage,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getSubagentByBotAndName = `-- name: GetSubagentByBotAndName :one
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
FROM subagents
|
||||
WHERE bot_id = $1 AND name = $2 AND deleted = false
|
||||
`
|
||||
|
||||
type GetSubagentByBotAndNameParams struct {
|
||||
BotID pgtype.UUID `json:"bot_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSubagentByBotAndName(ctx context.Context, arg GetSubagentByBotAndNameParams) (Subagent, error) {
|
||||
row := q.db.QueryRow(ctx, getSubagentByBotAndName, arg.BotID, arg.Name)
|
||||
var i Subagent
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Deleted,
|
||||
&i.DeletedAt,
|
||||
&i.BotID,
|
||||
&i.Messages,
|
||||
&i.Metadata,
|
||||
&i.Skills,
|
||||
&i.Usage,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getSubagentByID = `-- name: GetSubagentByID :one
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
FROM subagents
|
||||
WHERE id = $1 AND deleted = false
|
||||
`
|
||||
@@ -73,12 +105,13 @@ func (q *Queries) GetSubagentByID(ctx context.Context, id pgtype.UUID) (Subagent
|
||||
&i.Messages,
|
||||
&i.Metadata,
|
||||
&i.Skills,
|
||||
&i.Usage,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listSubagentsByBot = `-- name: ListSubagentsByBot :many
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills
|
||||
SELECT id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
FROM subagents
|
||||
WHERE bot_id = $1 AND deleted = false
|
||||
ORDER BY created_at DESC
|
||||
@@ -105,6 +138,7 @@ func (q *Queries) ListSubagentsByBot(ctx context.Context, botID pgtype.UUID) ([]
|
||||
&i.Messages,
|
||||
&i.Metadata,
|
||||
&i.Skills,
|
||||
&i.Usage,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -136,7 +170,7 @@ SET name = $2,
|
||||
metadata = $4,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted = false
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
`
|
||||
|
||||
type UpdateSubagentParams struct {
|
||||
@@ -166,6 +200,7 @@ func (q *Queries) UpdateSubagent(ctx context.Context, arg UpdateSubagentParams)
|
||||
&i.Messages,
|
||||
&i.Metadata,
|
||||
&i.Skills,
|
||||
&i.Usage,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -175,7 +210,7 @@ UPDATE subagents
|
||||
SET messages = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted = false
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
`
|
||||
|
||||
type UpdateSubagentMessagesParams struct {
|
||||
@@ -198,6 +233,42 @@ func (q *Queries) UpdateSubagentMessages(ctx context.Context, arg UpdateSubagent
|
||||
&i.Messages,
|
||||
&i.Metadata,
|
||||
&i.Skills,
|
||||
&i.Usage,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateSubagentMessagesAndUsage = `-- name: UpdateSubagentMessagesAndUsage :one
|
||||
UPDATE subagents
|
||||
SET messages = $2,
|
||||
usage = $3,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted = false
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
`
|
||||
|
||||
type UpdateSubagentMessagesAndUsageParams struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Messages []byte `json:"messages"`
|
||||
Usage []byte `json:"usage"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSubagentMessagesAndUsage(ctx context.Context, arg UpdateSubagentMessagesAndUsageParams) (Subagent, error) {
|
||||
row := q.db.QueryRow(ctx, updateSubagentMessagesAndUsage, arg.ID, arg.Messages, arg.Usage)
|
||||
var i Subagent
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Deleted,
|
||||
&i.DeletedAt,
|
||||
&i.BotID,
|
||||
&i.Messages,
|
||||
&i.Metadata,
|
||||
&i.Skills,
|
||||
&i.Usage,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -207,7 +278,7 @@ UPDATE subagents
|
||||
SET skills = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1 AND deleted = false
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills
|
||||
RETURNING id, name, description, created_at, updated_at, deleted, deleted_at, bot_id, messages, metadata, skills, usage
|
||||
`
|
||||
|
||||
type UpdateSubagentSkillsParams struct {
|
||||
@@ -230,6 +301,7 @@ func (q *Queries) UpdateSubagentSkills(ctx context.Context, arg UpdateSubagentSk
|
||||
&i.Messages,
|
||||
&i.Metadata,
|
||||
&i.Skills,
|
||||
&i.Usage,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ func (h *SubagentHandler) GetContext(c echo.Context) error {
|
||||
if _, err := h.authorizeBotAccess(c.Request().Context(), channelIdentityID, botID); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.JSON(http.StatusOK, subagent.ContextResponse{Messages: item.Messages})
|
||||
return c.JSON(http.StatusOK, subagent.ContextResponse{Messages: item.Messages, Usage: item.Usage})
|
||||
}
|
||||
|
||||
// UpdateContext godoc
|
||||
@@ -300,7 +300,7 @@ func (h *SubagentHandler) UpdateContext(c echo.Context) error {
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return c.JSON(http.StatusOK, subagent.ContextResponse{Messages: updated.Messages})
|
||||
return c.JSON(http.StatusOK, subagent.ContextResponse{Messages: updated.Messages, Usage: updated.Usage})
|
||||
}
|
||||
|
||||
// GetSkills godoc
|
||||
|
||||
@@ -83,6 +83,32 @@ func (s *Service) Get(ctx context.Context, id string) (Subagent, error) {
|
||||
return toSubagent(row)
|
||||
}
|
||||
|
||||
func (s *Service) GetByBotAndName(ctx context.Context, botID string, name string) (Subagent, error) {
|
||||
pgBotID, err := db.ParseUUID(botID)
|
||||
if err != nil {
|
||||
return Subagent{}, err
|
||||
}
|
||||
row, err := s.queries.GetSubagentByBotAndName(ctx, sqlc.GetSubagentByBotAndNameParams{
|
||||
BotID: pgBotID,
|
||||
Name: strings.TrimSpace(name),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return Subagent{}, fmt.Errorf("subagent not found")
|
||||
}
|
||||
return Subagent{}, err
|
||||
}
|
||||
return toSubagent(row)
|
||||
}
|
||||
|
||||
func (s *Service) GetOrCreate(ctx context.Context, botID string, req CreateRequest) (Subagent, error) {
|
||||
existing, err := s.GetByBotAndName(ctx, botID, req.Name)
|
||||
if err == nil {
|
||||
return existing, nil
|
||||
}
|
||||
return s.Create(ctx, botID, req)
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, botID string) ([]Subagent, error) {
|
||||
pgBotID, err := db.ParseUUID(botID)
|
||||
if err != nil {
|
||||
@@ -155,6 +181,21 @@ func (s *Service) UpdateContext(ctx context.Context, id string, req UpdateContex
|
||||
if err != nil {
|
||||
return Subagent{}, err
|
||||
}
|
||||
if req.Usage != nil {
|
||||
usagePayload, err := marshalUsage(req.Usage)
|
||||
if err != nil {
|
||||
return Subagent{}, err
|
||||
}
|
||||
row, err := s.queries.UpdateSubagentMessagesAndUsage(ctx, sqlc.UpdateSubagentMessagesAndUsageParams{
|
||||
ID: pgID,
|
||||
Messages: messagesPayload,
|
||||
Usage: usagePayload,
|
||||
})
|
||||
if err != nil {
|
||||
return Subagent{}, err
|
||||
}
|
||||
return toSubagent(row)
|
||||
}
|
||||
row, err := s.queries.UpdateSubagentMessages(ctx, sqlc.UpdateSubagentMessagesParams{
|
||||
ID: pgID,
|
||||
Messages: messagesPayload,
|
||||
@@ -229,6 +270,10 @@ func toSubagent(row sqlc.Subagent) (Subagent, error) {
|
||||
if err != nil {
|
||||
return Subagent{}, err
|
||||
}
|
||||
usage, err := unmarshalUsage(row.Usage)
|
||||
if err != nil {
|
||||
return Subagent{}, err
|
||||
}
|
||||
item := Subagent{
|
||||
ID: row.ID.String(),
|
||||
Name: row.Name,
|
||||
@@ -237,6 +282,7 @@ func toSubagent(row sqlc.Subagent) (Subagent, error) {
|
||||
Messages: messages,
|
||||
Metadata: metadata,
|
||||
Skills: skills,
|
||||
Usage: usage,
|
||||
Deleted: row.Deleted,
|
||||
}
|
||||
if row.CreatedAt.Valid {
|
||||
@@ -294,6 +340,27 @@ func unmarshalMetadata(payload []byte) (map[string]any, error) {
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func marshalUsage(usage map[string]any) ([]byte, error) {
|
||||
if usage == nil {
|
||||
usage = map[string]any{}
|
||||
}
|
||||
return json.Marshal(usage)
|
||||
}
|
||||
|
||||
func unmarshalUsage(payload []byte) (map[string]any, error) {
|
||||
if len(payload) == 0 {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
var usage map[string]any
|
||||
if err := json.Unmarshal(payload, &usage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if usage == nil {
|
||||
usage = map[string]any{}
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func marshalSkills(skills []string) ([]byte, error) {
|
||||
return json.Marshal(normalizeSkills(skills))
|
||||
}
|
||||
@@ -334,4 +401,3 @@ func mergeSkills(existing []string, incoming []string) []string {
|
||||
merged = append(merged, incoming...)
|
||||
return normalizeSkills(merged)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ type Subagent struct {
|
||||
Messages []map[string]any `json:"messages"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Skills []string `json:"skills"`
|
||||
Usage map[string]any `json:"usage"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Deleted bool `json:"deleted"`
|
||||
@@ -32,6 +33,7 @@ type UpdateRequest struct {
|
||||
|
||||
type UpdateContextRequest struct {
|
||||
Messages []map[string]any `json:"messages"`
|
||||
Usage map[string]any `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateSkillsRequest struct {
|
||||
@@ -48,6 +50,7 @@ type ListResponse struct {
|
||||
|
||||
type ContextResponse struct {
|
||||
Messages []map[string]any `json:"messages"`
|
||||
Usage map[string]any `json:"usage"`
|
||||
}
|
||||
|
||||
type SkillsResponse struct {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
"@ai-sdk/mcp": "^1.0.6",
|
||||
"@ai-sdk/openai": "^3.0.7",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@types/jsdom": "^27.0.0",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"ai": "^6.0.25",
|
||||
"jsdom": "^27.4.0",
|
||||
"toml": "^3.0.0",
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { tool } from 'ai'
|
||||
import { tool, type ModelMessage } from 'ai'
|
||||
import { z } from 'zod'
|
||||
import { createAgent } from '../agent'
|
||||
import { ModelConfig, AgentAuthContext } from '../types'
|
||||
import { AuthFetcher } from '../types'
|
||||
import { AgentAction, IdentityContext } from '../types/agent'
|
||||
import type { ModelConfig, AgentAuthContext, AuthFetcher } from '../types'
|
||||
import { AgentAction, type IdentityContext } from '../types/agent'
|
||||
import {
|
||||
createSubagentClient,
|
||||
toSubagentUsage,
|
||||
addUsage,
|
||||
} from '../utils/subagent'
|
||||
|
||||
export interface SubagentToolParams {
|
||||
fetch: AuthFetcher
|
||||
@@ -14,7 +18,7 @@ export interface SubagentToolParams {
|
||||
|
||||
export const getSubagentTools = ({ fetch, model, identity, auth }: SubagentToolParams) => {
|
||||
const botId = identity.botId.trim()
|
||||
const base = `/bots/${botId}/subagents`
|
||||
const client = createSubagentClient(fetch, botId)
|
||||
|
||||
const listSubagents = tool({
|
||||
description: 'List subagents for current user',
|
||||
@@ -23,27 +27,7 @@ export const getSubagentTools = ({ fetch, model, identity, auth }: SubagentToolP
|
||||
if (!botId) {
|
||||
throw new Error('bot_id is required')
|
||||
}
|
||||
const response = await fetch(base, { method: 'GET' })
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
const createSubagent = tool({
|
||||
description: 'Create a new subagent',
|
||||
inputSchema: z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
execute: async ({ name, description }) => {
|
||||
if (!botId) {
|
||||
throw new Error('bot_id is required')
|
||||
}
|
||||
const response = await fetch(base, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description }),
|
||||
})
|
||||
return response.json()
|
||||
return client.list()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -56,51 +40,56 @@ export const getSubagentTools = ({ fetch, model, identity, auth }: SubagentToolP
|
||||
if (!botId) {
|
||||
throw new Error('bot_id is required')
|
||||
}
|
||||
const response = await fetch(`${base}/${id}`, { method: 'DELETE' })
|
||||
return response.status === 204 ? { success: true } : response.json()
|
||||
return client.remove(id)
|
||||
},
|
||||
})
|
||||
|
||||
const querySubagent = tool({
|
||||
description: 'Query a subagent',
|
||||
description: 'Query a subagent. If the subagent does not exist it will be created automatically.',
|
||||
inputSchema: z.object({
|
||||
name: z.string(),
|
||||
name: z.string().describe('The name of the subagent'),
|
||||
description: z.string().describe('A short description of the subagent purpose (used when creating)'),
|
||||
query: z.string().describe('The prompt to ask the subagent to do.'),
|
||||
}),
|
||||
execute: async ({ name, query }) => {
|
||||
execute: async ({ name, description, query }) => {
|
||||
if (!botId) {
|
||||
throw new Error('bot_id is required')
|
||||
}
|
||||
const listResponse = await fetch(base, { method: 'GET' })
|
||||
const listPayload = await listResponse.json()
|
||||
const items = Array.isArray(listPayload?.items) ? listPayload.items : []
|
||||
const target = items.find((item: { name?: string }) => item?.name === name)
|
||||
if (!target?.id) {
|
||||
throw new Error(`subagent not found: ${name}`)
|
||||
}
|
||||
const contextResponse = await fetch(`${base}/${target.id}/context`, { method: 'GET' })
|
||||
const contextPayload = await contextResponse.json()
|
||||
const contextMessages = Array.isArray(contextPayload?.messages) ? contextPayload.messages : []
|
||||
|
||||
// Get or create the subagent
|
||||
const target = await client.getOrCreate({ name, description })
|
||||
|
||||
// Load persisted context (messages + usage)
|
||||
const ctx = await client.getContext(target.id)
|
||||
const contextMessages = (Array.isArray(ctx.messages) ? ctx.messages : []) as ModelMessage[]
|
||||
const existingUsage = toSubagentUsage(ctx.usage)
|
||||
|
||||
// Create a scoped agent instance for the subagent
|
||||
const { askAsSubagent } = createAgent({
|
||||
model,
|
||||
allowedActions: [
|
||||
AgentAction.Web,
|
||||
],
|
||||
allowedActions: [AgentAction.Web],
|
||||
identity,
|
||||
auth,
|
||||
}, fetch)
|
||||
|
||||
const result = await askAsSubagent({
|
||||
messages: contextMessages,
|
||||
input: query,
|
||||
name: target.name,
|
||||
description: target.description,
|
||||
})
|
||||
|
||||
// Accumulate usage
|
||||
const newUsage = addUsage(existingUsage, result.usage)
|
||||
|
||||
// Persist updated messages + usage
|
||||
const updatedMessages = [...contextMessages, ...result.messages]
|
||||
await fetch(`${base}/${target.id}/context`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ messages: updatedMessages }),
|
||||
})
|
||||
await client.updateContext(
|
||||
target.id,
|
||||
updatedMessages as Record<string, unknown>[],
|
||||
newUsage,
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: result.messages[result.messages.length - 1].content,
|
||||
@@ -110,8 +99,7 @@ export const getSubagentTools = ({ fetch, model, identity, auth }: SubagentToolP
|
||||
|
||||
return {
|
||||
'list_subagents': listSubagents,
|
||||
'create_subagent': createSubagent,
|
||||
'delete_subagent': deleteSubagent,
|
||||
'query_subagent': querySubagent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './attachments'
|
||||
export * from './headers'
|
||||
export * from './headers'
|
||||
export * from './subagent'
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { AuthFetcher } from '../types'
|
||||
import type { LanguageModelUsage } from 'ai'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface SubagentItem {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
bot_id: string
|
||||
messages: Record<string, unknown>[]
|
||||
metadata: Record<string, unknown>
|
||||
skills: string[]
|
||||
usage: SubagentUsage
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted: boolean
|
||||
deleted_at?: string
|
||||
}
|
||||
|
||||
export interface SubagentUsage {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
totalTokens: number
|
||||
}
|
||||
|
||||
export interface SubagentListResponse {
|
||||
items: SubagentItem[]
|
||||
}
|
||||
|
||||
export interface SubagentContextResponse {
|
||||
messages: Record<string, unknown>[]
|
||||
usage: SubagentUsage
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Usage helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const emptyUsage: SubagentUsage = {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0,
|
||||
}
|
||||
|
||||
export const toSubagentUsage = (raw: unknown): SubagentUsage => {
|
||||
if (!raw || typeof raw !== 'object') return { ...emptyUsage }
|
||||
const obj = raw as Record<string, unknown>
|
||||
return {
|
||||
inputTokens: typeof obj.inputTokens === 'number' ? obj.inputTokens : 0,
|
||||
outputTokens: typeof obj.outputTokens === 'number' ? obj.outputTokens : 0,
|
||||
totalTokens: typeof obj.totalTokens === 'number' ? obj.totalTokens : 0,
|
||||
}
|
||||
}
|
||||
|
||||
export const addUsage = (
|
||||
existing: SubagentUsage,
|
||||
delta: LanguageModelUsage,
|
||||
): SubagentUsage => ({
|
||||
inputTokens: existing.inputTokens + (delta.inputTokens ?? 0),
|
||||
outputTokens: existing.outputTokens + (delta.outputTokens ?? 0),
|
||||
totalTokens: existing.totalTokens + (delta.totalTokens ?? 0),
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Client factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const createSubagentClient = (fetch: AuthFetcher, botId: string) => {
|
||||
const base = `/bots/${botId}/subagents`
|
||||
|
||||
const list = async (): Promise<SubagentListResponse> => {
|
||||
const res = await fetch(base, { method: 'GET' })
|
||||
return res.json() as Promise<SubagentListResponse>
|
||||
}
|
||||
|
||||
const create = async (params: {
|
||||
name: string
|
||||
description: string
|
||||
}): Promise<SubagentItem> => {
|
||||
const res = await fetch(base, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
return res.json() as Promise<SubagentItem>
|
||||
}
|
||||
|
||||
const get = async (id: string): Promise<SubagentItem> => {
|
||||
const res = await fetch(`${base}/${id}`, { method: 'GET' })
|
||||
return res.json() as Promise<SubagentItem>
|
||||
}
|
||||
|
||||
const getContext = async (id: string): Promise<SubagentContextResponse> => {
|
||||
const res = await fetch(`${base}/${id}/context`, { method: 'GET' })
|
||||
return res.json() as Promise<SubagentContextResponse>
|
||||
}
|
||||
|
||||
const updateContext = async (
|
||||
id: string,
|
||||
messages: Record<string, unknown>[],
|
||||
usage: SubagentUsage,
|
||||
): Promise<SubagentContextResponse> => {
|
||||
const res = await fetch(`${base}/${id}/context`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ messages, usage }),
|
||||
})
|
||||
return res.json() as Promise<SubagentContextResponse>
|
||||
}
|
||||
|
||||
const getOrCreate = async (params: {
|
||||
name: string
|
||||
description: string
|
||||
}): Promise<SubagentItem> => {
|
||||
const { items } = await list()
|
||||
const existing = items.find((item) => item.name === params.name)
|
||||
if (existing) return existing
|
||||
return create(params)
|
||||
}
|
||||
|
||||
const remove = async (id: string): Promise<{ success: boolean }> => {
|
||||
const res = await fetch(`${base}/${id}`, { method: 'DELETE' })
|
||||
return res.status === 204 ? { success: true } : (res.json() as Promise<{ success: boolean }>)
|
||||
}
|
||||
|
||||
return { list, create, get, getContext, updateContext, getOrCreate, remove }
|
||||
}
|
||||
|
||||
@@ -944,6 +944,9 @@ export type SubagentContextResponse = {
|
||||
messages?: Array<{
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
usage?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type SubagentCreateRequest = {
|
||||
@@ -982,12 +985,18 @@ export type SubagentSubagent = {
|
||||
name?: string;
|
||||
skills?: Array<string>;
|
||||
updated_at?: string;
|
||||
usage?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type SubagentUpdateContextRequest = {
|
||||
messages?: Array<{
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
usage?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type SubagentUpdateRequest = {
|
||||
|
||||
Generated
+6
@@ -121,6 +121,12 @@ importers:
|
||||
'@mozilla/readability':
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0
|
||||
'@types/jsdom':
|
||||
specifier: ^27.0.0
|
||||
version: 27.0.0
|
||||
'@types/turndown':
|
||||
specifier: ^5.0.6
|
||||
version: 5.0.6
|
||||
ai:
|
||||
specifier: ^6.0.25
|
||||
version: 6.0.25(zod@4.3.6)
|
||||
|
||||
@@ -7657,6 +7657,10 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7753,6 +7757,10 @@ const docTemplate = `{
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"usage": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7765,6 +7773,10 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7648,6 +7648,10 @@
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7744,6 +7748,10 @@
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"usage": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7756,6 +7764,10 @@
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"usage": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1553,6 +1553,9 @@ definitions:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
type: array
|
||||
usage:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
type: object
|
||||
subagent.CreateRequest:
|
||||
properties:
|
||||
@@ -1617,6 +1620,9 @@ definitions:
|
||||
type: array
|
||||
updated_at:
|
||||
type: string
|
||||
usage:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
type: object
|
||||
subagent.UpdateContextRequest:
|
||||
properties:
|
||||
@@ -1625,6 +1631,9 @@ definitions:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
type: array
|
||||
usage:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
type: object
|
||||
subagent.UpdateRequest:
|
||||
properties:
|
||||
|
||||
Reference in New Issue
Block a user