@openharness/core 0.5.2 → 0.6.0

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/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 so nested children can inherit them). */
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 `subagentBackground` is configured. */
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; // null = not loaded yet
35
- cachedInstructions = null; // null = not loaded yet
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
- // Merge the task tool into the toolset when subagents are provided and depth > 0
53
- if (options.subagents?.length && this.maxSubagentDepth > 0) {
54
- const bgConfig = options.subagentBackground
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
- else {
74
- this.tools = options.tools;
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
- // Merge static tools with MCP tools and skill tool
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 ? { skill: createSkillTool(this.cachedSkills) } : {}),
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 ? part.error : new Error(String(part.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: { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined },
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
- // No approve subagents run autonomously
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 names = subagents.map((a) => a.name);
296
- const byName = new Map(subagents.map((a) => [a.name, a]));
297
- const listing = subagents.map((a) => `- ${a.name}: ${a.description ?? a.name}`).join("\n");
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 an agent ID.", "Use agent_status, agent_await, or agent_cancel to manage background agents.");
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: z.enum(names).describe("Which agent to use"),
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 bgSchema = baseSchema.extend({
312
- background: z
313
- .boolean()
314
- .optional()
315
- .describe("If true, spawn in background and return immediately with an agent ID"),
316
- });
317
- const inputSchema = registry ? bgSchema : baseSchema;
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, background } = rawInput;
323
- const template = byName.get(agentName);
324
- const child = createChildFromTemplate(template, remainingDepth, onSubagentEvent);
325
- // Background mode: spawn and return immediately
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 `<background_spawn agent_id="${id}">\nAgent "${agentName}" spawned in background with id "${id}".\nUse agent_status, agent_await, or agent_cancel to manage it.\n</background_spawn>`;
391
+ return formatBackgroundSpawn(agentName, id, sessionId);
332
392
  }
333
- // Foreground mode: run to completion
334
393
  let lastText = "";
335
- for await (const event of child.run([], prompt, { signal: abortSignal })) {
336
- onSubagentEvent?.([agentName], event);
337
- if (event.type === "text.done") {
338
- lastText = event.text;
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
- await child.close();
342
- return `<task_result>\n${lastText || "(no output)"}\n</task_result>`;
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 agent without blocking.",
411
+ description: "Check the status of a background run without blocking.",
349
412
  inputSchema: z.object({
350
- id: z.string().describe("The agent ID returned by a background task spawn"),
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 `<agent_status id="${id}" status="done">\n${status.result}\n</agent_status>`;
420
+ return formatAgentStatus(id, status.status, status.result, undefined, status.sessionId);
358
421
  }
359
422
  if (status.status === "failed") {
360
- return `<agent_status id="${id}" status="failed" error="${status.error ?? "unknown"}" />`;
423
+ return formatAgentStatus(id, status.status, undefined, status.error ?? "unknown", status.sessionId);
361
424
  }
362
425
  if (status.status === "cancelled") {
363
- return `<agent_status id="${id}" status="cancelled" />`;
426
+ return formatAgentStatus(id, status.status, undefined, undefined, status.sessionId);
364
427
  }
365
- return `<agent_status id="${id}" status="running" />`;
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 agent.",
434
+ description: "Cancel a running background run.",
372
435
  inputSchema: z.object({
373
- id: z.string().describe("The agent ID to cancel"),
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 agents to complete.",
455
+ "Wait for one or more background runs to complete.",
393
456
  "",
394
457
  "Modes:",
395
- ...modes.map((m) => `- ${modeDescriptions[m]}`),
458
+ ...modes.map((mode) => `- ${modeDescriptions[mode]}`),
396
459
  ].join("\n"),
397
460
  inputSchema: z.object({
398
- ids: z.array(z.string()).min(1).describe("Agent IDs to wait for"),
399
- mode: z.enum(modes).describe("How to wait for agents"),
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]) => `<agent id="${id}">\n${result}\n</agent>`);
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 (e) {
410
- const msg = e instanceof Error ? e.message : String(e);
411
- return `<await_result mode="all" error="${msg}" />`;
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, r]) => {
417
- if (r.status === "done") {
418
- return `<agent id="${id}" status="done">\n${r.result}\n</agent>`;
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="${r.status}" error="${r.error ?? "unknown"}" />`;
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, t] of Object.entries(tools)) {
472
- if (!t.execute) {
473
- wrapped[name] = t;
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 = t.execute;
755
+ const originalExecute = toolDef.execute;
477
756
  wrapped[name] = {
478
- ...t,
757
+ ...toolDef,
479
758
  execute: async (input, options) => {
480
759
  const allowed = await approve({
481
760
  toolName: name,