@xingular/harness 1.2.0 → 1.3.1-unstable.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.
Files changed (186) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +26 -17
  3. package/builtin/skills/plugin-authoring/SKILL.md +34 -0
  4. package/dist/cli.js +7362 -6239
  5. package/dist/sdk/agents/builtin.js +3 -3
  6. package/dist/sdk/agents/planResearch.d.ts +23 -1
  7. package/dist/sdk/agents/planResearch.js +71 -8
  8. package/dist/sdk/agents/profiles.d.ts +16 -4
  9. package/dist/sdk/agents/profiles.js +75 -12
  10. package/dist/sdk/annotations/bridge.d.ts +2 -0
  11. package/dist/sdk/annotations/bridge.js +4 -0
  12. package/dist/sdk/annotations/capture.d.ts +44 -2
  13. package/dist/sdk/annotations/capture.js +104 -3
  14. package/dist/sdk/annotations/delivery.d.ts +24 -17
  15. package/dist/sdk/annotations/delivery.js +82 -49
  16. package/dist/sdk/annotations/inspect.js +7 -6
  17. package/dist/sdk/annotations/pageScript.js +4 -4
  18. package/dist/sdk/annotations/part.d.ts +0 -2
  19. package/dist/sdk/annotations/part.js +4 -33
  20. package/dist/sdk/annotations/queue.d.ts +2 -2
  21. package/dist/sdk/annotations/queue.js +12 -7
  22. package/dist/sdk/annotations/serverMethods.js +11 -7
  23. package/dist/sdk/annotations/store.d.ts +13 -21
  24. package/dist/sdk/annotations/store.js +47 -37
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/commands/list.js +2 -0
  27. package/dist/sdk/commands/registry.d.ts +11 -3
  28. package/dist/sdk/commands/registry.js +3 -2
  29. package/dist/sdk/core/links.d.ts +51 -0
  30. package/dist/sdk/core/links.js +155 -0
  31. package/dist/sdk/core/shell.d.ts +20 -0
  32. package/dist/sdk/core/shell.js +73 -0
  33. package/dist/sdk/core/workspaceBashTool.d.ts +17 -1
  34. package/dist/sdk/core/workspaceBashTool.js +16 -2
  35. package/dist/sdk/core/workspaceSandbox.d.ts +10 -0
  36. package/dist/sdk/core/workspaceSandbox.js +52 -12
  37. package/dist/sdk/core/workspaceWriteOperations.d.ts +11 -0
  38. package/dist/sdk/core/workspaceWriteOperations.js +26 -8
  39. package/dist/sdk/doctor/checks.js +71 -12
  40. package/dist/sdk/harness.d.ts +5 -0
  41. package/dist/sdk/harness.js +11 -6
  42. package/dist/sdk/hub/hub.js +5 -3
  43. package/dist/sdk/index.d.ts +3 -2
  44. package/dist/sdk/index.js +2 -1
  45. package/dist/sdk/plugins/builtin.js +14 -9
  46. package/dist/sdk/plugins/builtinRegistry.d.ts +3 -1
  47. package/dist/sdk/plugins/builtinRegistry.js +9 -3
  48. package/dist/sdk/plugins/discover.d.ts +7 -1
  49. package/dist/sdk/plugins/discover.js +41 -2
  50. package/dist/sdk/plugins/identity.js +1 -1
  51. package/dist/sdk/plugins/install.d.ts +6 -2
  52. package/dist/sdk/plugins/install.js +29 -5
  53. package/dist/sdk/plugins/inventory.js +3 -3
  54. package/dist/sdk/plugins/manifest.d.ts +2 -0
  55. package/dist/sdk/plugins/manifest.js +21 -4
  56. package/dist/sdk/plugins/plugins.d.ts +2 -0
  57. package/dist/sdk/plugins/plugins.js +1 -0
  58. package/dist/sdk/plugins/proposals.d.ts +17 -0
  59. package/dist/sdk/plugins/proposals.js +147 -0
  60. package/dist/sdk/plugins/types.d.ts +1 -1
  61. package/dist/sdk/plugins/types.js +1 -1
  62. package/dist/sdk/prompt/compile.d.ts +13 -4
  63. package/dist/sdk/prompt/compile.js +46 -20
  64. package/dist/sdk/prompt/standard.d.ts +1 -1
  65. package/dist/sdk/prompt/standard.js +1 -1
  66. package/dist/sdk/server/methods.js +14 -2
  67. package/dist/sdk/sessions/compactionCheckpoint.d.ts +3 -3
  68. package/dist/sdk/sessions/compactionCheckpoint.js +12 -12
  69. package/dist/sdk/sessions/continuation.d.ts +4 -2
  70. package/dist/sdk/sessions/continuation.js +7 -6
  71. package/dist/sdk/sessions/contracts.d.ts +22 -4
  72. package/dist/sdk/sessions/contracts.js +12 -7
  73. package/dist/sdk/sessions/planMode.js +2 -2
  74. package/dist/sdk/sessions/runStore.js +3 -1
  75. package/dist/sdk/sessions/runner.js +2 -2
  76. package/dist/sdk/sessions/sessionState.d.ts +13 -0
  77. package/dist/sdk/sessions/sessionState.js +31 -0
  78. package/dist/sdk/sessions/sessions.d.ts +7 -0
  79. package/dist/sdk/sessions/sessions.js +73 -15
  80. package/dist/sdk/sessions/subagentService.d.ts +15 -0
  81. package/dist/sdk/sessions/subagentService.js +195 -55
  82. package/dist/sdk/sessions/subagents.d.ts +10 -1
  83. package/dist/sdk/sessions/subagents.js +4 -0
  84. package/dist/sdk/sessions/workSlice.d.ts +8 -5
  85. package/dist/sdk/sessions/workSlice.js +77 -45
  86. package/dist/sdk/terminal/controller.d.ts +20 -0
  87. package/dist/sdk/terminal/controller.js +43 -18
  88. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  89. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  90. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  91. package/dist/sdk/terminal/opentui/model.js +482 -0
  92. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  93. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  94. package/dist/sdk/terminal/opentui/theme.d.ts +2 -0
  95. package/dist/sdk/terminal/opentui/theme.js +2 -0
  96. package/dist/sdk/terminal/piExtension.js +98 -15
  97. package/dist/sdk/terminal/subagentMonitor.d.ts +17 -4
  98. package/dist/sdk/terminal/subagentMonitor.js +29 -8
  99. package/dist/sdk/terminal/terminals.d.ts +16 -0
  100. package/dist/sdk/terminal/terminals.js +130 -26
  101. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  102. package/dist/sdk/terminal/trajectory.js +8 -0
  103. package/dist/sdk/terminal/tui/annotateCommand.d.ts +5 -1
  104. package/dist/sdk/terminal/tui/annotateCommand.js +21 -7
  105. package/dist/sdk/terminal/tui/annotateSend.d.ts +30 -0
  106. package/dist/sdk/terminal/tui/annotateSend.js +40 -0
  107. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  108. package/dist/sdk/terminal/tui/chrome.js +13 -0
  109. package/dist/sdk/terminal/tui/commandPages.d.ts +1 -1
  110. package/dist/sdk/terminal/tui/commandPages.js +16 -11
  111. package/dist/sdk/terminal/tui/commands.d.ts +4 -0
  112. package/dist/sdk/terminal/tui/commands.js +12 -2
  113. package/dist/sdk/terminal/tui/dialogs.js +1 -0
  114. package/dist/sdk/terminal/tui/pageOverlay.d.ts +3 -0
  115. package/dist/sdk/terminal/tui/pageOverlay.js +25 -7
  116. package/dist/sdk/terminal/tui/pages/agentsPage.js +5 -2
  117. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +70 -16
  118. package/dist/sdk/terminal/tui/pages/annotatePage.js +349 -115
  119. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  120. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  121. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  122. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +14 -1
  123. package/dist/sdk/terminal/tui/pages/pluginsPage.js +295 -25
  124. package/dist/sdk/terminal/tui/pages/subagentsPage.js +8 -5
  125. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +4 -2
  126. package/dist/sdk/terminal/tui/pages/usagePage.js +39 -12
  127. package/dist/sdk/terminal/tui/pluginCommands.d.ts +1 -1
  128. package/dist/sdk/terminal/tui/pluginCommands.js +13 -3
  129. package/dist/sdk/tools/defaultSkills.d.ts +4 -0
  130. package/dist/sdk/tools/defaultSkills.js +20 -0
  131. package/dist/sdk/tools/definitions.d.ts +3 -1
  132. package/dist/sdk/tools/definitions.js +17 -1
  133. package/dist/sdk/tools/dispatch.js +12 -0
  134. package/dist/sdk/tools/harnessTools.js +1 -1
  135. package/dist/sdk/tools/sessionName.d.ts +9 -0
  136. package/dist/sdk/tools/sessionName.js +58 -11
  137. package/dist/sdk/tools/spawnSubagent.js +9 -7
  138. package/dist/sdk/tools/todo.d.ts +15 -0
  139. package/dist/sdk/tools/todo.js +64 -0
  140. package/dist/sdk/world/world.d.ts +0 -2
  141. package/dist/terminal-opentui.js +3331 -1584
  142. package/dist/terminal-pi.js +421 -133
  143. package/docs/README.md +3 -0
  144. package/docs/architecture/00-overview.md +1 -1
  145. package/docs/architecture/01-extension-points.md +1 -1
  146. package/docs/architecture/02-sessions-and-runs.md +73 -36
  147. package/docs/architecture/03-variants.md +1 -3
  148. package/docs/architecture/04-prompt.md +24 -13
  149. package/docs/architecture/06-plugins-and-capabilities.md +46 -10
  150. package/docs/architecture/07-tools-and-gate.md +49 -8
  151. package/docs/architecture/09-accounts-and-connectors.md +8 -2
  152. package/docs/architecture/10-server-protocol.md +68 -16
  153. package/docs/architecture/11-terminal.md +94 -45
  154. package/docs/architecture/12-storage.md +1 -0
  155. package/docs/architecture/13-world-tools.md +8 -6
  156. package/docs/architecture/14-surfaces-and-parity.md +4 -1
  157. package/docs/architecture/README.md +20 -18
  158. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  159. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +6 -2
  160. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +32 -21
  161. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  162. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  163. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  164. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  165. package/docs/architecture/proposals/PROP-0048-simple-annotate.md +85 -0
  166. package/docs/architecture/proposals/README.md +11 -6
  167. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  168. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  169. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  170. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  171. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  172. package/docs/decisions/ADR-0044-annotations-pending-or-sent.md +24 -0
  173. package/docs/decisions/ADR-0045-default-plugin-authoring-skill.md +9 -0
  174. package/docs/decisions/ADR-0046-hash-bound-plugin-proposals.md +20 -0
  175. package/docs/decisions/ADR-0047-links-open-with-the-os-handler.md +47 -0
  176. package/docs/doctor.md +14 -1
  177. package/docs/parity.md +16 -13
  178. package/docs/releases/1.3.0.md +21 -0
  179. package/docs/releases/1.3.1-unstable.1.md +8 -0
  180. package/docs/releases/README.md +9 -6
  181. package/docs/terminal-tui.md +127 -65
  182. package/package.json +1 -1
  183. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +0 -75
  184. package/dist/sdk/terminal/tui/pages/notesPage.js +0 -201
  185. package/dist/sdk/tools/checklist.d.ts +0 -9
  186. package/dist/sdk/tools/checklist.js +0 -55
