forked from deepseek-ai/deepseek-harness
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlist-agents.ts
More file actions
192 lines (182 loc) · 7.8 KB
/
Copy pathlist-agents.ts
File metadata and controls
192 lines (182 loc) · 7.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/**
* 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' | '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` starts a new turn on the same '
+ 'conversation, 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')
}
},
}))
}