@xingular/harness 1.1.1 → 1.3.1-unstable.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.
Files changed (264) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +36 -28
  3. package/dist/cli.js +10837 -6159
  4. package/dist/sdk/accounts/accounts.d.ts +27 -3
  5. package/dist/sdk/accounts/accounts.js +78 -24
  6. package/dist/sdk/accounts/secrets.d.ts +12 -0
  7. package/dist/sdk/accounts/secrets.js +114 -7
  8. package/dist/sdk/accounts/store.d.ts +26 -1
  9. package/dist/sdk/accounts/store.js +1 -1
  10. package/dist/sdk/agents/builtin.js +18 -4
  11. package/dist/sdk/agents/planResearch.d.ts +27 -0
  12. package/dist/sdk/agents/planResearch.js +82 -0
  13. package/dist/sdk/agents/profiles.d.ts +5 -1
  14. package/dist/sdk/agents/profiles.js +21 -5
  15. package/dist/sdk/annotations/pageScript.js +349 -48
  16. package/dist/sdk/annotations/part.js +34 -23
  17. package/dist/sdk/annotations/store.d.ts +7 -0
  18. package/dist/sdk/annotations/store.js +13 -5
  19. package/dist/sdk/browser/backend.d.ts +16 -0
  20. package/dist/sdk/browser/backend.js +17 -0
  21. package/dist/sdk/browser/contracts.d.ts +19 -0
  22. package/dist/sdk/browser/contracts.js +116 -0
  23. package/dist/sdk/browser/tools.d.ts +1 -4
  24. package/dist/sdk/browser/tools.js +14 -91
  25. package/dist/sdk/capabilities/runSnapshot.js +10 -4
  26. package/dist/sdk/cli.js +5 -4
  27. package/dist/sdk/commands/list.d.ts +3 -0
  28. package/dist/sdk/commands/list.js +75 -0
  29. package/dist/sdk/commands/registry.d.ts +258 -0
  30. package/dist/sdk/commands/registry.js +48 -0
  31. package/dist/sdk/commands/routes.d.ts +16 -0
  32. package/dist/sdk/commands/routes.js +33 -0
  33. package/dist/sdk/core/commandBudget.d.ts +23 -0
  34. package/dist/sdk/core/commandBudget.js +57 -0
  35. package/dist/sdk/core/index.js +17 -1
  36. package/dist/sdk/core/interactiveProfile.d.ts +46 -0
  37. package/dist/sdk/core/interactiveProfile.js +143 -0
  38. package/dist/sdk/core/shell.d.ts +20 -0
  39. package/dist/sdk/core/shell.js +73 -0
  40. package/dist/sdk/core/workspace.d.ts +13 -0
  41. package/dist/sdk/core/workspace.js +88 -0
  42. package/dist/sdk/core/workspaceBashTool.d.ts +20 -0
  43. package/dist/sdk/core/workspaceBashTool.js +50 -0
  44. package/dist/sdk/core/workspaceSandbox.d.ts +41 -0
  45. package/dist/sdk/core/workspaceSandbox.js +360 -0
  46. package/dist/sdk/core/workspaceWriteOperations.d.ts +33 -0
  47. package/dist/sdk/core/workspaceWriteOperations.js +224 -0
  48. package/dist/sdk/doctor/checks.js +71 -12
  49. package/dist/sdk/doctor/types.d.ts +2 -2
  50. package/dist/sdk/harness.d.ts +7 -0
  51. package/dist/sdk/harness.js +42 -2
  52. package/dist/sdk/hub/hub.js +5 -3
  53. package/dist/sdk/index.d.ts +8 -3
  54. package/dist/sdk/index.js +4 -1
  55. package/dist/sdk/pi/runtime.d.ts +2 -1
  56. package/dist/sdk/pi/runtime.js +2 -2
  57. package/dist/sdk/plugins/builtin.js +32 -15
  58. package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
  59. package/dist/sdk/plugins/builtinRegistry.js +30 -8
  60. package/dist/sdk/plugins/catalogue.js +2 -1
  61. package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
  62. package/dist/sdk/plugins/commandPrefix.js +21 -0
  63. package/dist/sdk/plugins/firstParty.d.ts +1 -1
  64. package/dist/sdk/plugins/firstParty.js +8 -2
  65. package/dist/sdk/plugins/install.d.ts +6 -2
  66. package/dist/sdk/plugins/install.js +29 -5
  67. package/dist/sdk/plugins/inventory.js +6 -5
  68. package/dist/sdk/plugins/manifest.d.ts +2 -0
  69. package/dist/sdk/plugins/manifest.js +2 -1
  70. package/dist/sdk/plugins/surfaces.d.ts +5 -0
  71. package/dist/sdk/plugins/surfaces.js +11 -0
  72. package/dist/sdk/plugins/types.d.ts +15 -0
  73. package/dist/sdk/prompt/compile.d.ts +14 -0
  74. package/dist/sdk/prompt/compile.js +65 -31
  75. package/dist/sdk/prompt/standard.d.ts +17 -0
  76. package/dist/sdk/prompt/standard.js +21 -0
  77. package/dist/sdk/server/methods.js +22 -2
  78. package/dist/sdk/server/remote.js +36 -11
  79. package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
  80. package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
  81. package/dist/sdk/sessions/continuation.d.ts +7 -2
  82. package/dist/sdk/sessions/continuation.js +11 -6
  83. package/dist/sdk/sessions/contracts.d.ts +190 -4
  84. package/dist/sdk/sessions/contracts.js +71 -8
  85. package/dist/sdk/sessions/control.d.ts +72 -0
  86. package/dist/sdk/sessions/control.js +158 -0
  87. package/dist/sdk/sessions/initialBash.d.ts +7 -0
  88. package/dist/sdk/sessions/initialBash.js +7 -0
  89. package/dist/sdk/sessions/planMode.d.ts +14 -3
  90. package/dist/sdk/sessions/planMode.js +74 -17
  91. package/dist/sdk/sessions/registry.d.ts +1 -1
  92. package/dist/sdk/sessions/registry.js +2 -2
  93. package/dist/sdk/sessions/runStore.d.ts +14 -0
  94. package/dist/sdk/sessions/runStore.js +38 -1
  95. package/dist/sdk/sessions/runner.d.ts +40 -2
  96. package/dist/sdk/sessions/runner.js +161 -13
  97. package/dist/sdk/sessions/sessions.d.ts +23 -3
  98. package/dist/sdk/sessions/sessions.js +225 -22
  99. package/dist/sdk/sessions/subagentService.d.ts +24 -0
  100. package/dist/sdk/sessions/subagentService.js +225 -26
  101. package/dist/sdk/sessions/subagents.d.ts +10 -1
  102. package/dist/sdk/sessions/subagents.js +4 -0
  103. package/dist/sdk/sessions/workSlice.d.ts +106 -0
  104. package/dist/sdk/sessions/workSlice.js +329 -0
  105. package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
  106. package/dist/sdk/sessions/workspaceBoundary.js +46 -0
  107. package/dist/sdk/terminal/browserControl.js +2 -1
  108. package/dist/sdk/terminal/controller.d.ts +31 -3
  109. package/dist/sdk/terminal/controller.js +81 -29
  110. package/dist/sdk/terminal/opentui/ansi.d.ts +25 -0
  111. package/dist/sdk/terminal/opentui/ansi.js +192 -0
  112. package/dist/sdk/terminal/opentui/model.d.ts +163 -0
  113. package/dist/sdk/terminal/opentui/model.js +482 -0
  114. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +3 -3
  115. package/dist/sdk/terminal/opentui/subagentsWidget.js +7 -5
  116. package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
  117. package/dist/sdk/terminal/opentui/theme.js +88 -0
  118. package/dist/sdk/terminal/piExtension.d.ts +6 -1
  119. package/dist/sdk/terminal/piExtension.js +177 -63
  120. package/dist/sdk/terminal/standalone.d.ts +7 -0
  121. package/dist/sdk/terminal/standalone.js +72 -13
  122. package/dist/sdk/terminal/subagentMonitor.d.ts +28 -6
  123. package/dist/sdk/terminal/subagentMonitor.js +61 -9
  124. package/dist/sdk/terminal/terminals.d.ts +26 -2
  125. package/dist/sdk/terminal/terminals.js +325 -43
  126. package/dist/sdk/terminal/trajectory.d.ts +6 -1
  127. package/dist/sdk/terminal/trajectory.js +8 -0
  128. package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
  129. package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
  130. package/dist/sdk/terminal/tui/chrome.d.ts +4 -0
  131. package/dist/sdk/terminal/tui/chrome.js +13 -0
  132. package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
  133. package/dist/sdk/terminal/tui/commandPages.js +17 -4
  134. package/dist/sdk/terminal/tui/commands.d.ts +6 -0
  135. package/dist/sdk/terminal/tui/commands.js +213 -124
  136. package/dist/sdk/terminal/tui/list.d.ts +14 -1
  137. package/dist/sdk/terminal/tui/list.js +96 -14
  138. package/dist/sdk/terminal/tui/pageOverlay.js +21 -5
  139. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
  140. package/dist/sdk/terminal/tui/pages/browserPage.js +5 -4
  141. package/dist/sdk/terminal/tui/pages/hubPage.d.ts +14 -1
  142. package/dist/sdk/terminal/tui/pages/hubPage.js +192 -28
  143. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
  144. package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
  145. package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +8 -6
  146. package/dist/sdk/terminal/tui/pages/pluginsPage.js +206 -28
  147. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
  148. package/dist/sdk/terminal/tui/pages/subagentsPage.js +104 -19
  149. package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
  150. package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
  151. package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
  152. package/dist/sdk/terminal/tui/piAdapter.js +3 -5
  153. package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
  154. package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
  155. package/dist/sdk/terminal/tui/runtime.js +23 -3
  156. package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
  157. package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
  158. package/dist/sdk/tools/checkpoint.d.ts +27 -0
  159. package/dist/sdk/tools/checkpoint.js +154 -0
  160. package/dist/sdk/tools/getCapabilities.d.ts +31 -0
  161. package/dist/sdk/tools/getCapabilities.js +34 -0
  162. package/dist/sdk/tools/sessionName.d.ts +2 -0
  163. package/dist/sdk/tools/sessionName.js +5 -3
  164. package/dist/sdk/tools/spawnSubagent.js +11 -8
  165. package/dist/sdk/tools/todo.d.ts +15 -0
  166. package/dist/sdk/tools/todo.js +64 -0
  167. package/dist/sdk/usage/analytics.d.ts +20 -0
  168. package/dist/sdk/usage/analytics.js +151 -0
  169. package/dist/sdk/usage/ledger.d.ts +38 -0
  170. package/dist/sdk/usage/ledger.js +142 -0
  171. package/dist/sdk/usage/limits/adapters.d.ts +11 -0
  172. package/dist/sdk/usage/limits/adapters.js +349 -0
  173. package/dist/sdk/usage/limits/capture.d.ts +8 -0
  174. package/dist/sdk/usage/limits/capture.js +47 -0
  175. package/dist/sdk/usage/limits/http.d.ts +12 -0
  176. package/dist/sdk/usage/limits/http.js +51 -0
  177. package/dist/sdk/usage/limits/normalize.d.ts +15 -0
  178. package/dist/sdk/usage/limits/normalize.js +211 -0
  179. package/dist/sdk/usage/limits/service.d.ts +30 -0
  180. package/dist/sdk/usage/limits/service.js +176 -0
  181. package/dist/sdk/usage/limits/types.d.ts +65 -0
  182. package/dist/sdk/usage/limits/types.js +1 -0
  183. package/dist/sdk/usage/serverMethods.d.ts +3 -0
  184. package/dist/sdk/usage/serverMethods.js +22 -0
  185. package/dist/sdk/usage/service.d.ts +32 -0
  186. package/dist/sdk/usage/service.js +66 -0
  187. package/dist/sdk/usage/settings.d.ts +7 -0
  188. package/dist/sdk/usage/settings.js +28 -0
  189. package/dist/sdk/usage/types.d.ts +60 -0
  190. package/dist/sdk/usage/types.js +4 -0
  191. package/dist/terminal-opentui.js +9204 -4812
  192. package/dist/terminal-pi.js +1848 -272
  193. package/docs/architecture/00-overview.md +1 -1
  194. package/docs/architecture/01-extension-points.md +1 -1
  195. package/docs/architecture/02-sessions-and-runs.md +169 -36
  196. package/docs/architecture/04-prompt.md +55 -15
  197. package/docs/architecture/06-plugins-and-capabilities.md +36 -15
  198. package/docs/architecture/07-tools-and-gate.md +131 -25
  199. package/docs/architecture/08-memory-and-history.md +4 -2
  200. package/docs/architecture/09-accounts-and-connectors.md +39 -3
  201. package/docs/architecture/10-server-protocol.md +71 -9
  202. package/docs/architecture/11-terminal.md +148 -60
  203. package/docs/architecture/12-storage.md +30 -2
  204. package/docs/architecture/13-world-tools.md +22 -1
  205. package/docs/architecture/14-surfaces-and-parity.md +58 -8
  206. package/docs/architecture/README.md +20 -17
  207. package/docs/architecture/proposals/PROP-0006-run-checklist.md +2 -0
  208. package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
  209. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
  210. package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
  211. package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
  212. package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
  213. package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
  214. package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
  215. package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
  216. package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
  217. package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
  218. package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
  219. package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
  220. package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +258 -0
  221. package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
  222. package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
  223. package/docs/architecture/proposals/PROP-0044-work-slice-defaults.md +93 -0
  224. package/docs/architecture/proposals/PROP-0045-rename-checklist-to-todo.md +132 -0
  225. package/docs/architecture/proposals/PROP-0046-progress-aware-stall-guards.md +82 -0
  226. package/docs/architecture/proposals/PROP-0047-plugins-hub-separation.md +122 -0
  227. package/docs/architecture/proposals/README.md +24 -6
  228. package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
  229. package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
  230. package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
  231. package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
  232. package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
  233. package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
  234. package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
  235. package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
  236. package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
  237. package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
  238. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +39 -0
  239. package/docs/decisions/ADR-0040-architecture-change-workflow.md +31 -0
  240. package/docs/decisions/ADR-0041-plugins-hub-separation.md +41 -0
  241. package/docs/decisions/ADR-0042-windows-shell-without-sandbox.md +40 -0
  242. package/docs/decisions/ADR-0043-parents-continue-stopped-subagents.md +49 -0
  243. package/docs/doctor.md +14 -1
  244. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
  245. package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
  246. package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
  247. package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
  248. package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
  249. package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
  250. package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
  251. package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
  252. package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
  253. package/docs/parity.md +83 -27
  254. package/docs/releases/1.1.1.md +28 -0
  255. package/docs/releases/1.2.0.md +49 -0
  256. package/docs/releases/1.3.0.md +21 -0
  257. package/docs/releases/4.0.0.md +10 -9
  258. package/docs/releases/README.md +35 -30
  259. package/docs/study/00-index.md +3 -0
  260. package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
  261. package/docs/terminal-tui.md +162 -82
  262. package/package.json +4 -3
  263. package/dist/sdk/tools/checklist.d.ts +0 -9
  264. package/dist/sdk/tools/checklist.js +0 -54
