@openharness/core 0.5.3 → 0.6.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/README.md +2 -2
- package/dist/__tests__/agent-registry.test.js +28 -3
- package/dist/__tests__/agent-registry.test.js.map +1 -1
- package/dist/__tests__/subagents.test.d.ts +2 -0
- package/dist/__tests__/subagents.test.d.ts.map +1 -0
- package/dist/__tests__/subagents.test.js +282 -0
- package/dist/__tests__/subagents.test.js.map +1 -0
- package/dist/__tests__/tools/create-fs-tools.test.js +45 -0
- package/dist/__tests__/tools/create-fs-tools.test.js.map +1 -1
- package/dist/__tests__/ui-stream.test.js +24 -0
- package/dist/__tests__/ui-stream.test.js.map +1 -1
- package/dist/agent-registry.d.ts +5 -0
- package/dist/agent-registry.d.ts.map +1 -1
- package/dist/agent-registry.js +20 -5
- package/dist/agent-registry.js.map +1 -1
- package/dist/agent.d.ts +10 -5
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +372 -93
- package/dist/agent.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/subagents.d.ts +44 -0
- package/dist/subagents.d.ts.map +1 -0
- package/dist/subagents.js +22 -0
- package/dist/subagents.js.map +1 -0
- package/dist/tools/create-fs-tools.d.ts +34 -3
- package/dist/tools/create-fs-tools.d.ts.map +1 -1
- package/dist/tools/create-fs-tools.js +159 -11
- package/dist/tools/create-fs-tools.js.map +1 -1
- package/dist/tools/create-local-tools.d.ts +31 -0
- package/dist/tools/create-local-tools.d.ts.map +1 -1
- package/dist/types/ui-message.d.ts +3 -0
- package/dist/types/ui-message.d.ts.map +1 -1
- package/dist/ui-stream.d.ts.map +1 -1
- package/dist/ui-stream.js +22 -0
- package/dist/ui-stream.js.map +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { tool, streamText, stepCountIs, } from "ai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { Session } from "./session.js";
|
|
3
4
|
import { loadInstructions } from "./instructions.js";
|
|
4
5
|
import { connectMCPServers, closeMCPClients, } from "./mcp.js";
|
|
5
|
-
import { discoverSkills } from "./skills.js";
|
|
6
|
+
import { discoverSkills, } from "./skills.js";
|
|
7
|
+
import { InMemorySubagentSessionMetadataStore, isSubagentCatalog, } from "./subagents.js";
|
|
6
8
|
import { createSkillTool } from "./tools/skill.js";
|
|
7
9
|
import { AgentRegistry, normalizeBackgroundConfig, } from "./agent-registry.js";
|
|
10
|
+
const SUBAGENT_SESSION_RUNTIME = new WeakMap();
|
|
11
|
+
const TASK_SESSION_MODES = ["stateless", "new", "resume", "fork"];
|
|
8
12
|
// ── Agent ────────────────────────────────────────────────────────────
|
|
9
13
|
export class Agent {
|
|
10
14
|
name;
|
|
@@ -18,11 +22,13 @@ export class Agent {
|
|
|
18
22
|
maxSubagentDepth;
|
|
19
23
|
approve;
|
|
20
24
|
onSubagentEvent;
|
|
21
|
-
/** Original subagent templates (stored
|
|
25
|
+
/** Original subagent templates or catalog (stored for nested children). */
|
|
22
26
|
subagents;
|
|
27
|
+
/** Optional resumable subagent session config. */
|
|
28
|
+
subagentSessions;
|
|
23
29
|
/** Background config (stored so nested children can inherit it). */
|
|
24
30
|
subagentBackground;
|
|
25
|
-
/** Registry for background subagents. Only present when
|
|
31
|
+
/** Registry for background subagents. Only present when configured. */
|
|
26
32
|
agentRegistry;
|
|
27
33
|
/** Static tools provided at construction time. */
|
|
28
34
|
tools;
|
|
@@ -31,8 +37,8 @@ export class Agent {
|
|
|
31
37
|
mcpConnection = null;
|
|
32
38
|
/** Skills config — discovered lazily on first run. */
|
|
33
39
|
skillsConfig;
|
|
34
|
-
cachedSkills = null;
|
|
35
|
-
cachedInstructions = null;
|
|
40
|
+
cachedSkills = null;
|
|
41
|
+
cachedInstructions = null;
|
|
36
42
|
constructor(options) {
|
|
37
43
|
this.name = options.name;
|
|
38
44
|
this.description = options.description;
|
|
@@ -46,32 +52,19 @@ export class Agent {
|
|
|
46
52
|
this.approve = options.approve;
|
|
47
53
|
this.onSubagentEvent = options.onSubagentEvent;
|
|
48
54
|
this.subagents = options.subagents;
|
|
55
|
+
this.subagentSessions = options.subagentSessions;
|
|
49
56
|
this.subagentBackground = options.subagentBackground;
|
|
50
57
|
this.mcpServerConfigs = options.mcpServers;
|
|
51
58
|
this.skillsConfig = options.skills;
|
|
52
|
-
|
|
53
|
-
if (options.
|
|
54
|
-
|
|
55
|
-
? normalizeBackgroundConfig(options.subagentBackground)
|
|
56
|
-
: undefined;
|
|
57
|
-
const registry = bgConfig ? new AgentRegistry(bgConfig) : undefined;
|
|
58
|
-
this.agentRegistry = registry;
|
|
59
|
-
this.tools = {
|
|
60
|
-
...(options.tools ?? {}),
|
|
61
|
-
task: createTaskTool(options.subagents, this.maxSubagentDepth, this.onSubagentEvent, registry),
|
|
62
|
-
...(registry && bgConfig.tools.status
|
|
63
|
-
? { agent_status: createStatusTool(registry) }
|
|
64
|
-
: {}),
|
|
65
|
-
...(registry && bgConfig.tools.cancel
|
|
66
|
-
? { agent_cancel: createCancelTool(registry) }
|
|
67
|
-
: {}),
|
|
68
|
-
...(registry && bgConfig.tools.await
|
|
69
|
-
? { agent_await: createAwaitTool(registry, bgConfig.tools.await) }
|
|
70
|
-
: {}),
|
|
71
|
-
};
|
|
59
|
+
this.tools = options.tools;
|
|
60
|
+
if (options.subagentSessions) {
|
|
61
|
+
getSubagentSessionRuntime(options.subagentSessions);
|
|
72
62
|
}
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
if (options.subagents && this.maxSubagentDepth > 0 && options.subagentBackground) {
|
|
64
|
+
const bgConfig = normalizeBackgroundConfig(options.subagentBackground);
|
|
65
|
+
if (bgConfig) {
|
|
66
|
+
this.agentRegistry = new AgentRegistry(bgConfig);
|
|
67
|
+
}
|
|
75
68
|
}
|
|
76
69
|
}
|
|
77
70
|
/**
|
|
@@ -87,7 +80,6 @@ export class Agent {
|
|
|
87
80
|
}
|
|
88
81
|
}
|
|
89
82
|
async *run(history, input, options) {
|
|
90
|
-
// Build messages: history + new input (Agent does NOT mutate history)
|
|
91
83
|
const messages = [...history];
|
|
92
84
|
if (typeof input === "string") {
|
|
93
85
|
messages.push({ role: "user", content: input });
|
|
@@ -95,25 +87,25 @@ export class Agent {
|
|
|
95
87
|
else {
|
|
96
88
|
messages.push(...input);
|
|
97
89
|
}
|
|
98
|
-
// Load AGENTS.md once per agent lifetime
|
|
99
90
|
if (this.instructions && this.cachedInstructions === null) {
|
|
100
91
|
this.cachedInstructions = await loadInstructions();
|
|
101
92
|
}
|
|
102
|
-
// Connect MCP servers once per agent lifetime
|
|
103
93
|
if (this.mcpServerConfigs && !this.mcpConnection) {
|
|
104
94
|
this.mcpConnection = await connectMCPServers(this.mcpServerConfigs);
|
|
105
95
|
}
|
|
106
|
-
// Discover skills once per agent lifetime
|
|
107
96
|
if (this.skillsConfig && this.cachedSkills === null) {
|
|
108
97
|
this.cachedSkills = await discoverSkills(this.skillsConfig);
|
|
109
98
|
}
|
|
110
99
|
const systemParts = [this.systemPrompt, this.cachedInstructions].filter(Boolean);
|
|
111
100
|
const system = systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
|
|
112
|
-
|
|
101
|
+
const subagentTools = await createSubagentTools(this.subagents, this.maxSubagentDepth, this.onSubagentEvent, this.agentRegistry, this.subagentBackground, this.subagentSessions);
|
|
113
102
|
const allTools = {
|
|
114
|
-
...(this.cachedSkills?.length
|
|
103
|
+
...(this.cachedSkills?.length
|
|
104
|
+
? { skill: createSkillTool(this.cachedSkills) }
|
|
105
|
+
: {}),
|
|
115
106
|
...(this.mcpConnection?.tools ?? {}),
|
|
116
107
|
...(this.tools ?? {}),
|
|
108
|
+
...(subagentTools ?? {}),
|
|
117
109
|
};
|
|
118
110
|
const tools = this.approve && Object.keys(allTools).length > 0
|
|
119
111
|
? wrapToolsWithApproval(allTools, this.approve)
|
|
@@ -201,7 +193,9 @@ export class Agent {
|
|
|
201
193
|
case "error":
|
|
202
194
|
yield {
|
|
203
195
|
type: "error",
|
|
204
|
-
error: part.error instanceof Error
|
|
196
|
+
error: part.error instanceof Error
|
|
197
|
+
? part.error
|
|
198
|
+
: new Error(String(part.error)),
|
|
205
199
|
};
|
|
206
200
|
break;
|
|
207
201
|
case "finish": {
|
|
@@ -258,12 +252,40 @@ export class Agent {
|
|
|
258
252
|
type: "done",
|
|
259
253
|
result: "error",
|
|
260
254
|
messages,
|
|
261
|
-
totalUsage: {
|
|
255
|
+
totalUsage: {
|
|
256
|
+
inputTokens: undefined,
|
|
257
|
+
outputTokens: undefined,
|
|
258
|
+
totalTokens: undefined,
|
|
259
|
+
},
|
|
262
260
|
};
|
|
263
261
|
}
|
|
264
262
|
}
|
|
265
263
|
}
|
|
266
264
|
// ── Subagent tools ──────────────────────────────────────────────────
|
|
265
|
+
async function createSubagentTools(subagents, remainingDepth, onSubagentEvent, registry, backgroundConfig, sessionConfig) {
|
|
266
|
+
if (!subagents || remainingDepth <= 0)
|
|
267
|
+
return undefined;
|
|
268
|
+
if (Array.isArray(subagents) && subagents.length === 0)
|
|
269
|
+
return undefined;
|
|
270
|
+
const bgConfig = registry && backgroundConfig
|
|
271
|
+
? normalizeBackgroundConfig(backgroundConfig)
|
|
272
|
+
: undefined;
|
|
273
|
+
const task = await createTaskTool(subagents, remainingDepth, onSubagentEvent, registry, sessionConfig);
|
|
274
|
+
return {
|
|
275
|
+
task,
|
|
276
|
+
...(registry && bgConfig?.tools.status
|
|
277
|
+
? { agent_status: createStatusTool(registry) }
|
|
278
|
+
: {}),
|
|
279
|
+
...(registry && bgConfig?.tools.cancel
|
|
280
|
+
? { agent_cancel: createCancelTool(registry) }
|
|
281
|
+
: {}),
|
|
282
|
+
...(registry && bgConfig?.tools.await
|
|
283
|
+
? {
|
|
284
|
+
agent_await: createAwaitTool(registry, bgConfig.tools.await),
|
|
285
|
+
}
|
|
286
|
+
: {}),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
267
289
|
function createChildFromTemplate(template, remainingDepth, onSubagentEvent) {
|
|
268
290
|
const nextDepth = remainingDepth - 1;
|
|
269
291
|
const childOnSubagentEvent = onSubagentEvent
|
|
@@ -279,11 +301,10 @@ function createChildFromTemplate(template, remainingDepth, onSubagentEvent) {
|
|
|
279
301
|
temperature: template.temperature,
|
|
280
302
|
maxTokens: template.maxTokens,
|
|
281
303
|
instructions: template.instructions,
|
|
282
|
-
|
|
283
|
-
// Pass subagents + background config through when there is remaining depth
|
|
284
|
-
...(nextDepth > 0 && template.subagents?.length
|
|
304
|
+
...(nextDepth > 0 && template.subagents
|
|
285
305
|
? {
|
|
286
306
|
subagents: template.subagents,
|
|
307
|
+
subagentSessions: template.subagentSessions,
|
|
287
308
|
maxSubagentDepth: nextDepth,
|
|
288
309
|
onSubagentEvent: childOnSubagentEvent,
|
|
289
310
|
subagentBackground: template.subagentBackground,
|
|
@@ -291,86 +312,128 @@ function createChildFromTemplate(template, remainingDepth, onSubagentEvent) {
|
|
|
291
312
|
: {}),
|
|
292
313
|
});
|
|
293
314
|
}
|
|
294
|
-
function createTaskTool(subagents, remainingDepth, onSubagentEvent, registry) {
|
|
295
|
-
const
|
|
296
|
-
const
|
|
297
|
-
const listing =
|
|
315
|
+
async function createTaskTool(subagents, remainingDepth, onSubagentEvent, registry, sessionConfig) {
|
|
316
|
+
const descriptors = await listSubagentDescriptors(subagents);
|
|
317
|
+
const names = descriptors.map((subagent) => subagent.name);
|
|
318
|
+
const listing = descriptors
|
|
319
|
+
.map((subagent) => `- ${subagent.name}: ${subagent.description ?? subagent.name}`)
|
|
320
|
+
.join("\n");
|
|
298
321
|
const descriptionLines = [
|
|
299
322
|
"Spawn a subagent to handle a task autonomously.",
|
|
300
323
|
"The subagent runs with its own tools, completes the work, and returns the result.",
|
|
301
324
|
"Launch multiple agents concurrently when possible by calling this tool multiple times in one response.",
|
|
302
325
|
];
|
|
326
|
+
if (sessionConfig) {
|
|
327
|
+
descriptionLines.push("", "Set session.mode=new to create a resumable subagent session.", "Set session.mode=resume with session.id to continue an earlier subagent session.", "Set session.mode=fork with session.id to clone an earlier session into a new one.", 'When session is omitted, the default mode is "' +
|
|
328
|
+
(sessionConfig.defaultMode ?? "stateless") +
|
|
329
|
+
'".');
|
|
330
|
+
}
|
|
303
331
|
if (registry) {
|
|
304
|
-
descriptionLines.push("", "Set background=true to spawn the agent in the background and return immediately with
|
|
332
|
+
descriptionLines.push("", "Set background=true to spawn the agent in the background and return immediately with a run ID.", "Use agent_status, agent_await, or agent_cancel to manage background runs.");
|
|
333
|
+
}
|
|
334
|
+
if (listing) {
|
|
335
|
+
descriptionLines.push("", "Available agents:", listing);
|
|
336
|
+
}
|
|
337
|
+
else if (isSubagentCatalog(subagents)) {
|
|
338
|
+
descriptionLines.push("", "Available agents are resolved dynamically at runtime.");
|
|
305
339
|
}
|
|
306
|
-
descriptionLines.push("", "Available agents:", listing);
|
|
307
340
|
const baseSchema = z.object({
|
|
308
|
-
agent:
|
|
341
|
+
agent: createAgentSelectionSchema(names).describe("Which agent to use"),
|
|
309
342
|
prompt: z.string().describe("Detailed task description for the subagent"),
|
|
310
343
|
});
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
344
|
+
const withSession = sessionConfig
|
|
345
|
+
? baseSchema.extend({
|
|
346
|
+
session: createTaskSessionSchema()
|
|
347
|
+
.optional()
|
|
348
|
+
.describe("Optional resumable session instructions"),
|
|
349
|
+
})
|
|
350
|
+
: baseSchema;
|
|
351
|
+
const inputSchema = registry
|
|
352
|
+
? withSession.extend({
|
|
353
|
+
background: z
|
|
354
|
+
.boolean()
|
|
355
|
+
.optional()
|
|
356
|
+
.describe("If true, spawn in background and return immediately with a run ID"),
|
|
357
|
+
})
|
|
358
|
+
: withSession;
|
|
318
359
|
return tool({
|
|
319
360
|
description: descriptionLines.join("\n"),
|
|
320
361
|
inputSchema,
|
|
321
362
|
execute: async (rawInput, { abortSignal }) => {
|
|
322
|
-
const { agent: agentName, prompt
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
363
|
+
const { agent: agentName, prompt } = rawInput;
|
|
364
|
+
const background = "background" in rawInput ? rawInput.background : undefined;
|
|
365
|
+
const session = "session" in rawInput ? rawInput.session : undefined;
|
|
366
|
+
const template = await resolveSubagent(subagents, agentName);
|
|
367
|
+
if (!template) {
|
|
368
|
+
const suffix = names.length > 0 ? ` Available agents: ${names.join(", ")}.` : "";
|
|
369
|
+
throw new Error(`Unknown subagent "${agentName}".${suffix}`);
|
|
370
|
+
}
|
|
371
|
+
const prepared = sessionConfig
|
|
372
|
+
? await prepareSubagentChild({
|
|
373
|
+
agentName,
|
|
374
|
+
template,
|
|
375
|
+
remainingDepth,
|
|
376
|
+
onSubagentEvent,
|
|
377
|
+
sessionConfig,
|
|
378
|
+
session,
|
|
379
|
+
})
|
|
380
|
+
: {
|
|
381
|
+
child: createChildFromTemplate(template, remainingDepth, onSubagentEvent),
|
|
382
|
+
sessionId: undefined,
|
|
383
|
+
};
|
|
384
|
+
const { child, sessionId } = prepared;
|
|
326
385
|
if (background && registry) {
|
|
327
386
|
const id = registry.spawn(agentName, child, prompt, {
|
|
328
387
|
signal: abortSignal,
|
|
329
388
|
onEvent: onSubagentEvent,
|
|
389
|
+
sessionId,
|
|
330
390
|
});
|
|
331
|
-
return
|
|
391
|
+
return formatBackgroundSpawn(agentName, id, sessionId);
|
|
332
392
|
}
|
|
333
|
-
// Foreground mode: run to completion
|
|
334
393
|
let lastText = "";
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
394
|
+
try {
|
|
395
|
+
for await (const event of child.run([], prompt, { signal: abortSignal })) {
|
|
396
|
+
onSubagentEvent?.([agentName], event);
|
|
397
|
+
if (event.type === "text.done") {
|
|
398
|
+
lastText = event.text;
|
|
399
|
+
}
|
|
339
400
|
}
|
|
340
401
|
}
|
|
341
|
-
|
|
342
|
-
|
|
402
|
+
finally {
|
|
403
|
+
await child.close();
|
|
404
|
+
}
|
|
405
|
+
return formatTaskResult(lastText || "(no output)", sessionId);
|
|
343
406
|
},
|
|
344
407
|
});
|
|
345
408
|
}
|
|
346
409
|
function createStatusTool(registry) {
|
|
347
410
|
return tool({
|
|
348
|
-
description: "Check the status of a background
|
|
411
|
+
description: "Check the status of a background run without blocking.",
|
|
349
412
|
inputSchema: z.object({
|
|
350
|
-
id: z.string().describe("The
|
|
413
|
+
id: z.string().describe("The run ID returned by a background task spawn"),
|
|
351
414
|
}),
|
|
352
415
|
execute: async ({ id }) => {
|
|
353
416
|
const status = registry.getStatus(id);
|
|
354
417
|
if (!status)
|
|
355
418
|
return `Agent "${id}" not found.`;
|
|
356
419
|
if (status.status === "done") {
|
|
357
|
-
return
|
|
420
|
+
return formatAgentStatus(id, status.status, status.result, undefined, status.sessionId);
|
|
358
421
|
}
|
|
359
422
|
if (status.status === "failed") {
|
|
360
|
-
return
|
|
423
|
+
return formatAgentStatus(id, status.status, undefined, status.error ?? "unknown", status.sessionId);
|
|
361
424
|
}
|
|
362
425
|
if (status.status === "cancelled") {
|
|
363
|
-
return
|
|
426
|
+
return formatAgentStatus(id, status.status, undefined, undefined, status.sessionId);
|
|
364
427
|
}
|
|
365
|
-
return
|
|
428
|
+
return formatAgentStatus(id, status.status, undefined, undefined, status.sessionId);
|
|
366
429
|
},
|
|
367
430
|
});
|
|
368
431
|
}
|
|
369
432
|
function createCancelTool(registry) {
|
|
370
433
|
return tool({
|
|
371
|
-
description: "Cancel a running background
|
|
434
|
+
description: "Cancel a running background run.",
|
|
372
435
|
inputSchema: z.object({
|
|
373
|
-
id: z.string().describe("The
|
|
436
|
+
id: z.string().describe("The run ID to cancel"),
|
|
374
437
|
}),
|
|
375
438
|
execute: async ({ id }) => {
|
|
376
439
|
const cancelled = registry.cancel(id);
|
|
@@ -389,42 +452,46 @@ function createAwaitTool(registry, modes) {
|
|
|
389
452
|
};
|
|
390
453
|
return tool({
|
|
391
454
|
description: [
|
|
392
|
-
"Wait for one or more background
|
|
455
|
+
"Wait for one or more background runs to complete.",
|
|
393
456
|
"",
|
|
394
457
|
"Modes:",
|
|
395
|
-
...modes.map((
|
|
458
|
+
...modes.map((mode) => `- ${modeDescriptions[mode]}`),
|
|
396
459
|
].join("\n"),
|
|
397
460
|
inputSchema: z.object({
|
|
398
|
-
ids: z.array(z.string()).min(1).describe("
|
|
399
|
-
mode: z.enum(modes).describe("How to wait for
|
|
461
|
+
ids: z.array(z.string()).min(1).describe("Run IDs to wait for"),
|
|
462
|
+
mode: z.enum(modes).describe("How to wait for runs"),
|
|
400
463
|
}),
|
|
401
464
|
execute: async ({ ids, mode }) => {
|
|
402
465
|
switch (mode) {
|
|
403
466
|
case "all": {
|
|
404
467
|
try {
|
|
405
468
|
const results = await registry.awaitAll(ids);
|
|
406
|
-
const entries = [...results.entries()].map(([id, result]) =>
|
|
469
|
+
const entries = [...results.entries()].map(([id, result]) => {
|
|
470
|
+
const sessionId = registry.getStatus(id)?.sessionId;
|
|
471
|
+
return `<agent id="${id}"${formatOptionalAttr("session_id", sessionId)}>\n${result}\n</agent>`;
|
|
472
|
+
});
|
|
407
473
|
return `<await_result mode="all">\n${entries.join("\n")}\n</await_result>`;
|
|
408
474
|
}
|
|
409
|
-
catch (
|
|
410
|
-
const
|
|
411
|
-
return `<await_result mode="all" error="${
|
|
475
|
+
catch (error) {
|
|
476
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
477
|
+
return `<await_result mode="all" error="${message}" />`;
|
|
412
478
|
}
|
|
413
479
|
}
|
|
414
480
|
case "allSettled": {
|
|
415
481
|
const results = await registry.awaitAllSettled(ids);
|
|
416
|
-
const entries = [...results.entries()].map(([id,
|
|
417
|
-
|
|
418
|
-
|
|
482
|
+
const entries = [...results.entries()].map(([id, result]) => {
|
|
483
|
+
const sessionAttr = formatOptionalAttr("session_id", result.sessionId);
|
|
484
|
+
if (result.status === "done") {
|
|
485
|
+
return `<agent id="${id}" status="done"${sessionAttr}>\n${result.result}\n</agent>`;
|
|
419
486
|
}
|
|
420
|
-
return `<agent id="${id}" status="${
|
|
487
|
+
return `<agent id="${id}" status="${result.status}"${sessionAttr} error="${result.error ?? "unknown"}" />`;
|
|
421
488
|
});
|
|
422
489
|
return `<await_result mode="allSettled">\n${entries.join("\n")}\n</await_result>`;
|
|
423
490
|
}
|
|
424
491
|
case "any": {
|
|
425
492
|
try {
|
|
426
|
-
const { id, result } = await registry.awaitAny(ids);
|
|
427
|
-
return `<await_result mode="any" winner="${id}">\n${result}\n</await_result>`;
|
|
493
|
+
const { id, sessionId, result } = await registry.awaitAny(ids);
|
|
494
|
+
return `<await_result mode="any" winner="${id}"${formatOptionalAttr("session_id", sessionId)}>\n${result}\n</await_result>`;
|
|
428
495
|
}
|
|
429
496
|
catch {
|
|
430
497
|
return `<await_result mode="any" error="All agents failed." />`;
|
|
@@ -432,15 +499,227 @@ function createAwaitTool(registry, modes) {
|
|
|
432
499
|
}
|
|
433
500
|
case "race": {
|
|
434
501
|
const settled = await registry.awaitRace(ids);
|
|
502
|
+
const sessionAttr = formatOptionalAttr("session_id", settled.sessionId);
|
|
435
503
|
if (settled.error) {
|
|
436
|
-
return `<await_result mode="race" settled="${settled.id}" status="failed" error="${settled.error}" />`;
|
|
504
|
+
return `<await_result mode="race" settled="${settled.id}"${sessionAttr} status="failed" error="${settled.error}" />`;
|
|
437
505
|
}
|
|
438
|
-
return `<await_result mode="race" settled="${settled.id}">\n${settled.result}\n</await_result>`;
|
|
506
|
+
return `<await_result mode="race" settled="${settled.id}"${sessionAttr}>\n${settled.result}\n</await_result>`;
|
|
439
507
|
}
|
|
440
508
|
}
|
|
441
509
|
},
|
|
442
510
|
});
|
|
443
511
|
}
|
|
512
|
+
async function prepareSubagentChild(params) {
|
|
513
|
+
const { agentName, template, remainingDepth, onSubagentEvent, sessionConfig, session, } = params;
|
|
514
|
+
const child = createChildFromTemplate(template, remainingDepth, onSubagentEvent);
|
|
515
|
+
const mode = session?.mode ?? sessionConfig.defaultMode ?? "stateless";
|
|
516
|
+
if (mode === "stateless") {
|
|
517
|
+
return { child };
|
|
518
|
+
}
|
|
519
|
+
const runtime = getSubagentSessionRuntime(sessionConfig);
|
|
520
|
+
const now = new Date().toISOString();
|
|
521
|
+
let sessionId;
|
|
522
|
+
let initialMessages = [];
|
|
523
|
+
let metadata;
|
|
524
|
+
try {
|
|
525
|
+
switch (mode) {
|
|
526
|
+
case "new":
|
|
527
|
+
sessionId = crypto.randomUUID();
|
|
528
|
+
metadata = {
|
|
529
|
+
sessionId,
|
|
530
|
+
agentName,
|
|
531
|
+
createdAt: now,
|
|
532
|
+
updatedAt: now,
|
|
533
|
+
};
|
|
534
|
+
await runtime.metadataStore.save(metadata);
|
|
535
|
+
break;
|
|
536
|
+
case "resume": {
|
|
537
|
+
sessionId = requireSessionId(session, mode);
|
|
538
|
+
metadata = await loadRequiredSessionMetadata(runtime.metadataStore, sessionId, agentName);
|
|
539
|
+
const storedMessages = await sessionConfig.messages.load(sessionId);
|
|
540
|
+
if (!storedMessages) {
|
|
541
|
+
throw new Error(`Subagent session "${sessionId}" could not be loaded.`);
|
|
542
|
+
}
|
|
543
|
+
initialMessages = structuredClone(storedMessages);
|
|
544
|
+
metadata = { ...metadata, updatedAt: now };
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case "fork": {
|
|
548
|
+
const sourceId = requireSessionId(session, mode);
|
|
549
|
+
await loadRequiredSessionMetadata(runtime.metadataStore, sourceId, agentName);
|
|
550
|
+
const sourceMessages = await sessionConfig.messages.load(sourceId);
|
|
551
|
+
if (!sourceMessages) {
|
|
552
|
+
throw new Error(`Subagent session "${sourceId}" could not be loaded.`);
|
|
553
|
+
}
|
|
554
|
+
sessionId = crypto.randomUUID();
|
|
555
|
+
initialMessages = structuredClone(sourceMessages);
|
|
556
|
+
metadata = {
|
|
557
|
+
sessionId,
|
|
558
|
+
agentName,
|
|
559
|
+
createdAt: now,
|
|
560
|
+
updatedAt: now,
|
|
561
|
+
};
|
|
562
|
+
await runtime.metadataStore.save(metadata);
|
|
563
|
+
await sessionConfig.messages.save(sessionId, initialMessages);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
default:
|
|
567
|
+
return { child };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
await child.close();
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
if (runtime.activeSessionIds.has(sessionId)) {
|
|
575
|
+
await child.close();
|
|
576
|
+
throw new Error(`Subagent session "${sessionId}" is already running.`);
|
|
577
|
+
}
|
|
578
|
+
runtime.activeSessionIds.add(sessionId);
|
|
579
|
+
let released = false;
|
|
580
|
+
const close = async () => {
|
|
581
|
+
if (released)
|
|
582
|
+
return;
|
|
583
|
+
released = true;
|
|
584
|
+
runtime.activeSessionIds.delete(sessionId);
|
|
585
|
+
await child.close();
|
|
586
|
+
};
|
|
587
|
+
const statefulChild = {
|
|
588
|
+
run: async function* (_history, input, options) {
|
|
589
|
+
const session = new Session({
|
|
590
|
+
agent: child,
|
|
591
|
+
sessionId,
|
|
592
|
+
sessionStore: sessionConfig.messages,
|
|
593
|
+
...(sessionConfig.sessionOptions ?? {}),
|
|
594
|
+
});
|
|
595
|
+
session.messages = structuredClone(initialMessages);
|
|
596
|
+
try {
|
|
597
|
+
for await (const event of session.send(input, options)) {
|
|
598
|
+
if (event.type === "turn.start" ||
|
|
599
|
+
event.type === "turn.done" ||
|
|
600
|
+
event.type === "compaction.start" ||
|
|
601
|
+
event.type === "compaction.pruned" ||
|
|
602
|
+
event.type === "compaction.summary" ||
|
|
603
|
+
event.type === "compaction.done" ||
|
|
604
|
+
event.type === "retry") {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
yield event;
|
|
608
|
+
}
|
|
609
|
+
await runtime.metadataStore.save({
|
|
610
|
+
sessionId,
|
|
611
|
+
agentName,
|
|
612
|
+
createdAt: metadata.createdAt,
|
|
613
|
+
updatedAt: new Date().toISOString(),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
await runtime.metadataStore.save({
|
|
618
|
+
sessionId,
|
|
619
|
+
agentName,
|
|
620
|
+
createdAt: metadata.createdAt,
|
|
621
|
+
updatedAt: new Date().toISOString(),
|
|
622
|
+
});
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
close,
|
|
627
|
+
};
|
|
628
|
+
return { child: statefulChild, sessionId };
|
|
629
|
+
}
|
|
630
|
+
function getSubagentSessionRuntime(config) {
|
|
631
|
+
let runtime = SUBAGENT_SESSION_RUNTIME.get(config);
|
|
632
|
+
if (!runtime) {
|
|
633
|
+
runtime = {
|
|
634
|
+
activeSessionIds: new Set(),
|
|
635
|
+
metadataStore: config.metadata ?? new InMemorySubagentSessionMetadataStore(),
|
|
636
|
+
};
|
|
637
|
+
SUBAGENT_SESSION_RUNTIME.set(config, runtime);
|
|
638
|
+
}
|
|
639
|
+
return runtime;
|
|
640
|
+
}
|
|
641
|
+
function createAgentSelectionSchema(names) {
|
|
642
|
+
if (names.length > 0) {
|
|
643
|
+
return z.enum(names);
|
|
644
|
+
}
|
|
645
|
+
return z.string().min(1);
|
|
646
|
+
}
|
|
647
|
+
function createTaskSessionSchema() {
|
|
648
|
+
return z
|
|
649
|
+
.object({
|
|
650
|
+
mode: z.enum(TASK_SESSION_MODES).describe("How to handle subagent memory"),
|
|
651
|
+
id: z.string().optional().describe("Existing session ID to resume or fork"),
|
|
652
|
+
})
|
|
653
|
+
.superRefine((value, ctx) => {
|
|
654
|
+
if ((value.mode === "resume" || value.mode === "fork") && !value.id) {
|
|
655
|
+
ctx.addIssue({
|
|
656
|
+
code: z.ZodIssueCode.custom,
|
|
657
|
+
message: `session.id is required when session.mode="${value.mode}"`,
|
|
658
|
+
path: ["id"],
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if ((value.mode === "stateless" || value.mode === "new") && value.id) {
|
|
662
|
+
ctx.addIssue({
|
|
663
|
+
code: z.ZodIssueCode.custom,
|
|
664
|
+
message: `session.id is not used when session.mode="${value.mode}"`,
|
|
665
|
+
path: ["id"],
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
async function listSubagentDescriptors(subagents) {
|
|
671
|
+
if (Array.isArray(subagents)) {
|
|
672
|
+
return subagents.map((agent) => ({
|
|
673
|
+
name: agent.name,
|
|
674
|
+
description: agent.description,
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
return subagents.list();
|
|
678
|
+
}
|
|
679
|
+
async function resolveSubagent(subagents, name) {
|
|
680
|
+
if (Array.isArray(subagents)) {
|
|
681
|
+
return subagents.find((agent) => agent.name === name);
|
|
682
|
+
}
|
|
683
|
+
return subagents.resolve(name);
|
|
684
|
+
}
|
|
685
|
+
async function loadRequiredSessionMetadata(store, sessionId, agentName) {
|
|
686
|
+
const metadata = await store.load(sessionId);
|
|
687
|
+
if (!metadata) {
|
|
688
|
+
throw new Error(`Unknown subagent session "${sessionId}".`);
|
|
689
|
+
}
|
|
690
|
+
if (metadata.agentName !== agentName) {
|
|
691
|
+
throw new Error(`Subagent session "${sessionId}" belongs to "${metadata.agentName}", not "${agentName}".`);
|
|
692
|
+
}
|
|
693
|
+
return metadata;
|
|
694
|
+
}
|
|
695
|
+
function requireSessionId(session, mode) {
|
|
696
|
+
if (!session?.id) {
|
|
697
|
+
throw new Error(`session.id is required when session.mode="${mode}".`);
|
|
698
|
+
}
|
|
699
|
+
return session.id;
|
|
700
|
+
}
|
|
701
|
+
function formatBackgroundSpawn(agentName, runId, sessionId) {
|
|
702
|
+
return `<background_spawn agent_id="${runId}"${formatOptionalAttr("session_id", sessionId)}>\nAgent "${agentName}" spawned in background with id "${runId}".\nUse agent_status, agent_await, or agent_cancel to manage it.\n</background_spawn>`;
|
|
703
|
+
}
|
|
704
|
+
function formatTaskResult(result, sessionId) {
|
|
705
|
+
return `<task_result${formatOptionalAttr("session_id", sessionId)}>\n${result}\n</task_result>`;
|
|
706
|
+
}
|
|
707
|
+
function formatAgentStatus(runId, status, result, error, sessionId) {
|
|
708
|
+
const sessionAttr = formatOptionalAttr("session_id", sessionId);
|
|
709
|
+
if (status === "done") {
|
|
710
|
+
return `<agent_status id="${runId}" status="done"${sessionAttr}>\n${result}\n</agent_status>`;
|
|
711
|
+
}
|
|
712
|
+
if (status === "failed") {
|
|
713
|
+
return `<agent_status id="${runId}" status="failed"${sessionAttr} error="${error ?? "unknown"}" />`;
|
|
714
|
+
}
|
|
715
|
+
if (status === "cancelled") {
|
|
716
|
+
return `<agent_status id="${runId}" status="cancelled"${sessionAttr} />`;
|
|
717
|
+
}
|
|
718
|
+
return `<agent_status id="${runId}" status="running"${sessionAttr} />`;
|
|
719
|
+
}
|
|
720
|
+
function formatOptionalAttr(name, value) {
|
|
721
|
+
return value ? ` ${name}="${value}"` : "";
|
|
722
|
+
}
|
|
444
723
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
445
724
|
function toTokenUsage(usage) {
|
|
446
725
|
return {
|
|
@@ -468,14 +747,14 @@ function buildAbortedStepMessages(text, reasoning) {
|
|
|
468
747
|
}
|
|
469
748
|
function wrapToolsWithApproval(tools, approve) {
|
|
470
749
|
const wrapped = {};
|
|
471
|
-
for (const [name,
|
|
472
|
-
if (!
|
|
473
|
-
wrapped[name] =
|
|
750
|
+
for (const [name, toolDef] of Object.entries(tools)) {
|
|
751
|
+
if (!toolDef.execute) {
|
|
752
|
+
wrapped[name] = toolDef;
|
|
474
753
|
continue;
|
|
475
754
|
}
|
|
476
|
-
const originalExecute =
|
|
755
|
+
const originalExecute = toolDef.execute;
|
|
477
756
|
wrapped[name] = {
|
|
478
|
-
...
|
|
757
|
+
...toolDef,
|
|
479
758
|
execute: async (input, options) => {
|
|
480
759
|
const allowed = await approve({
|
|
481
760
|
toolName: name,
|