open-multi-agent-kit 0.91.0 → 0.93.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 (168) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +2 -2
  3. package/dist/core/agent-session.d.ts +29 -0
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +89 -21
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/command-safety.d.ts.map +1 -1
  8. package/dist/core/command-safety.js +0 -50
  9. package/dist/core/command-safety.js.map +1 -1
  10. package/dist/core/compaction/branch-summarization.d.ts +5 -1
  11. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  12. package/dist/core/compaction/branch-summarization.js +5 -7
  13. package/dist/core/compaction/branch-summarization.js.map +1 -1
  14. package/dist/core/compaction/compaction.d.ts +12 -3
  15. package/dist/core/compaction/compaction.d.ts.map +1 -1
  16. package/dist/core/compaction/compaction.js +28 -15
  17. package/dist/core/compaction/compaction.js.map +1 -1
  18. package/dist/core/compaction/transaction.d.ts.map +1 -1
  19. package/dist/core/compaction/transaction.js +7 -3
  20. package/dist/core/compaction/transaction.js.map +1 -1
  21. package/dist/core/context-budget-headroom-candidates.d.ts.map +1 -1
  22. package/dist/core/context-budget-headroom-candidates.js +21 -11
  23. package/dist/core/context-budget-headroom-candidates.js.map +1 -1
  24. package/dist/core/context-budget-system-prompt-items.d.ts +1 -0
  25. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  26. package/dist/core/context-budget-system-prompt-items.js +7 -8
  27. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  28. package/dist/core/extensions/builtin/command-safety-gate.d.ts +8 -0
  29. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  30. package/dist/core/extensions/builtin/command-safety-gate.js +23 -0
  31. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  32. package/dist/core/extensions/runner.d.ts.map +1 -1
  33. package/dist/core/extensions/runner.js +4 -0
  34. package/dist/core/extensions/runner.js.map +1 -1
  35. package/dist/core/extensions/types.d.ts +2 -0
  36. package/dist/core/extensions/types.d.ts.map +1 -1
  37. package/dist/core/extensions/types.js.map +1 -1
  38. package/dist/core/keybindings.d.ts +5 -0
  39. package/dist/core/keybindings.d.ts.map +1 -1
  40. package/dist/core/keybindings.js +5 -0
  41. package/dist/core/keybindings.js.map +1 -1
  42. package/dist/core/loadouts.d.ts.map +1 -1
  43. package/dist/core/loadouts.js +27 -48
  44. package/dist/core/loadouts.js.map +1 -1
  45. package/dist/core/model-resolver.d.ts +41 -3
  46. package/dist/core/model-resolver.d.ts.map +1 -1
  47. package/dist/core/model-resolver.js +17 -9
  48. package/dist/core/model-resolver.js.map +1 -1
  49. package/dist/core/provider-resilience.d.ts.map +1 -1
  50. package/dist/core/provider-resilience.js +3 -1
  51. package/dist/core/provider-resilience.js.map +1 -1
  52. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  53. package/dist/core/reasoning-router-v4-weights.js +9 -3
  54. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  55. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  56. package/dist/core/reasoning-router-v4.js +94 -24
  57. package/dist/core/reasoning-router-v4.js.map +1 -1
  58. package/dist/core/resource-loader.d.ts.map +1 -1
  59. package/dist/core/resource-loader.js +11 -5
  60. package/dist/core/resource-loader.js.map +1 -1
  61. package/dist/core/session-file-compaction.d.ts.map +1 -1
  62. package/dist/core/session-file-compaction.js +39 -5
  63. package/dist/core/session-file-compaction.js.map +1 -1
  64. package/dist/core/session-termination.d.ts.map +1 -1
  65. package/dist/core/session-termination.js +1 -1
  66. package/dist/core/session-termination.js.map +1 -1
  67. package/dist/core/settings-manager.d.ts +3 -0
  68. package/dist/core/settings-manager.d.ts.map +1 -1
  69. package/dist/core/settings-manager.js +8 -0
  70. package/dist/core/settings-manager.js.map +1 -1
  71. package/dist/core/system-prompt.d.ts.map +1 -1
  72. package/dist/core/system-prompt.js +36 -31
  73. package/dist/core/system-prompt.js.map +1 -1
  74. package/dist/core/tools/bash.d.ts +2 -0
  75. package/dist/core/tools/bash.d.ts.map +1 -1
  76. package/dist/core/tools/bash.js +38 -11
  77. package/dist/core/tools/bash.js.map +1 -1
  78. package/dist/main.d.ts.map +1 -1
  79. package/dist/main.js +112 -11
  80. package/dist/main.js.map +1 -1
  81. package/dist/modes/interactive/components/control-panel-box.d.ts +27 -0
  82. package/dist/modes/interactive/components/control-panel-box.d.ts.map +1 -1
  83. package/dist/modes/interactive/components/control-panel-box.js +60 -8
  84. package/dist/modes/interactive/components/control-panel-box.js.map +1 -1
  85. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  86. package/dist/modes/interactive/components/control-panel-layout.js +100 -45
  87. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  88. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +9 -0
  89. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/control-panel-runtime-status.js +10 -8
  91. package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
  92. package/dist/modes/interactive/components/control-panel-sparkles.d.ts +5 -0
  93. package/dist/modes/interactive/components/control-panel-sparkles.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/control-panel-sparkles.js +14 -9
  95. package/dist/modes/interactive/components/control-panel-sparkles.js.map +1 -1
  96. package/dist/modes/interactive/components/footer.d.ts +10 -0
  97. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  98. package/dist/modes/interactive/components/footer.js +3 -3
  99. package/dist/modes/interactive/components/footer.js.map +1 -1
  100. package/dist/modes/interactive/components/status-sidebar.d.ts +49 -0
  101. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -0
  102. package/dist/modes/interactive/components/status-sidebar.js +253 -0
  103. package/dist/modes/interactive/components/status-sidebar.js.map +1 -0
  104. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  105. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/dist/modes/interactive/interactive-mode.js +94 -20
  107. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/dist/modes/interactive/theme/omk-neon-control.json +13 -13
  109. package/dist/modes/print-mode.d.ts.map +1 -1
  110. package/dist/modes/print-mode.js +9 -0
  111. package/dist/modes/print-mode.js.map +1 -1
  112. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  113. package/dist/modes/rpc/rpc-mode.js +1 -0
  114. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  115. package/dist/utils/clipboard.d.ts.map +1 -1
  116. package/dist/utils/clipboard.js +19 -8
  117. package/dist/utils/clipboard.js.map +1 -1
  118. package/docs/adaptorch-preview-spec.md +174 -0
  119. package/docs/adaptorch-preview.md +1 -1
  120. package/docs/compaction.md +20 -13
  121. package/docs/correctness-wall.md +13 -5
  122. package/docs/custom-provider.md +8 -8
  123. package/docs/development.md +2 -2
  124. package/docs/docs.json +4 -0
  125. package/docs/environment-variables.md +90 -0
  126. package/docs/extensions.md +8 -8
  127. package/docs/index.md +1 -0
  128. package/docs/json.md +4 -4
  129. package/docs/providers.md +1 -1
  130. package/docs/rpc.md +5 -2
  131. package/docs/session-format.md +5 -5
  132. package/docs/settings.md +1 -0
  133. package/docs/skills.md +2 -0
  134. package/docs/themes.md +2 -2
  135. package/docs/tui.md +4 -4
  136. package/docs/usage.md +4 -0
  137. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  138. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  139. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  140. package/examples/extensions/gondolin/package-lock.json +2 -2
  141. package/examples/extensions/gondolin/package.json +1 -1
  142. package/examples/extensions/sandbox/package-lock.json +2 -2
  143. package/examples/extensions/sandbox/package.json +1 -1
  144. package/examples/extensions/subagent/README.md +35 -10
  145. package/examples/extensions/subagent/adaptive-agent-runtime.test.ts +234 -0
  146. package/examples/extensions/subagent/adaptive-agent-runtime.ts +240 -0
  147. package/examples/extensions/subagent/adaptive-result.ts +112 -0
  148. package/examples/extensions/subagent/agents.test.ts +86 -0
  149. package/examples/extensions/subagent/agents.ts +18 -4
  150. package/examples/extensions/subagent/capabilities.ts +15 -1
  151. package/examples/extensions/subagent/checkpoint-runtime.test.ts +125 -0
  152. package/examples/extensions/subagent/checkpoint-runtime.ts +198 -0
  153. package/examples/extensions/subagent/deadline-budget.test.ts +135 -0
  154. package/examples/extensions/subagent/deadline-budget.ts +242 -0
  155. package/examples/extensions/subagent/deadline-profile-store.test.ts +72 -0
  156. package/examples/extensions/subagent/deadline-profile-store.ts +172 -0
  157. package/examples/extensions/subagent/domain-profiles.ts +272 -0
  158. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +13 -0
  159. package/examples/extensions/subagent/fixtures/hanging-process-tree.mjs +40 -0
  160. package/examples/extensions/subagent/index.ts +253 -137
  161. package/examples/extensions/subagent/managed-process.test.ts +178 -0
  162. package/examples/extensions/subagent/managed-process.ts +236 -0
  163. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +112 -0
  164. package/examples/extensions/subagent/subagent-runtime-types.ts +76 -0
  165. package/examples/extensions/with-deps/package-lock.json +2 -2
  166. package/examples/extensions/with-deps/package.json +1 -1
  167. package/npm-shrinkwrap.json +21 -21
  168. package/package.json +10 -7
