@gpzhang2001/sharpkit-team 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +20 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/lib/index.d.ts +1749 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +453 -0
- package/lib/index.js.map +1 -0
- package/package.json +49 -0
- package/src/index.ts +427 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Red-team orchestration — the M4 tool-team layer: strix-named tools
|
|
3
|
+
* (create_agent / send_message_to_agent / wait_for_agents /
|
|
4
|
+
* view_agent_graph / stop_agent / agent_finish) composed over the dsh
|
|
5
|
+
* subagent SERVICE (ctx.subagents + the spawn provider's continuable
|
|
6
|
+
* children), not over tools. Children inherit the parent Agent's preset and
|
|
7
|
+
* full tool surface (in-process spawn semantics); requested skills are
|
|
8
|
+
* injected as a directive line in the child's prompt. S2-verified semantics
|
|
9
|
+
* hold: send steers at step boundaries, interrupt parks the inbox (children
|
|
10
|
+
* stay resumable), completion notices reach the parent's turn boundary.
|
|
11
|
+
* A token-budget circuit breaker interrupts every tracked child and blocks
|
|
12
|
+
* new spawns when the session's accumulated usage crosses the ceiling.
|
|
13
|
+
* @module @gpzhang2001/sharpkit-team
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
17
|
+
import type Schema from '@deepseek-ai/schemastery'
|
|
18
|
+
import z from '@deepseek-ai/schemastery'
|
|
19
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
20
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
|
21
|
+
import type {} from '@deepseek-ai/dsh-subagent'
|
|
22
|
+
import type {} from '@deepseek-ai/dsh-session'
|
|
23
|
+
|
|
24
|
+
/** Structural view of the subagent service this package drives. */
|
|
25
|
+
export interface SubagentsLike {
|
|
26
|
+
start(name: string, request: {
|
|
27
|
+
readonly label?: string
|
|
28
|
+
readonly prompt: ContentBlock[]
|
|
29
|
+
readonly parent: unknown
|
|
30
|
+
readonly signal: AbortSignal
|
|
31
|
+
}): { readonly id: { readonly [key: string]: unknown } | string; readonly result: Promise<{ readonly stopReason?: string; readonly output?: readonly ContentBlock[] }> }
|
|
32
|
+
sendMessage(sender: unknown, targetId: unknown, content: ContentBlock[], options?: unknown): Promise<unknown>
|
|
33
|
+
interrupt(targetSessionId: unknown, authority:
|
|
34
|
+
| { readonly kind: 'user'; readonly parentSessionId: unknown }
|
|
35
|
+
| { readonly kind: 'ancestor'; readonly agent: unknown }): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Deployment-tunable configuration. */
|
|
39
|
+
export interface Config {
|
|
40
|
+
/** Maximum delegation depth (root=1 spawns children; children don't spawn). */
|
|
41
|
+
readonly maxTeamDepth?: number
|
|
42
|
+
/** Explicit session token ceiling for the circuit breaker (overrides budget estimates). */
|
|
43
|
+
readonly maxSessionTokens?: number
|
|
44
|
+
/** USD budget ceiling for the circuit breaker (token-estimated; see usdPerMillionTokens). */
|
|
45
|
+
readonly maxBudgetUsd?: number
|
|
46
|
+
/** Estimated blended $/1M tokens mapping a USD budget to a token ceiling. */
|
|
47
|
+
readonly usdPerMillionTokens?: number
|
|
48
|
+
/** Skills catalog root — only used to validate requested skill names exist. */
|
|
49
|
+
readonly skillsRoot?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const name = 'pentest-tool-team'
|
|
53
|
+
|
|
54
|
+
export const inject = ['tools']
|
|
55
|
+
|
|
56
|
+
export const Config: Schema<Config> = z.object({
|
|
57
|
+
maxTeamDepth: z.number().default(2),
|
|
58
|
+
maxSessionTokens: z.number(),
|
|
59
|
+
maxBudgetUsd: z.number(),
|
|
60
|
+
usdPerMillionTokens: z.number(),
|
|
61
|
+
skillsRoot: z.string(),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
/** Default blended $/1M tokens when neither config nor preset supplies a mapping. */
|
|
65
|
+
const DEFAULT_USD_PER_MILLION_TOKENS = 0.5
|
|
66
|
+
|
|
67
|
+
/** Fallback token ceiling when no budget source is configured. */
|
|
68
|
+
const DEFAULT_MAX_SESSION_TOKENS = 2_000_000
|
|
69
|
+
|
|
70
|
+
/** One tracked child (roster row). */
|
|
71
|
+
interface TrackedChild {
|
|
72
|
+
readonly id: string
|
|
73
|
+
readonly label: string
|
|
74
|
+
readonly task: string
|
|
75
|
+
readonly skills: readonly string[]
|
|
76
|
+
readonly startedAt: string
|
|
77
|
+
status: 'running' | 'completed' | 'stopped' | 'failed'
|
|
78
|
+
completionReport: string | undefined
|
|
79
|
+
readonly result: Promise<{ readonly stopReason?: string; readonly output?: readonly ContentBlock[] }>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The orchestrator-facing team handle (tests + UI consume). */
|
|
83
|
+
export interface TeamHandle {
|
|
84
|
+
children(): ReadonlyArray<Readonly<TrackedChild>>
|
|
85
|
+
/** True once the circuit breaker tripped. */
|
|
86
|
+
isBreached(): boolean
|
|
87
|
+
tokensUsed(): number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Brand-free id extraction from the runtime's branded session ids. */
|
|
91
|
+
function idText(value: unknown): string {
|
|
92
|
+
return typeof value === 'string' ? value : String(value)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function apply(ctx: Context, config: Config = {}): TeamHandle {
|
|
96
|
+
const children = new Map<string, TrackedChild>()
|
|
97
|
+
/** Sessions whose usage counts toward this team's budget (root + children). */
|
|
98
|
+
const teamSessions = new Set<string>()
|
|
99
|
+
/** The first caller's Agent — the ancestor authority for breaker interrupts. */
|
|
100
|
+
let rootAgent: unknown
|
|
101
|
+
let tokensUsed = 0
|
|
102
|
+
let breached = false
|
|
103
|
+
const maxTeamDepth = config.maxTeamDepth ?? 2
|
|
104
|
+
const usdPerMillionTokens = config.usdPerMillionTokens ?? DEFAULT_USD_PER_MILLION_TOKENS
|
|
105
|
+
|
|
106
|
+
const subagents = (): SubagentsLike => ctx.subagents as unknown as SubagentsLike
|
|
107
|
+
|
|
108
|
+
/** Budget ceiling in tokens: explicit tokens > config USD > preset USD > fallback. */
|
|
109
|
+
const tokenCeiling = (): number => {
|
|
110
|
+
if (config.maxSessionTokens !== undefined) return config.maxSessionTokens
|
|
111
|
+
const preset = ctx.get('pentestPreset') as { maxBudgetUsd?: number } | undefined
|
|
112
|
+
const budgetUsd = config.maxBudgetUsd ?? preset?.maxBudgetUsd
|
|
113
|
+
if (budgetUsd !== undefined) return Math.max(1, Math.floor((budgetUsd / usdPerMillionTokens) * 1_000_000))
|
|
114
|
+
return DEFAULT_MAX_SESSION_TOKENS
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Budget circuit breaker: accumulate this team's session usage and trip the
|
|
118
|
+
// breaker. The interrupt goes out under ancestor authority from the root
|
|
119
|
+
// agent (kind:'user' requires a human-presented parent session id, which a
|
|
120
|
+
// plugin-side breaker does not have).
|
|
121
|
+
void ctx.on('session/event', (session: { id?: unknown }, event: unknown) => {
|
|
122
|
+
const record = event as { type?: string; data?: { usage?: { inputTokens?: number; outputTokens?: number; input?: number; output?: number } } }
|
|
123
|
+
if (record.type !== 'assistant/message') return
|
|
124
|
+
const sessionId = session.id === undefined ? '' : String(session.id)
|
|
125
|
+
if (!teamSessions.has(sessionId)) return
|
|
126
|
+
const usage = record.data?.usage
|
|
127
|
+
if (usage === undefined || usage === null) return
|
|
128
|
+
const input = usage.inputTokens ?? usage.input ?? 0
|
|
129
|
+
const output = usage.outputTokens ?? usage.output ?? 0
|
|
130
|
+
if (typeof input === 'number' && typeof output === 'number') tokensUsed += input + output
|
|
131
|
+
if (!breached && tokensUsed > tokenCeiling()) {
|
|
132
|
+
breached = true
|
|
133
|
+
ctx.logger.warn(`pentest-team: session token budget exceeded (${String(tokensUsed)} > ${String(tokenCeiling())} tokens); interrupting ${String(children.size)} child agent(s)`)
|
|
134
|
+
for (const child of children.values()) {
|
|
135
|
+
if (child.status !== 'running') continue
|
|
136
|
+
if (rootAgent !== undefined) {
|
|
137
|
+
try {
|
|
138
|
+
subagents().interrupt(child.id as never, { kind: 'ancestor', agent: rootAgent })
|
|
139
|
+
} catch {
|
|
140
|
+
// Best-effort interruption; the child result settles the status.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
child.status = 'stopped'
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const handle: TeamHandle = {
|
|
149
|
+
children: () => [...children.values()],
|
|
150
|
+
isBreached: () => breached,
|
|
151
|
+
tokensUsed: () => tokensUsed,
|
|
152
|
+
}
|
|
153
|
+
ctx.provide('pentestTeam', handle)
|
|
154
|
+
|
|
155
|
+
const textBlock = (text: string): ContentBlock => ({ type: 'text', text })
|
|
156
|
+
|
|
157
|
+
ctx.tools.register(defineTool({
|
|
158
|
+
name: 'create_agent',
|
|
159
|
+
description: `Delegate a focused subtask to a specialist child agent. The child runs with this scan's preset and full tool surface. Pass skills (max 5, category/name) to point it at specialist knowledge. Do not run hands-on tests yourself that a child should run.`,
|
|
160
|
+
parameters: {
|
|
161
|
+
name: { type: 'string', required: true, description: 'Short specialist label (e.g. "recon", "web-sqli").' },
|
|
162
|
+
task: { type: 'string', required: true, description: 'The complete, self-contained task for the child.' },
|
|
163
|
+
skills: { type: 'array', items: { type: 'string' }, description: 'Up to 5 specialist skills (category/name) injected into the brief.' },
|
|
164
|
+
},
|
|
165
|
+
output: {
|
|
166
|
+
schema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
success: { type: 'boolean', required: true },
|
|
170
|
+
agent_id: { type: 'string' },
|
|
171
|
+
status: { type: 'string' },
|
|
172
|
+
error: { type: 'string' },
|
|
173
|
+
},
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
},
|
|
176
|
+
render: (_args, value) => {
|
|
177
|
+
const result = value as { success: boolean; agent_id?: string; error?: string }
|
|
178
|
+
if (!result.success) return [{ type: 'text', text: `create_agent failed: ${result.error ?? 'unknown'}` }]
|
|
179
|
+
return [{ type: 'text', text: `child ${String(result.agent_id)} started` }]
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
execute: async (args, exec) => {
|
|
183
|
+
if (breached) return { success: false, error: `session token budget exhausted (${String(tokensUsed)} tokens) — wrap up and finish instead of spawning new agents` }
|
|
184
|
+
const runningCount = [...children.values()].filter(child => child.status === 'running').length
|
|
185
|
+
if (runningCount >= maxTeamDepth + 3) {
|
|
186
|
+
return { success: false, error: `too many concurrent children (${String(runningCount)}); wait_for_agents or stop_agent first` }
|
|
187
|
+
}
|
|
188
|
+
rootAgent ??= exec.agent
|
|
189
|
+
if (exec.agent !== undefined) teamSessions.add(String(exec.agent.session.id))
|
|
190
|
+
const brief: string[] = [`You are specialist agent "${args.name}" in an authorized penetration test.`, ``, `TASK:`, args.task]
|
|
191
|
+
if (args.skills !== undefined && args.skills.length > 0) {
|
|
192
|
+
brief.push('', 'SPECIALIST KNOWLEDGE: consult these skill areas and follow them.', ...args.skills.map(skill => `- ${skill}`))
|
|
193
|
+
}
|
|
194
|
+
brief.push('', 'Work autonomously. Report findings via create_vulnerability_report / create_dependency_report; record coverage with record_coverage; finish with a concise completion report as your final message.')
|
|
195
|
+
try {
|
|
196
|
+
const run = subagents().start('spawn', {
|
|
197
|
+
label: args.name,
|
|
198
|
+
prompt: [textBlock(brief.join('\n'))],
|
|
199
|
+
parent: exec.agent,
|
|
200
|
+
signal: exec.signal,
|
|
201
|
+
})
|
|
202
|
+
const id = idText(run.id)
|
|
203
|
+
// run.id is the child session id: its usage joins the team budget.
|
|
204
|
+
teamSessions.add(id)
|
|
205
|
+
const child: TrackedChild = {
|
|
206
|
+
id,
|
|
207
|
+
label: args.name,
|
|
208
|
+
task: args.task,
|
|
209
|
+
skills: args.skills ?? [],
|
|
210
|
+
startedAt: new Date().toISOString(),
|
|
211
|
+
status: 'running',
|
|
212
|
+
completionReport: undefined,
|
|
213
|
+
result: run.result,
|
|
214
|
+
}
|
|
215
|
+
children.set(id, child)
|
|
216
|
+
void run.result.then(outcome => {
|
|
217
|
+
if (child.status === 'running') {
|
|
218
|
+
child.status = outcome?.stopReason === 'error' ? 'failed' : 'completed'
|
|
219
|
+
}
|
|
220
|
+
// Fallback completion report: the child's final assistant text.
|
|
221
|
+
if (child.completionReport === undefined && outcome?.output !== undefined) {
|
|
222
|
+
const text = outcome.output.map(block => block.type === 'text' ? block.text : '').filter(part => part !== '').join('\n')
|
|
223
|
+
if (text !== '') child.completionReport = text
|
|
224
|
+
}
|
|
225
|
+
}, () => {
|
|
226
|
+
if (child.status === 'running') child.status = 'failed'
|
|
227
|
+
})
|
|
228
|
+
return { success: true, agent_id: id, status: 'running' }
|
|
229
|
+
} catch (error) {
|
|
230
|
+
return { success: false, error: String(error instanceof Error ? error.message : error) }
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
}))
|
|
234
|
+
|
|
235
|
+
ctx.tools.register(defineTool({
|
|
236
|
+
name: 'send_message_to_agent',
|
|
237
|
+
description: 'Steer a running child mid-run: new information, a course correction, or a request to wrap up. The message is delivered at the child\'s next step boundary.',
|
|
238
|
+
parameters: {
|
|
239
|
+
agent_id: { type: 'string', required: true, description: 'Child id from create_agent.' },
|
|
240
|
+
message: { type: 'string', required: true, description: 'The message text.' },
|
|
241
|
+
},
|
|
242
|
+
output: {
|
|
243
|
+
schema: {
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: {
|
|
246
|
+
success: { type: 'boolean', required: true },
|
|
247
|
+
error: { type: 'string' },
|
|
248
|
+
},
|
|
249
|
+
additionalProperties: false,
|
|
250
|
+
},
|
|
251
|
+
render: (_args, value) => {
|
|
252
|
+
const result = value as { success: boolean; error?: string }
|
|
253
|
+
return [{ type: 'text', text: result.success ? 'message delivered' : `send_message_to_agent failed: ${result.error ?? 'unknown'}` }]
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
execute: async (args, exec) => {
|
|
257
|
+
const child = children.get(args.agent_id)
|
|
258
|
+
if (child === undefined) return { success: false, error: `No child agent '${args.agent_id}'. Known: ${[...children.keys()].join(', ') || 'none'}.` }
|
|
259
|
+
try {
|
|
260
|
+
await subagents().sendMessage(exec.agent, args.agent_id as never, [textBlock(args.message)])
|
|
261
|
+
return { success: true }
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return { success: false, error: String(error instanceof Error ? error.message : error) }
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}))
|
|
267
|
+
|
|
268
|
+
ctx.tools.register(defineTool({
|
|
269
|
+
name: 'wait_for_agents',
|
|
270
|
+
description: 'Block until the named children report back (settlement notices also arrive automatically). Issue exactly ONE wait and react to what it returns.',
|
|
271
|
+
parameters: {
|
|
272
|
+
agent_ids: { type: 'array', items: { type: 'string' }, required: true, description: 'Child ids to wait for.' },
|
|
273
|
+
},
|
|
274
|
+
output: {
|
|
275
|
+
schema: {
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
success: { type: 'boolean', required: true },
|
|
279
|
+
agents: {
|
|
280
|
+
type: 'array',
|
|
281
|
+
required: true,
|
|
282
|
+
items: { type: 'object', properties: { agent_id: { type: 'string' }, status: { type: 'string' }, completion_report: { type: 'string' } }, additionalProperties: false },
|
|
283
|
+
},
|
|
284
|
+
error: { type: 'string' },
|
|
285
|
+
},
|
|
286
|
+
additionalProperties: false,
|
|
287
|
+
},
|
|
288
|
+
render: (_args, value) => {
|
|
289
|
+
const result = value as { agents?: Array<{ status: string }> }
|
|
290
|
+
return [{ type: 'text', text: `${String(result.agents?.length ?? 0)} child agent(s) settled` }]
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
execute: async args => {
|
|
294
|
+
const tracked = args.agent_ids.map(id => children.get(id)).filter((child): child is TrackedChild => child !== undefined)
|
|
295
|
+
if (tracked.length === 0) {
|
|
296
|
+
return { success: false, agents: [], error: `No known children among: ${args.agent_ids.join(', ')}` }
|
|
297
|
+
}
|
|
298
|
+
await Promise.all(tracked.map(child => child.result.catch(() => undefined)))
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
agents: tracked.map(child => ({
|
|
302
|
+
agent_id: child.id,
|
|
303
|
+
status: child.status,
|
|
304
|
+
...(child.completionReport !== undefined ? { completion_report: child.completionReport } : {}),
|
|
305
|
+
})),
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
}))
|
|
309
|
+
|
|
310
|
+
ctx.tools.register(defineTool({
|
|
311
|
+
name: 'view_agent_graph',
|
|
312
|
+
description: 'Your live map of the team: every child agent with its id, label, task, skills, and status. Call it before spawning (avoid duplicates) and before finishing (no child still running).',
|
|
313
|
+
parameters: {},
|
|
314
|
+
output: {
|
|
315
|
+
schema: {
|
|
316
|
+
type: 'object',
|
|
317
|
+
properties: {
|
|
318
|
+
success: { type: 'boolean', required: true },
|
|
319
|
+
agents: {
|
|
320
|
+
type: 'array',
|
|
321
|
+
required: true,
|
|
322
|
+
items: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
properties: {
|
|
325
|
+
agent_id: { type: 'string', required: true },
|
|
326
|
+
name: { type: 'string' },
|
|
327
|
+
task: { type: 'string' },
|
|
328
|
+
skills: { type: 'array', items: { type: 'string' } },
|
|
329
|
+
status: { type: 'string', required: true },
|
|
330
|
+
started_at: { type: 'string' },
|
|
331
|
+
},
|
|
332
|
+
additionalProperties: false,
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
tokens_used: { type: 'integer' },
|
|
336
|
+
budget_breached: { type: 'boolean' },
|
|
337
|
+
},
|
|
338
|
+
additionalProperties: false,
|
|
339
|
+
},
|
|
340
|
+
render: (_args, value) => {
|
|
341
|
+
const result = value as { agents: Array<{ name?: string; status: string }>; tokens_used: number }
|
|
342
|
+
return [{ type: 'text', text: `${String(result.agents.length)} child agent(s), ${String(result.tokens_used)} tokens used` }]
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
execute: async () => ({
|
|
346
|
+
success: true,
|
|
347
|
+
agents: [...children.values()].map(child => ({
|
|
348
|
+
agent_id: child.id,
|
|
349
|
+
name: child.label,
|
|
350
|
+
task: child.task,
|
|
351
|
+
skills: [...child.skills],
|
|
352
|
+
status: child.status,
|
|
353
|
+
started_at: child.startedAt,
|
|
354
|
+
})),
|
|
355
|
+
tokens_used: tokensUsed,
|
|
356
|
+
budget_breached: breached,
|
|
357
|
+
}),
|
|
358
|
+
}))
|
|
359
|
+
|
|
360
|
+
ctx.tools.register(defineTool({
|
|
361
|
+
name: 'stop_agent',
|
|
362
|
+
description: 'Gracefully cancel a child whose work is redundant or misdirected. Prefer send_message_to_agent to redirect a child that is merely off-track.',
|
|
363
|
+
parameters: {
|
|
364
|
+
agent_id: { type: 'string', required: true, description: 'Child id to cancel.' },
|
|
365
|
+
},
|
|
366
|
+
output: {
|
|
367
|
+
schema: {
|
|
368
|
+
type: 'object',
|
|
369
|
+
properties: {
|
|
370
|
+
success: { type: 'boolean', required: true },
|
|
371
|
+
error: { type: 'string' },
|
|
372
|
+
},
|
|
373
|
+
additionalProperties: false,
|
|
374
|
+
},
|
|
375
|
+
render: (_args, value) => {
|
|
376
|
+
const result = value as { success: boolean; error?: string }
|
|
377
|
+
return [{ type: 'text', text: result.success ? 'child cancelled' : `stop_agent failed: ${result.error ?? 'unknown'}` }]
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
execute: async (args, exec) => {
|
|
381
|
+
const child = children.get(args.agent_id)
|
|
382
|
+
if (child === undefined) return { success: false, error: `No child agent '${args.agent_id}'.` }
|
|
383
|
+
if (child.status !== 'running') return { success: false, error: `Child '${args.agent_id}' is already ${child.status}.` }
|
|
384
|
+
try {
|
|
385
|
+
const parentSessionId = exec.agent?.session.id
|
|
386
|
+
subagents().interrupt(args.agent_id as never, { kind: 'user', parentSessionId })
|
|
387
|
+
child.status = 'stopped'
|
|
388
|
+
return { success: true }
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return { success: false, error: String(error instanceof Error ? error.message : error) }
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
}))
|
|
394
|
+
|
|
395
|
+
ctx.tools.register(defineTool({
|
|
396
|
+
name: 'agent_finish',
|
|
397
|
+
description: 'Child agents: submit your structured completion summary and end your turn. The final message IS the completion report the parent receives — this tool records that you are done.',
|
|
398
|
+
parameters: {
|
|
399
|
+
summary: { type: 'string', required: true, description: 'The structured completion report (findings, coverage, open items).' },
|
|
400
|
+
},
|
|
401
|
+
output: {
|
|
402
|
+
schema: {
|
|
403
|
+
type: 'object',
|
|
404
|
+
properties: {
|
|
405
|
+
success: { type: 'boolean', required: true },
|
|
406
|
+
message: { type: 'string' },
|
|
407
|
+
},
|
|
408
|
+
additionalProperties: false,
|
|
409
|
+
},
|
|
410
|
+
render: (_args, value) => {
|
|
411
|
+
const result = value as { message?: string }
|
|
412
|
+
return [{ type: 'text', text: String(result.message ?? 'completion recorded') }]
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
execute: async (args, exec) => {
|
|
416
|
+
// agent_finish executes inside the CHILD agent; its session id is the
|
|
417
|
+
// create_agent run id, so the summary lands on the tracked roster row.
|
|
418
|
+
const sessionKey = exec.agent === undefined ? undefined : String(exec.agent.session.id)
|
|
419
|
+
const child = sessionKey === undefined ? undefined : children.get(sessionKey)
|
|
420
|
+
if (child !== undefined && child.completionReport === undefined) child.completionReport = args.summary
|
|
421
|
+
return { success: true, message: `Completion recorded (${String(args.summary.length)} chars). End your turn now — your final message is the report.` }
|
|
422
|
+
},
|
|
423
|
+
}))
|
|
424
|
+
|
|
425
|
+
return handle
|
|
426
|
+
}
|
|
427
|
+
|