@@ -18,15 +18,17 @@ import { FIRST_PARTY_PLUGINS, firstPartyExtensions } from "../plugins/firstParty
18
18
  import { imagePruning } from "../world/images.js";
19
19
  import { materialise } from "../variant/materialise.js";
20
20
  import { ASK_USER } from "../tools/askUser.js";
21
- import { CHECKLIST_WRITE } from "../tools/checklist.js";
21
+ import { defaultSkillPaths } from "../tools/defaultSkills.js";
22
+ import { TODO_WRITE } from "../tools/todo.js";
22
23
  import { CHECKPOINT_WRITE, checkpointOf } from "../tools/checkpoint.js";
23
- import { SESSION_NAME, sessionNameTool } from "../tools/sessionName.js";
24
+ import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessionName.js";
24
25
  import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
25
26
  import { GET_CAPABILITIES, getCapabilitiesTool } from "../tools/getCapabilities.js";
26
27
  import { isActiveStatus } from "./contracts.js";
27
28
  import { SPAWN_SUBAGENT, WAIT_SUBAGENTS, subagentDepth, subagentHistory, subagentNesting } from "./subagents.js";
28
29
  import { selectHistory } from "./history.js";
29
30
  import { runLineageJoin } from "./lineageJoin.js";
31
+ import { latestSessionName } from "./sessionState.js";
30
32
  import { compactionCheckpoint, COMPACTION_CHECKPOINT_EXTENSION } from "./compactionCheckpoint.js";
31
33
  import { createSliceState, parseWorkSlice, sliceOf, workSliceGuard, WorkSliceError, WORK_SLICE_EXTENSION } from "./workSlice.js";
32
34
  import { piSessionFactory, runTurn, summarize } from "./runner.js";