@@ -12,7 +12,6 @@
12
12
  * Uses JSON mode to capture structured output from subagents.
13
13
  */
14
14
 
15
- import { spawn } from "node:child_process";
16
15
  import * as fs from "node:fs";
17
16
  import * as os from "node:os";
18
17
  import * as path from "node:path";
@@ -22,6 +21,7 @@ import { StringEnum } from "omk-ai";
22
21
  import { Container, Markdown, Spacer, Text } from "omk-tui";
23
22
  import { type ExtensionAPI, getAgentDir, getMarkdownTheme, withFileMutationQueue } from "open-multi-agent-kit";
24
23
  import { Type } from "typebox";
24
+ import { type RunAttemptInput, runAdaptiveAgent } from "./adaptive-agent-runtime.ts";
25
25
  import { deriveCapabilities } from "./agent-capability-router.ts";
26
26
  import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.ts";
27
27
  import {
@@ -33,15 +33,27 @@ import {
33
33
  parseEmbeddedCapabilities,
34
34
  validateCapabilities,
35
35
  } from "./capabilities.ts";
36
+ import { createExecutionBudget, type ExecutionBudget, remainingExecutionMs } from "./deadline-budget.ts";
37
+ import { DeadlineProfileStore } from "./deadline-profile-store.ts";
38
+ import { runManagedProcess } from "./managed-process.ts";
39
+ import { emptyUsage, type SingleResult, type SubagentAttemptResult } from "./subagent-runtime-types.ts";
36
40
 
37
41
  const MAX_PARALLEL_TASKS = 8;
38
42
  const MAX_CONCURRENCY = 4;
39
43
  const COLLAPSED_ITEM_COUNT = 10;
40
44
  const PER_TASK_OUTPUT_CAP = 50 * 1024;
45
+ const DEFAULT_EXECUTION_BUDGET_MS = 840_000;
46
+ const MAX_EXECUTION_BUDGET_MS = 840_000;
47
+ const EXECUTION_CLEANUP_RESERVE_MS = 15_000;
48
+ const DEFAULT_ATTEMPT_CUTOFF_MS = 600_000;
49
+ const MINIMUM_ATTEMPT_MS = 30_000;
50
+ const MAX_TASK_SHARDS = 3;
51
+ const MAX_TOKENS_PER_TASK_SHARD = 6_000;
41
52
 
42
53
  // Capability catalog is expensive to build (scans every SKILL.md on disk); memoize
43
54
  // once per dispatcher process.
44
55
  let cachedCapabilityCatalog: CapabilityCatalog | null = null;
56
+ let cachedDeadlineProfileStore: DeadlineProfileStore | null = null;
45
57
 
46
58
  function getCapabilityCatalog(): CapabilityCatalog {
47
59
  if (!cachedCapabilityCatalog) {
@@ -50,6 +62,15 @@ function getCapabilityCatalog(): CapabilityCatalog {
50
62
  return cachedCapabilityCatalog;
51
63
  }
52
64
 
65
+ function getDeadlineProfileStore(): DeadlineProfileStore {
66
+ if (!cachedDeadlineProfileStore) {
67
+ cachedDeadlineProfileStore = new DeadlineProfileStore(
68
+ path.join(getAgentDir(), "state", "subagent-deadline-profiles.json"),
69
+ );
70
+ }
71
+ return cachedDeadlineProfileStore;
72
+ }
73
+
53
74
  function formatTokens(count: number): string {
54
75
  if (count < 1000) return count.toString();
55
76
  if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
@@ -151,28 +172,14 @@ function formatToolCall(
151
172
  }
152
173
  }
153
174
 
154
- interface UsageStats {
155
- input: number;
156
- output: number;
157
- cacheRead: number;
158
- cacheWrite: number;
159
- cost: number;
160
- contextTokens: number;
161
- turns: number;
162
- }
163
-
164
- interface SingleResult {
165
- agent: string;
166
- agentSource: "user" | "project" | "unknown";
167
- task: string;
168
- exitCode: number;
169
- messages: Message[];
170
- stderr: string;
171
- usage: UsageStats;
172
- model?: string;
173
- stopReason?: string;
174
- errorMessage?: string;
175
- step?: number;
175
+ interface ExecutionBudgetDetails {
176
+ hardDeadlineMs: number;
177
+ elapsedMs: number;
178
+ remainingMs: number;
179
+ estimatedTokens: number;
180
+ plannedShards: number;
181
+ completedShards: number;
182
+ resumeCount: number;
176
183
  }
177
184
 
178
185
  interface SubagentDetails {
@@ -180,6 +187,19 @@ interface SubagentDetails {
180
187
  agentScope: AgentScope;
181
188
  projectAgentsDir: string | null;
182
189
  results: SingleResult[];
190
+ executionBudget: ExecutionBudgetDetails;
191
+ }
192
+
193
+ function summarizeExecutionBudget(budget: ExecutionBudget, results: readonly SingleResult[]): ExecutionBudgetDetails {
194
+ return {
195
+ hardDeadlineMs: budget.hardDeadlineMs,
196
+ elapsedMs: Date.now() - budget.startedAtMs,
197
+ remainingMs: remainingExecutionMs(budget),
198
+ estimatedTokens: results.reduce((sum, result) => sum + (result.deadline?.estimatedTokens ?? 0), 0),
199
+ plannedShards: results.reduce((sum, result) => sum + (result.deadline?.plannedShardIds.length ?? 0), 0),
200
+ completedShards: results.reduce((sum, result) => sum + (result.deadline?.completedShardIds.length ?? 0), 0),
201
+ resumeCount: results.reduce((sum, result) => sum + (result.deadline?.resumeCount ?? 0), 0),
202
+ };
183
203
  }
184
204
 
185
205
  function getFinalOutput(messages: Message[]): string {
@@ -199,10 +219,15 @@ function isFailedResult(result: SingleResult): boolean {
199
219
  }
200
220
 
201
221
  function getResultOutput(result: SingleResult): string {
222
+ const partialOutput = result.output || getFinalOutput(result.messages);
202
223
  if (isFailedResult(result)) {
203
- return result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
224
+ const diagnostic = result.errorMessage || result.stderr;
225
+ if (diagnostic && partialOutput && !diagnostic.includes(partialOutput)) {
226
+ return `${diagnostic}\n\nCheckpointed partial output:\n${partialOutput}`;
227
+ }
228
+ return diagnostic || partialOutput || "(no output)";
204
229
  }
205
- return getFinalOutput(result.messages) || "(no output)";
230
+ return partialOutput || "(no output)";
206
231
  }
207
232
 
208
233
  function truncateParallelOutput(output: string): string {
@@ -278,33 +303,22 @@ function getOmkInvocation(args: string[]): { command: string; args: string[] } {
278
303
  }
279
304
 
280
305
  type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
306
+ type ResultUpdateCallback = (partial: SingleResult) => void;
281
307
 
282
- async function runSingleAgent(
308
+ async function runSingleAgentAttempt(
283
309
  defaultCwd: string,
284
310
  agents: AgentConfig[],
285
311
  agentName: string,
312
+ logicalTask: string,
286
313
  task: string,
287
314
  cwd: string | undefined,
288
315
  step: number | undefined,
316
+ cutoffMs: number,
289
317
  signal: AbortSignal | undefined,
290
- onUpdate: OnUpdateCallback | undefined,
291
- makeDetails: (results: SingleResult[]) => SubagentDetails,
292
- ): Promise<SingleResult> {
293
- const agent = agents.find((a) => a.name === agentName);
294
-
295
- if (!agent) {
296
- const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
297
- return {
298
- agent: agentName,
299
- agentSource: "unknown",
300
- task,
301
- exitCode: 1,
302
- messages: [],
303
- stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
304
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
305
- step,
306
- };
307
- }
318
+ onUpdate: ResultUpdateCallback | undefined,
319
+ ): Promise<SubagentAttemptResult> {
320
+ const agent = agents.find((candidate) => candidate.name === agentName);
321
+ if (!agent) throw new Error(`Unknown agent: ${agentName}`);
308
322
 
309
323
  const args: string[] = ["--mode", "json", "-p", "--no-session"];
310
324
  if (agent.model) args.push("--model", agent.model);
@@ -353,22 +367,17 @@ async function runSingleAgent(
353
367
  const currentResult: SingleResult = {
354
368
  agent: agentName,
355
369
  agentSource: agent.source,
356
- task,
370
+ task: logicalTask,
357
371
  exitCode: 0,
358
372
  messages: [],
359
373
  stderr: "",
360
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
374
+ usage: emptyUsage(),
361
375
  model: agent.model,
362
376
  step,
363
377
  };
364
378
 
365
379
  const emitUpdate = () => {
366
- if (onUpdate) {
367
- onUpdate({
368
- content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
369
- details: makeDetails([currentResult]),
370
- });
371
- }
380
+ onUpdate?.({ ...currentResult, messages: [...currentResult.messages], usage: { ...currentResult.usage } });
372
381
  };
373
382
 
374
383
  try {
@@ -390,90 +399,72 @@ async function runSingleAgent(
390
399
  }
391
400
 
392
401
  args.push(`Task: ${task}`);
393
- let wasAborted = false;
394
-
395
- const exitCode = await new Promise<number>((resolve) => {
396
- const invocation = getOmkInvocation(args);
397
- const proc = spawn(invocation.command, invocation.args, {
398
- cwd: cwd ?? defaultCwd,
399
- shell: false,
400
- stdio: ["ignore", "pipe", "pipe"],
401
- });
402
- let buffer = "";
403
-
404
- const processLine = (line: string) => {
405
- if (!line.trim()) return;
406
- let event: any;
407
- try {
408
- event = JSON.parse(line);
409
- } catch {
410
- return;
411
- }
412
-
413
- if (event.type === "message_end" && event.message) {
414
- const msg = event.message as Message;
415
- currentResult.messages.push(msg);
416
-
417
- if (msg.role === "assistant") {
418
- currentResult.usage.turns++;
419
- const usage = msg.usage;
420
- if (usage) {
421
- currentResult.usage.input += usage.input || 0;
422
- currentResult.usage.output += usage.output || 0;
423
- currentResult.usage.cacheRead += usage.cacheRead || 0;
424
- currentResult.usage.cacheWrite += usage.cacheWrite || 0;
425
- currentResult.usage.cost += usage.cost?.total || 0;
426
- currentResult.usage.contextTokens = usage.totalTokens || 0;
427
- }
428
- if (!currentResult.model && msg.model) currentResult.model = msg.model;
429
- if (msg.stopReason) currentResult.stopReason = msg.stopReason;
430
- if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
402
+ let buffer = "";
403
+ const processLine = (line: string) => {
404
+ const event = parseSubagentEvent(line);
405
+ if (event === undefined) return;
406
+ if (event.type === "message_end" && event.message !== undefined) {
407
+ const msg = event.message;
408
+ currentResult.messages.push(msg);
409
+ if (msg.role === "assistant") {
410
+ currentResult.usage.turns += 1;
411
+ const usage = msg.usage;
412
+ if (usage) {
413
+ currentResult.usage.input += usage.input || 0;
414
+ currentResult.usage.output += usage.output || 0;
415
+ currentResult.usage.cacheRead += usage.cacheRead || 0;
416
+ currentResult.usage.cacheWrite += usage.cacheWrite || 0;
417
+ currentResult.usage.cost += usage.cost?.total || 0;
418
+ currentResult.usage.contextTokens = Math.max(
419
+ currentResult.usage.contextTokens,
420
+ usage.totalTokens || 0,
421
+ );
431
422
  }
432
- emitUpdate();
423
+ if (!currentResult.model && msg.model) currentResult.model = msg.model;
424
+ if (msg.stopReason) currentResult.stopReason = msg.stopReason;
425
+ if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
433
426
  }
434
-
435
- if (event.type === "tool_result_end" && event.message) {
436
- currentResult.messages.push(event.message as Message);
437
- emitUpdate();
438
- }
439
- };
440
-
441
- proc.stdout.on("data", (data) => {
442
- buffer += data.toString();
427
+ emitUpdate();
428
+ }
429
+ if (event.type === "tool_result_end" && event.message !== undefined) {
430
+ currentResult.messages.push(event.message);
431
+ emitUpdate();
432
+ }
433
+ };
434
+ const invocation = getOmkInvocation(args);
435
+ const processResult = await runManagedProcess({
436
+ command: invocation.command,
437
+ args: invocation.args,
438
+ cwd: cwd ?? defaultCwd,
439
+ cutoffMs,
440
+ signal,
441
+ onStdout: (chunk) => {
442
+ buffer += chunk;
443
443
  const lines = buffer.split("\n");
444
444
  buffer = lines.pop() || "";
445
445
  for (const line of lines) processLine(line);
446
- });
447
-
448
- proc.stderr.on("data", (data) => {
449
- currentResult.stderr += data.toString();
450
- });
451
-
452
- proc.on("close", (code) => {
453
- if (buffer.trim()) processLine(buffer);
454
- resolve(code ?? 0);
455
- });
456
-
457
- proc.on("error", () => {
458
- resolve(1);
459
- });
460
-
461
- if (signal) {
462
- const killProc = () => {
463
- wasAborted = true;
464
- proc.kill("SIGTERM");
465
- setTimeout(() => {
466
- if (!proc.killed) proc.kill("SIGKILL");
467
- }, 5000);
468
- };
469
- if (signal.aborted) killProc();
470
- else signal.addEventListener("abort", killProc, { once: true });
471
- }
446
+ },
447
+ onStderr: (chunk) => {
448
+ currentResult.stderr += chunk;
449
+ },
472
450
  });
473
-
474
- currentResult.exitCode = exitCode;
475
- if (wasAborted) throw new Error("Subagent was aborted");
476
- return currentResult;
451
+ if (buffer.trim()) processLine(buffer);
452
+ currentResult.exitCode = processResult.exitCode;
453
+ currentResult.output = getFinalOutput(currentResult.messages);
454
+ if (processResult.reason === "cutoff") {
455
+ currentResult.exitCode = 124;
456
+ currentResult.stopReason = "deadline";
457
+ currentResult.errorMessage = `Subagent attempt exceeded ${cutoffMs}ms cutoff`;
458
+ } else if (processResult.reason === "aborted") {
459
+ currentResult.exitCode = 130;
460
+ currentResult.stopReason = "aborted";
461
+ currentResult.errorMessage = "Subagent was aborted";
462
+ } else if (processResult.reason === "spawn-error") {
463
+ currentResult.exitCode = 1;
464
+ currentResult.stopReason = "error";
465
+ currentResult.errorMessage = processResult.errorMessage ?? "Unable to spawn subagent";
466
+ }
467
+ return { result: currentResult, process: processResult };
477
468
  } finally {
478
469
  if (tmpPromptPath)
479
470
  try {
@@ -490,6 +481,96 @@ async function runSingleAgent(
490
481
  }
491
482
  }
492
483
 
484
+ interface SubagentJsonEvent {
485
+ readonly type: string;
486
+ readonly message?: Message;
487
+ }
488
+
489
+ function parseSubagentEvent(line: string): SubagentJsonEvent | undefined {
490
+ if (line.trim() === "") return undefined;
491
+ let value: unknown;
492
+ try {
493
+ value = JSON.parse(line);
494
+ } catch {
495
+ return undefined;
496
+ }
497
+ if (!isRecord(value) || typeof value.type !== "string") return undefined;
498
+ const message = value.message;
499
+ if (message === undefined) return { type: value.type };
500
+ if (!isRecord(message) || typeof message.role !== "string" || !Array.isArray(message.content)) return undefined;
501
+ return { type: value.type, message: message as unknown as Message };
502
+ }
503
+
504
+ function isRecord(value: unknown): value is Record<string, unknown> {
505
+ return typeof value === "object" && value !== null && !Array.isArray(value);
506
+ }
507
+
508
+ async function runSingleAgent(
509
+ defaultCwd: string,
510
+ agents: AgentConfig[],
511
+ agentName: string,
512
+ task: string,
513
+ cwd: string | undefined,
514
+ step: number | undefined,
515
+ signal: AbortSignal | undefined,
516
+ onUpdate: OnUpdateCallback | undefined,
517
+ makeDetails: (results: SingleResult[]) => SubagentDetails,
518
+ budget: ExecutionBudget,
519
+ profileStore: DeadlineProfileStore,
520
+ pendingSequentialTasks: number,
521
+ ): Promise<SingleResult> {
522
+ const agent = agents.find((candidate) => candidate.name === agentName);
523
+ if (!agent) {
524
+ const available = agents.map((candidate) => `"${candidate.name}"`).join(", ") || "none";
525
+ return {
526
+ agent: agentName,
527
+ agentSource: "unknown",
528
+ task,
529
+ exitCode: 1,
530
+ messages: [],
531
+ stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
532
+ usage: emptyUsage(),
533
+ step,
534
+ };
535
+ }
536
+ return await runAdaptiveAgent({
537
+ agentName,
538
+ agentSource: agent.source,
539
+ logicalTask: task,
540
+ model: agent.model,
541
+ step,
542
+ signal,
543
+ budget,
544
+ pendingSequentialTasks,
545
+ profileStore,
546
+ policy: {
547
+ fallbackAttemptMs: DEFAULT_ATTEMPT_CUTOFF_MS,
548
+ minimumAttemptMs: MINIMUM_ATTEMPT_MS,
549
+ maxTokensPerShard: MAX_TOKENS_PER_TASK_SHARD,
550
+ },
551
+ onUpdate: onUpdate
552
+ ? (partial) =>
553
+ onUpdate({
554
+ content: [{ type: "text", text: getResultOutput(partial) || "(running...)" }],
555
+ details: makeDetails([partial]),
556
+ })
557
+ : undefined,
558
+ runAttempt: async (input: RunAttemptInput) =>
559
+ await runSingleAgentAttempt(
560
+ defaultCwd,
561
+ agents,
562
+ agentName,
563
+ input.logicalTask,
564
+ input.task,
565
+ cwd,
566
+ step,
567
+ input.cutoffMs,
568
+ signal,
569
+ input.onUpdate,
570
+ ),
571
+ });
572
+ }
573
+
493
574
  const TaskItem = Type.Object({
494
575
  agent: Type.String({ description: "Name of the agent to invoke" }),
495
576
  task: Type.String({ description: "Task to delegate to the agent" }),
@@ -516,6 +597,23 @@ const SubagentParams = Type.Object({
516
597
  confirmProjectAgents: Type.Optional(
517
598
  Type.Boolean({ description: "Prompt before running project-local agents. Default: true.", default: true }),
518
599
  ),
600
+ executionBudgetMs: Type.Optional(
601
+ Type.Integer({
602
+ description:
603
+ "Internal wall-clock budget. Defaults to 840000ms, leaving cleanup time before the 900000ms tool timeout.",
604
+ minimum: 60_000,
605
+ maximum: MAX_EXECUTION_BUDGET_MS,
606
+ default: DEFAULT_EXECUTION_BUDGET_MS,
607
+ }),
608
+ ),
609
+ maxResumeAttempts: Type.Optional(
610
+ Type.Integer({
611
+ description: "Bounded retries for only the unfinished shard. Default: 1.",
612
+ minimum: 0,
613
+ maximum: 2,
614
+ default: 1,
615
+ }),
616
+ ),
519
617
  cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
520
618
  });
521
619
 
@@ -526,6 +624,7 @@ export default function (omk: ExtensionAPI) {
526
624
  description: [
527
625
  "Delegate tasks to specialized subagents with isolated context.",
528
626
  "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
627
+ "Long work is predictively sharded by token and wall-clock demand, checkpointed, and resumed only from unfinished work.",
529
628
  'Default agent scope is "user" (from ~/.omk/agent/agents).',
530
629
  'To enable project-local agents in .omk/agents, set agentScope: "both" (or "project").',
531
630
  ].join(" "),
@@ -536,6 +635,13 @@ export default function (omk: ExtensionAPI) {
536
635
  const discovery = discoverAgents(ctx.cwd, agentScope);
537
636
  const agents = discovery.agents;
538
637
  const confirmProjectAgents = params.confirmProjectAgents ?? true;
638
+ const executionBudget = createExecutionBudget({
639
+ hardDeadlineMs: params.executionBudgetMs ?? DEFAULT_EXECUTION_BUDGET_MS,
640
+ cleanupReserveMs: EXECUTION_CLEANUP_RESERVE_MS,
641
+ maxResumeAttempts: params.maxResumeAttempts ?? 1,
642
+ maxTaskShards: MAX_TASK_SHARDS,
643
+ });
644
+ const profileStore = getDeadlineProfileStore();
539
645
 
540
646
  const hasChain = (params.chain?.length ?? 0) > 0;
541
647
  const hasTasks = (params.tasks?.length ?? 0) > 0;
@@ -549,6 +655,7 @@ export default function (omk: ExtensionAPI) {
549
655
  agentScope,
550
656
  projectAgentsDir: discovery.projectAgentsDir,
551
657
  results,
658
+ executionBudget: summarizeExecutionBudget(executionBudget, results),
552
659
  });
553
660
 
554
661
  if (modeCount !== 1) {
@@ -622,6 +729,9 @@ export default function (omk: ExtensionAPI) {
622
729
  signal,
623
730
  chainUpdate,
624
731
  makeDetails("chain"),
732
+ executionBudget,
733
+ profileStore,
734
+ params.chain.length - i,
625
735
  );
626
736
  results.push(result);
627
737
 
@@ -634,10 +744,10 @@ export default function (omk: ExtensionAPI) {
634
744
  isError: true,
635
745
  };
636
746
  }
637
- previousOutput = getFinalOutput(result.messages);
747
+ previousOutput = getResultOutput(result);
638
748
  }
639
749
  return {
640
- content: [{ type: "text", text: getFinalOutput(results[results.length - 1].messages) || "(no output)" }],
750
+ content: [{ type: "text", text: getResultOutput(results[results.length - 1]) }],
641
751
  details: makeDetails("chain")(results),
642
752
  };
643
753
  }
@@ -666,7 +776,7 @@ export default function (omk: ExtensionAPI) {
666
776
  exitCode: -1, // -1 = still running
667
777
  messages: [],
668
778
  stderr: "",
669
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
779
+ usage: emptyUsage(),
670
780
  };
671
781
  }
672
782
 
@@ -700,6 +810,9 @@ export default function (omk: ExtensionAPI) {
700
810
  }
701
811
  },
702
812
  makeDetails("parallel"),
813
+ executionBudget,
814
+ profileStore,
815
+ 1,
703
816
  );
704
817
  allResults[index] = result;
705
818
  emitParallelUpdate();
@@ -736,6 +849,9 @@ export default function (omk: ExtensionAPI) {
736
849
  signal,
737
850
  onUpdate,
738
851
  makeDetails("single"),
852
+ executionBudget,
853
+ profileStore,
854
+ 1,
739
855
  );
740
856
  const isError = isFailedResult(result);
741
857
  if (isError) {
@@ -747,7 +863,7 @@ export default function (omk: ExtensionAPI) {
747
863
  };
748
864
  }
749
865
  return {
750
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(no output)" }],
866
+ content: [{ type: "text", text: getResultOutput(result) }],
751
867
  details: makeDetails("single")([result]),
752
868
  };
753
869
  }
@@ -833,7 +949,7 @@ export default function (omk: ExtensionAPI) {
833
949
  const isError = isFailedResult(r);
834
950
  const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
835
951
  const displayItems = getDisplayItems(r.messages);
836
- const finalOutput = getFinalOutput(r.messages);
952
+ const finalOutput = r.output || getFinalOutput(r.messages);
837
953
 
838
954
  if (expanded) {
839
955
  const container = new Container();
@@ -919,7 +1035,7 @@ export default function (omk: ExtensionAPI) {
919
1035
  for (const r of details.results) {
920
1036
  const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
921
1037
  const displayItems = getDisplayItems(r.messages);
922
- const finalOutput = getFinalOutput(r.messages);
1038
+ const finalOutput = r.output || getFinalOutput(r.messages);
923
1039
 
924
1040
  container.addChild(new Spacer(1));
925
1041
  container.addChild(
@@ -1008,7 +1124,7 @@ export default function (omk: ExtensionAPI) {
1008
1124
  for (const r of details.results) {
1009
1125
  const rIcon = isFailedResult(r) ? theme.fg("error", "✗") : theme.fg("success", "✓");
1010
1126
  const displayItems = getDisplayItems(r.messages);
1011
- const finalOutput = getFinalOutput(r.messages);
1127
+ const finalOutput = r.output || getFinalOutput(r.messages);
1012
1128
 
1013
1129
  container.addChild(new Spacer(1));
1014
1130
  container.addChild(