@@ -4,6 +4,8 @@ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import terminalExtension from "../piExtension.js";
5
5
  import { normalModeTools, planModeTools, QUESTION_TOOL_NAMES } from "./planMode.js";
6
6
  import { claimTerminalControl, controlCall } from "../control.js";
7
+ import { observeProviderResponses } from "../../usage/limits/capture.js";
8
+ import { limitHeaders } from "../../usage/limits/normalize.js";
7
9
  import { loadPiAdapter } from "./piAdapter.js";
8
10
  import { loadPi } from "../../pi/runtime.js";
9
11
  import { sessionUiContext } from "./sessionUi.js";
@@ -101,8 +103,16 @@ export async function createTerminalRuntime(args, host) {
101
103
  const identity = await controlCall('accounts.session').catch(() => null);
102
104
  const account = identity && typeof identity.accountId === 'string' && typeof identity.label === 'string'
103
105
  ? Object.freeze({ accountId: identity.accountId, label: identity.label }) : null;
104
- const adapter = await loadPiAdapter();
106
+ await loadPiAdapter();
105
107
  const modelRuntime = await ModelRuntime.create({ authPath: join(agentDir, 'auth.json'), modelsPath: join(agentDir, 'models.json') });
108
+ // This process makes the model requests, so it is where response headers are seen. Only the
109
+ // limit-bearing headers cross the socket, and the parent throttles what it stores.
110
+ observeProviderResponses(modelRuntime, (provider, response) => {
111
+ const headers = limitHeaders(response.headers);
112
+ if (!Object.keys(headers).length)
113
+ return;
114
+ void controlCall('terminal.usage.headers', { provider, status: response.status, headers }).catch(() => undefined);
115
+ });
106
116
  const listeners = new Set();
107
117
  const changes = new Set();
108
118
  const beforeChanges = new Set();
@@ -157,11 +167,21 @@ export async function createTerminalRuntime(args, host) {
157
167
  additionalSkillPaths: prepared.skills, additionalPromptTemplatePaths: prepared.prompts,
158
168
  additionalThemePaths: prepared.themes,
159
169
  extensionFactories: [
160
- ...adapter.builtins,
161
170
  { name: 'harness-terminal-bridge', factory: pi => terminalExtension(pi, planRun, () => native.session), hidden: true },
162
171
  // A model picked in the terminal reaches Pi as this event. A restore is not a pick.
163
172
  { name: 'harness-mode-choices', factory: pi => { pi.on('model_select', event => { if (event.source !== 'restore')
164
- void modeChoices.record(recordingMode(), { model: `${event.model.provider}/${event.model.id}` }); }); }, hidden: true }
173
+ void modeChoices.record(recordingMode(), { model: `${event.model.provider}/${event.model.id}` }); }); }, hidden: true },
174
+ // The bridge registers parent capabilities when delegation is available. Runs without
175
+ // delegation still need the same frozen snapshot, after the bridge has begun the run.
176
+ { name: 'harness-run-capabilities', factory: pi => {
177
+ pi.on('before_agent_start', async () => {
178
+ if (!pi.getAllTools().some(tool => tool.name === 'spawn_subagent')) {
179
+ const snapshot = await controlCall('terminal.subagents.register', { tools: pi.getActiveTools(), plan: planRun.active });
180
+ if (Array.isArray(snapshot.connectors))
181
+ planRun.connectors = snapshot.connectors;
182
+ }
183
+ });
184
+ }, hidden: true }
165
185
  ],
