Files
deepseek-harness/packages/subagent/tool-subagent-control/src/list-agents.ts
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

189 lines
7.5 KiB
TypeScript

/**
* 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 { assertNever } from '@deepseek-ai/dsh-llm'
import type { SubagentDescendantListEntry, SubagentListEntry } from '@deepseek-ai/dsh-subagent'
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' | 'complete'
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 `complete` when no live Agent remains.
*/
function statusOf(agents: { get(id: SessionId): Agent | undefined }, id: SessionId): 'running' | 'idle' | 'complete' {
const agent = agents.get(id)
if (agent === undefined) return 'complete'
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. 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 complete means it exists only in storage — 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', 'complete'] },
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')
}
},
}))
}