mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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.
189 lines
7.5 KiB
TypeScript
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')
|
|
}
|
|
},
|
|
}))
|
|
}
|