166
186
  appendSystemPromptOverride: (base) => [...base, ...prepared.appendSystemPrompt]
167
187
  });
@@ -0,0 +1,40 @@
1
+ import type { SessionInfo, Theme } from '@earendil-works/pi-coding-agent';
2
+ import { type TuiMouseEventResult } from '@earendil-works/pi-tui';
3
+ import type { TerminalPage } from "./page.js";
4
+ import type { TerminalMouseInput } from "./mouse.js";
5
+ export declare function formatSessionRelativeTime(value: Date, now?: number): string;
6
+ export declare function formatSessionSize(bytes: number | undefined): string;
7
+ export interface ResumeSessionsPageOptions {
8
+ theme: () => Theme;
9
+ cwd(): string;
10
+ branch(): string | null;
11
+ listSessions(signal: AbortSignal): Promise<SessionInfo[]>;
12
+ select(path: string): void;
13
+ cancel(): void;
14
+ changed(): void;
15
+ }
16
+ export declare class ResumeSessionsPage implements TerminalPage {
17
+ private readonly options;
18
+ readonly presentation: "fullscreen";
19
+ private readonly list;
20
+ private loading;
21
+ private loaded;
22
+ private disposed;
23
+ private error;
24
+ private abort?;
25
+ constructor(options: ResumeSessionsPageOptions);
26
+ get focused(): boolean;
27
+ set focused(value: boolean);
28
+ tabs(): {
29
+ id: string;
30
+ title: string;
31
+ }[];
32
+ activeTab(): string;
33
+ hint(): string;
34
+ activate(): void;
35
+ dispose(): void;
36
+ private load;
37
+ handleInput(data: string): boolean;
38
+ handleMouse(event: TerminalMouseInput): TuiMouseEventResult | undefined;
39
+ render(width: number, rows: number): string[];
40
+ }
@@ -0,0 +1,141 @@
1
+ import { stat } from 'node:fs/promises';
2
+ import { stripTerminalSequences } from '@earendil-works/pi-tui';
3
+ import { displayProjectName, singleLine } from "./status.js";
4
+ import { FilterList } from "./list.js";
5
+ const RELATIVE_TIME = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
6
+ const TIME_UNITS = [
7
+ [365 * 24 * 60 * 60 * 1000, 'year'],
8
+ [30 * 24 * 60 * 60 * 1000, 'month'],
9
+ [7 * 24 * 60 * 60 * 1000, 'week'],
10
+ [24 * 60 * 60 * 1000, 'day'],
11
+ [60 * 60 * 1000, 'hour'],
12
+ [60 * 1000, 'minute'],
13
+ [1000, 'second']
14
+ ];
15
+ export function formatSessionRelativeTime(value, now = Date.now()) {
16
+ const delta = value.getTime() - now;
17
+ if (!Number.isFinite(delta))
18
+ return 'unknown time';
19
+ const absolute = Math.abs(delta);
20
+ const unit = TIME_UNITS.find(([size]) => absolute >= size);
21
+ return unit ? RELATIVE_TIME.format(Math.round(delta / unit[0]), unit[1]) : RELATIVE_TIME.format(0, 'second');
22
+ }
23
+ export function formatSessionSize(bytes) {
24
+ if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes < 0)
25
+ return '—';
26
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
27
+ let size = bytes;
28
+ let unit = 0;
29
+ while (size >= 1024 && unit < units.length - 1) {
30
+ size /= 1024;
31
+ unit += 1;
32
+ }
33
+ return `${unit === 0 ? Math.round(size) : size.toFixed(1)}${units[unit]}`;
34
+ }
35
+ function safe(value) {
36
+ return stripTerminalSequences(value).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, ' ');
37
+ }
38
+ export class ResumeSessionsPage {
39
+ options;
40
+ presentation = 'fullscreen';
41
+ list;
42
+ loading = false;
43
+ loaded = false;
44
+ disposed = false;
45
+ error;
46
+ abort;
47
+ constructor(options) {
48
+ this.options = options;
49
+ this.list = new FilterList({
50
+ theme: options.theme,
51
+ search: 'always',
52
+ rowGap: 1,
53
+ noun: ['session', 'sessions'],
54
+ empty: 'No saved sessions in this project',
55
+ onCancel: options.cancel,
56
+ onActivate: row => { if (row.data)
57
+ options.select(row.data); }
58
+ });
59
+ }
60
+ get focused() { return this.list.focused; }
61
+ set focused(value) { this.list.focused = value; }
62
+ tabs() { return [{ id: 'sessions', title: 'Resume session' }]; }
63
+ activeTab() { return 'sessions'; }
64
+ hint() { return '↑/↓ select · type to search · Enter resume'; }
65
+ activate() {
66
+ if (this.loaded || this.loading || this.disposed)
67
+ return;
68
+ this.abort = new AbortController();
69
+ void this.load(this.abort.signal);
70
+ }
71
+ dispose() {
72
+ this.disposed = true;
73
+ this.abort?.abort();
74
+ }
75
+ async load(signal) {
76
+ this.loading = true;
77
+ this.options.changed();
78
+ try {
79
+ const sessions = await this.options.listSessions(signal);
80
+ const branch = singleLine(this.options.branch() ?? '') || '—';
81
+ const rows = await Promise.all(sessions.map(async (session) => {
82
+ let bytes;
83
+ try {
84
+ bytes = (await stat(session.path)).size;
85
+ }
86
+ catch { /* A removed or unreadable file remains selectable and shows an unknown size. */ }
87
+ const title = session.name?.trim() || session.firstMessage.trim() || session.id;
88
+ const project = displayProjectName(session.cwd || this.options.cwd());
89
+ const modified = formatSessionRelativeTime(session.modified);
90
+ const subtitle = `${modified} · ${project}:${branch} · ${formatSessionSize(bytes)}`;
91
+ return {
92
+ id: session.path,
93
+ label: singleLine(title),
94
+ subtitle,
95
+ searchText: `${title} ${session.firstMessage} ${session.id} ${subtitle}`,
96
+ data: session.path
97
+ };
98
+ }));
99
+ if (this.disposed || signal.aborted)
100
+ return;
101
+ this.list.setRows(rows);
102
+ this.error = undefined;
103
+ }
104
+ catch (error) {
105
+ if (!this.disposed && !signal.aborted)
106
+ this.error = error instanceof Error ? error.message : String(error);
107
+ }
108
+ finally {
109
+ this.loaded = true;
110
+ this.loading = false;
111
+ if (!this.disposed)
112
+ this.options.changed();
113
+ }
114
+ }
115
+ handleInput(data) {
116
+ if (!this.list.handleInput(data))
117
+ return false;
118
+ this.options.changed();
119
+ return true;
120
+ }
121
+ handleMouse(event) {
122
+ if (event.type === 'move')
123
+ return { handled: true, render: false };
124
+ if (event.type === 'wheel')
125
+ return undefined;
126
+ const result = this.list.handleMouse(event);
127
+ if (result?.render)
128
+ this.options.changed();
129
+ return result;
130
+ }
131
+ render(width, rows) {
132
+ if (width < 1 || rows < 1)
133
+ return [];
134
+ const theme = this.options.theme();
135
+ if (this.error)
136
+ return [theme.fg('error', `Could not read saved sessions: ${safe(this.error)}`)];
137
+ if (this.loading || !this.loaded)
138
+ return [theme.fg('dim', 'Loading saved sessions…')];
139
+ return this.list.render(width, rows);
140
+ }
141
+ }
@@ -0,0 +1,27 @@
1
+ import { type Checkpoint } from "../sessions/contracts.js";
2
+ import type { ActiveRun } from "../sessions/registry.js";
3
+ import type { RunEvent } from "../sessions/contracts.js";
4
+ import type { ToolSpec } from "./types.js";
5
+ export declare const CHECKPOINT_WRITE = "checkpoint_write";
6
+ /** What the model may send: the record without the numbers the harness fills in. */
7
+ export type CheckpointInput = Omit<Checkpoint, 'slice' | 'reason'> & {
8
+ reason?: Checkpoint['reason'];
9
+ };
10
+ export type CheckpointParse = {
11
+ ok: true;
12
+ value: CheckpointInput;
13
+ } | {
14
+ ok: false;
15
+ error: string;
16
+ };
17
+ /** The checkpoint as the model sent it, or one line saying what to fix. */
18
+ export declare function parseCheckpoint(value: unknown): CheckpointParse;
19
+ /** The model's own checkpoint, filled in with this slice and why it was written. */
20
+ export declare function checkpointRecord(input: CheckpointInput, slice: number, fallbackReason: Checkpoint['reason']): Checkpoint;
21
+ /** The latest checkpoint a run recorded, if any. */
22
+ export declare function checkpointOf(events: readonly RunEvent[]): Checkpoint | null;
23
+ /** The checkpoint as the next slice reads it. Bounded by its parts; the caller clips the whole. */
24
+ export declare function describeCheckpoint(checkpoint: Checkpoint): string;
25
+ /** The checkpoint as the model reads it back after writing it. */
26
+ export declare function ackCheckpoint(checkpoint: Checkpoint): string;
27
+ export declare function checkpointTool(run: Pick<ActiveRun, 'emit'>, slice: number | (() => number)): ToolSpec;
@@ -0,0 +1,154 @@
1
+ // The reply's handoff when it reaches a work-slice budget (PROP-0041). The model writes it once,
2
+ // the run records it as a `checkpoint` event, and the next slice is told where this one stopped.
3
+ // It only changes run state, so it stays callable in plan mode.
4
+ import { checkpointCheckSchema, checkpointDecisionSchema, checkpointFileSchema, checkpointItemSchema, checkpointReasonSchema, MAX_CHECKPOINT_CHECKS, MAX_CHECKPOINT_DECISIONS, MAX_CHECKPOINT_DONE, MAX_CHECKPOINT_FILES, MAX_CHECKPOINT_SUMMARY, runEventPayloadSchemas } from "../sessions/contracts.js";
5
+ export const CHECKPOINT_WRITE = 'checkpoint_write';
6
+ const text = (value) => (typeof value === 'string' ? value.trim() : '');
7
+ /** Trims and bounds one entry; an unusable entry is dropped rather than failing the whole record. */
8
+ function collect(value, parse, limit) {
9
+ if (!Array.isArray(value))
10
+ return [];
11
+ const out = [];
12
+ for (const entry of value.slice(0, limit)) {
13
+ const parsed = parse(entry);
14
+ if (parsed)
15
+ out.push(parsed);
16
+ }
17
+ return out;
18
+ }
19
+ /** The checkpoint as the model sent it, or one line saying what to fix. */
20
+ export function parseCheckpoint(value) {
21
+ if (!value || typeof value !== 'object')
22
+ return { ok: false, error: 'Send checkpoint_write an object with summary and next.' };
23
+ const input = value;
24
+ const summary = text(input.summary);
25
+ if (summary.length > MAX_CHECKPOINT_SUMMARY)
26
+ return { ok: false, error: `Keep summary to ${MAX_CHECKPOINT_SUMMARY} characters.` };
27
+ if (text(input.next).length > 500)
28
+ return { ok: false, error: 'Keep next to 500 characters.' };
29
+ if (text(input.blocked).length > 500)
30
+ return { ok: false, error: 'Keep blocked to 500 characters.' };
31
+ const done = collect(input.done, (entry) => {
32
+ const parsed = checkpointItemSchema.safeParse(entry);
33
+ return parsed.success ? parsed.data : null;
34
+ }, MAX_CHECKPOINT_DONE);
35
+ const files = collect(input.files, (entry) => {
36
+ const parsed = checkpointFileSchema.safeParse(entry);
37
+ return parsed.success ? parsed.data : null;
38
+ }, MAX_CHECKPOINT_FILES);
39
+ const checks = collect(input.checks, (entry) => {
40
+ const parsed = checkpointCheckSchema.safeParse(entry);
41
+ return parsed.success ? parsed.data : null;
42
+ }, MAX_CHECKPOINT_CHECKS);
43
+ const decisions = collect(input.decisions, (entry) => {
44
+ const parsed = checkpointDecisionSchema.safeParse(entry);
45
+ return parsed.success ? parsed.data : null;
46
+ }, MAX_CHECKPOINT_DECISIONS);
47
+ if (!summary && !done.length && !text(input.next) && !files.length) {
48
+ return { ok: false, error: 'A checkpoint needs a summary, a next step, or what is done; write the handoff the next slice will read.' };
49
+ }
50
+ const reason = input.reason === undefined ? undefined : checkpointReasonSchema.safeParse(input.reason);
51
+ if (reason && !reason.success)
52
+ return { ok: false, error: 'reason is budget, stuck, compaction-thrash or requested.' };
53
+ return {
54
+ ok: true,
55
+ value: {
56
+ summary,
57
+ done,
58
+ next: text(input.next),
59
+ ...(text(input.blocked) ? { blocked: text(input.blocked) } : {}),
60
+ files,
61
+ checks,
62
+ decisions,
63
+ ...(reason && reason.success ? { reason: reason.data } : {})
64
+ }
65
+ };
66
+ }
67
+ /** The model's own checkpoint, filled in with this slice and why it was written. */
68
+ export function checkpointRecord(input, slice, fallbackReason) {
69
+ return { ...input, slice, reason: input.reason ?? fallbackReason };
70
+ }
71
+ /** The latest checkpoint a run recorded, if any. */
72
+ export function checkpointOf(events) {
73
+ const last = events.filter((event) => event.type === 'checkpoint').at(-1);
74
+ if (!last)
75
+ return null;
76
+ const parsed = runEventPayloadSchemas.checkpoint.safeParse(last.payload);
77
+ return parsed.success ? parsed.data : null;
78
+ }
79
+ const line = (value) => value.replace(/\s+/gu, ' ').trim();
80
+ /** The checkpoint as the next slice reads it. Bounded by its parts; the caller clips the whole. */
81
+ export function describeCheckpoint(checkpoint) {
82
+ const parts = [];
83
+ if (checkpoint.summary)
84
+ parts.push(`Where the last slice stopped: ${line(checkpoint.summary)}`);
85
+ if (checkpoint.done.length)
86
+ parts.push(`Done:\n${checkpoint.done.map((entry) => `- ${line(entry.item)}${entry.evidence.length ? ` (${entry.evidence.map(line).join('; ')})` : ''}`).join('\n')}`);
87
+ if (checkpoint.files.length)
88
+ parts.push(`Files touched:\n${checkpoint.files.map((entry) => `- ${line(entry.path)}${entry.why ? `: ${line(entry.why)}` : ''}`).join('\n')}`);
89
+ if (checkpoint.checks.length) {
90
+ const failing = checkpoint.checks.filter((entry) => !entry.ok);
91
+ const passing = checkpoint.checks.filter((entry) => entry.ok);
92
+ if (passing.length)
93
+ parts.push(`Checks that passed: ${passing.map((entry) => line(entry.command)).join('; ')}`);
94
+ if (failing.length)
95
+ parts.push(`Checks that failed:\n${failing.map((entry) => `- ${line(entry.command)}${entry.detail ? `: ${line(entry.detail)}` : ''}`).join('\n')}`);
96
+ }
97
+ if (checkpoint.decisions.length)
98
+ parts.push(`Decisions:\n${checkpoint.decisions.map((entry) => `- ${line(entry.what)}${entry.why ? `: ${line(entry.why)}` : ''}`).join('\n')}`);
99
+ if (checkpoint.blocked)
100
+ parts.push(`Blocked: ${line(checkpoint.blocked)}`);
101
+ if (checkpoint.next)
102
+ parts.push(`Next step: ${line(checkpoint.next)}`);
103
+ return parts.join('\n\n');
104
+ }
105
+ /** The checkpoint as the model reads it back after writing it. */
106
+ export function ackCheckpoint(checkpoint) {
107
+ const counts = [
108
+ checkpoint.done.length ? `${checkpoint.done.length} done` : '',
109
+ checkpoint.files.length ? `${checkpoint.files.length} files` : '',
110
+ checkpoint.checks.length ? `${checkpoint.checks.length} checks` : ''
111
+ ].filter(Boolean).join(', ');
112
+ return `Checkpoint recorded for slice ${checkpoint.slice}${counts ? ` (${counts})` : ''}.${checkpoint.next ? ` Next: ${line(checkpoint.next)}` : ''}`;
113
+ }
114
+ export function checkpointTool(run, slice) {
115
+ return {
116
+ name: CHECKPOINT_WRITE,
117
+ label: 'Write the handoff checkpoint',
118
+ description: 'Record where this reply stopped and what the next slice must know. Call it when the harness asks for a handoff at the end of a work slice, or before you stop on purpose. ' +
119
+ 'Include what is done (with the file or check that proves it), the single next step, anything blocking, the files you touched, the checks you ran with their outcome, and decisions the next slice must not revisit. ' +
120
+ 'It records run state for the next slice; it does not change files and does not replace your reply to the user.',
121
+ parameters: (Type) => Type.Object({
122
+ summary: Type.Optional(Type.String({ description: 'Where this slice stopped, in 1 to 3 sentences (max 1200 characters)' })),
123
+ done: Type.Optional(Type.Array(Type.Object({
124
+ item: Type.String({ description: 'One finished step' }),
125
+ evidence: Type.Optional(Type.Array(Type.String({ description: 'A path, a check command, or a decision that proves it' }), { maxItems: 5 }))
126
+ }), { maxItems: MAX_CHECKPOINT_DONE })),
127
+ next: Type.Optional(Type.String({ description: 'The single next step (max 500 characters)' })),
128
+ blocked: Type.Optional(Type.String({ description: 'What stops progress, if anything (max 500 characters)' })),
129
+ files: Type.Optional(Type.Array(Type.Object({
130
+ path: Type.String(),
131
+ why: Type.Optional(Type.String({ description: 'Why this file matters to the next slice' }))
132
+ }), { maxItems: MAX_CHECKPOINT_FILES })),
133
+ checks: Type.Optional(Type.Array(Type.Object({
134
+ command: Type.String(),
135
+ ok: Type.Boolean(),
136
+ detail: Type.Optional(Type.String({ description: 'The failing line, when it failed' }))
137
+ }), { maxItems: MAX_CHECKPOINT_CHECKS })),
138
+ decisions: Type.Optional(Type.Array(Type.Object({
139
+ what: Type.String(),
140
+ why: Type.Optional(Type.String())
141
+ }), { maxItems: MAX_CHECKPOINT_DECISIONS }))
142
+ }),
143
+ readOnly: true,
144
+ planModeSafe: () => true,
145
+ execute: async (input) => {
146
+ const parsed = parseCheckpoint(input);
147
+ if (!parsed.ok)
148
+ return parsed.error;
149
+ const checkpoint = checkpointRecord(parsed.value, typeof slice === 'function' ? slice() : slice, 'requested');
150
+ run.emit('checkpoint', checkpoint);
151
+ return ackCheckpoint(checkpoint);
152
+ }
153
+ };
154
+ }
@@ -0,0 +1,31 @@
1
+ import type { CapabilitySet } from "../capabilities/types.js";
2
+ import type { ToolSpec } from "./types.js";
3
+ export declare const GET_CAPABILITIES = "get_capabilities";
4
+ export declare const CAPABILITY_DISCOVERY: {
5
+ readonly name: "get_capabilities";
6
+ readonly label: "Inspect current capabilities";
7
+ readonly description: "List the tools callable in this run, or inspect one tool and its input schema. Availability is not permission: calls still follow the gate. Disabled, unavailable and unregistered tools are not advertised as callable.";
8
+ readonly parameters: {
9
+ readonly type: "object";
10
+ readonly additionalProperties: false;
11
+ readonly properties: {
12
+ readonly tool: {
13
+ readonly type: "string";
14
+ readonly minLength: 1;
15
+ readonly maxLength: 200;
16
+ readonly description: "An exact tool name to inspect. Omit to list callable tools.";
17
+ };
18
+ };
19
+ };
20
+ };
21
+ export interface RegisteredCapabilityTool {
22
+ name: string;
23
+ description?: string;
24
+ parameters?: unknown;
25
+ }
26
+ export declare function describeCapabilities(input: {
27
+ capabilities: CapabilitySet;
28
+ active: readonly string[];
29
+ registered: readonly RegisteredCapabilityTool[];
30
+ }, name?: string): Record<string, unknown>;
31
+ export declare function getCapabilitiesTool(snapshot: () => Parameters<typeof describeCapabilities>[0]): ToolSpec;
@@ -0,0 +1,34 @@
1
+ export const GET_CAPABILITIES = 'get_capabilities';
2
+ export const CAPABILITY_DISCOVERY = {
3
+ name: GET_CAPABILITIES,
4
+ label: 'Inspect current capabilities',
5
+ description: 'List the tools callable in this run, or inspect one tool and its input schema. Availability is not permission: calls still follow the gate. Disabled, unavailable and unregistered tools are not advertised as callable.',
6
+ parameters: {
7
+ type: 'object', additionalProperties: false,
8
+ properties: { tool: { type: 'string', minLength: 1, maxLength: 200, description: 'An exact tool name to inspect. Omit to list callable tools.' } }
9
+ }
10
+ };
11
+ export function describeCapabilities(input, name) {
12
+ const allowed = new Set(input.capabilities.tools);
13
+ const active = new Set(input.active);
14
+ const tools = input.registered.filter(tool => allowed.has(tool.name) && active.has(tool.name));
15
+ if (name) {
16
+ const tool = tools.find(tool => tool.name === name);
17
+ return tool
18
+ ? { tool: { name: tool.name, description: tool.description ?? '', ...(tool.parameters !== undefined ? { inputSchema: tool.parameters } : {}), state: 'callable', permission: 'checked per call' } }
19
+ : { tool: { name, state: 'unavailable', reason: 'This tool is not callable in the current run.' } };
20
+ }
21
+ return {
22
+ tools: tools.map(tool => ({ name: tool.name, description: tool.description ?? '', state: 'callable' })),
23
+ connectors: [...input.capabilities.connectors],
24
+ permission: 'Availability does not grant permission. Every call still passes the gate.'
25
+ };
26
+ }
27
+ export function getCapabilitiesTool(snapshot) {
28
+ return {
29
+ ...CAPABILITY_DISCOVERY,
30
+ readOnly: true,
31
+ parameters: Type => Type.Unsafe(CAPABILITY_DISCOVERY.parameters),
32
+ execute: async (input) => JSON.stringify(describeCapabilities(snapshot(), typeof input.tool === 'string' ? input.tool : undefined))
33
+ };
34
+ }
@@ -1,5 +1,7 @@
1
1
  import type { ToolSpec } from "./types.js";
