| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- /**
- * The globally named `list_agents` tool: a thin model-facing adapter over
- * the continuable projection of `ctx.subagents.listChildren()` and, for the
- * `descendants` scope, `ctx.subagents.listDescendants()`. It stays separately
- * loadable from the root `send_message` plugin so a deployment can register
- * continuation delivery without exposing discovery.
- * @module @deepseek-ai/dsh-tool-subagent-control/list-agents
- */
- import type { Context } from '@deepseek-ai/cordis'
- import { defineTool } from '@deepseek-ai/dsh-tools'
- import type { Agent } from '@deepseek-ai/dsh-agent'
- import type { SessionId } from '@deepseek-ai/dsh-session'
- import type { SubagentDescendantListEntry, SubagentListEntry } from '@deepseek-ai/dsh-subagent'
- import { assertNever } from '@deepseek-ai/dsh-util-values'
- export const name = 'tool-subagent-list-agents'
- export const inject = ['tools', 'subagents', 'agents']
- type ListAgentsScope = 'children' | 'descendants'
- interface ListAgentsRequest {
- readonly scope?: ListAgentsScope
- }
- interface ListAgentsSpec {
- readonly scope: ListAgentsScope
- }
- type ListAgentsEntry =
- | {
- readonly kind: 'child'
- readonly id: SessionId
- readonly label: string
- readonly status: 'running' | 'idle' | 'ready'
- readonly parent?: SessionId
- readonly depth?: number
- }
- | {
- readonly kind: 'diagnostic'
- readonly id: SessionId
- readonly reason: 'corrupt' | 'unsupported' | 'unavailable'
- readonly parent?: SessionId
- readonly depth?: number
- }
- /** Resolve the optional model request into an internal required-scope spec. */
- function resolveListAgentsRequest(request: ListAgentsRequest): ListAgentsSpec {
- return { scope: request.scope ?? 'children' }
- }
- /**
- * Refine one candidate's status through the live Agent registry: `running`
- * for an active driver, `idle` for a resident Agent between turns (possibly
- * waiting on agents it started), and `ready` when no live Agent remains.
- * `ready` preserves resumability without presenting an inactive conversation
- * as a terminal result to collect.
- */
- function statusOf(agents: { get(id: SessionId): Agent | undefined }, id: SessionId): 'running' | 'idle' | 'ready' {
- const agent = agents.get(id)
- if (agent === undefined) return 'ready'
- return agent.status === 'running' ? 'running' : 'idle'
- }
- /** Project one service row into the model-facing entry, or omit a one-shot child. */
- function project(
- agents: { get(id: SessionId): Agent | undefined },
- entry: SubagentListEntry,
- position?: Pick<SubagentDescendantListEntry, 'parentId' | 'depth'>,
- ): ListAgentsEntry | undefined {
- const at = position === undefined ? {} : { parent: position.parentId, depth: position.depth }
- if (entry.kind === 'diagnostic') {
- return { kind: 'diagnostic', id: entry.id, reason: entry.reason, ...at }
- }
- // One-shot children cannot be continued by send_message, so the model
- // never selects them; discovery still traversed them for descendants.
- if (entry.mode !== 'continuable') return undefined
- return {
- kind: 'child',
- id: entry.id,
- label: entry.label,
- status: statusOf(agents, entry.id),
- ...at,
- }
- }
- /**
- * Register the `list_agents` tool.
- * @param ctx - context carrying the tool registry, subagent service, and live Agent registry.
- */
- export function apply(ctx: Context): void {
- ctx.tools.register(defineTool({
- name: 'list_agents',
- description:
- 'List your continuable background subagents by durable id and label. Use it to recall which ones '
- + 'you started, not to poll for completion — you are told when one finishes. Status comes from the live '
- + 'registry: running means the agent is working right now, idle means it is loaded but between turns '
- + '(it may be waiting on agents it started), and ready means it exists only in storage — resumable, not '
- + 'terminal, and not a result waiting to be collected; a `send_message` steers a running child at its nearest '
- + 'step boundary or starts a turn for an idle or ready child, and a direct child remains a `send_message` '
- + 'candidate in every status. The snapshot is not a delivery '
- + 'promise — `send_message` performs the authoritative check and may still fail. Children that could '
- + 'not be read are reported as diagnostics instead of being silently dropped. Scope `descendants` '
- + 'walks the whole tree below you in stable pre-order, annotating each entry with its durable direct-parent '
- + 'session id and depth. You may use `send_message` only for depth-1 entries; deeper entries are '
- + 'candidates for `interrupt_agent` only.',
- parameters: {
- scope: {
- type: 'string',
- enum: ['children', 'descendants'],
- description: 'children (default) lists direct children only; descendants walks the complete tree below you.',
- },
- },
- output: {
- schema: {
- type: 'array',
- items: {
- oneOf: [
- {
- type: 'object',
- additionalProperties: false,
- properties: {
- kind: { type: 'string', required: true, enum: ['child'] },
- id: { type: 'string', required: true },
- label: { type: 'string', required: true },
- status: { type: 'string', required: true, enum: ['running', 'idle', 'ready'] },
- parent: { type: 'string' },
- depth: { type: 'number' },
- },
- },
- {
- type: 'object',
- additionalProperties: false,
- properties: {
- kind: { type: 'string', required: true, enum: ['diagnostic'] },
- id: { type: 'string', required: true },
- reason: { type: 'string', required: true, enum: ['corrupt', 'unsupported', 'unavailable'] },
- parent: { type: 'string' },
- depth: { type: 'number' },
- },
- },
- ],
- },
- },
- render: (args, entries) => {
- const request = resolveListAgentsRequest(args)
- return [{
- type: 'text',
- text: entries.length === 0
- ? '(no subagents)'
- : entries.map((entry) => {
- // A descendants row always carries its position; children rows
- // never render it. String() spans the schema-optional shape
- // without a dead fallback branch.
- const at = request.scope === 'descendants'
- ? ` parent=${String(entry.parent)} depth=${String(entry.depth)}`
- : ''
- return entry.kind === 'child'
- ? `${entry.id} [${entry.status}]${at} — ${entry.label}`
- : `${entry.id} [diagnostic: ${entry.reason}]${at}`
- }).join('\n'),
- }]
- },
- },
- async execute(args, exec) {
- const parent = exec.agent
- if (!parent) {
- // Non-agent callers have no session whose children could be listed.
- throw new Error('list_agents requires a calling agent (exec.agent was undefined)')
- }
- const request = resolveListAgentsRequest(args)
- // The registry drains started tool bodies, so the scan must observe the
- // call's signal rather than finish a slow catalog after cancellation.
- switch (request.scope) {
- case 'children': {
- const entries = await ctx.subagents.listChildren(parent.id, exec.signal)
- return entries
- .map(entry => project(ctx.agents, entry))
- .filter(entry => entry !== undefined)
- }
- case 'descendants': {
- const entries = await ctx.subagents.listDescendants(parent.id, exec.signal)
- return entries
- .map(entry => project(ctx.agents, entry, entry))
- .filter(entry => entry !== undefined)
- }
- /* v8 ignore next 2 -- the resolver normalizes the schema-validated closed scope before dispatch. */
- default:
- return assertNever(request.scope, 'list_agents scope')
- }
- },
- }))
- }
|