diff --git a/db/migrations/0001_init.up.sql b/db/migrations/0001_init.up.sql index ff24ef37..81d24833 100644 --- a/db/migrations/0001_init.up.sql +++ b/db/migrations/0001_init.up.sql @@ -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) ); diff --git a/db/migrations/0012_subagent_usage.down.sql b/db/migrations/0012_subagent_usage.down.sql new file mode 100644 index 00000000..d55faa5f --- /dev/null +++ b/db/migrations/0012_subagent_usage.down.sql @@ -0,0 +1,5 @@ +-- 0012_subagent_usage (rollback) +-- Remove usage column from subagents table. + +ALTER TABLE subagents DROP COLUMN IF EXISTS usage; + diff --git a/db/migrations/0012_subagent_usage.up.sql b/db/migrations/0012_subagent_usage.up.sql new file mode 100644 index 00000000..717c97aa --- /dev/null +++ b/db/migrations/0012_subagent_usage.up.sql @@ -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; + diff --git a/db/queries/subagents.sql b/db/queries/subagents.sql index 3a913334..979d3369 100644 --- a/db/queries/subagents.sql +++ b/db/queries/subagents.sql @@ -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; - diff --git a/internal/db/sqlc/models.go b/internal/db/sqlc/models.go index 02ecf3a1..f609b9b5 100644 --- a/internal/db/sqlc/models.go +++ b/internal/db/sqlc/models.go @@ -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 { diff --git a/internal/db/sqlc/subagents.sql.go b/internal/db/sqlc/subagents.sql.go index d49528b5..f2559a1f 100644 --- a/internal/db/sqlc/subagents.sql.go +++ b/internal/db/sqlc/subagents.sql.go @@ -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 } diff --git a/internal/handlers/subagent.go b/internal/handlers/subagent.go index bd55c264..9067cef8 100644 --- a/internal/handlers/subagent.go +++ b/internal/handlers/subagent.go @@ -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 diff --git a/internal/subagent/service.go b/internal/subagent/service.go index c9e5ce77..d0f20335 100644 --- a/internal/subagent/service.go +++ b/internal/subagent/service.go @@ -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) } - diff --git a/internal/subagent/types.go b/internal/subagent/types.go index 38207e0d..90126be0 100644 --- a/internal/subagent/types.go +++ b/internal/subagent/types.go @@ -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 { diff --git a/packages/agent/package.json b/packages/agent/package.json index 894d2a4b..684d1708 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -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", diff --git a/packages/agent/src/tools/subagent.ts b/packages/agent/src/tools/subagent.ts index 42c9ba02..c6237c82 100644 --- a/packages/agent/src/tools/subagent.ts +++ b/packages/agent/src/tools/subagent.ts @@ -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[], + 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, } -} \ No newline at end of file +} diff --git a/packages/agent/src/utils/index.ts b/packages/agent/src/utils/index.ts index 48ff40e2..1b833b16 100644 --- a/packages/agent/src/utils/index.ts +++ b/packages/agent/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './attachments' -export * from './headers' \ No newline at end of file +export * from './headers' +export * from './subagent' \ No newline at end of file diff --git a/packages/agent/src/utils/subagent.ts b/packages/agent/src/utils/subagent.ts new file mode 100644 index 00000000..3175bd15 --- /dev/null +++ b/packages/agent/src/utils/subagent.ts @@ -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[] + metadata: Record + 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[] + 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 + 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 => { + const res = await fetch(base, { method: 'GET' }) + return res.json() as Promise + } + + const create = async (params: { + name: string + description: string + }): Promise => { + const res = await fetch(base, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + }) + return res.json() as Promise + } + + const get = async (id: string): Promise => { + const res = await fetch(`${base}/${id}`, { method: 'GET' }) + return res.json() as Promise + } + + const getContext = async (id: string): Promise => { + const res = await fetch(`${base}/${id}/context`, { method: 'GET' }) + return res.json() as Promise + } + + const updateContext = async ( + id: string, + messages: Record[], + usage: SubagentUsage, + ): Promise => { + const res = await fetch(`${base}/${id}/context`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ messages, usage }), + }) + return res.json() as Promise + } + + const getOrCreate = async (params: { + name: string + description: string + }): Promise => { + 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 } +} + diff --git a/packages/sdk/src/types.gen.ts b/packages/sdk/src/types.gen.ts index 5e456290..df16aa36 100644 --- a/packages/sdk/src/types.gen.ts +++ b/packages/sdk/src/types.gen.ts @@ -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; updated_at?: string; + usage?: { + [key: string]: unknown; + }; }; export type SubagentUpdateContextRequest = { messages?: Array<{ [key: string]: unknown; }>; + usage?: { + [key: string]: unknown; + }; }; export type SubagentUpdateRequest = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d62ca38..ade02e89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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) diff --git a/spec/docs.go b/spec/docs.go index 066bcae1..8692881f 100644 --- a/spec/docs.go +++ b/spec/docs.go @@ -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": {} } } }, diff --git a/spec/swagger.json b/spec/swagger.json index 8dd014e8..376212a3 100644 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -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": {} } } }, diff --git a/spec/swagger.yaml b/spec/swagger.yaml index 34f6d532..5bb9001b 100644 --- a/spec/swagger.yaml +++ b/spec/swagger.yaml @@ -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: