open-multi-agent-kit 0.91.0 → 0.92.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 (72) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +2 -2
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +8 -7
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/command-safety.d.ts.map +1 -1
  7. package/dist/core/command-safety.js +0 -50
  8. package/dist/core/command-safety.js.map +1 -1
  9. package/dist/core/compaction/transaction.d.ts.map +1 -1
  10. package/dist/core/compaction/transaction.js +7 -3
  11. package/dist/core/compaction/transaction.js.map +1 -1
  12. package/dist/core/context-budget-headroom-candidates.d.ts.map +1 -1
  13. package/dist/core/context-budget-headroom-candidates.js +21 -11
  14. package/dist/core/context-budget-headroom-candidates.js.map +1 -1
  15. package/dist/core/context-budget-system-prompt-items.d.ts +1 -0
  16. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  17. package/dist/core/context-budget-system-prompt-items.js +7 -8
  18. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  19. package/dist/core/loadouts.d.ts.map +1 -1
  20. package/dist/core/loadouts.js +27 -48
  21. package/dist/core/loadouts.js.map +1 -1
  22. package/dist/core/system-prompt.d.ts.map +1 -1
  23. package/dist/core/system-prompt.js +35 -30
  24. package/dist/core/system-prompt.js.map +1 -1
  25. package/dist/main.d.ts.map +1 -1
  26. package/dist/main.js +112 -11
  27. package/dist/main.js.map +1 -1
  28. package/dist/modes/print-mode.d.ts.map +1 -1
  29. package/dist/modes/print-mode.js +9 -0
  30. package/dist/modes/print-mode.js.map +1 -1
  31. package/docs/adaptorch-preview-spec.md +174 -0
  32. package/docs/adaptorch-preview.md +1 -1
  33. package/docs/compaction.md +13 -13
  34. package/docs/correctness-wall.md +13 -5
  35. package/docs/custom-provider.md +8 -8
  36. package/docs/development.md +2 -2
  37. package/docs/extensions.md +8 -8
  38. package/docs/json.md +4 -4
  39. package/docs/providers.md +1 -1
  40. package/docs/session-format.md +5 -5
  41. package/docs/skills.md +2 -0
  42. package/docs/themes.md +2 -2
  43. package/docs/tui.md +4 -4
  44. package/docs/usage.md +4 -0
  45. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  46. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  47. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  48. package/examples/extensions/gondolin/package-lock.json +2 -2
  49. package/examples/extensions/gondolin/package.json +1 -1
  50. package/examples/extensions/sandbox/package-lock.json +2 -2
  51. package/examples/extensions/sandbox/package.json +1 -1
  52. package/examples/extensions/subagent/README.md +35 -10
  53. package/examples/extensions/subagent/adaptive-agent-runtime.test.ts +234 -0
  54. package/examples/extensions/subagent/adaptive-agent-runtime.ts +240 -0
  55. package/examples/extensions/subagent/adaptive-result.ts +112 -0
  56. package/examples/extensions/subagent/checkpoint-runtime.test.ts +125 -0
  57. package/examples/extensions/subagent/checkpoint-runtime.ts +198 -0
  58. package/examples/extensions/subagent/deadline-budget.test.ts +135 -0
  59. package/examples/extensions/subagent/deadline-budget.ts +242 -0
  60. package/examples/extensions/subagent/deadline-profile-store.test.ts +72 -0
  61. package/examples/extensions/subagent/deadline-profile-store.ts +172 -0
  62. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +13 -0
  63. package/examples/extensions/subagent/fixtures/hanging-process-tree.mjs +40 -0
  64. package/examples/extensions/subagent/index.ts +253 -137
  65. package/examples/extensions/subagent/managed-process.test.ts +178 -0
  66. package/examples/extensions/subagent/managed-process.ts +236 -0
  67. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +112 -0
  68. package/examples/extensions/subagent/subagent-runtime-types.ts +76 -0
  69. package/examples/extensions/with-deps/package-lock.json +2 -2
  70. package/examples/extensions/with-deps/package.json +1 -1
  71. package/npm-shrinkwrap.json +21 -21
  72. package/package.json +5 -5