2
2
  export declare const SESSION_NAME = "session_name";
3
+ /** The trigger the prompt and the tool description share, so naming guidance cannot drift. */
4
+ export declare const SESSION_GUIDANCE: string;
3
5
  export interface NamedSession {
4
6
  readonly sessionName?: string;
5
7
  setSessionName(name: string): void;
@@ -2,13 +2,15 @@
2
2
  // the current native session; it never receives a target session id from the model.
3
3
  import { invalid } from "../core/errors.js";
4
4
  export const SESSION_NAME = 'session_name';
5
+ /** The trigger the prompt and the tool description share, so naming guidance cannot drift. */
6
+ export const SESSION_GUIDANCE = 'If the request needs tools or changes state — not a greeting, thanks, or a short conversational reply — call session_name once before your first other tool call, naming the work from the request. ' +
7
+ 'Use one short imperative sentence of at most six words. Revise the name only when the overall objective changes; keep the current name when it does not.';
5
8
  export function sessionNameTool(current) {
6
9
  return {
7
10
  name: SESSION_NAME,
8
11
  label: 'Name the session',
9
- description: 'Set or revise this session’s name. Name meaningful work early, then revise the name when the overall objective changes; greetings alone need no name. ' +
10
- 'Use one short imperative sentence describing the objective, at most six words, such as "Add web browser integrations". ' +
11
- 'You may revise a manually assigned name. Keep a useful current name when the objective is unchanged. This only names your own session.',
12
+ description: `Set or revise this session’s name. ${SESSION_GUIDANCE} ` +
13
+ 'For example, "Add web browser integrations". You may revise a manually assigned name. This only names your own session.',
12
14
  parameters: Type => Type.Object({ name: Type.String({ description: 'One short sentence describing the objective, at most six words' }) }),
13
15
  planModeSafe: () => true,
14
16
  execute: async (input) => {
@@ -27,19 +27,22 @@ export function subagentTools(service, parentRunId) {
27
27
  return [{
28
28
  name: SPAWN_SUBAGENT,
29
29
  label: 'Start a subagent',
30
- description: 'Start a fresh child run for independent work: a copy of yourself, or an agent profile from your Subagents list when you name it in agent (for example worker to make changes, explorer to find and explain). Give a short title, which the user sees in the roster, and a complete brief: the child has no conversation history and no more capabilities than you. Returns its run ID immediately by default so you can continue working. Results arrive automatically before you finish. Use wait_subagents to join selected children, or wait: true for an inline result. Stopping you stops your children.',
30
+ description: 'Start a fresh child run for independent work. Delegate substantial, bounded tasks that benefit from a separate context, not simple lookups, tiny edits or tightly coupled steps. Prefer a named specialist profile; omit agent only when no profile fits. Plan mode permits only eligible read-only research profiles. Give a short title and a self-contained brief: the child has no conversation history and no more capabilities than you. The optional tools field only narrows the profile and parent intersection; in plan mode, omit it unless you need a stricter subset. Returns its run ID immediately by default so you can continue other useful work. Results arrive automatically before you finish. Use wait_subagents when you need results before proceeding, or wait: true for an inline result. Avoid overlapping write assignments. Stopping you stops your children. To resume a child that stopped before finishing (interrupted or failed), pass continue with its run ID and no title, agent or tools: it reopens that child\'s own conversation with the same profile and tools, and brief becomes an optional extra instruction.',
31
+ planModeSafe: input => service.planSpawnRefusal(parentRunId, input) === null,
31
32
  parameters: (Type) => Type.Object({
32
- title: Type.String({ description: `A short imperative title for the roster, at most ${MAX_SUBAGENT_TITLE_WORDS} words, such as "Map the auth flow"` }),
33
- brief: Type.String(),
34
- agent: Type.Optional(Type.String({ description: 'The name of an agent profile from your Subagents list; omit for a copy of yourself' })),
35
- tools: Type.Optional(Type.Array(Type.String())),
33
+ title: Type.Optional(Type.String({ description: `Required for a new child: a short imperative title for the roster, at most ${MAX_SUBAGENT_TITLE_WORDS} words, such as "Map the auth flow"` })),
34
+ brief: Type.Optional(Type.String({ description: 'Required for a new child: a self-contained task: goal, relevant context or paths, non-goals, expected result, and acceptance or verification. The child cannot see this conversation. With continue, an optional extra instruction.' })),
35
+ continue: Type.Optional(Type.String({ description: 'The run ID of one of your subagents that stopped before finishing, to continue it from where it stopped instead of starting a fresh child' })),
36
+ agent: Type.Optional(Type.String({ description: 'The name of a specialist profile from your Subagents list; omit only when no listed profile fits (omitting starts an unprofiled copy)' })),
37
+ tools: Type.Optional(Type.Array(Type.String({ description: 'Only request a stricter subset of the profile and parent tools. In plan mode, omit this field to inherit the safe available research intersection.' }))),
36
38
  wait: Type.Optional(Type.Boolean({ description: 'Wait for the child and return its result; default false' }))
37
39
  }),
38
40
  execute: execute(async (input) => {
39
- const title = subagentTitle(input.title);
40
- if (!title)
41
+ const resume = input.continue === undefined ? undefined : input.continue;
42
+ const title = resume === undefined ? subagentTitle(input.title) : undefined;
43
+ if (resume === undefined && !title)
41
44
  throw invalid(`Give the subagent a title of at most ${MAX_SUBAGENT_TITLE_WORDS} words, with no control characters.`);
42
- const child = await service.spawn({ parentRunId, brief: input.brief, title, agent: input.agent, tools: input.tools, wait: input.wait });
45
+ const child = await service.spawn({ parentRunId, brief: input.brief, title, agent: input.agent, tools: input.tools, wait: input.wait, ...(resume === undefined ? {} : { continue: resume }) });
43
46
  if (input.wait === true)
44
47
  return reported(parentRunId, service.results({ parentRunId, runIds: [child.runId] }), [child.runId]);
45
48
  return { text: JSON.stringify(child), details: { [SUBAGENT_RECEIPT]: { version: 1, ownerRunId: parentRunId, spawned: [child.runId] } } };
@@ -0,0 +1,15 @@
1
+ import { type TodoItem, type RunEvent } from "../sessions/contracts.js";
2
+ import type { ActiveRun } from "../sessions/registry.js";
3
+ import type { ToolSpec } from "./types.js";
4
+ export declare const TODO_WRITE = "todo_write";
5
+ /** The trigger the prompt and the tool description share, so progress guidance cannot drift. */
6
+ export declare const TODO_GUIDANCE: string;
7
+ /** The todo list as the model reads it back. */
8
+ export declare function describeTodo(items: readonly TodoItem[]): string;
9
+ /** The latest todo list a run wrote, or an empty list. Reads the pre-rename event too. */
10
+ export declare function todoOf(events: readonly RunEvent[]): TodoItem[];
11
+ export declare function todoTool(run: Pick<ActiveRun, 'emit'>): ToolSpec;
12
+ /** @deprecated Use `TODO_WRITE`; kept for one release so embedders can migrate. */
13
+ export declare const CHECKLIST_WRITE = "todo_write";
14
+ /** @deprecated Use `todoOf`; kept for one release so embedders can migrate. */
15
+ export declare const checklistOf: typeof todoOf;
@@ -0,0 +1,64 @@
1
+ // The run's own todo list: the model writes the whole list each time, and the run records it as
2
+ // a `todo.updated` event. It only changes run state, so it stays callable in plan mode.
3
+ import { todoItemSchema, MAX_TODO_ITEMS } from "../sessions/contracts.js";
4
+ export const TODO_WRITE = 'todo_write';
5
+ /** The trigger the prompt and the tool description share, so progress guidance cannot drift. */
6
+ export const TODO_GUIDANCE = 'If the work has several distinct steps or will take more than one tool call, call todo_write before your first other tool call with the steps you know so far, and keep the list current as steps start and finish. ' +
7
+ 'Before your final reply, write one final update: mark every fully completed step completed and leave unfinished or blocked steps accurate. Never mark unfinished work completed. ' +
8
+ 'The person follows your work from it, and a subagent’s todo list shows as its progress in the person’s list of subagents.';
9
+ /** The event a run writes; `checklist.updated` is the pre-rename name, still read for old runs. */
10
+ const TODO_EVENTS = new Set(['todo.updated', 'checklist.updated']);
11
+ const MARK = { pending: '[ ]', in_progress: '[~]', completed: '[x]' };
12
+ /** The todo list as the model reads it back. */
13
+ export function describeTodo(items) {
14
+ if (!items.length)
15
+ return 'The todo list is empty.';
16
+ return items.map((item) => `${MARK[item.status]} ${item.text}`).join('\n');
17
+ }
18
+ /** The latest todo list a run wrote, or an empty list. Reads the pre-rename event too. */
19
+ export function todoOf(events) {
20
+ const last = events.filter((event) => TODO_EVENTS.has(event.type)).at(-1);
21
+ const items = last?.payload?.items;
22
+ return Array.isArray(items) ? items.flatMap((item) => {
23
+ const parsed = todoItemSchema.safeParse(item);
24
+ return parsed.success ? [parsed.data] : [];
25
+ }) : [];
26
+ }
27
+ export function todoTool(run) {
28
+ return {
29
+ name: TODO_WRITE,
30
+ label: 'Write the todo list',
31
+ description: `Keep a todo list of the steps of the current work. The user watches this list to follow your progress. ${TODO_GUIDANCE} ` +
32
+ 'Send the whole list every time; it replaces the previous one. Do not use it for a single, simple step or for conversation. ' +
33
+ 'Statuses: pending, in_progress, completed. Keep exactly one step in_progress while you work, mark a step completed as soon as it is done (not in batches), and add steps you discover. ' +
34
+ 'Only mark a step completed when it is fully done; if something blocks it, keep it in_progress and add a step for the blocker.',
35
+ parameters: (Type) => Type.Object({
36
+ items: Type.Array(Type.Object({
37
+ text: Type.String({ description: 'One step, as a short imperative line' }),
38
+ status: Type.Union([Type.Literal('pending'), Type.Literal('in_progress'), Type.Literal('completed')])
39
+ }), { maxItems: MAX_TODO_ITEMS })
40
+ }),
41
+ readOnly: true,
42
+ planModeSafe: () => true,
43
+ execute: async (input) => {
44
+ if (!Array.isArray(input.items))
45
+ return 'Send items: the whole todo list as an array.';
46
+ if (input.items.length > MAX_TODO_ITEMS)
47
+ return `Keep the todo list to ${MAX_TODO_ITEMS} steps or fewer.`;
48
+ const items = [];
49
+ for (const raw of input.items) {
50
+ const entry = raw;
51
+ const parsed = todoItemSchema.safeParse({ text: typeof entry?.text === 'string' ? entry.text.trim() : entry?.text, status: entry?.status });
52
+ if (!parsed.success)
53
+ return 'Every step needs text of 1 to 500 characters and a status of pending, in_progress or completed.';
54
+ items.push(parsed.data);
55
+ }
56
+ run.emit('todo.updated', { items });
57
+ return describeTodo(items);
58
+ }
59
+ };
60
+ }
61
+ /** @deprecated Use `TODO_WRITE`; kept for one release so embedders can migrate. */
62
+ export const CHECKLIST_WRITE = TODO_WRITE;
63
+ /** @deprecated Use `todoOf`; kept for one release so embedders can migrate. */
64
+ export const checklistOf = todoOf;