@@ -400,7 +402,7 @@ export class Sessions {
400
402
  const canAskUser = pinnedAskUser || (interactivePlan && harnessToolsEnabled);
401
403
  const requestedTools = [...new Set([
402
404
  ...input.tools.allow.filter((name) => name !== SPAWN_SUBAGENT && name !== WAIT_SUBAGENTS && (name !== ASK_USER || canAskUser)),
403
- ...(harnessToolsEnabled ? [...(canAskUser ? [ASK_USER] : []), CHECKLIST_WRITE, CHECKPOINT_WRITE, SESSION_NAME, GET_CAPABILITIES, ...DEFAULT_TOOL_NAMES] : []),
405
+ ...(harnessToolsEnabled ? [...(canAskUser ? [ASK_USER] : []), TODO_WRITE, CHECKPOINT_WRITE, SESSION_NAME, GET_CAPABILITIES, ...DEFAULT_TOOL_NAMES] : []),
404
406
  ...(broker?.toolNames ?? []),
405
407
  ...(canSpawn ? [SPAWN_SUBAGENT, WAIT_SUBAGENTS] : [])
406
408
  ])];
@@ -452,7 +454,9 @@ export class Sessions {
452
454
  variantScope: variant?.scope ?? null,
453
455
  variantVersion: variant?.fingerprint ?? null,
454
456
  ...(input.run.profile ? { profile: input.run.profile } : {}),
455
- ...(input.run.title ? { title: input.run.title } : {})
457
+ ...(input.run.title ? { title: input.run.title } : {}),
458
+ ...(input.model ? { model: input.model } : {}),
459
+ ...(input.effort ? { effort: input.effort } : {})
456
460
  });
457
461
  // What the session is, kept with the run so later consolidation knows which memory it had.
458
462
  try {
@@ -466,6 +470,28 @@ export class Sessions {
466
470
  updateRun(root, runId, (record) => Object.assign(record, { status: 'failed', endedAt: now(), terminalReason: message }));
467
471
  log(`session ${runId} did not start: ${message}`, code);
468
472
  };
473
+ // Plugins are resolved once per run: for the profiles the prompt lists, then for what Pi loads.
474
+ const { plugins } = this.options;
475
+ let pluginLoading = null;
476
+ const loadPlugins = plugins
477
+ ? () => (pluginLoading ??= plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }))
478
+ : null;
479
+ // A person's main session sees its current name and whether this run's todo is empty. A
480
+ // command, child, side question or unattended run has no such state to show.
481
+ const stateEligible = !input.command
482
+ && !input.run.relation
483
+ && (input.depth ?? 0) === 0
484
+ && !input.automatic
485
+ && (input.run.trigger == null || input.run.trigger === 'user');
486
+ const existingName = stateEligible ? latestSessionName(historyFile) : null;
487
+ const sessionState = stateEligible
488
+ ? {
489
+ name: existingName,
490
+ todo: [],
491
+ ...(continuation?.todo.length ? { previousTodo: continuation.todo } : {}),
492
+ firstTurn: !historyFile
493
+ }
494
+ : undefined;
469
495
  let system;
470
496
  let update;