@@ -0,0 +1,234 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import type { Message } from "omk-ai";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import { type RunAttemptInput, runAdaptiveAgent } from "./adaptive-agent-runtime.ts";
7
+ import { createExecutionBudget } from "./deadline-budget.ts";
8
+ import { DeadlineProfileStore } from "./deadline-profile-store.ts";
9
+ import { emptyUsage, type SingleResult, type SubagentAttemptResult } from "./subagent-runtime-types.ts";
10
+
11
+ const cleanupPaths: string[] = [];
12
+
13
+ function assistant(text: string): Message {
14
+ return { role: "assistant", content: [{ type: "text", text }] } as Message;
15
+ }
16
+
17
+ function attempt(input: RunAttemptInput, outcome: "completed" | "cutoff" | "failed", text = ""): SubagentAttemptResult {
18
+ const exitCode = outcome === "completed" ? 0 : outcome === "failed" ? 1 : 124;
19
+ const result: SingleResult = {
20
+ agent: "reviewer",
21
+ agentSource: "user",
22
+ task: input.logicalTask,
23
+ exitCode,
24
+ messages: text === "" ? [] : [assistant(text)],
25
+ stderr: "",
26
+ usage: emptyUsage(),
27
+ output: text,
28
+ model: "openai-codex/gpt-5.6",
29
+ ...(outcome === "cutoff"
30
+ ? { stopReason: "deadline", errorMessage: "attempt cutoff" }
31
+ : { stopReason: outcome === "failed" ? "error" : "end" }),
32
+ };
33
+ return {
34
+ result,
35
+ process: {
36
+ reason: outcome === "failed" ? "completed" : outcome,
37
+ elapsedMs: outcome === "completed" ? 10 : input.cutoffMs,
38
+ cleanup: { termSent: outcome === "cutoff", killSent: false, processGroup: true },
39
+ },
40
+ };
41
+ }
42
+
43
+ afterEach(async () => {
44
+ await Promise.all(cleanupPaths.splice(0).map((entry) => fs.promises.rm(entry, { recursive: true, force: true })));
45
+ });
46
+
47
+ describe("adaptive subagent execution", () => {
48
+ it("resumes only the unfinished shard from bounded checkpoint evidence", async () => {
49
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-adaptive-resume-"));
50
+ cleanupPaths.push(dir);
51
+ const calls: RunAttemptInput[] = [];
52
+ const outputs = ["completed boundary inspection", "finished remaining cleanup"];
53
+ const result = await runAdaptiveAgent({
54
+ agentName: "reviewer",
55
+ agentSource: "user",
56
+ logicalTask: "Review and fix the process boundary.",
57
+ model: "openai-codex/gpt-5.6",
58
+ budget: createExecutionBudget({
59
+ hardDeadlineMs: 5_000,
60
+ cleanupReserveMs: 100,
61
+ maxResumeAttempts: 1,
62
+ maxTaskShards: 3,
63
+ }),
64
+ pendingSequentialTasks: 1,
65
+ checkpointPrefix: path.join(dir, "checkpoint-"),
66
+ profileStore: new DeadlineProfileStore(path.join(dir, "profiles.json")),
67
+ policy: { fallbackAttemptMs: 1_000, minimumAttemptMs: 1, maxTokensPerShard: 6_000 },
68
+ runAttempt: async (input) => {
69
+ calls.push(input);
70
+ return attempt(input, calls.length === 1 ? "cutoff" : "completed", outputs[calls.length - 1]);
71
+ },
72
+ });
73
+
74
+ expect(calls).toHaveLength(2);
75
+ expect(calls[1]?.task).toContain("completed boundary inspection");
76
+ expect(calls[1]?.task).toContain("Do not repeat completed side effects");
77
+ expect(result.deadline).toMatchObject({ outcome: "completed", resumeCount: 1, completedShardIds: ["shard-1"] });
78
+ expect(result.output).toContain("completed boundary inspection");
79
+ expect(result.output).toContain("finished remaining cleanup");
80
+ });
81
+
82
+ it("learns the provider/model observed from output when the agent inherits its model", async () => {
83
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-adaptive-observed-model-"));
84
+ cleanupPaths.push(dir);
85
+ const store = new DeadlineProfileStore(path.join(dir, "profiles.json"));
86
+ const result = await runAdaptiveAgent({
87
+ agentName: "reviewer",
88
+ agentSource: "user",
89
+ logicalTask: "Review the process boundary.",
90
+ budget: createExecutionBudget({
91
+ hardDeadlineMs: 5_000,
92
+ cleanupReserveMs: 100,
93
+ maxResumeAttempts: 1,
94
+ maxTaskShards: 3,
95
+ }),
96
+ pendingSequentialTasks: 1,
97
+ checkpointPrefix: path.join(dir, "checkpoint-"),
98
+ profileStore: store,
99
+ policy: { fallbackAttemptMs: 1_000, minimumAttemptMs: 1, maxTokensPerShard: 6_000 },
100
+ runAttempt: async (input) => attempt(input, "completed", "review complete"),
101
+ });
102
+
103
+ expect(result.deadline).toMatchObject({ provider: "openai-codex", model: "gpt-5.6" });
104
+ expect(await store.profileFor("openai-codex", "gpt-5.6")).toMatchObject({ samples: 1, completions: 1 });
105
+ });
106
+
107
+ it("preserves process failures instead of labeling them as deadline cutoffs", async () => {
108
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-adaptive-failure-"));
109
+ cleanupPaths.push(dir);
110
+ const result = await runAdaptiveAgent({
111
+ agentName: "reviewer",
112
+ agentSource: "user",
113
+ logicalTask: "Run the failing fixture.",
114
+ budget: createExecutionBudget({
115
+ hardDeadlineMs: 5_000,
116
+ cleanupReserveMs: 100,
117
+ maxResumeAttempts: 1,
118
+ maxTaskShards: 3,
119
+ }),
120
+ pendingSequentialTasks: 1,
121
+ checkpointPrefix: path.join(dir, "checkpoint-"),
122
+ profileStore: new DeadlineProfileStore(path.join(dir, "profiles.json")),
123
+ policy: { fallbackAttemptMs: 1_000, minimumAttemptMs: 1, maxTokensPerShard: 6_000 },
124
+ runAttempt: async (input) => attempt(input, "failed", ""),
125
+ });
126
+
127
+ expect(result).toMatchObject({ exitCode: 1, stopReason: "error", errorMessage: "Subagent process failed" });
128
+ expect(result.deadline).toMatchObject({ outcome: "failed" });
129
+ });
130
+
131
+ it("does not retry a cutoff that produced no checkpoint or streamed progress", async () => {
132
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-adaptive-duplicate-"));
133
+ cleanupPaths.push(dir);
134
+ let calls = 0;
135
+ const result = await runAdaptiveAgent({
136
+ agentName: "reviewer",
137
+ agentSource: "user",
138
+ logicalTask: "Review the process boundary.",
139
+ model: "openai-codex/gpt-5.6",
140
+ budget: createExecutionBudget({
141
+ hardDeadlineMs: 5_000,
142
+ cleanupReserveMs: 100,
143
+ maxResumeAttempts: 2,
144
+ maxTaskShards: 3,
145
+ }),
146
+ pendingSequentialTasks: 1,
147
+ checkpointPrefix: path.join(dir, "checkpoint-"),
148
+ profileStore: new DeadlineProfileStore(path.join(dir, "profiles.json")),
149
+ policy: { fallbackAttemptMs: 1_000, minimumAttemptMs: 1, maxTokensPerShard: 6_000 },
150
+ runAttempt: async (input) => {
151
+ calls += 1;
152
+ return attempt(input, "cutoff");
153
+ },
154
+ });
155
+
156
+ expect(calls).toBe(1);
157
+ expect(result.deadline).toMatchObject({ outcome: "cutoff", resumeCount: 0, duplicateResumeBlocked: true });
158
+ });
159
+
160
+ it("never re-executes shards that already completed", async () => {
161
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-adaptive-shards-"));
162
+ cleanupPaths.push(dir);
163
+ const shardIds: string[] = [];
164
+ const task = [
165
+ "Upgrade runtime:",
166
+ "1. Inspect the process.",
167
+ "2. Add cleanup.",
168
+ "3. Implement metadata.",
169
+ "4. Run tests.",
170
+ ].join("\n");
171
+ const result = await runAdaptiveAgent({
172
+ agentName: "worker",
173
+ agentSource: "user",
174
+ logicalTask: task,
175
+ model: "anthropic/claude-sonnet",
176
+ budget: createExecutionBudget({
177
+ hardDeadlineMs: 5_000,
178
+ cleanupReserveMs: 100,
179
+ maxResumeAttempts: 1,
180
+ maxTaskShards: 3,
181
+ }),
182
+ pendingSequentialTasks: 1,
183
+ checkpointPrefix: path.join(dir, "checkpoint-"),
184
+ profileStore: new DeadlineProfileStore(path.join(dir, "profiles.json")),
185
+ policy: { fallbackAttemptMs: 200, minimumAttemptMs: 1, maxTokensPerShard: 20 },
186
+ runAttempt: async (input) => {
187
+ shardIds.push(input.shardId);
188
+ return attempt(input, "completed", `done ${input.shardId}`);
189
+ },
190
+ });
191
+
192
+ expect(shardIds.length).toBeGreaterThan(1);
193
+ expect(new Set(shardIds).size).toBe(shardIds.length);
194
+ expect(result.deadline?.remainingShardIds).toEqual([]);
195
+ });
196
+
197
+ it("caps resume cost across the whole logical task rather than once per shard", async () => {
198
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "omk-adaptive-cost-cap-"));
199
+ cleanupPaths.push(dir);
200
+ const shardIds: string[] = [];
201
+ const task = [
202
+ "Upgrade runtime:",
203
+ "1. Inspect the process.",
204
+ "2. Add cleanup.",
205
+ "3. Implement metadata.",
206
+ "4. Run tests.",
207
+ ].join("\n");
208
+ const result = await runAdaptiveAgent({
209
+ agentName: "worker",
210
+ agentSource: "user",
211
+ logicalTask: task,
212
+ model: "openai-codex/gpt-5.6",
213
+ budget: createExecutionBudget({
214
+ hardDeadlineMs: 5_000,
215
+ cleanupReserveMs: 100,
216
+ maxResumeAttempts: 1,
217
+ maxTaskShards: 3,
218
+ }),
219
+ pendingSequentialTasks: 1,
220
+ checkpointPrefix: path.join(dir, "checkpoint-"),
221
+ profileStore: new DeadlineProfileStore(path.join(dir, "profiles.json")),
222
+ policy: { fallbackAttemptMs: 200, minimumAttemptMs: 1, maxTokensPerShard: 20 },
223
+ runAttempt: async (input) => {
224
+ shardIds.push(input.shardId);
225
+ const outcome = shardIds.length === 2 ? "completed" : "cutoff";
226
+ return attempt(input, outcome, `progress ${input.shardId} ${shardIds.length}`);
227
+ },
228
+ });
229
+
230
+ expect(shardIds).toEqual(["shard-1", "shard-1", "shard-2"]);
231
+ expect(result.deadline).toMatchObject({ resumeCount: 1, outcome: "cutoff" });
232
+ expect(result.deadline?.remainingShardIds).toContain("shard-2");
233
+ });
234
+ });
@@ -0,0 +1,240 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import {
4
+ attemptOutcome,
5
+ createBaseResult,
6
+ finalizeResult,
7
+ mergeForUpdate,
8
+ mergeResult,
9
+ parseProviderModel,
10
+ resultOutput,
11
+ } from "./adaptive-result.ts";
12
+ import {
13
+ buildCheckpointTask,
14
+ checkpointMadeProgress,
15
+ createCheckpointWorkspace,
16
+ createEmptyCheckpoint,
17
+ readCheckpoint,
18
+ removeCheckpointWorkspace,
19
+ writeCheckpoint,
20
+ } from "./checkpoint-runtime.ts";
21
+ import {
22
+ allocateAttemptBudget,
23
+ type DeadlineProfile,
24
+ type ExecutionBudget,
25
+ estimateTaskDemand,
26
+ planTaskShards,
27
+ recommendAttemptCutoff,
28
+ remainingExecutionMs,
29
+ } from "./deadline-budget.ts";
30
+ import type { DeadlineProfileStore } from "./deadline-profile-store.ts";
31
+ import type {
32
+ DeadlineAttemptMetadata,
33
+ DeadlineOutcome,
34
+ SingleResult,
35
+ SubagentAttemptResult,
36
+ } from "./subagent-runtime-types.ts";
37
+
38
+ export interface RunAttemptInput {
39
+ readonly logicalTask: string;
40
+ readonly task: string;
41
+ readonly cutoffMs: number;
42
+ readonly attempt: number;
43
+ readonly shardId: string;
44
+ readonly shardIndex: number;
45
+ readonly shardCount: number;
46
+ readonly checkpointFilePath: string;
47
+ readonly onUpdate?: (partial: SingleResult) => void;
48
+ }
49
+
50
+ interface AdaptiveRuntimePolicy {
51
+ readonly fallbackAttemptMs: number;
52
+ readonly minimumAttemptMs: number;
53
+ readonly maxTokensPerShard: number;
54
+ }
55
+
56
+ interface RunAdaptiveAgentOptions {
57
+ readonly agentName: string;
58
+ readonly agentSource: "user" | "project";
59
+ readonly logicalTask: string;
60
+ readonly model?: string;
61
+ readonly step?: number;
62
+ readonly signal?: AbortSignal;
63
+ readonly budget: ExecutionBudget;
64
+ readonly pendingSequentialTasks: number;
65
+ readonly checkpointPrefix?: string;
66
+ readonly profileStore: DeadlineProfileStore;
67
+ readonly policy: AdaptiveRuntimePolicy;
68
+ readonly onUpdate?: (partial: SingleResult) => void;
69
+ readonly runAttempt: (input: RunAttemptInput) => Promise<SubagentAttemptResult>;
70
+ }
71
+
72
+ export async function runAdaptiveAgent(options: RunAdaptiveAgentOptions): Promise<SingleResult> {
73
+ const startedAtMs = Date.now();
74
+ let identity = parseProviderModel(options.model);
75
+ let profile = await options.profileStore.profileFor(identity.provider, identity.model);
76
+ const initialCutoff = cutoffFor(profile, options, remainingExecutionMs(options.budget));
77
+ const demand = estimateTaskDemand(options.logicalTask);
78
+ const shards = planTaskShards(options.logicalTask, demand, {
79
+ attemptCutoffMs: initialCutoff,
80
+ maxTaskShards: options.budget.maxTaskShards,
81
+ maxTokensPerShard: options.policy.maxTokensPerShard,
82
+ });
83
+ const taskAllocation = allocateAttemptBudget({
84
+ budget: options.budget,
85
+ pendingSequentialTasks: options.pendingSequentialTasks,
86
+ pendingShards: 1,
87
+ recommendedCutoffMs: remainingExecutionMs(options.budget),
88
+ minimumAttemptMs: 1,
89
+ });
90
+ const taskBudget: ExecutionBudget = {
91
+ ...options.budget,
92
+ hardDeadlineMs: Math.max(1, taskAllocation.taskDeadlineAtMs - startedAtMs),
93
+ deadlineAtMs: Math.min(options.budget.deadlineAtMs, taskAllocation.taskDeadlineAtMs),
94
+ };
95
+ const aggregate = createBaseResult(options);
96
+ const attempts: DeadlineAttemptMetadata[] = [];
97
+ const completedShardIds: string[] = [];
98
+ let resumeCount = 0;
99
+ let duplicateResumeBlocked = false;
100
+ let lastCheckpoint: Awaited<ReturnType<typeof readCheckpoint>> | undefined;
101
+ let terminalOutcome: DeadlineOutcome = "budget-exhausted";
102
+ const checkpointPrefix = options.checkpointPrefix ?? path.join(os.tmpdir(), "omk-subagent-checkpoint-");
103
+ const workspace = await createCheckpointWorkspace(checkpointPrefix);
104
+
105
+ try {
106
+ outer: for (const shard of shards) {
107
+ let shardAttempt = 0;
108
+ let shardMessageCount = 0;
109
+ let checkpoint: typeof lastCheckpoint;
110
+ while (true) {
111
+ const pendingShards = shards.length - completedShardIds.length;
112
+ const recommended = cutoffFor(profile, options, remainingExecutionMs(taskBudget));
113
+ const allocation = allocateAttemptBudget({
114
+ budget: taskBudget,
115
+ pendingSequentialTasks: 1,
116
+ pendingShards,
117
+ recommendedCutoffMs: recommended,
118
+ minimumAttemptMs: options.policy.minimumAttemptMs,
119
+ });
120
+ if (allocation.cutoffMs === 0 || options.signal?.aborted) {
121
+ terminalOutcome = options.signal?.aborted ? "aborted" : "budget-exhausted";
122
+ break outer;
123
+ }
124
+
125
+ const attemptNumber = shardAttempt + 1;
126
+ const task = buildCheckpointTask({
127
+ originalTask: options.logicalTask,
128
+ shardTask: shard.task,
129
+ shardId: shard.id,
130
+ shardIndex: shard.index,
131
+ shardCount: shards.length,
132
+ attempt: attemptNumber,
133
+ cutoffMs: allocation.cutoffMs,
134
+ checkpointFilePath: workspace.filePath,
135
+ ...(checkpoint === undefined ? {} : { checkpoint }),
136
+ });
137
+ const attempt = await options.runAttempt({
138
+ logicalTask: options.logicalTask,
139
+ task,
140
+ cutoffMs: allocation.cutoffMs,
141
+ attempt: attemptNumber,
142
+ shardId: shard.id,
143
+ shardIndex: shard.index,
144
+ shardCount: shards.length,
145
+ checkpointFilePath: workspace.filePath,
146
+ onUpdate: options.onUpdate
147
+ ? (partial) => options.onUpdate?.(mergeForUpdate(aggregate, partial))
148
+ : undefined,
149
+ });
150
+ mergeResult(aggregate, attempt.result);
151
+ shardMessageCount += attempt.result.messages.length;
152
+ const outcome = attemptOutcome(attempt);
153
+ const observedIdentity = parseProviderModel(attempt.result.model ?? options.model);
154
+ if (observedIdentity.provider !== identity.provider || observedIdentity.model !== identity.model) {
155
+ identity = observedIdentity;
156
+ profile = await options.profileStore.profileFor(identity.provider, identity.model);
157
+ }
158
+ try {
159
+ profile = await options.profileStore.record({
160
+ provider: identity.provider,
161
+ model: identity.model,
162
+ outcome,
163
+ elapsedMs: attempt.process.elapsedMs,
164
+ estimatedTokens: demand.estimatedTokens,
165
+ workUnits: demand.workUnits,
166
+ });
167
+ } catch {
168
+ profile = await options.profileStore.profileFor(identity.provider, identity.model);
169
+ }
170
+ lastCheckpoint = await readCheckpoint(workspace.filePath, {
171
+ messageCount: shardMessageCount,
172
+ outputTail: resultOutput(attempt.result),
173
+ lastEventAtMs: Date.now(),
174
+ });
175
+ attempts.push({
176
+ attempt: attemptNumber,
177
+ shardId: shard.id,
178
+ cutoffMs: allocation.cutoffMs,
179
+ elapsedMs: attempt.process.elapsedMs,
180
+ outcome,
181
+ exitCode: attempt.result.exitCode,
182
+ messageCount: attempt.result.messages.length,
183
+ processReason: attempt.process.reason,
184
+ cleanup: attempt.process.cleanup,
185
+ });
186
+
187
+ if (outcome === "completed") {
188
+ completedShardIds.push(shard.id);
189
+ terminalOutcome = "completed";
190
+ checkpoint = undefined;
191
+ await writeCheckpoint(workspace.filePath, createEmptyCheckpoint());
192
+ continue outer;
193
+ }
194
+ terminalOutcome = outcome;
195
+ if (outcome !== "cutoff" || resumeCount >= taskBudget.maxResumeAttempts) break outer;
196
+ if (!checkpointMadeProgress(checkpoint, lastCheckpoint)) {
197
+ duplicateResumeBlocked = true;
198
+ break outer;
199
+ }
200
+ checkpoint = lastCheckpoint;
201
+ shardAttempt += 1;
202
+ resumeCount += 1;
203
+ }
204
+ }
205
+ } finally {
206
+ await removeCheckpointWorkspace(workspace);
207
+ }
208
+
209
+ const remainingShardIds = shards.map((shard) => shard.id).filter((id) => !completedShardIds.includes(id));
210
+ if (remainingShardIds.length === 0) terminalOutcome = "completed";
211
+ finalizeResult(aggregate, terminalOutcome);
212
+ aggregate.deadline = {
213
+ outcome: terminalOutcome,
214
+ startedAtMs,
215
+ elapsedMs: Date.now() - startedAtMs,
216
+ hardDeadlineMs: taskBudget.hardDeadlineMs,
217
+ estimatedTokens: demand.estimatedTokens,
218
+ workUnits: demand.workUnits,
219
+ predictedMs: demand.predictedMs,
220
+ plannedShardIds: shards.map((shard) => shard.id),
221
+ completedShardIds,
222
+ remainingShardIds,
223
+ resumeCount,
224
+ duplicateResumeBlocked,
225
+ provider: identity.provider,
226
+ model: identity.model,
227
+ profileSamples: profile?.samples ?? 0,
228
+ attempts,
229
+ ...(lastCheckpoint === undefined ? {} : { checkpoint: lastCheckpoint }),
230
+ };
231
+ return aggregate;
232
+ }
233
+
234
+ function cutoffFor(profile: DeadlineProfile | undefined, options: RunAdaptiveAgentOptions, maximumMs: number): number {
235
+ return recommendAttemptCutoff(profile, {
236
+ fallbackMs: options.policy.fallbackAttemptMs,
237
+ minimumMs: Math.min(options.policy.minimumAttemptMs, maximumMs),
238
+ maximumMs: Math.max(1, maximumMs),
239
+ });
240
+ }
@@ -0,0 +1,112 @@
1
+ import type { Message } from "omk-ai";
2
+ import {
3
+ type DeadlineOutcome,
4
+ emptyUsage,
5
+ type SingleResult,
6
+ type SubagentAttemptResult,
7
+ type UsageStats,
8
+ } from "./subagent-runtime-types.ts";
9
+
10
+ interface BaseResultOptions {
11
+ readonly agentName: string;
12
+ readonly agentSource: "user" | "project";
13
+ readonly logicalTask: string;
14
+ readonly model?: string;
15
+ readonly step?: number;
16
+ }
17
+
18
+ export function createBaseResult(options: BaseResultOptions): SingleResult {
19
+ return {
20
+ agent: options.agentName,
21
+ agentSource: options.agentSource,
22
+ task: options.logicalTask,
23
+ exitCode: 0,
24
+ messages: [],
25
+ stderr: "",
26
+ usage: emptyUsage(),
27
+ model: options.model,
28
+ step: options.step,
29
+ };
30
+ }
31
+
32
+ export function attemptOutcome(attempt: SubagentAttemptResult): Exclude<DeadlineOutcome, "budget-exhausted"> {
33
+ if (attempt.process.reason === "cutoff") return "cutoff";
34
+ if (attempt.process.reason === "aborted") return "aborted";
35
+ return attempt.result.exitCode === 0 && attempt.result.stopReason !== "error" ? "completed" : "failed";
36
+ }
37
+
38
+ export function mergeResult(target: SingleResult, source: SingleResult): void {
39
+ target.messages.push(...source.messages);
40
+ target.stderr = [target.stderr, source.stderr].filter(Boolean).join("\n");
41
+ target.usage = mergeUsage(target.usage, source.usage);
42
+ target.output = mergeOutput(target.output ?? "", resultOutput(source));
43
+ target.exitCode = source.exitCode;
44
+ target.stopReason = source.stopReason;
45
+ target.errorMessage = source.errorMessage;
46
+ target.model = source.model ?? target.model;
47
+ }
48
+
49
+ export function mergeForUpdate(base: SingleResult, partial: SingleResult): SingleResult {
50
+ const merged: SingleResult = { ...base, messages: [...base.messages], usage: { ...base.usage } };
51
+ mergeResult(merged, partial);
52
+ return merged;
53
+ }
54
+
55
+ export function resultOutput(result: SingleResult): string {
56
+ if (result.output !== undefined) return result.output;
57
+ for (let index = result.messages.length - 1; index >= 0; index--) {
58
+ const message: Message = result.messages[index];
59
+ if (message.role !== "assistant") continue;
60
+ for (const part of message.content) if (part.type === "text") return part.text;
61
+ }
62
+ return "";
63
+ }
64
+
65
+ export function finalizeResult(result: SingleResult, outcome: DeadlineOutcome): void {
66
+ if (outcome === "completed") {
67
+ result.exitCode = 0;
68
+ result.stopReason = "end";
69
+ result.errorMessage = undefined;
70
+ return;
71
+ }
72
+ if (outcome === "failed") {
73
+ result.exitCode = Math.max(1, result.exitCode);
74
+ result.stopReason = "error";
75
+ result.errorMessage ??= "Subagent process failed";
76
+ return;
77
+ }
78
+ result.exitCode = outcome === "aborted" ? 130 : 124;
79
+ result.stopReason = outcome === "aborted" ? "aborted" : "deadline";
80
+ result.errorMessage ??=
81
+ outcome === "budget-exhausted" ? "Subagent execution budget exhausted" : "Subagent deadline cutoff";
82
+ }
83
+
84
+ export function parseProviderModel(value: string | undefined): { provider: string; model: string } {
85
+ if (!value) return { provider: "unknown", model: "default" };
86
+ const separator = value.indexOf("/");
87
+ return separator < 1
88
+ ? { provider: "unknown", model: value }
89
+ : { provider: value.slice(0, separator), model: value.slice(separator + 1) || "default" };
90
+ }
91
+
92
+ function mergeUsage(left: UsageStats, right: UsageStats): UsageStats {
93
+ return {
94
+ input: left.input + right.input,
95
+ output: left.output + right.output,
96
+ cacheRead: left.cacheRead + right.cacheRead,
97
+ cacheWrite: left.cacheWrite + right.cacheWrite,
98
+ cost: left.cost + right.cost,
99
+ contextTokens: Math.max(left.contextTokens, right.contextTokens),
100
+ turns: left.turns + right.turns,
101
+ };
102
+ }
103
+
104
+ function mergeOutput(left: string, right: string): string {
105
+ if (right === "" || left === right || left.endsWith(right)) return left;
106
+ if (left === "") return right;
107
+ const maxOverlap = Math.min(left.length, right.length, 4_096);
108
+ for (let overlap = maxOverlap; overlap > 0; overlap--) {
109
+ if (left.endsWith(right.slice(0, overlap))) return `${left}${right.slice(overlap)}`;
110
+ }
111
+ return `${left}\n\n${right}`;
112
+ }