471
497
  try {
@@ -476,12 +502,22 @@ export class Sessions {
476
502
  const delivered = historyFile ? deliveredSnapshot(historyFile) : null;
477
503
  const resumed = continuationSection(continuation, delivered?.some((entry) => entry.id === CONTINUATION_SECTION_ID) ?? false);
478
504
  // The profiles replace the variant's own subagent list, which they include.
479
- const profiles = capabilities.tools.includes(SPAWN_SUBAGENT) ? profileSection(root, input.cwd, variant?.dir ?? null, input.plan === true) : null;
505
+ const researchContext = input.plan === true ? {
506
+ frozenTools: capabilities.tools,
507
+ contributions: capabilities.contributions,
508
+ descriptors: BUILTIN_PLUGIN_REGISTRY.descriptors()
509
+ } : undefined;
510
+ if (capabilities.tools.includes(SPAWN_SUBAGENT) && loadPlugins && !input.automatic)
511
+ input.packageProfiles = (await loadPlugins()).profilePaths;
512
+ const profiles = capabilities.tools.includes(SPAWN_SUBAGENT)
513
+ ? profileSection(root, input.cwd, variant?.dir ?? null, input.plan === true, researchContext, input.packageProfiles ?? [])
514
+ : null;
480
515
  if (profiles && variant)
481
516
  variant = { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) };
482
517
  const own = [plan, resumed, profiles].filter((section) => section !== null);
483
518
  const effectiveLaunch = {
484
519
  ...launch,
520
+ ...(sessionState ? { sessionState } : {}),
485
521
  sections: own.length ? [...(launch.sections ?? []).filter((section) => !own.some((entry) => entry.id === section.id)), ...own] : launch.sections,
486
522
  tools: capabilities.tools,
487
523
  connectionTools: (launch.connectionTools ?? []).filter((name) => capabilities.tools.includes(name)),
@@ -493,8 +529,10 @@ export class Sessions {
493
529
  fail(error);
494
530
  throw error;
495
531
  }
496
- // A reply is bounded only when its checkpoint tool is part of the frozen capability set.
497
- const sliceEnabled = slicePolicy.enabled && capabilities.tools.includes(CHECKPOINT_WRITE);
532
+ // Plan-research children are bounded too, but their read-only capability set omits checkpoint_write.
533
+ const checkpointAvailable = capabilities.tools.includes(CHECKPOINT_WRITE);
534
+ const planResearchChild = input.plan === true && input.run.relation === 'subagent';
535
+ const sliceEnabled = slicePolicy.enabled && (checkpointAvailable || planResearchChild);
498
536
  const sliceState = createSliceState(slicePolicy, sliceOf(root, runId));
499
537
  const run = this.options.runs.start(input.run.sessionId, runId);
500
538
  if (run.finished)
@@ -505,7 +543,6 @@ export class Sessions {
505
543
  // What every session has, after the tools the caller switched on and in a fixed order.
506
544
  const allow = capabilities.tools;
507
545
  const session = { run, caller, allow, depth, cwd: input.cwd, capabilities };
508
- const { plugins } = this.options;
509
546
  let pi;
510
547
  const naming = sessionNameTool(() => {
511
548
  const current = pi;
@@ -587,17 +624,17 @@ export class Sessions {
587
624
  preamble: input.preamble ?? null,
588
625
  automatic: input.automatic,
589
626
  account: context,
590
- resources: { ids: [], skillPaths: variant?.skillPaths ?? [], promptPaths: [] },
627
+ resources: { ids: [], skillPaths: [...(variant?.skillPaths ?? []), ...defaultSkillPaths(variant?.dir)], promptPaths: [] },
591
628
  variantExtensions: variant?.extensionPaths ?? [],
592
629
  inline: [
593
630
  ...firstPartyFor(root, capabilities.contributions),
594
631
  runLineageJoin(root, runId),
595
632
  this.options.subagents.extension(runId),
596
633
  { name: 'image-pruning', factory: imagePruning() },
597
- { name: COMPACTION_CHECKPOINT_EXTENSION, factory: compactionCheckpoint({ initialChecklist: continuation?.checklist, planInstructions: checkpointPlanInstructions }) },
598
- ...(sliceEnabled ? [{ name: WORK_SLICE_EXTENSION, factory: workSliceGuard({ policy: slicePolicy, state: sliceState }) }] : [])
634
+ { name: COMPACTION_CHECKPOINT_EXTENSION, factory: compactionCheckpoint({ initialTodo: continuation?.todo, planInstructions: checkpointPlanInstructions }) },
635
+ ...(sliceEnabled ? [{ name: WORK_SLICE_EXTENSION, factory: workSliceGuard({ policy: slicePolicy, state: sliceState, checkpointAvailable }) }] : [])
599
636
  ],
600
- ...(plugins ? { plugins: { loading: () => plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }), loaded: (extensions) => plugins.loaded(extensions) } } : {}),
637
+ ...(plugins && loadPlugins ? { plugins: { loading: loadPlugins, loaded: (extensions) => plugins.loaded(extensions) } } : {}),
601
638
  system,
602
639
  ...(sliceEnabled ? { slice: { state: sliceState, maxToolCalls: slicePolicy.maxToolCalls } } : {}),
603
640
  // A command sends no request of its own, so the context update waits for the next turn.
@@ -624,11 +661,32 @@ export class Sessions {
624
661
  run.finish('failed', { code: 'native_resume_unavailable', message: 'The saved conversation could not be restored.' });
625
662
  throw new HarnessError('native_resume_unavailable', 'The saved conversation could not be restored.');
626
663
  }
664
+ // The fallback is a name, not a task step: a refused or failed name never fails the run.
665
+ if (stateEligible && input.autoName !== false && !existingName && !pi.sessionName
666
+ && pi.setSessionName && capabilities.tools.includes(SESSION_NAME)) {
667
+ const name = deriveSessionName(input.text);
668
+ if (name) {
669
+ try {
670
+ if (await gate.check(SESSION_NAME, { name }) === null)
671
+ pi.setSessionName(name);
672
+ }
673
+ catch {
674
+ // Session naming is metadata; leave the session unnamed.
675
+ }
676
+ }
677
+ }
627
678
  const opened = pi;
628
- updateRun(root, runId, (record) => void (record.nativeSessionId = opened.sessionId));
679
+ const openedModel = pi.model?.id && pi.model.provider ? `${pi.model.provider}/${pi.model.id}` : null;
680
+ updateRun(root, runId, (record) => {
681
+ record.nativeSessionId = opened.sessionId;
682
+ if (openedModel)
683
+ record.model = openedModel;
684
+ if (opened.thinkingLevel)
685
+ record.effort = opened.thinkingLevel;
686
+ });
629
687
  this.options.subagents.register(input, account.id, capabilities, Boolean(pi.sessionManager));
630
- if (pi.model?.id && pi.model.provider)
631
- this.options.subagents.resolvedModel(runId, `${pi.model.provider}/${pi.model.id}`, pi.thinkingLevel);
688
+ if (openedModel)
689
+ this.options.subagents.resolvedModel(runId, openedModel, pi.thinkingLevel);
632
690
  params.settle = () => this.options.subagents.verify(runId, opened.sessionManager ? subagentHistory(opened.sessionManager.getBranch()) : undefined);
633
691
  void runTurn(run, pi, params).then(() => (sliceEnabled && !input.command ? this.continueSlice({ root, runId, source: input, policy: slicePolicy }) : undefined));
634
692
  return { runId, nativeSessionId: pi.sessionId };
@@ -14,6 +14,7 @@ type Started = {
14
14
  export type SpawnedSubagent = Started & {
15
15
  agent?: string;
16
16
  unavailableTools?: string[];
17
+ continued?: string;
17
18
  };
18
19
  export interface SubagentRuntimeSettings {
19
20
  model: string | null;
@@ -42,9 +43,23 @@ export declare class Subagents {
42
43
  private rememberRuntimeSettings;
43
44
  resolvedModel(runId: string, model: string, effort?: Effort): void;
44
45
  private parent;
46
+ private planSpawnCheck;
45
47
  /** The plan-mode per-call gate and spawn implementation share this eligibility check. */
46
48
  planSpawnRefusal(parentRunId: string, args: unknown): string | null;
47
49
  spawn(request: SubagentInput): Promise<SpawnedSubagent>;
50
+ /** Records ownership, starts the child, and writes a failed record when it never opened. */
51
+ private launchChild;
52
+ /** The children this parent may read or continue: its own, and those recovered from its history. */
53
+ private owned;
54
+ /** The newest run in a continued child's chain, among the runs this parent owns. */
55
+ private latestLink;
56
+ /** The first run of a chain: its folder holds the child's history for every later link. */
57
+ private firstLink;
58
+ /**
59
+ * Continues a stopped child as a new child run with `continueFrom`: the same history, profile,
60
+ * account, variant version and capability set, which must still lie within the parent's.
61
+ */
62
+ private continueChild;
48
63
  private selected;
49
64
  results(selection: SubagentSelection): SubagentResult[];
50
65
  wait(selection: SubagentSelection, signal?: AbortSignal): Promise<SubagentResult[]>;
@@ -6,10 +6,11 @@ import { now } from "../core/fs.js";
6
6
  import { takeChars } from "../core/text.js";
7
7
  import { isActiveStatus } from "./contracts.js";
8
8
  import { narrowForProfile } from "../agents/narrow.js";
9
- import { PLAN_RESEARCH_EVIDENCE_TOOLS, PLAN_RESEARCH_TOOLS, planResearchProfile } from "../agents/planResearch.js";
9
+ import { planResearchEligibility } from "../agents/planResearch.js";
10
10
  import { resolveProfile } from "../agents/profiles.js";
11
+ import { continuationOf } from "./continuation.js";
11
12
  import { findRun, loadRun, readEvents, recordedSubagents, recordSubagent, saveRun } from "./runStore.js";
12
- import { SPAWN_SUBAGENT, SUBAGENT_RELATION, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, briefTitle, subagentHistory, subagentTitle } from "./subagents.js";
13
+ import { SPAWN_SUBAGENT, SUBAGENT_RELATION, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, briefTitle, continueHint, subagentHistory, subagentTitle } from "./subagents.js";
13
14
  function until(promise, signal) {
14
15
  if (signal.aborted) {
15
16
  void promise.catch(() => { });
@@ -95,39 +96,53 @@ export class Subagents {
95
96
  throw new HarnessError('run_not_active', 'The parent run is not accepting subagents.');
96
97
  return parent;
97
98
  }
98
- /** The plan-mode per-call gate and spawn implementation share this eligibility check. */
99
- planSpawnRefusal(parentRunId, args) {
99
+ planSpawnCheck(parentRunId, args) {
100
100
  const parent = this.parents.get(parentRunId);
101
101
  if (!parent || !this.runs.active(parentRunId))
102
- return 'The parent run is not accepting subagents.';
102
+ return { refusal: 'The parent run is not accepting subagents.' };
103
103
  if (!parent.input.plan)
104
- return 'Read-only research delegation is available only during plan mode.';
104
+ return { refusal: 'Read-only research delegation is available only during plan mode.' };
105
105
  if (!parent.capabilities.tools.includes(SPAWN_SUBAGENT))
106
- return 'Subagents are not enabled for this run.';
106
+ return { refusal: 'Subagents are not enabled for this run.' };
107
107
  if (!args || typeof args !== 'object' || Array.isArray(args))
108
- return 'Plan-mode delegation requires a named, eligible read-only research profile.';
109
- const request = args;
108
+ return { refusal: 'Plan-mode delegation requires a named, eligible read-only research profile.' };
109
+ let request = args;
110
+ if (request.continue !== undefined) {
111
+ // A continued child keeps its own profile; judge that profile.
112
+ const id = typeof request.continue === 'string' ? request.continue.trim() : '';
113
+ const record = id && this.owned(parentRunId, parent).has(id) ? findRun(this.env.root, id) : null;
114
+ if (!record?.profile)
115
+ return { refusal: 'Plan mode can continue only a stopped research subagent of this conversation that has a named profile.' };
116
+ if (request.agent !== undefined || request.tools !== undefined)
117
+ return { refusal: 'A continued subagent keeps its profile and tools; omit agent and tools.' };
118
+ request = { agent: record.profile };
119
+ }
110
120
  if (typeof request.agent !== 'string' || !request.agent.trim())
111
- return 'Plan-mode delegation requires a named, eligible read-only research profile.';
112
- const profile = resolveProfile(this.env.root, parent.input.cwd, request.agent.trim(), parent.input.variant && 'dir' in parent.input.variant ? parent.input.variant.dir : null);
113
- if (!profile)
114
- return `There is no agent profile named ${request.agent.trim()}.`;
115
- if (!planResearchProfile(profile))
116
- return 'This profile is not eligible for plan-mode research; use an explicit read-only research profile.';
117
- if (request.tools !== undefined && (!Array.isArray(request.tools) || request.tools.some(name => typeof name !== 'string' || !PLAN_RESEARCH_TOOLS.has(name) || !profile.tools.includes(name) || !parent.capabilities.tools.includes(name)))) {
118
- return 'Plan-mode subagent tools must be plan-safe and included in the selected profile and parent capabilities.';
121
+ return { refusal: 'Plan-mode delegation requires a named, eligible read-only research profile.' };
122
+ if (request.tools !== undefined && (!Array.isArray(request.tools) || request.tools.some(name => typeof name !== 'string'))) {
123
+ return { refusal: 'The optional tools field must be an array of tool names. Omit it to inherit the selected profile’s safe research intersection.' };
119
124
  }
120
- const requested = Array.isArray(request.tools) ? request.tools : null;
121
- const available = profile.tools.filter(name => parent.capabilities.tools.includes(name) && (!requested || requested.includes(name)));
122
- if (!available.some(name => PLAN_RESEARCH_EVIDENCE_TOOLS.has(name)))
123
- return 'The selected profile has no available read-only research tools in this run.';
124
- return null;
125
+ const profile = resolveProfile(this.env.root, parent.input.cwd, request.agent.trim(), parent.input.variant && 'dir' in parent.input.variant ? parent.input.variant.dir : null, parent.input.packageProfiles);
126
+ if (!profile)
127
+ return { refusal: `There is no agent profile named ${request.agent.trim()}.` };
128
+ const eligibility = planResearchEligibility(profile, {
129
+ frozenTools: parent.capabilities.tools,
130
+ contributions: parent.capabilities.contributions,
131
+ descriptors: this.options.descriptors?.() ?? []
132
+ }, request.tools);
133
+ return { refusal: eligibility.refusal, profile, eligibility };
134
+ }
135
+ /** The plan-mode per-call gate and spawn implementation share this eligibility check. */
136
+ planSpawnRefusal(parentRunId, args) {
137
+ return this.planSpawnCheck(parentRunId, args).refusal;
125
138
  }
126
139
  async spawn(request) {
127
140
  const parent = this.parent(request.parentRunId);
128
141
  const { input, accountId, capabilities } = parent;
129
142
  if (!capabilities.tools.includes(SPAWN_SUBAGENT))
130
143
  throw invalid('Subagents are not enabled for this run.');
144
+ if (request.continue !== undefined)
145
+ return this.continueChild(parent, request);
131
146
  const brief = typeof request.brief === 'string' ? request.brief.trim() : '';
132
147
  if (!brief)
133
148
  throw invalid('Give the subagent a brief.');
@@ -138,27 +153,33 @@ export class Subagents {
138
153
  throw invalid('wait must be a boolean.');
139
154
  if (request.tools !== undefined && (!Array.isArray(request.tools) || request.tools.some((name) => typeof name !== 'string')))
140
155
  throw invalid('Tools must be an array of tool names.');
141
- const outside = request.tools?.filter((name) => !capabilities.tools.includes(name)) ?? [];
142
- if (outside.length)
143
- throw invalid(`A subagent cannot have tools you do not have: ${outside.join(', ')}.`);
156
+ let profile = null;
157
+ let planEligibility = null;
144
158
  if (input.plan) {
145
- const refusal = this.planSpawnRefusal(request.parentRunId, request);
146
- if (refusal)
147
- throw invalid(refusal);
159
+ const check = this.planSpawnCheck(request.parentRunId, request);
160
+ if (check.refusal || !check.profile || !check.eligibility)
161
+ throw invalid(check.refusal ?? 'The selected profile has no eligible research tools in this run.');
162
+ profile = check.profile;
163
+ planEligibility = check.eligibility;
164
+ }
165
+ else {
166
+ const outside = request.tools?.filter((name) => !capabilities.tools.includes(name)) ?? [];
167
+ if (outside.length)
168
+ throw invalid(`A subagent cannot have tools you do not have: ${outside.join(', ')}.`);
169
+ if (request.agent !== undefined) {
170
+ if (typeof request.agent !== 'string' || !request.agent.trim())
171
+ throw invalid('agent must be the name of an agent profile.');
172
+ profile = resolveProfile(this.env.root, input.cwd, request.agent.trim(), input.variant && 'dir' in input.variant ? input.variant.dir : null, input.packageProfiles);
173
+ if (!profile)
174
+ throw invalid(`There is no agent profile named ${request.agent.trim()}.`);
175
+ }
148
176
  }
149
177
  await until(parent.ready, parent.stopped.signal);
150
178
  let variant = input.variant ?? null;
151
179
  let launch = input.launch;
152
180
  let model = input.model;
153
181
  let effort = input.effort;
154
- let profile = null;
155
- if (request.agent !== undefined) {
156
- if (typeof request.agent !== 'string' || !request.agent.trim())
157
- throw invalid('agent must be the name of an agent profile.');
158
- const dir = variant && 'dir' in variant ? variant.dir : null;
159
- profile = resolveProfile(this.env.root, input.cwd, request.agent.trim(), dir);
160
- if (!profile)
161
- throw invalid(`There is no agent profile named ${request.agent.trim()}.`);
182
+ if (profile) {
162
183
  // A variant's own subagent works from its definition alone; a profile keeps the parent's
163
184
  // variant unless it names another one, or none.
164
185
  if (profile.source === 'variant' || profile.variant === 'none')
@@ -171,20 +192,13 @@ export class Subagents {
171
192
  effort = profile.effort;
172
193
  launch = { ...(input.launch ?? {}), sections: [...(input.launch?.sections ?? []).filter((section) => section.id !== 'subagent'), { id: 'subagent', title: `Your role: ${profile.name}`, body: profile.instructions || profile.description, budget: 16_000, order: 22 }] };
173
194
  }
174
- if (input.plan) {
175
- const refusal = this.planSpawnRefusal(request.parentRunId, request);
176
- if (refusal)
177
- throw invalid(refusal);
178
- }
179
- const narrowed = narrowForProfile(capabilities, profile, request.tools, this.options.descriptors?.() ?? []);
180
- const childCapabilities = input.plan
181
- ? { ...narrowed.capabilities, tools: narrowed.capabilities.tools.filter(name => PLAN_RESEARCH_TOOLS.has(name)), connectors: [] }
182
- : narrowed.capabilities;
195
+ const narrowed = input.plan
196
+ ? { capabilities: planEligibility.capabilities, unavailable: planEligibility.unavailableTools }
197
+ : narrowForProfile(capabilities, profile, request.tools, this.options.descriptors?.() ?? []);
198
+ const childCapabilities = narrowed.capabilities;
183
199
  const tools = childCapabilities.tools;
184
200
  const runId = randomUUID();
185
201
  const sessionId = randomUUID();
186
- this.rememberRuntimeSettings(runId, { model: model ?? null, effort: effort ?? null });
187
- recordSubagent(this.env.root, request.parentRunId, runId);
188
202
  const child = {
189
203
  ...input,
190
204
  run: { ...input.run, ...(input.plan ? { worktreeId: null } : {}), id: runId, sessionId, parentRunId: request.parentRunId, relation: SUBAGENT_RELATION, goal: takeChars(brief, 4000), title, attachmentCount: 0, resumedFromRunId: null, handoffId: null, profile: profile?.name ?? null },
@@ -196,10 +210,18 @@ export class Subagents {
196
210
  variant, launch, model, effort,
197
211
  ...(input.plan ? { connections: false, subagents: null } : {})
198
212
  };
213
+ const result = await this.launchChild(parent, request, child);
214
+ return { ...result, ...(profile ? { agent: profile.name } : {}), ...(narrowed.unavailable.length ? { unavailableTools: narrowed.unavailable } : {}) };
215
+ }
216
+ /** Records ownership, starts the child, and writes a failed record when it never opened. */
217
+ async launchChild(parent, request, child) {
218
+ const runId = child.run.id;
219
+ this.rememberRuntimeSettings(runId, { model: child.model ?? null, effort: child.effort ?? null });
220
+ recordSubagent(this.env.root, request.parentRunId, runId);
199
221
  const started = this.launch(child).catch((error) => {
200
222
  if (!findRun(this.env.root, runId)) {
201
223
  const source = loadRun(this.env.root, request.parentRunId);
202
- saveRun(this.env.root, { ...source, id: runId, sessionId, parentRunId: request.parentRunId, relation: SUBAGENT_RELATION, nativeSessionId: null, resumedFromRunId: null, handoffId: null, exitCode: null, goal: takeChars(brief, 4000), title, attachmentCount: 0, status: 'failed', startedAt: now(), endedAt: now(), terminalReason: error instanceof HarnessError ? error.message : 'Subagent startup failed before its session opened.', profile: profile?.name ?? null });
224
+ saveRun(this.env.root, { ...source, id: runId, sessionId: child.run.sessionId, parentRunId: request.parentRunId, relation: SUBAGENT_RELATION, nativeSessionId: null, resumedFromRunId: child.run.resumedFromRunId ?? null, handoffId: null, exitCode: null, goal: child.run.goal ?? null, title: child.run.title ?? null, attachmentCount: 0, status: 'failed', startedAt: now(), endedAt: now(), terminalReason: error instanceof HarnessError ? error.message : 'Subagent startup failed before its session opened.', profile: child.run.profile ?? null, model: child.model ?? null, effort: child.effort ?? null });
203
225
  }
204
226
  return { runId, nativeSessionId: null };
205
227
  });
@@ -208,10 +230,113 @@ export class Subagents {
208
230
  const result = await until(started, parent.stopped.signal);
209
231
  if (request.wait)
210
232
  await this.wait({ parentRunId: request.parentRunId, runIds: [runId] });
211
- return { ...result, ...(profile ? { agent: profile.name } : {}), ...(narrowed.unavailable.length ? { unavailableTools: narrowed.unavailable } : {}) };
233
+ return result;
234
+ }
235
+ /** The children this parent may read or continue: its own, and those recovered from its history. */
236
+ owned(parentRunId, parent) {
237
+ return new Set([...recordedSubagents(this.env.root, parentRunId), ...(parent?.recovered ?? [])]);
238
+ }
239
+ /** The newest run in a continued child's chain, among the runs this parent owns. */
240
+ latestLink(start, owned) {
241
+ const records = [...owned].flatMap((id) => findRun(this.env.root, id) ?? []);
242
+ let latest = start;
243
+ const seen = new Set([start.id]);
244
+ for (;;) {
245
+ const next = records.filter((record) => record.resumedFromRunId === latest.id && !seen.has(record.id)).sort((left, right) => left.startedAt.localeCompare(right.startedAt)).at(-1);
246
+ if (!next)
247
+ return latest;
248
+ seen.add(next.id);
249
+ latest = next;
250
+ }
251
+ }
252
+ /** The first run of a chain: its folder holds the child's history for every later link. */
253
+ firstLink(record) {
254
+ let first = record;
255
+ const seen = new Set([record.id]);
256
+ while (first.resumedFromRunId && !seen.has(first.resumedFromRunId)) {
257
+ const previous = findRun(this.env.root, first.resumedFromRunId);
258
+ if (!previous || previous.relation !== SUBAGENT_RELATION)
259
+ break;
260
+ seen.add(previous.id);
261
+ first = previous;
262
+ }
263
+ return first;
264
+ }
265
+ /**
266
+ * Continues a stopped child as a new child run with `continueFrom`: the same history, profile,
267
+ * account, variant version and capability set, which must still lie within the parent's.
268
+ */
269
+ async continueChild(parent, request) {
270
+ const { root } = this.env;
271
+ const { input, accountId, capabilities } = parent;
272
+ const requested = typeof request.continue === 'string' ? request.continue.trim() : '';
273
+ if (!requested)
274
+ throw invalid('continue must be the run ID of a subagent that stopped.');
275
+ if (request.agent !== undefined || request.tools !== undefined)
276
+ throw invalid('A continued subagent keeps its profile and tools; omit agent and tools.');
277
+ if (request.brief !== undefined && typeof request.brief !== 'string')
278
+ throw invalid('brief must be text.');
279
+ if (request.wait !== undefined && typeof request.wait !== 'boolean')
280
+ throw invalid('wait must be a boolean.');
281
+ await until(parent.ready, parent.stopped.signal);
282
+ const owned = this.owned(request.parentRunId, parent);
283
+ const named = owned.has(requested) ? findRun(root, requested) : null;
284
+ if (!named || named.relation !== SUBAGENT_RELATION)
285
+ throw invalid('Continue only a subagent that this conversation started.');
286
+ const latest = this.latestLink(named, owned);
287
+ if (isActiveStatus(latest.status))
288
+ throw invalid(`Subagent ${latest.id} is still running; wait for it with wait_subagents.`);
289
+ if (latest.status === 'completed')
290
+ throw invalid(`Subagent ${latest.id} already finished and its result was delivered. Start a new subagent for more work.`);
291
+ const sessionDir = join(input.sessionDir, 'subagents', this.firstLink(latest).id);
292
+ const continuation = continuationOf(root, latest.id, sessionDir);
293
+ if (!continuation.resumable || !continuation.resume)
294
+ throw invalid(`Subagent ${latest.id} cannot be continued: ${continuation.reason === 'no_history' ? 'it stopped before its conversation started' : 'its saved conversation is missing'}. Start a new subagent instead.`);
295
+ if (continuation.accountId !== accountId)
296
+ throw invalid(`Subagent ${latest.id} ran on another account; start a new subagent instead.`);
297
+ const pinned = continuation.capabilities;
298
+ if (!pinned)
299
+ throw invalid(`Subagent ${latest.id} has no recorded capabilities; start a new subagent instead.`);
300
+ const missing = [
301
+ ...pinned.tools.filter((name) => !capabilities.tools.includes(name)),
302
+ ...pinned.connectors.filter((id) => !capabilities.connectors.includes(id)),
303
+ ...pinned.contributions.filter((id) => !capabilities.contributions.includes(id))
304
+ ];
305
+ if (missing.length)
306
+ throw invalid(`Subagent ${latest.id} needs capabilities you do not have in this run: ${missing.join(', ')}. Start a new subagent instead.`);
307
+ let profile = null;
308
+ if (latest.profile)
309
+ profile = resolveProfile(root, input.cwd, latest.profile, input.variant && 'dir' in input.variant ? input.variant.dir : null, input.packageProfiles);
310
+ if (input.plan) {
311
+ // A plan run continues only a research child whose pinned tools still qualify.
312
+ const check = latest.profile ? this.planSpawnCheck(request.parentRunId, { agent: latest.profile }) : { refusal: 'Plan mode can continue only a stopped research subagent that has a named profile.' };
313
+ if (check.refusal || !check.eligibility)
314
+ throw invalid(check.refusal ?? 'This subagent is not eligible for plan-mode research.');
315
+ const outside = pinned.tools.filter((name) => !check.eligibility.capabilities.tools.includes(name));
316
+ if (outside.length)
317
+ throw invalid(`Subagent ${latest.id} has tools plan mode does not allow: ${outside.join(', ')}.`);
318
+ }
319
+ const launch = profile
320
+ ? { ...(input.launch ?? {}), sections: [...(input.launch?.sections ?? []).filter((section) => section.id !== 'subagent'), { id: 'subagent', title: `Your role: ${profile.name}`, body: profile.instructions || profile.description, budget: 16_000, order: 22 }] }
321
+ : input.launch;
322
+ const runId = randomUUID();
323
+ const child = {
324
+ ...input,
325
+ run: { ...input.run, ...(input.plan ? { worktreeId: null } : {}), id: runId, sessionId: latest.sessionId, parentRunId: request.parentRunId, relation: SUBAGENT_RELATION, goal: latest.goal, title: latest.title ?? briefTitle(latest.goal), attachmentCount: 0, resumedFromRunId: latest.id, handoffId: null, profile: latest.profile ?? null },
326
+ accountId, poolId: null, sessionDir, resume: null, continueFrom: latest.id,
327
+ text: typeof request.brief === 'string' ? request.brief.trim() : '', images: undefined, requiresImageInput: false, preamble: null,
328
+ caller: { ...input.caller, runId: undefined, sessionId: latest.sessionId },
329
+ tools: { allow: pinned.tools, gated: (input.tools.gated ?? []).filter((name) => pinned.tools.includes(name)) },
330
+ capabilities: pinned, depth: (input.depth ?? 0) + 1,
331
+ variant: null, launch,
332
+ model: latest.model ?? input.model, effort: latest.effort ?? input.effort,
333
+ ...(input.plan ? { connections: false, subagents: null } : {})
334
+ };
335
+ const result = await this.launchChild(parent, request, child);
336
+ return { ...result, continued: latest.id, ...(latest.profile ? { agent: latest.profile } : {}) };
212
337
  }
213
338
  selected(selection, parent) {
214
- const owned = new Set([...recordedSubagents(this.env.root, selection.parentRunId), ...(parent?.recovered ?? [])]);
339
+ const owned = this.owned(selection.parentRunId, parent);
215
340
  if (selection.runIds !== undefined && (!Array.isArray(selection.runIds) || selection.runIds.some((id) => typeof id !== 'string')))
216
341
  throw invalid('runIds must be an array of child run IDs.');
217
342
  const ids = [...new Set(selection.runIds ?? [...owned])];
@@ -251,7 +376,9 @@ export class Subagents {
251
376
  if (owner === parentRunId)
252
377
  continue;
253
378
  const old = findRun(this.env.root, owner);
254
- if (!old || old.sessionId !== current.sessionId || old.accountId !== current.accountId || old.agentId !== current.agentId || old.projectId !== current.projectId || old.workingDirectory !== current.workingDirectory || !old.nativeSessionId || old.nativeSessionId !== current.nativeSessionId)
379
+ // The owner marker on this native branch is the lineage; a host may give each launch of the
380
+ // same conversation a new session id (INV-RUN-4).
381
+ if (!old || old.accountId !== current.accountId || old.agentId !== current.agentId || old.projectId !== current.projectId || old.workingDirectory !== current.workingDirectory || !old.nativeSessionId || old.nativeSessionId !== current.nativeSessionId)
255
382
  continue;
256
383
  for (const id of recordedSubagents(this.env.root, owner)) {
257
384
  let child = findRun(this.env.root, id);
@@ -277,12 +404,19 @@ export class Subagents {
277
404
  }
278
405
  }
279
406
  const delivered = new Set(history.receipts.flatMap((receipt) => receipt.delivered ?? []));
280
- const pending = this.results({ parentRunId }).filter((result) => !isActiveStatus(result.status) && !delivered.has(result.runId));
407
+ const all = this.results({ parentRunId });
408
+ const records = new Map(all.map((result) => [result.runId, findRun(this.env.root, result.runId)]));
409
+ // A stopped child that was continued is reported through its continuation instead.
410
+ const superseded = new Set([...records.values()].flatMap((record) => record?.resumedFromRunId ? [record.resumedFromRunId] : []));
411
+ const pending = all.filter((result) => !isActiveStatus(result.status) && !delivered.has(result.runId) && !superseded.has(result.runId));
281
412
  const chunks = [];
282
413
  const ids = [];
283
414
  let size = 0;
284
415
  for (const result of pending) {
285
- const text = `Subagent run ${result.runId} · ${result.status}\n${result.text || result.terminalReason || 'No written reply.'}`;
416
+ const record = records.get(result.runId);
417
+ const label = [record?.profile, record?.title].filter(Boolean).join(' · ');
418
+ const stopped = (result.status === 'interrupted' || result.status === 'failed') && continuationOf(this.env.root, result.runId).resumable;
419
+ const text = `Subagent run ${result.runId} · ${result.status}${label ? ` (${label})` : ''}\n${result.text || result.terminalReason || 'No written reply.'}${stopped ? `\nIt stopped before finishing. ${continueHint(result.runId)}` : ''}`;
286
420
  if (size + text.length > 32_000 && ids.length)
287
421
  break;
288
422
  chunks.push(text);
@@ -301,12 +435,18 @@ export class Subagents {
301
435
  }
302
436
  extension(runId) {
303
437
  return { name: 'native-subagents', factory: (pi) => {
304
- pi.on('before_agent_start', (_event, ctx) => {
438
+ pi.on('before_agent_start', async (_event, ctx) => {
305
439
  if (!this.parents.has(runId))
306
440
  return;
307
- this.restore(runId, subagentHistory(ctx.sessionManager.getBranch()));
441
+ const history = subagentHistory(ctx.sessionManager.getBranch());
442
+ this.restore(runId, history);
308
443
  pi.appendEntry(SUBAGENT_OWNER, { runId });
309
444
  this.parent(runId).started();
445
+ // Children an earlier reply left stopped are reported before the model answers, so it can
446
+ // continue them instead of starting over.
447
+ const notice = await this.delivery(runId, history, false);
448
+ if (notice)
449
+ return { message: { customType: SUBAGENT_RESULTS, content: notice.text, display: false, details: { [SUBAGENT_RECEIPT]: notice.receipt } } };
310
450
  });
311
451
  const deliver = async (event, ctx, settle) => {
312
452
  if (!this.parents.has(runId) || (settle && event.outcome !== 'completed') || ctx.signal?.aborted)
@@ -11,14 +11,23 @@ export interface SubagentOptions {
11
11
  export declare function subagentDepth(options: SubagentOptions | undefined): number;
12
12
  export interface SubagentInput {
13
13
  parentRunId: string;
14
- brief: string;
14
+ /** The task for a new child; with `continue`, an optional extra instruction. */
15
+ brief?: string;
15
16
  /** The short name the roster shows for the child (PROP-0030); the spawn tool requires it. */
16
17
  title?: string;
17
18
  tools?: string[];
18
19
  wait?: boolean;
19
20
  /** An agent profile by name (PROP-0029), or one of the parent variant's subagents (c5). */
20
21
  agent?: string;
22
+ /**
23
+ * A stopped child to continue instead of starting a fresh one: its run id. The new child reopens
24
+ * that child's own history and keeps its profile, tools, account and variant; `brief` is then an
25
+ * optional extra instruction.
26
+ */
27
+ continue?: string;
21
28
  }
29
+ /** The words that tell a parent a stopped child can be continued rather than started again. */
30
+ export declare function continueHint(runId: string): string;
22
31
  /** A subagent title is one short imperative, at most this many words (PROP-0030). */
23
32
  export declare const MAX_SUBAGENT_TITLE_WORDS = 6;
24
33
  /** The title a subagent title input means after cleaning, or undefined when it is not one. */
@@ -10,6 +10,10 @@ export function subagentDepth(options) {
10
10
  const value = Math.trunc(options?.maxDepth ?? DEFAULT_SUBAGENT_DEPTH);
11
11
  return Math.min(MAX_SUBAGENT_DEPTH, Math.max(1, Number.isFinite(value) ? value : DEFAULT_SUBAGENT_DEPTH));
12
12
  }
13
+ /** The words that tell a parent a stopped child can be continued rather than started again. */
14
+ export function continueHint(runId) {
15
+ return `Its conversation is saved: continue it with spawn_subagent { "continue": "${runId}" } if the work is still needed, instead of starting it again from scratch.`;
16
+ }
13
17
  /** A subagent title is one short imperative, at most this many words (PROP-0030). */
14
18
  export const MAX_SUBAGENT_TITLE_WORDS = 6;
15
19
  const BRIEF_TITLE_LENGTH = 60;
@@ -7,7 +7,7 @@ export interface WorkSliceInput {
7
7
  maxToolCalls?: number;
8
8
  /** Minutes in one reply; 0 disables this limit. */
9
9
  maxMinutes?: number;
10
- /** New tokens in one reply (input + cache read + output); 0 disables this limit. */
10
+ /** New tokens in one reply (new input + cache writes + output); 0 disables this limit, and no limit is the default. */
11
11
  maxNewTokens?: number;
12
12
  /** Slices one task may use before it is handed back to the person; 0 disables the cap. */
13
13
  maxSlices?: number;
@@ -54,7 +54,7 @@ export declare function createSliceState(_policy: SlicePolicy, slice?: number, n
54
54
  export declare function resetSliceState(state: SliceState, now?: number): void;
55
55
  /** The first limit this reply has passed, named as a person would say it. */
56
56
  export declare function sliceLimitReached(policy: SlicePolicy, state: SliceState, now?: number): string | null;
57
- /** Commands that read as a check, for "no progress" and "still failing" rules. */
57
+ /** Commands that read as a check for the repeated-failure rule. */
58
58
  export declare function looksLikeCheck(command: string): boolean;
59
59
  interface ToolCall {
60
60
  toolName: string;
@@ -85,12 +85,15 @@ interface GuardApi {
85
85
  export interface WorkSliceGuardOptions {
86
86
  policy: SlicePolicy;
87
87
  state: SliceState;
88
- /** Called once the guard has decided the reply must stop; the runner ends it. */
88
+ /** Whether checkpoint_write is active; a function supports the terminal's live plan-mode switch. */
89
+ checkpointAvailable?: boolean | (() => boolean);
90
+ /** Called when a triggering call terminates the reply; the runner ends it. */
89
91
  onEnd?: (end: SliceEnd, limit: string) => void;
90
92
  }
91
93
  /**
92
- * Bounds one reply and names repeated work. The first refusal asks for a checkpoint; a tool call
93
- * after that refusal ends the reply. A repeated call or failing command is named once, then ends it.
94
+ * Bounds one reply and names repeated work. Refusals request a checkpoint when available; without
95
+ * one, they ask for a partial report and prohibit further tools. A repeated call or failing command
96
+ * is named once, then ends the reply.
94
97
  */
95
98
  export declare function workSliceGuard(options: WorkSliceGuardOptions): (pi: GuardApi) => void;
96
99
  /**