@deepstrike/wasm 0.2.50 → 0.2.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/runtime/canonical-kernel-step.d.ts +146 -0
- package/dist/runtime/canonical-kernel-step.js +1484 -0
- package/dist/runtime/execution-plane.d.ts +0 -3
- package/dist/runtime/execution-plane.js +0 -25
- package/dist/runtime/facade.js +3 -0
- package/dist/runtime/index.d.ts +7 -7
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/kernel-event-log.d.ts +0 -1
- package/dist/runtime/kernel-event-log.js +7 -13
- package/dist/runtime/kernel-journal.d.ts +332 -0
- package/dist/runtime/kernel-journal.js +681 -0
- package/dist/runtime/kernel-step.d.ts +34 -89
- package/dist/runtime/kernel-step.js +67 -167
- package/dist/runtime/os-snapshot.d.ts +2 -2
- package/dist/runtime/os-snapshot.js +2 -6
- package/dist/runtime/payload-store.d.ts +30 -0
- package/dist/runtime/payload-store.js +66 -0
- package/dist/runtime/runner.d.ts +27 -75
- package/dist/runtime/runner.js +408 -632
- package/dist/runtime/session-log.d.ts +24 -27
- package/dist/runtime/session-log.js +11 -52
- package/dist/runtime/session-repair.d.ts +2 -36
- package/dist/runtime/session-repair.js +2 -47
- package/dist/runtime/sub-agent-orchestrator.d.ts +2 -2
- package/dist/runtime/sub-agent-orchestrator.js +36 -40
- package/dist/runtime/types/agent.d.ts +13 -16
- package/dist/runtime/types/agent.js +16 -41
- package/package.json +4 -3
- package/dist/runtime/kernel-rebuild.d.ts +0 -8
- package/dist/runtime/kernel-rebuild.js +0 -65
- package/dist/runtime/kernel-transaction-log.d.ts +0 -61
- package/dist/runtime/kernel-transaction-log.js +0 -140
- package/dist/runtime/large-result-spool.d.ts +0 -57
- package/dist/runtime/large-result-spool.js +0 -169
package/dist/runtime/runner.js
CHANGED
|
@@ -2,19 +2,20 @@ import { extractSessionMemories } from "../memory/extraction.js";
|
|
|
2
2
|
import { resolvePermissionRequest } from "./execution-plane.js";
|
|
3
3
|
import { governancePolicyToKernelEvent, governanceFilterSchema } from "../governance.js";
|
|
4
4
|
import { getKernel } from "./kernel.js";
|
|
5
|
+
import { CanonicalRunnerRuntime, canonicalKernelAction, canonicalKernelApply, canonicalKernelMaybeAction, canonicalStartAgent, canonicalStartWorkflow, sha256 as canonicalSha256, } from "./canonical-kernel-step.js";
|
|
5
6
|
import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-replay.js";
|
|
6
7
|
import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
7
8
|
import { formatToolError } from "../tools/errors.js";
|
|
8
|
-
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent,
|
|
9
|
-
import {
|
|
10
|
-
import { agentRunSpecToKernel,
|
|
9
|
+
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, } from "./session-repair.js";
|
|
10
|
+
import { messageToKernelMessage, skillMetadataToKernel, taskUpdateToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
11
|
+
import { agentRunSpecToKernel, MILESTONE_UNVERIFIED_REASON, milestoneCheckFail, milestoneCheckPass, milestoneCheckResultToKernel, subAgentResultToKernel, workflowBudgetNote, workflowNodeSpecToKernel, workflowNodeOutcomeFromKernel, workflowNodeStatusFromTermination, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
|
|
11
12
|
import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
12
13
|
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
13
14
|
import { resolveReducer } from "./reducers.js";
|
|
14
15
|
import { loopInstruction, classifyInstruction, judgeGoal, dependencyOutputsNote, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
|
|
15
16
|
import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
|
|
16
17
|
import { assertNativeProfile } from "./os-profile.js";
|
|
17
|
-
import {
|
|
18
|
+
import { PayloadStore } from "./payload-store.js";
|
|
18
19
|
import { contextPolicyV1, normalizeContextPolicyV1, } from "./context-policy.js";
|
|
19
20
|
export function schedulerPolicyToKernel(policy) {
|
|
20
21
|
const allowed = new Set([
|
|
@@ -31,6 +32,42 @@ export function schedulerPolicyToKernel(policy) {
|
|
|
31
32
|
token_cost_weight: policy.tokenCostWeight,
|
|
32
33
|
};
|
|
33
34
|
}
|
|
35
|
+
function kernelReliabilityToKernel(policy) {
|
|
36
|
+
const allowed = new Set(["providerRecoveryAttempts", "outputRecoveryAttempts", "maxInputBytes"]);
|
|
37
|
+
const unknown = Object.keys(policy).filter(key => !allowed.has(key));
|
|
38
|
+
if (unknown.length > 0) {
|
|
39
|
+
throw new TypeError(`unknown kernel reliability field(s): ${unknown.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
...(policy.providerRecoveryAttempts !== undefined
|
|
43
|
+
? { provider_recovery_attempts: policy.providerRecoveryAttempts }
|
|
44
|
+
: {}),
|
|
45
|
+
...(policy.outputRecoveryAttempts !== undefined
|
|
46
|
+
? { output_recovery_attempts: policy.outputRecoveryAttempts }
|
|
47
|
+
: {}),
|
|
48
|
+
...(policy.maxInputBytes !== undefined ? { max_input_bytes: policy.maxInputBytes } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function memoryPolicyToKernel(policy) {
|
|
52
|
+
const allowed = new Set([
|
|
53
|
+
"staleWarningDays",
|
|
54
|
+
"retrievalTopK",
|
|
55
|
+
"validationEnabled",
|
|
56
|
+
"maxContentBytes",
|
|
57
|
+
"maxNameLength",
|
|
58
|
+
]);
|
|
59
|
+
const unknown = Object.keys(policy).filter(key => !allowed.has(key));
|
|
60
|
+
if (unknown.length > 0) {
|
|
61
|
+
throw new TypeError(`unknown memory policy field(s): ${unknown.join(", ")}`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
...(policy.staleWarningDays !== undefined ? { stale_warning_days: policy.staleWarningDays } : {}),
|
|
65
|
+
...(policy.retrievalTopK !== undefined ? { retrieval_top_k: policy.retrievalTopK } : {}),
|
|
66
|
+
...(policy.validationEnabled !== undefined ? { validation_enabled: policy.validationEnabled } : {}),
|
|
67
|
+
...(policy.maxContentBytes !== undefined ? { max_content_bytes: policy.maxContentBytes } : {}),
|
|
68
|
+
...(policy.maxNameLength !== undefined ? { max_name_length: policy.maxNameLength } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
34
71
|
function pendingCallIds(action) {
|
|
35
72
|
switch (action.kind) {
|
|
36
73
|
case "call_provider": return [action.effectId];
|
|
@@ -61,6 +98,7 @@ export class RuntimeRunner {
|
|
|
61
98
|
pendingObservations = [];
|
|
62
99
|
activeKernel = null;
|
|
63
100
|
currentSessionId = null;
|
|
101
|
+
fallbackPayloadStore = null;
|
|
64
102
|
/** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
|
|
65
103
|
injectedSignals = [];
|
|
66
104
|
/** Skill names whose content has already been pushed into the durable `knowledge` slot this
|
|
@@ -74,9 +112,7 @@ export class RuntimeRunner {
|
|
|
74
112
|
nextArchiveStart = 0;
|
|
75
113
|
pendingPageOutArchives = [];
|
|
76
114
|
activePageOutArchive;
|
|
77
|
-
/**
|
|
78
|
-
* at the next safe point (after the tool turn resolves, kernel back in Reason). */
|
|
79
|
-
pendingAuthoredWorkflows = [];
|
|
115
|
+
/** Provider continuation emitted after a canonical nested workflow completes. */
|
|
80
116
|
workflowContinuation = null;
|
|
81
117
|
constructor(opts) {
|
|
82
118
|
this.opts = opts;
|
|
@@ -85,6 +121,44 @@ export class RuntimeRunner {
|
|
|
85
121
|
throw new RangeError("workflowSchemaValidationAttempts must be an integer between 1 and 16");
|
|
86
122
|
}
|
|
87
123
|
}
|
|
124
|
+
resolveKernelJournal() {
|
|
125
|
+
const embedded = this.opts.sessionLog;
|
|
126
|
+
const journal = this.opts.kernelJournal ?? embedded.kernelJournal;
|
|
127
|
+
if (!journal) {
|
|
128
|
+
throw new Error("RuntimeOptions.kernelJournal is required when SessionLog has no canonical journal");
|
|
129
|
+
}
|
|
130
|
+
return journal;
|
|
131
|
+
}
|
|
132
|
+
async createCanonicalRuntime(runId = crypto.randomUUID(), sessionId = this.currentSessionId ?? "wasm-session") {
|
|
133
|
+
const { CanonicalKernel } = await getKernel();
|
|
134
|
+
return new CanonicalRunnerRuntime(new CanonicalKernel(), this.resolveKernelJournal(), `wasm-operation-${runId}`, {
|
|
135
|
+
maxContextTokens: this.opts.maxTokens,
|
|
136
|
+
maxTurns: this.opts.maxTurns,
|
|
137
|
+
maxTotalTokens: this.opts.maxTotalTokens,
|
|
138
|
+
maxWallMs: this.opts.timeoutMs,
|
|
139
|
+
memoryBindingId: `wasm-memory-${this.opts.agentId ?? "root"}`,
|
|
140
|
+
persistPayload: async (_callId, content, previewBytes) => {
|
|
141
|
+
const digest = canonicalSha256(content);
|
|
142
|
+
const payloadRef = `payload:${digest.replace(/^sha256:/, "").slice(0, 32)}`;
|
|
143
|
+
await this.payloadStore().persistPayload(sessionId, payloadRef, content);
|
|
144
|
+
return {
|
|
145
|
+
payloadRef,
|
|
146
|
+
digest,
|
|
147
|
+
originalSize: String(new TextEncoder().encode(content).byteLength),
|
|
148
|
+
preview: new TextDecoder().decode(new TextEncoder().encode(content).slice(0, previewBytes)),
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async commitKernelApply(runtime, pending, event) {
|
|
154
|
+
return canonicalKernelApply(runtime, pending, event);
|
|
155
|
+
}
|
|
156
|
+
async commitKernelMaybeAction(runtime, pending, event) {
|
|
157
|
+
return canonicalKernelMaybeAction(runtime, pending, event);
|
|
158
|
+
}
|
|
159
|
+
async commitKernelAction(runtime, pending, event) {
|
|
160
|
+
return canonicalKernelAction(runtime, pending, event);
|
|
161
|
+
}
|
|
88
162
|
get hostOptions() { return this.opts; }
|
|
89
163
|
interrupt(reason = "user") {
|
|
90
164
|
this.interrupted = true;
|
|
@@ -139,7 +213,7 @@ export class RuntimeRunner {
|
|
|
139
213
|
async consumeInboundSignal(delivery, consume) {
|
|
140
214
|
try {
|
|
141
215
|
const observationStart = this.pendingObservations.length;
|
|
142
|
-
const result = consume(delivery);
|
|
216
|
+
const result = await consume(delivery);
|
|
143
217
|
const dispositions = this.pendingObservations.slice(observationStart).filter(observation => observation.kind === "signal_delivery_disposed"
|
|
144
218
|
&& observation.delivery_id === delivery.deliveryId
|
|
145
219
|
&& observation.attempt === delivery.deliveryAttempt);
|
|
@@ -157,17 +231,32 @@ export class RuntimeRunner {
|
|
|
157
231
|
}
|
|
158
232
|
async *run(req) {
|
|
159
233
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
160
|
-
const
|
|
234
|
+
const resumedStart = [...prior].reverse().find(entry => entry.event.kind === "run_started");
|
|
235
|
+
// Inherited parent events are transcript input for a fresh child operation, never recovery
|
|
236
|
+
// evidence for the child's own canonical journal.
|
|
237
|
+
let midRun = req.inheritEvents ? false : isMidRun(prior);
|
|
238
|
+
if (!midRun && resumedStart?.event.kind === "run_started" && !req.inheritEvents) {
|
|
239
|
+
const operationId = `wasm-operation-${resumedStart.event.run_id}`;
|
|
240
|
+
if (await this.resolveKernelJournal().head(operationId)) {
|
|
241
|
+
const authoritative = await this.createCanonicalRuntime(resumedStart.event.run_id, req.sessionId);
|
|
242
|
+
await authoritative.restore();
|
|
243
|
+
if (!authoritative.isTerminal())
|
|
244
|
+
midRun = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
161
247
|
// Idempotent per session: an earlier run's `run_started` already carries these attachments
|
|
162
248
|
// (same-session retry attempt), so replay reconstructs them — recording and seeding again
|
|
163
249
|
// would double them in history. Deduping at the append keeps live and replay in agreement.
|
|
164
250
|
const attachments = req.attachments?.length && !attachmentsAlreadySeeded(prior, req.attachments)
|
|
165
251
|
? req.attachments
|
|
166
252
|
: undefined;
|
|
253
|
+
const runId = midRun && resumedStart?.event.kind === "run_started"
|
|
254
|
+
? resumedStart.event.run_id
|
|
255
|
+
: crypto.randomUUID();
|
|
167
256
|
if (!midRun) {
|
|
168
257
|
await this.opts.sessionLog.append(req.sessionId, {
|
|
169
258
|
kind: "run_started",
|
|
170
|
-
run_id:
|
|
259
|
+
run_id: runId,
|
|
171
260
|
goal: req.goal,
|
|
172
261
|
criteria: req.criteria ?? [],
|
|
173
262
|
agent_id: this.opts.agentId,
|
|
@@ -175,46 +264,42 @@ export class RuntimeRunner {
|
|
|
175
264
|
...(attachments ? { attachments } : {}),
|
|
176
265
|
});
|
|
177
266
|
}
|
|
178
|
-
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, attachments);
|
|
267
|
+
yield* this.execute(req.sessionId, runId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, attachments);
|
|
179
268
|
}
|
|
180
269
|
async *wake(sessionId, extensions) {
|
|
181
270
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const startEntry = [...events].reverse().find(e => e.event.kind === "run_started");
|
|
271
|
+
const startIndex = events.reduce((latest, entry, index) => entry.event.kind === "run_started" ? index : latest, -1);
|
|
272
|
+
const startEntry = startIndex >= 0 ? events[startIndex] : undefined;
|
|
185
273
|
if (!startEntry)
|
|
186
274
|
throw new Error(`No run_started event for session: ${sessionId}`);
|
|
187
275
|
const start = startEntry.event;
|
|
188
|
-
|
|
276
|
+
const journalHead = await this.resolveKernelJournal().head(`wasm-operation-${start.run_id}`);
|
|
277
|
+
if (!journalHead) {
|
|
278
|
+
const projectedTail = events.slice(startIndex + 1);
|
|
279
|
+
if (projectedTail.some(e => e.event.kind === "run_terminal")) {
|
|
280
|
+
throw new Error("run_terminal projection has no canonical journal");
|
|
281
|
+
}
|
|
282
|
+
if (projectedTail.some(entry => entry.event.kind === "tool_requested" || entry.event.kind === "tool_completed")) {
|
|
283
|
+
throw new Error("restored canonical operation has no pending effect or terminal");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const authoritative = await this.createCanonicalRuntime(start.run_id, sessionId);
|
|
288
|
+
await authoritative.restore();
|
|
289
|
+
if (authoritative.isTerminal())
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
yield* this.execute(sessionId, start.run_id, start.goal, start.criteria, extensions, events, true, start.attachments);
|
|
189
293
|
}
|
|
190
294
|
async writeMemory(memory, sessionId) {
|
|
191
295
|
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
192
296
|
return;
|
|
193
|
-
const kernel = await getKernel();
|
|
194
|
-
const runtime = new kernel.KernelRuntime({ maxTokens: this.opts.maxTokens, maxTurns: this.opts.maxTurns ?? 25 });
|
|
195
|
-
const observations = [];
|
|
196
|
-
const action = kernelMaybeAction(runtime, observations, { kind: "write_memory", memory });
|
|
197
|
-
if (!action) {
|
|
198
|
-
await this.appendMemorySyscallObservations(sessionId, observations);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (action.kind !== "persist_memory")
|
|
202
|
-
throw new Error(`write_memory returned unexpected kernel effect: ${action.kind}`);
|
|
203
|
-
let error;
|
|
204
297
|
try {
|
|
205
|
-
await this.opts.dreamStore.upsert(this.opts.agentId,
|
|
298
|
+
await this.opts.dreamStore.upsert(this.opts.agentId, memory);
|
|
206
299
|
}
|
|
207
300
|
catch (cause) {
|
|
208
|
-
|
|
301
|
+
throw new Error(formatToolError(cause));
|
|
209
302
|
}
|
|
210
|
-
kernelApply(runtime, observations, {
|
|
211
|
-
kind: "memory_persist_result",
|
|
212
|
-
effect_id: action.effectId,
|
|
213
|
-
...(error ? { error } : {}),
|
|
214
|
-
});
|
|
215
|
-
await this.appendMemorySyscallObservations(sessionId, observations);
|
|
216
|
-
if (error)
|
|
217
|
-
throw new Error(error);
|
|
218
303
|
}
|
|
219
304
|
async appendMemorySyscallObservations(sessionId, observations) {
|
|
220
305
|
if (!sessionId)
|
|
@@ -231,10 +316,10 @@ export class RuntimeRunner {
|
|
|
231
316
|
* K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
|
|
232
317
|
* compaction/renewal boundary) instead of appending a duplicate. `opts.pinned` exempts the
|
|
233
318
|
* entry from the knowledge-budget sweep. */
|
|
234
|
-
pushKnowledge(message, tokens, opts) {
|
|
319
|
+
async pushKnowledge(message, tokens, opts) {
|
|
235
320
|
if (!this.activeKernel)
|
|
236
321
|
return;
|
|
237
|
-
|
|
322
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, {
|
|
238
323
|
kind: "add_knowledge_message",
|
|
239
324
|
content: message.content ?? "",
|
|
240
325
|
tokens: tokens ?? Math.max(1, Math.ceil((message.content?.length ?? 0) / 4)),
|
|
@@ -242,19 +327,25 @@ export class RuntimeRunner {
|
|
|
242
327
|
...(opts?.pinned ? { pinned: true } : {}),
|
|
243
328
|
});
|
|
244
329
|
}
|
|
330
|
+
payloadStore() {
|
|
331
|
+
if (this.opts.payloadStore)
|
|
332
|
+
return this.opts.payloadStore;
|
|
333
|
+
this.fallbackPayloadStore ??= new PayloadStore();
|
|
334
|
+
return this.fallbackPayloadStore;
|
|
335
|
+
}
|
|
245
336
|
/** K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
|
|
246
337
|
* Errs-open: an unknown key is a kernel-side no-op. */
|
|
247
|
-
removeKnowledge(key) {
|
|
338
|
+
async removeKnowledge(key) {
|
|
248
339
|
if (!this.activeKernel)
|
|
249
340
|
return;
|
|
250
|
-
|
|
341
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, { kind: "remove_knowledge", key });
|
|
251
342
|
}
|
|
252
343
|
/** K3: host-driven skill deactivation — toolset re-widens at the next provider call, the
|
|
253
344
|
* skill's knowledge pin drops at the next boundary. Errs-open: not-active is a no-op. */
|
|
254
|
-
deactivateSkill(name) {
|
|
345
|
+
async deactivateSkill(name) {
|
|
255
346
|
if (!this.activeKernel)
|
|
256
347
|
return;
|
|
257
|
-
|
|
348
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, { kind: "skill_deactivated", name });
|
|
258
349
|
this.knowledgePushedSkills.delete(name);
|
|
259
350
|
}
|
|
260
351
|
async resolveKernelSuspend(requests, runtime, sessionId) {
|
|
@@ -334,63 +425,7 @@ export class RuntimeRunner {
|
|
|
334
425
|
}
|
|
335
426
|
return { approved, denied, events };
|
|
336
427
|
}
|
|
337
|
-
|
|
338
|
-
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
339
|
-
* output. Resolution order: (a) the result spool committed by `large_result_spool_result`,
|
|
340
|
-
* (b) a session-log scan for the original `tool_completed` event carrying that `call_id`.
|
|
341
|
-
* Slices the resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
342
|
-
*/
|
|
343
|
-
async resolveReadResult(sessionId, argsJson) {
|
|
344
|
-
let callId = "";
|
|
345
|
-
let offset = 0;
|
|
346
|
-
let maxBytes = 4000;
|
|
347
|
-
try {
|
|
348
|
-
const args = JSON.parse(argsJson || "{}");
|
|
349
|
-
callId = typeof args.call_id === "string" ? args.call_id : "";
|
|
350
|
-
if (typeof args.offset === "number" && Number.isFinite(args.offset))
|
|
351
|
-
offset = args.offset;
|
|
352
|
-
if (typeof args.max_bytes === "number" && Number.isFinite(args.max_bytes))
|
|
353
|
-
maxBytes = args.max_bytes;
|
|
354
|
-
}
|
|
355
|
-
catch {
|
|
356
|
-
// malformed arguments — callId stays empty, falls through to "not found" below
|
|
357
|
-
}
|
|
358
|
-
let full;
|
|
359
|
-
if (full === undefined && this.opts.resultSpool) {
|
|
360
|
-
try {
|
|
361
|
-
full = await this.opts.resultSpool.findByCallId(sessionId, callId);
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
full = undefined;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
if (full === undefined) {
|
|
368
|
-
try {
|
|
369
|
-
const events = await this.opts.sessionLog.read(sessionId);
|
|
370
|
-
for (const { event } of events) {
|
|
371
|
-
if (event.kind !== "tool_completed")
|
|
372
|
-
continue;
|
|
373
|
-
const match = event.results.find(r => r.call_id === callId);
|
|
374
|
-
if (match)
|
|
375
|
-
full = match.output;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
catch {
|
|
379
|
-
full = undefined;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (full === undefined) {
|
|
383
|
-
return { text: `no stored output for call_id "${callId}"`, isError: true };
|
|
384
|
-
}
|
|
385
|
-
const start = Math.max(0, offset);
|
|
386
|
-
const end = Math.min(full.length, start + Math.max(0, maxBytes));
|
|
387
|
-
const slice = full.slice(start, end);
|
|
388
|
-
return {
|
|
389
|
-
text: `[read_result ${callId}: chars ${start}–${end} of ${full.length}]\n${slice}`,
|
|
390
|
-
isError: false,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments) {
|
|
428
|
+
async *execute(sessionId, runId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments) {
|
|
394
429
|
this.interrupted = false;
|
|
395
430
|
this.cancellationReason = undefined;
|
|
396
431
|
this.abortController = new AbortController();
|
|
@@ -398,147 +433,137 @@ export class RuntimeRunner {
|
|
|
398
433
|
this.pendingPageOutArchives = [];
|
|
399
434
|
this.activePageOutArchive = undefined;
|
|
400
435
|
this.currentSessionId = sessionId;
|
|
401
|
-
const kernel = await getKernel();
|
|
402
436
|
const ext = { ...this.opts.extensions, ...(extensions ?? {}) };
|
|
403
437
|
const providerState = this.opts.provider.createRunState?.();
|
|
404
438
|
let nextCompressedArchiveStart = nextArchivedSeqStart(priorEvents);
|
|
405
439
|
const providerPolicy = this.opts.provider.runtimePolicy?.() ?? {};
|
|
406
440
|
const effectiveMaxTurns = this.opts.maxTurns ?? providerPolicy.maxTurns ?? 25;
|
|
407
441
|
const effectiveTimeoutMs = this.opts.timeoutMs ?? providerPolicy.timeoutMs;
|
|
408
|
-
const runtime =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// M4/G5: per-node token cap → child run's cumulative token budget (wasm LoopPolicy.maxTotalTokens is f64).
|
|
412
|
-
...(this.opts.maxTotalTokens !== undefined ? { maxTotalTokens: this.opts.maxTotalTokens } : {}),
|
|
413
|
-
timeoutMs: effectiveTimeoutMs,
|
|
414
|
-
});
|
|
442
|
+
const runtime = await this.createCanonicalRuntime(runId, sessionId);
|
|
443
|
+
if (resumeMidRun)
|
|
444
|
+
await runtime.restore();
|
|
415
445
|
this.activeKernel = runtime;
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
if (this.opts.enablePlanTool !== undefined) {
|
|
423
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
424
|
-
kind: "set_plan_tool_enabled",
|
|
425
|
-
enabled: this.opts.enablePlanTool,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
429
|
-
kind: "set_tools",
|
|
430
|
-
tools: this.opts.executionPlane.schemas().map(toolSchemaToKernel),
|
|
431
|
-
});
|
|
432
|
-
if (this.opts.systemPrompt) {
|
|
433
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
434
|
-
kind: "add_system_message",
|
|
435
|
-
content: this.opts.systemPrompt,
|
|
436
|
-
tokens: Math.max(1, Math.ceil(this.opts.systemPrompt.length / 4)),
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
if (this.opts.initialMemory) {
|
|
440
|
-
for (const mem of this.opts.initialMemory) {
|
|
441
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
442
|
-
kind: "add_knowledge_message",
|
|
443
|
-
content: mem,
|
|
444
|
-
tokens: Math.max(1, Math.ceil(mem.length / 4)),
|
|
446
|
+
if (!resumeMidRun) {
|
|
447
|
+
if (this.opts.tokenizer) {
|
|
448
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
449
|
+
kind: "set_tokenizer",
|
|
450
|
+
name: this.opts.tokenizer,
|
|
445
451
|
});
|
|
446
452
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
skills: metas.map(skillMetadataToKernel),
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
// P1-B/D: configure stable-core tool ids (always exposed under skill gating).
|
|
460
|
-
if (this.opts.stableCoreToolIds?.length) {
|
|
461
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
462
|
-
kind: "set_stable_core_tools",
|
|
463
|
-
tool_ids: this.opts.stableCoreToolIds,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
if (this.opts.dreamStore && this.opts.agentId) {
|
|
467
|
-
kernelApply(runtime, this.pendingObservations, { kind: "set_memory_enabled", enabled: true });
|
|
468
|
-
}
|
|
469
|
-
if (this.opts.knowledgeSource) {
|
|
470
|
-
kernelApply(runtime, this.pendingObservations, { kind: "set_knowledge_enabled", enabled: true });
|
|
471
|
-
}
|
|
472
|
-
if (this.opts.milestoneContract) {
|
|
473
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
474
|
-
kind: "load_milestone_contract",
|
|
475
|
-
contract: {
|
|
476
|
-
phases: this.opts.milestoneContract.phases.map(p => ({
|
|
477
|
-
id: p.id,
|
|
478
|
-
criteria: p.criteria ?? [],
|
|
479
|
-
unlocks: p.unlocks ?? [],
|
|
480
|
-
verifier: p.verifier ?? null,
|
|
481
|
-
required_evidence: p.requiredEvidence ?? [],
|
|
482
|
-
})),
|
|
483
|
-
},
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
const maxBytes = runtime.recoveryContentBytes();
|
|
487
|
-
if (priorEvents && priorEvents.length > 0) {
|
|
488
|
-
const repaired = repairEventsForRecovery(priorEvents, maxBytes);
|
|
489
|
-
seedProviderReplayFromEvents(this.opts.provider, repaired);
|
|
490
|
-
const replayed = await replayMessages(repaired, maxBytes, this.opts.compressionStore);
|
|
491
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
492
|
-
kind: "preload_history",
|
|
493
|
-
messages: replayed.map(messageToKernelMessage),
|
|
453
|
+
if (this.opts.enablePlanTool !== undefined) {
|
|
454
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
455
|
+
kind: "set_plan_tool_enabled",
|
|
456
|
+
enabled: this.opts.enablePlanTool,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
460
|
+
kind: "set_tools",
|
|
461
|
+
tools: this.opts.executionPlane.schemas().map(toolSchemaToKernel),
|
|
494
462
|
});
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
463
|
+
if (this.opts.systemPrompt) {
|
|
464
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
465
|
+
kind: "add_system_message",
|
|
466
|
+
content: this.opts.systemPrompt,
|
|
467
|
+
tokens: Math.max(1, Math.ceil(this.opts.systemPrompt.length / 4)),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (this.opts.initialMemory) {
|
|
471
|
+
for (const mem of this.opts.initialMemory) {
|
|
472
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
473
|
+
kind: "add_knowledge_message",
|
|
474
|
+
content: mem,
|
|
475
|
+
tokens: Math.max(1, Math.ceil(mem.length / 4)),
|
|
476
|
+
});
|
|
504
477
|
}
|
|
505
478
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
479
|
+
if (this.opts.skillContentMap && this.opts.skillContentMap.size > 0) {
|
|
480
|
+
const metas = [...this.opts.skillContentMap.keys()].map(name => ({
|
|
481
|
+
name,
|
|
482
|
+
description: "",
|
|
483
|
+
estimatedTokens: 0,
|
|
484
|
+
}));
|
|
485
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
486
|
+
kind: "set_available_skills",
|
|
487
|
+
skills: metas.map(skillMetadataToKernel),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
// P1-B/D: configure stable-core tool ids (always exposed under skill gating).
|
|
491
|
+
if (this.opts.stableCoreToolIds?.length) {
|
|
492
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
493
|
+
kind: "set_stable_core_tools",
|
|
494
|
+
tool_ids: this.opts.stableCoreToolIds,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
if (this.opts.dreamStore && this.opts.agentId) {
|
|
498
|
+
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_memory_enabled", enabled: true });
|
|
499
|
+
}
|
|
500
|
+
if (this.opts.knowledgeSource) {
|
|
501
|
+
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_knowledge_enabled", enabled: true });
|
|
502
|
+
}
|
|
503
|
+
if (this.opts.milestoneContract) {
|
|
504
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
505
|
+
kind: "load_milestone_contract",
|
|
506
|
+
contract: {
|
|
507
|
+
phases: this.opts.milestoneContract.phases.map(p => ({
|
|
508
|
+
id: p.id,
|
|
509
|
+
criteria: p.criteria ?? [],
|
|
510
|
+
unlocks: p.unlocks ?? [],
|
|
511
|
+
verifier: p.verifier ?? null,
|
|
512
|
+
required_evidence: p.requiredEvidence ?? [],
|
|
513
|
+
})),
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
const maxBytes = runtime.recoveryContentBytes();
|
|
518
|
+
if (priorEvents && priorEvents.length > 0) {
|
|
519
|
+
const replayed = await replayMessages(priorEvents, maxBytes, this.opts.compressionStore);
|
|
520
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
521
|
+
kind: "preload_history",
|
|
522
|
+
messages: replayed.map(messageToKernelMessage),
|
|
523
|
+
});
|
|
524
|
+
// Re-pin replayed skill content into knowledge for the fresh operation. Activation itself is
|
|
525
|
+
// owned by canonical provider-result reduction; the host must not assert it separately.
|
|
526
|
+
const toolResultByCallId = new Map();
|
|
527
|
+
for (const m of replayed) {
|
|
528
|
+
for (const part of m.contentParts ?? []) {
|
|
529
|
+
if (part.type === "tool_result" && part.callId && part.output !== undefined) {
|
|
530
|
+
toolResultByCallId.set(part.callId, part.output);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
for (const m of replayed) {
|
|
535
|
+
for (const tc of m.toolCalls ?? []) {
|
|
536
|
+
if (tc.name !== "skill")
|
|
513
537
|
continue;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
name
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
538
|
+
try {
|
|
539
|
+
const name = JSON.parse(tc.arguments || "{}").name;
|
|
540
|
+
if (!name)
|
|
541
|
+
continue;
|
|
542
|
+
const output = toolResultByCallId.get(tc.id);
|
|
543
|
+
if (output && !this.knowledgePushedSkills.has(name)) {
|
|
544
|
+
this.knowledgePushedSkills.add(name);
|
|
545
|
+
// K1: keyed — the kernel-side upsert is the authoritative dedup across wake replays.
|
|
546
|
+
this.pushKnowledge({ role: "system", content: output }, undefined, { key: `skill:${name}` });
|
|
547
|
+
}
|
|
524
548
|
}
|
|
549
|
+
catch { /* skip */ }
|
|
525
550
|
}
|
|
526
|
-
catch { /* skip */ }
|
|
527
551
|
}
|
|
528
552
|
}
|
|
553
|
+
// Multimodal upload: seed attachments before the canonical root start (parity with Node/Python).
|
|
554
|
+
if (!resumeMidRun && attachments?.length) {
|
|
555
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
556
|
+
kind: "add_history_message",
|
|
557
|
+
message: attachmentsToKernelMessage(attachments),
|
|
558
|
+
});
|
|
559
|
+
}
|
|
529
560
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
533
|
-
kind: "add_history_message",
|
|
534
|
-
message: attachmentsToKernelMessage(attachments),
|
|
535
|
-
});
|
|
561
|
+
if (priorEvents && priorEvents.length > 0) {
|
|
562
|
+
seedProviderReplayFromEvents(this.opts.provider, priorEvents);
|
|
536
563
|
}
|
|
537
564
|
const sessionStart = Date.now();
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
task: { goal, criteria },
|
|
541
|
-
};
|
|
565
|
+
const startTask = { goal, criteria };
|
|
566
|
+
let startRunSpec;
|
|
542
567
|
// P0-A: lower an explicit `runSpec`, the `allowedToolIds` ceiling, and/or the `baselineToolIds`
|
|
543
568
|
// pre-activation surface to the kernel run spec (reuses the existing run_spec wire — no new
|
|
544
569
|
// ABI). Unset on all ⇒ no gating (铁律: no config = old behavior).
|
|
@@ -559,9 +584,10 @@ export class RuntimeRunner {
|
|
|
559
584
|
: baseSpec;
|
|
560
585
|
if (hasBaseline)
|
|
561
586
|
spec = { ...spec, exposureBaseline: baselineToolIds };
|
|
562
|
-
|
|
587
|
+
startRunSpec = agentRunSpecToKernel(spec);
|
|
563
588
|
}
|
|
564
|
-
|
|
589
|
+
if (!resumeMidRun)
|
|
590
|
+
await this.applyKernelPolicies(runtime);
|
|
565
591
|
// I4: pre-fetch memory before the first LLM turn (mirrors Node). Strict dynamic context
|
|
566
592
|
// control: single-use retrieval content, not a stable skill — lands in `history` like an
|
|
567
593
|
// ordinary `memory` tool result, so it decays with the compression pyramid instead of
|
|
@@ -571,8 +597,8 @@ export class RuntimeRunner {
|
|
|
571
597
|
await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
572
598
|
}
|
|
573
599
|
let action = resumeMidRun
|
|
574
|
-
?
|
|
575
|
-
:
|
|
600
|
+
? runtime.resumeAction() ?? (() => { throw new Error("restored canonical operation has no pending effect or terminal"); })()
|
|
601
|
+
: await canonicalStartAgent(runtime, this.pendingObservations, startTask, startRunSpec);
|
|
576
602
|
// P0-C: the skill loaded and in effect going into the current turn → per-turn `activeSkill` metric.
|
|
577
603
|
let activeSkill;
|
|
578
604
|
// I0b: kernel-throw safety net — see Node runner for full rationale.
|
|
@@ -580,7 +606,7 @@ export class RuntimeRunner {
|
|
|
580
606
|
while (!runtime.isTerminal()) {
|
|
581
607
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
582
608
|
if (this.interrupted) {
|
|
583
|
-
action =
|
|
609
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
584
610
|
kind: "cancel_operation",
|
|
585
611
|
reason: this.cancellationReason ?? "user",
|
|
586
612
|
pending_call_ids: pendingCallIds(action),
|
|
@@ -590,7 +616,7 @@ export class RuntimeRunner {
|
|
|
590
616
|
if (this.opts.signalSource || this.injectedSignals.length > 0) {
|
|
591
617
|
const delivery = await this.nextInboundSignal();
|
|
592
618
|
if (delivery) {
|
|
593
|
-
const sigAction = await this.consumeInboundSignal(delivery, claimed =>
|
|
619
|
+
const sigAction = await this.consumeInboundSignal(delivery, claimed => this.commitKernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(claimed)));
|
|
594
620
|
if (sigAction)
|
|
595
621
|
action = sigAction;
|
|
596
622
|
// Critical attention/preemption is distinct from operation cancellation.
|
|
@@ -599,12 +625,6 @@ export class RuntimeRunner {
|
|
|
599
625
|
if (runtime.isTerminal())
|
|
600
626
|
break;
|
|
601
627
|
if (action.kind === "call_provider") {
|
|
602
|
-
// M5 v2.1: top-level auto-pivot at the safe point (kernel in Reason, not suspended). Loop-top
|
|
603
|
-
// placement catches every path to `call_provider` (incl. post-approval-resume), so a queued
|
|
604
|
-
// authored spec is never stranded. Drains the queue; fires once per authored batch.
|
|
605
|
-
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
606
|
-
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
607
|
-
}
|
|
608
628
|
const providerEffectId = action.effectId;
|
|
609
629
|
const finalToolCalls = [];
|
|
610
630
|
let finalText = "";
|
|
@@ -675,7 +695,7 @@ export class RuntimeRunner {
|
|
|
675
695
|
// compacted context, or `done` to terminate with an honest `ContextOverflow`. The
|
|
676
696
|
// classify + compact + retry + give-up policy lives in the kernel (one place), not
|
|
677
697
|
// duplicated across the four SDK runners.
|
|
678
|
-
action =
|
|
698
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
679
699
|
kind: "provider_error",
|
|
680
700
|
effect_id: providerEffectId,
|
|
681
701
|
message: formatToolError(err),
|
|
@@ -693,7 +713,7 @@ export class RuntimeRunner {
|
|
|
693
713
|
}
|
|
694
714
|
// #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
|
|
695
715
|
if (abortSignal?.aborted) {
|
|
696
|
-
action =
|
|
716
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
697
717
|
kind: "cancel_operation",
|
|
698
718
|
reason: this.cancellationReason ?? "user",
|
|
699
719
|
pending_call_ids: [providerEffectId],
|
|
@@ -712,10 +732,9 @@ export class RuntimeRunner {
|
|
|
712
732
|
message: messageToKernelMessage(assistantMessage),
|
|
713
733
|
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
714
734
|
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
715
|
-
now_ms: Date.now(),
|
|
716
735
|
...(turnStopReason ? { stop_reason: turnStopReason } : {}),
|
|
717
736
|
};
|
|
718
|
-
action =
|
|
737
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
|
|
719
738
|
const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
|
|
720
739
|
await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
|
|
721
740
|
turn: runtime.turn(),
|
|
@@ -755,7 +774,7 @@ export class RuntimeRunner {
|
|
|
755
774
|
const resolved = await this.resolveKernelSuspend(action.requests, runtime, sessionId);
|
|
756
775
|
for (const evt of resolved.events)
|
|
757
776
|
yield evt;
|
|
758
|
-
action =
|
|
777
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
759
778
|
kind: "approval_result",
|
|
760
779
|
effect_id: action.effectId,
|
|
761
780
|
approved_calls: resolved.approved,
|
|
@@ -772,7 +791,7 @@ export class RuntimeRunner {
|
|
|
772
791
|
catch (cause) {
|
|
773
792
|
error = formatToolError(cause);
|
|
774
793
|
}
|
|
775
|
-
action =
|
|
794
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
776
795
|
kind: "memory_persist_result",
|
|
777
796
|
effect_id: action.effectId,
|
|
778
797
|
...(error ? { error } : {}),
|
|
@@ -792,7 +811,7 @@ export class RuntimeRunner {
|
|
|
792
811
|
error = formatToolError(cause);
|
|
793
812
|
}
|
|
794
813
|
const obsStart = this.pendingObservations.length;
|
|
795
|
-
action =
|
|
814
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
796
815
|
kind: "memory_query_result",
|
|
797
816
|
effect_id: action.effectId,
|
|
798
817
|
hits,
|
|
@@ -808,23 +827,6 @@ export class RuntimeRunner {
|
|
|
808
827
|
}
|
|
809
828
|
}
|
|
810
829
|
}
|
|
811
|
-
else if (action.kind === "spool_large_result") {
|
|
812
|
-
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
813
|
-
let spoolRef;
|
|
814
|
-
let error;
|
|
815
|
-
try {
|
|
816
|
-
spoolRef = await spool.persistOutput(sessionId, action.callId, action.output);
|
|
817
|
-
}
|
|
818
|
-
catch (cause) {
|
|
819
|
-
error = formatToolError(cause);
|
|
820
|
-
}
|
|
821
|
-
action = kernelAction(runtime, this.pendingObservations, {
|
|
822
|
-
kind: "large_result_spool_result",
|
|
823
|
-
effect_id: action.effectId,
|
|
824
|
-
...(spoolRef ? { spool_ref: spoolRef } : {}),
|
|
825
|
-
...(error ? { error } : {}),
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
830
|
else if (action.kind === "archive_page_out") {
|
|
829
831
|
const archiveMeta = this.activePageOutArchive
|
|
830
832
|
?? this.pendingPageOutArchives.shift()
|
|
@@ -836,6 +838,10 @@ export class RuntimeRunner {
|
|
|
836
838
|
if (this.opts.compressionStore) {
|
|
837
839
|
archiveRef = await this.opts.compressionStore.write(sessionId, archiveMeta.archiveStart, action.archived);
|
|
838
840
|
}
|
|
841
|
+
if (action.payload) {
|
|
842
|
+
archiveRef ??= `payload:${action.payload.digest.replace(/^sha256:/, "").slice(0, 32)}`;
|
|
843
|
+
await this.payloadStore().persistPayload(sessionId, archiveRef, action.payload.content);
|
|
844
|
+
}
|
|
839
845
|
}
|
|
840
846
|
catch (cause) {
|
|
841
847
|
error = formatToolError(cause);
|
|
@@ -845,7 +851,7 @@ export class RuntimeRunner {
|
|
|
845
851
|
const archiveTier = action.tier;
|
|
846
852
|
if (!error)
|
|
847
853
|
this.activePageOutArchive = undefined;
|
|
848
|
-
action =
|
|
854
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
849
855
|
kind: "page_out_archive_result",
|
|
850
856
|
effect_id: action.effectId,
|
|
851
857
|
...(archiveRef ? { archive_ref: archiveRef } : {}),
|
|
@@ -855,6 +861,25 @@ export class RuntimeRunner {
|
|
|
855
861
|
void this.archiveSemanticPageOut(archived, archiveAction);
|
|
856
862
|
}
|
|
857
863
|
}
|
|
864
|
+
else if (action.kind === "load_payload") {
|
|
865
|
+
const content = await this.payloadStore().loadPayload(sessionId, action.payloadRef);
|
|
866
|
+
action = content === undefined
|
|
867
|
+
? await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
868
|
+
kind: "payload_load_failed",
|
|
869
|
+
effect_id: action.effectId,
|
|
870
|
+
error: `payload not found for opaque locator ${action.payloadRef}`,
|
|
871
|
+
})
|
|
872
|
+
: await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
873
|
+
kind: "payload_loaded",
|
|
874
|
+
effect_id: action.effectId,
|
|
875
|
+
handle_id: action.handleId,
|
|
876
|
+
payload: {
|
|
877
|
+
content,
|
|
878
|
+
digest: canonicalSha256(content),
|
|
879
|
+
original_size: String(new TextEncoder().encode(content).byteLength),
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
}
|
|
858
883
|
else if (action.kind === "execute_tool") {
|
|
859
884
|
const toolEffectId = action.effectId;
|
|
860
885
|
const allCalls = action.calls;
|
|
@@ -869,55 +894,25 @@ export class RuntimeRunner {
|
|
|
869
894
|
onPermissionRequest: this.opts.onPermissionRequest,
|
|
870
895
|
};
|
|
871
896
|
const toolResults = [];
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
// the orchestrator collects them and `runWorkflow` sends them to the parent kernel.
|
|
875
|
-
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path.
|
|
897
|
+
// Syscall tools are consumed by core from the provider result and must never escape as host
|
|
898
|
+
// tool effects. Keep an explicit invariant check below so a projection drift fails closed.
|
|
876
899
|
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
877
|
-
// O7: `read_result` re-fetches a tool output the kernel evicted from context. Content is
|
|
878
|
-
// host-resolved: (a) this turn's in-memory pending spool map, (b) the on-disk result spool,
|
|
879
|
-
// (c) a session-log scan for the original `tool_completed` event.
|
|
880
|
-
const readResultCalls = allCalls.filter(c => c.name === "read_result");
|
|
881
900
|
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
882
|
-
const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes" && c.name !== "start_workflow"
|
|
901
|
+
const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes" && c.name !== "start_workflow"
|
|
883
902
|
&& c.name !== "update_plan");
|
|
884
903
|
// `update_plan` is a kernel meta-tool (exposed via `enablePlanTool`), not a registered
|
|
885
904
|
// plane tool — resolve it here as an `update_task` apply, mirroring the node/python runners.
|
|
886
905
|
for (const call of planCalls) {
|
|
887
906
|
const update = parseUpdatePlanArgs(call.arguments);
|
|
888
|
-
|
|
907
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
889
908
|
kind: "update_task",
|
|
890
909
|
update: taskUpdateToKernel(update),
|
|
891
910
|
});
|
|
892
911
|
toolResults.push({ callId: call.id, output: "success", isError: false });
|
|
893
912
|
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
894
913
|
}
|
|
895
|
-
for (const call of readResultCalls) {
|
|
896
|
-
const out = await this.resolveReadResult(sessionId, call.arguments);
|
|
897
|
-
toolResults.push({ callId: call.id, output: out.text, isError: out.isError });
|
|
898
|
-
yield { type: "tool_result", callId: call.id, content: out.text, isError: out.isError };
|
|
899
|
-
}
|
|
900
914
|
for (const call of submitCalls) {
|
|
901
|
-
|
|
902
|
-
// spec and AUTO-PIVOT once this tool turn resolves. A workflow-NODE's `start_workflow` (and
|
|
903
|
-
// every `submit_workflow_nodes`) instead FLATTENS for the parent `runWorkflow` to append.
|
|
904
|
-
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
905
|
-
const spec = parseStartWorkflowSpec(call.arguments);
|
|
906
|
-
if (spec) {
|
|
907
|
-
this.pendingAuthoredWorkflows.push(spec);
|
|
908
|
-
const out = "workflow submitted for governance adjudication";
|
|
909
|
-
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
910
|
-
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
const nodes = call.name === "start_workflow"
|
|
915
|
-
? parseStartWorkflowArgs(call.arguments)
|
|
916
|
-
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
917
|
-
yield { type: "workflow_nodes_submitted", nodes };
|
|
918
|
-
const out = "workflow nodes submitted for parent governance adjudication";
|
|
919
|
-
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
920
|
-
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
915
|
+
throw new Error(`canonical kernel published model syscall ${call.name} as a host tool effect`);
|
|
921
916
|
}
|
|
922
917
|
// O5 (PreToolUse-hook analog): stateful host veto over each kernel-approved call.
|
|
923
918
|
// A blocked call never executes; its reason reaches the model as a denied result.
|
|
@@ -1036,9 +1031,10 @@ export class RuntimeRunner {
|
|
|
1036
1031
|
token_count: r.tokenCount,
|
|
1037
1032
|
})),
|
|
1038
1033
|
});
|
|
1039
|
-
//
|
|
1034
|
+
// Canonical provider-result reduction activates a successfully resolved `skill` call. The
|
|
1035
|
+
// host only pins its METHOD content — how to do something — for later turns.
|
|
1040
1036
|
//
|
|
1041
|
-
// Strict dynamic context control:
|
|
1037
|
+
// Strict dynamic context control: the skill text
|
|
1042
1038
|
// for the rest of the run, unlike a one-off memory/knowledge lookup (fact content, relevant
|
|
1043
1039
|
// for the moment it's used). So its text ALSO goes into the durable `knowledge` slot here
|
|
1044
1040
|
// (in addition to the ordinary tool_result already headed for `history`, where it will decay
|
|
@@ -1053,11 +1049,6 @@ export class RuntimeRunner {
|
|
|
1053
1049
|
const name = JSON.parse(call.arguments || "{}").name;
|
|
1054
1050
|
if (!name)
|
|
1055
1051
|
continue;
|
|
1056
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1057
|
-
kind: "skill_activated",
|
|
1058
|
-
name,
|
|
1059
|
-
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
1060
|
-
});
|
|
1061
1052
|
// With a lease configured, skip the Set optimization: an expired-then-reloaded skill
|
|
1062
1053
|
// must re-pin — only the kernel knows the lease state; its upsert dedupes anyway.
|
|
1063
1054
|
if (this.opts.skillLeaseTurns !== undefined || !this.knowledgePushedSkills.has(name)) {
|
|
@@ -1069,7 +1060,7 @@ export class RuntimeRunner {
|
|
|
1069
1060
|
catch { /* skip */ }
|
|
1070
1061
|
}
|
|
1071
1062
|
const entropyObsStart = this.pendingObservations.length;
|
|
1072
|
-
action =
|
|
1063
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1073
1064
|
kind: "tool_results",
|
|
1074
1065
|
effect_id: toolEffectId,
|
|
1075
1066
|
results: toolResults.map(toolResultToKernel),
|
|
@@ -1100,9 +1091,11 @@ export class RuntimeRunner {
|
|
|
1100
1091
|
}
|
|
1101
1092
|
}
|
|
1102
1093
|
else if (action.kind === "evaluate_milestone") {
|
|
1094
|
+
const milestoneEffectId = action.effectId;
|
|
1095
|
+
const milestonePhaseId = action.phaseId;
|
|
1103
1096
|
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
1104
1097
|
if (milestonePolicy === "auto_pass") {
|
|
1105
|
-
action =
|
|
1098
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1106
1099
|
kind: "milestone_result",
|
|
1107
1100
|
effect_id: action.effectId,
|
|
1108
1101
|
result: milestoneCheckResultToKernel(milestoneCheckPass(action.phaseId)),
|
|
@@ -1115,7 +1108,7 @@ export class RuntimeRunner {
|
|
|
1115
1108
|
criteria: action.criteria ?? [],
|
|
1116
1109
|
requiredEvidence: action.requiredEvidence ?? [],
|
|
1117
1110
|
});
|
|
1118
|
-
action =
|
|
1111
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1119
1112
|
kind: "milestone_result",
|
|
1120
1113
|
effect_id: action.effectId,
|
|
1121
1114
|
result: milestoneCheckResultToKernel(check),
|
|
@@ -1123,6 +1116,17 @@ export class RuntimeRunner {
|
|
|
1123
1116
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
1124
1117
|
}
|
|
1125
1118
|
else {
|
|
1119
|
+
// R-B27: resolve the effect before ending the run. Nothing here can attest the phase, but
|
|
1120
|
+
// the kernel is holding this milestone in its pending-effect table and only a matching
|
|
1121
|
+
// result removes it — a bare `return` leaves a dangling effect that a logical-checkpoint
|
|
1122
|
+
// recovery cannot resolve. Feed back the conservative "unverified" resolution: the wire
|
|
1123
|
+
// has no error field yet, so it rides as `passed: false`, which keeps the phase where it
|
|
1124
|
+
// is (fail-closed, no unlocks mounted). The run still ends as `milestone_pending`.
|
|
1125
|
+
await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1126
|
+
kind: "milestone_result",
|
|
1127
|
+
effect_id: milestoneEffectId,
|
|
1128
|
+
result: milestoneCheckResultToKernel(milestoneCheckFail(milestonePhaseId, MILESTONE_UNVERIFIED_REASON)),
|
|
1129
|
+
});
|
|
1126
1130
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
1127
1131
|
const turnsUsed = Math.max(1, runtime.turn());
|
|
1128
1132
|
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
@@ -1136,9 +1140,31 @@ export class RuntimeRunner {
|
|
|
1136
1140
|
return;
|
|
1137
1141
|
}
|
|
1138
1142
|
}
|
|
1143
|
+
else if (action.kind === "spawn_workflow") {
|
|
1144
|
+
await this.driveWorkflow(action, [], sessionId, runtime, new Map());
|
|
1145
|
+
action = runtime.resumeAction()
|
|
1146
|
+
?? (() => { throw new Error("canonical workflow completed without a terminal or continuation"); })();
|
|
1147
|
+
}
|
|
1148
|
+
else if (action.kind === "unsupported_effect") {
|
|
1149
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1150
|
+
kind: "unsupported_effect",
|
|
1151
|
+
effect_id: action.effectId,
|
|
1152
|
+
effect_kind: action.effectKind,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1139
1155
|
else if (action.kind === "done") {
|
|
1140
1156
|
break;
|
|
1141
1157
|
}
|
|
1158
|
+
else {
|
|
1159
|
+
// R-B28: fail-closed backstop. Without it an effect that reaches this position but has no
|
|
1160
|
+
// branch here (`spawn_workflow` / `preempt_sub_agents` are only driven inside the workflow
|
|
1161
|
+
// driver, and any effect kind a newer kernel adds) leaves `action` unreplaced and no event
|
|
1162
|
+
// in flight — `while (!runtime.isTerminal())` re-enters immediately and the run pins a core
|
|
1163
|
+
// at 100% forever while the kernel waits for a result that will never come. Terminating
|
|
1164
|
+
// through the loop's existing throw path makes the protocol mismatch visible instead
|
|
1165
|
+
// (run_terminal `error` + an `error` event) and cannot busy-wait.
|
|
1166
|
+
throw new Error(`unhandled kernel effect ${action.kind} in the main run loop`);
|
|
1167
|
+
}
|
|
1142
1168
|
}
|
|
1143
1169
|
}
|
|
1144
1170
|
catch (err) {
|
|
@@ -1213,54 +1239,6 @@ export class RuntimeRunner {
|
|
|
1213
1239
|
this.activeKernel = null;
|
|
1214
1240
|
this.currentSessionId = null;
|
|
1215
1241
|
}
|
|
1216
|
-
async spawnSubAgent(spec) {
|
|
1217
|
-
if (!this.activeKernel || !this.currentSessionId) {
|
|
1218
|
-
throw new Error("spawnSubAgent requires an active parent run");
|
|
1219
|
-
}
|
|
1220
|
-
const parentSessionId = this.currentSessionId;
|
|
1221
|
-
const runtime = this.activeKernel;
|
|
1222
|
-
const observations = kernelApply(runtime, this.pendingObservations, {
|
|
1223
|
-
kind: "spawn_sub_agent",
|
|
1224
|
-
spec: agentRunSpecToKernel(spec),
|
|
1225
|
-
parent_session_id: parentSessionId,
|
|
1226
|
-
});
|
|
1227
|
-
this.nextArchiveStart = await this.appendObservations(parentSessionId, runtime, this.nextArchiveStart);
|
|
1228
|
-
const spawned = findSpawnProcessObservation(observations);
|
|
1229
|
-
if (!spawned) {
|
|
1230
|
-
const rejected = controlRequestRejection(observations, "spawn_sub_agent");
|
|
1231
|
-
if (rejected) {
|
|
1232
|
-
return {
|
|
1233
|
-
agentId: rejected.subject ?? spec.identity.agentId,
|
|
1234
|
-
result: {
|
|
1235
|
-
termination: "error",
|
|
1236
|
-
finalMessage: {
|
|
1237
|
-
role: "assistant",
|
|
1238
|
-
content: `spawn_sub_agent denied: ${rejected.reason}`,
|
|
1239
|
-
toolCalls: [],
|
|
1240
|
-
},
|
|
1241
|
-
turnsUsed: 0,
|
|
1242
|
-
totalTokensUsed: 0,
|
|
1243
|
-
},
|
|
1244
|
-
};
|
|
1245
|
-
}
|
|
1246
|
-
throw new Error("spawn_sub_agent did not emit agent_process_changed");
|
|
1247
|
-
}
|
|
1248
|
-
const manifest = spawnObservationToManifest(spawned, spec, parentSessionId);
|
|
1249
|
-
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
1250
|
-
const result = await orchestrator.run({
|
|
1251
|
-
parentOpts: this.opts,
|
|
1252
|
-
parentSessionId,
|
|
1253
|
-
spec,
|
|
1254
|
-
manifest,
|
|
1255
|
-
sessionLog: this.opts.sessionLog,
|
|
1256
|
-
toolAccess: spec.toolAccess,
|
|
1257
|
-
});
|
|
1258
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1259
|
-
kind: "sub_agent_completed",
|
|
1260
|
-
result: subAgentResultToKernel(result),
|
|
1261
|
-
});
|
|
1262
|
-
return result;
|
|
1263
|
-
}
|
|
1264
1242
|
/**
|
|
1265
1243
|
* G3: run one workflow node, enforcing its `output_schema` (if any) by instructing the agent,
|
|
1266
1244
|
* validating its output (the supported JSON-Schema subset), and re-running once with the errors
|
|
@@ -1287,7 +1265,8 @@ export class RuntimeRunner {
|
|
|
1287
1265
|
spec: { ...baseSpec, goal: withBudget(goal) },
|
|
1288
1266
|
manifest,
|
|
1289
1267
|
sessionLog: this.opts.sessionLog,
|
|
1290
|
-
//
|
|
1268
|
+
// This child is a workflow node, so capability resolution applies workflow-node quarantine
|
|
1269
|
+
// semantics instead of treating it as an independently spawned agent.
|
|
1291
1270
|
isWorkflowNode: true,
|
|
1292
1271
|
// W-N1: trusted workflow nodes run on the parent's execution plane (they carry no grant list
|
|
1293
1272
|
// by design — filtering on the missing list ran every DAG node TOOL-LESS); quarantined nodes
|
|
@@ -1392,11 +1371,11 @@ export class RuntimeRunner {
|
|
|
1392
1371
|
*/
|
|
1393
1372
|
/**
|
|
1394
1373
|
* Lower the declarative governance / attention / scheduler-budget / resource-quota / memory policies
|
|
1395
|
-
* into a freshly-created kernel. Shared by `execute()` (full run) and `
|
|
1374
|
+
* into a freshly-created kernel. Shared by `execute()` (full run) and `initializeWorkflowKernel()`
|
|
1396
1375
|
* (standalone workflow) so a DAG's node spawns are gated and quota'd exactly as a mid-run spawn.
|
|
1397
|
-
* Must run
|
|
1376
|
+
* Must run before the canonical root start. No config ⇒ native-profile defaults.
|
|
1398
1377
|
*/
|
|
1399
|
-
applyKernelPolicies(runtime) {
|
|
1378
|
+
async applyKernelPolicies(runtime) {
|
|
1400
1379
|
const osProfile = assertNativeProfile(this.opts.osProfile ?? "native");
|
|
1401
1380
|
const signalPolicy = this.opts.signalPolicy ?? osProfile.signalPolicy;
|
|
1402
1381
|
const governancePolicy = this.opts.governancePolicy ?? osProfile.governancePolicy;
|
|
@@ -1426,21 +1405,7 @@ export class RuntimeRunner {
|
|
|
1426
1405
|
config.scheduler_policy = schedulerPolicyToKernel(this.opts.schedulerPolicy);
|
|
1427
1406
|
}
|
|
1428
1407
|
if (this.opts.kernelReliability) {
|
|
1429
|
-
|
|
1430
|
-
config.reliability = {
|
|
1431
|
-
...(r.eventReplayCapacity !== undefined ? { event_replay_capacity: r.eventReplayCapacity } : {}),
|
|
1432
|
-
...(r.completedEffectReplayCapacity !== undefined ? { completed_effect_replay_capacity: r.completedEffectReplayCapacity } : {}),
|
|
1433
|
-
...(r.providerRecoveryAttempts !== undefined ? { provider_recovery_attempts: r.providerRecoveryAttempts } : {}),
|
|
1434
|
-
...(r.outputRecoveryAttempts !== undefined ? { output_recovery_attempts: r.outputRecoveryAttempts } : {}),
|
|
1435
|
-
...(r.hostEffectRetryAttempts !== undefined ? { host_effect_retry_attempts: r.hostEffectRetryAttempts } : {}),
|
|
1436
|
-
...(r.spoolThresholdBytes !== undefined ? { spool_threshold_bytes: r.spoolThresholdBytes } : {}),
|
|
1437
|
-
...(r.spoolPreviewBytes !== undefined ? { spool_preview_bytes: r.spoolPreviewBytes } : {}),
|
|
1438
|
-
...(r.snapshotInputLimit !== undefined ? { snapshot_input_limit: r.snapshotInputLimit } : {}),
|
|
1439
|
-
...(r.maxInputBytes !== undefined ? { max_input_bytes: r.maxInputBytes } : {}),
|
|
1440
|
-
...(r.snapshotJournalBytesLimit !== undefined
|
|
1441
|
-
? { snapshot_journal_bytes_limit: r.snapshotJournalBytesLimit }
|
|
1442
|
-
: {}),
|
|
1443
|
-
};
|
|
1408
|
+
config.reliability = kernelReliabilityToKernel(this.opts.kernelReliability);
|
|
1444
1409
|
}
|
|
1445
1410
|
if (this.opts.resourceQuota) {
|
|
1446
1411
|
const q = this.opts.resourceQuota;
|
|
@@ -1484,54 +1449,30 @@ export class RuntimeRunner {
|
|
|
1484
1449
|
...(ew.notifyModel !== undefined ? { notify_model: ew.notifyModel } : {}),
|
|
1485
1450
|
};
|
|
1486
1451
|
}
|
|
1487
|
-
|
|
1452
|
+
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
1488
1453
|
if (this.opts.memoryPolicy) {
|
|
1489
|
-
|
|
1490
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1454
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1491
1455
|
kind: "set_memory_policy",
|
|
1492
|
-
...(
|
|
1493
|
-
...(m.staleWarningDays !== undefined ? { stale_warning_days: m.staleWarningDays } : {}),
|
|
1494
|
-
...(m.retrievalTopK !== undefined ? { retrieval_top_k: m.retrievalTopK } : {}),
|
|
1495
|
-
...(m.validationEnabled !== undefined ? { validation_enabled: m.validationEnabled } : {}),
|
|
1496
|
-
...(m.maxContentBytes !== undefined ? { max_content_bytes: m.maxContentBytes } : {}),
|
|
1497
|
-
...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
|
|
1456
|
+
...memoryPolicyToKernel(this.opts.memoryPolicy),
|
|
1498
1457
|
});
|
|
1499
1458
|
}
|
|
1500
1459
|
}
|
|
1501
1460
|
/**
|
|
1502
|
-
* Bootstrap a standalone kernel for a host-driven workflow with no active parent run
|
|
1503
|
-
*
|
|
1504
|
-
* `
|
|
1505
|
-
* best-effort `run_started` so the run is resumable. `runWorkflow` tears the kernel down afterward.
|
|
1461
|
+
* Bootstrap a standalone kernel for a host-driven workflow with no active parent run. The caller
|
|
1462
|
+
* durably records `run_started` with the same `runId` before this method can create journal state.
|
|
1463
|
+
* `StartOperation { Workflow }` is the sole root transition; `runWorkflow` tears the kernel down.
|
|
1506
1464
|
*/
|
|
1507
|
-
async
|
|
1465
|
+
async initializeWorkflowKernel(sessionId, runId) {
|
|
1508
1466
|
this.interrupted = false;
|
|
1509
1467
|
this.cancellationReason = undefined;
|
|
1468
|
+
this.abortController = new AbortController();
|
|
1510
1469
|
this.pendingObservations = [];
|
|
1511
1470
|
this.pendingPageOutArchives = [];
|
|
1512
1471
|
this.activePageOutArchive = undefined;
|
|
1513
1472
|
this.currentSessionId = sessionId;
|
|
1514
|
-
const
|
|
1515
|
-
const runtime = new kernel.KernelRuntime({
|
|
1516
|
-
maxTokens: this.opts.maxTokens,
|
|
1517
|
-
maxTurns: this.opts.maxTurns ?? 25,
|
|
1518
|
-
...(this.opts.maxTotalTokens !== undefined ? { maxTotalTokens: this.opts.maxTotalTokens } : {}),
|
|
1519
|
-
timeoutMs: this.opts.timeoutMs,
|
|
1520
|
-
});
|
|
1473
|
+
const runtime = await this.createCanonicalRuntime(runId, sessionId);
|
|
1521
1474
|
this.activeKernel = runtime;
|
|
1522
|
-
|
|
1523
|
-
void Promise.resolve(this.opts.sessionLog.append(sessionId, {
|
|
1524
|
-
kind: "run_started",
|
|
1525
|
-
run_id: crypto.randomUUID(),
|
|
1526
|
-
goal,
|
|
1527
|
-
criteria: [],
|
|
1528
|
-
...(this.opts.agentId ? { agent_id: this.opts.agentId } : {}),
|
|
1529
|
-
})).catch(() => { });
|
|
1530
|
-
this.applyKernelPolicies(runtime);
|
|
1531
|
-
kernelAction(runtime, this.pendingObservations, {
|
|
1532
|
-
kind: "start_run",
|
|
1533
|
-
task: { goal: `workflow session ${sessionId}`, criteria: [] },
|
|
1534
|
-
});
|
|
1475
|
+
await this.applyKernelPolicies(runtime);
|
|
1535
1476
|
return runtime;
|
|
1536
1477
|
}
|
|
1537
1478
|
async runWorkflow(spec, opts) {
|
|
@@ -1540,94 +1481,51 @@ export class RuntimeRunner {
|
|
|
1540
1481
|
// Mid-run callers keep the original in-place behavior with no teardown.
|
|
1541
1482
|
const bootstrapped = !this.activeKernel || !this.currentSessionId;
|
|
1542
1483
|
if (bootstrapped) {
|
|
1543
|
-
|
|
1484
|
+
const sessionId = opts?.sessionId ?? `wf-${crypto.randomUUID()}`;
|
|
1485
|
+
const runId = crypto.randomUUID();
|
|
1486
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1487
|
+
kind: "run_started",
|
|
1488
|
+
run_id: runId,
|
|
1489
|
+
goal: `workflow:${spec.nodes.length} nodes`,
|
|
1490
|
+
criteria: [],
|
|
1491
|
+
...(this.opts.agentId ? { agent_id: this.opts.agentId } : {}),
|
|
1492
|
+
});
|
|
1493
|
+
await this.initializeWorkflowKernel(sessionId, runId);
|
|
1544
1494
|
}
|
|
1545
1495
|
const parentSessionId = this.currentSessionId;
|
|
1546
1496
|
const runtime = this.activeKernel;
|
|
1547
1497
|
try {
|
|
1548
1498
|
const observationStart = this.pendingObservations.length;
|
|
1549
|
-
const initialAction =
|
|
1550
|
-
kind: "load_workflow",
|
|
1551
|
-
spec: workflowSpecToKernel(spec),
|
|
1552
|
-
parent_session_id: parentSessionId,
|
|
1553
|
-
// Exact typed terminal outcomes plus control-flow signals recovered from the journal.
|
|
1554
|
-
...(opts?.resumedOutcomes?.length
|
|
1555
|
-
? {
|
|
1556
|
-
resumed_outcomes: opts.resumedOutcomes.map(r => ({
|
|
1557
|
-
agent_id: r.agentId,
|
|
1558
|
-
status: r.status,
|
|
1559
|
-
termination: r.termination,
|
|
1560
|
-
...(r.output ? { output: messageToKernelMessage(r.output) } : {}),
|
|
1561
|
-
...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
|
|
1562
|
-
...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
|
|
1563
|
-
...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
|
|
1564
|
-
})),
|
|
1565
|
-
}
|
|
1566
|
-
: {}),
|
|
1567
|
-
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
1568
|
-
...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
1569
|
-
...(opts?.resumedSubmissionBases && opts.resumedSubmissionBases.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
|
|
1570
|
-
});
|
|
1499
|
+
const initialAction = await canonicalStartWorkflow(runtime, this.pendingObservations, workflowSpecToKernel(spec));
|
|
1571
1500
|
const observations = this.pendingObservations.slice(observationStart);
|
|
1572
|
-
|
|
1501
|
+
const outcome = await this.driveWorkflow(initialAction, observations, parentSessionId, runtime, new Map());
|
|
1502
|
+
if (bootstrapped) {
|
|
1503
|
+
let terminal = runtime.resumeAction();
|
|
1504
|
+
if (!terminal)
|
|
1505
|
+
throw new Error("completed canonical workflow has no terminal action");
|
|
1506
|
+
if (terminal.kind !== "done" && this.interrupted) {
|
|
1507
|
+
terminal = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1508
|
+
kind: "cancel_operation",
|
|
1509
|
+
effect_id: terminal.effectId,
|
|
1510
|
+
reason: this.cancellationReason ?? "user",
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
if (terminal.kind !== "done") {
|
|
1514
|
+
throw new Error("canonical workflow did not produce a terminal kernel action");
|
|
1515
|
+
}
|
|
1516
|
+
await this.appendObservations(parentSessionId, runtime, 0);
|
|
1517
|
+
}
|
|
1518
|
+
return outcome;
|
|
1573
1519
|
}
|
|
1574
1520
|
finally {
|
|
1575
1521
|
if (bootstrapped) {
|
|
1576
1522
|
this.activeKernel = null;
|
|
1577
1523
|
this.currentSessionId = null;
|
|
1524
|
+
this.abortController = null;
|
|
1578
1525
|
this.pendingObservations = [];
|
|
1579
1526
|
}
|
|
1580
1527
|
}
|
|
1581
1528
|
}
|
|
1582
|
-
/**
|
|
1583
|
-
* M5/G1: bootstrap an **agent-authored** workflow ("the model writes its own harness"). Routes the
|
|
1584
|
-
* spec through the agent-reachable `Syscall::LoadWorkflow` (`submit_workflow`): with no workflow
|
|
1585
|
-
* active the kernel bootstraps the DAG, else it flattens onto the running one (bootstrap-or-flatten —
|
|
1586
|
-
* one kernel, one quota). The same shared driver runs the resulting batches.
|
|
1587
|
-
*/
|
|
1588
|
-
async bootstrapWorkflow(spec, opts) {
|
|
1589
|
-
if (!this.activeKernel || !this.currentSessionId) {
|
|
1590
|
-
throw new Error("bootstrapWorkflow requires an active parent run");
|
|
1591
|
-
}
|
|
1592
|
-
const parentSessionId = this.currentSessionId;
|
|
1593
|
-
const runtime = this.activeKernel;
|
|
1594
|
-
const observationStart = this.pendingObservations.length;
|
|
1595
|
-
const initialAction = kernelMaybeAction(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
1596
|
-
const observations = this.pendingObservations.slice(observationStart);
|
|
1597
|
-
// W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
|
|
1598
|
-
// announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
|
|
1599
|
-
// had this spec, unlike the `runWorkflow` path.
|
|
1600
|
-
const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
|
|
1601
|
-
if (submitted) {
|
|
1602
|
-
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1603
|
-
turn: runtime.turn(),
|
|
1604
|
-
nodes: workflowSpecToKernel(spec).nodes ?? [],
|
|
1605
|
-
baseIndex: submitted.base,
|
|
1606
|
-
submitterAgentId: opts?.submitterAgentId,
|
|
1607
|
-
}));
|
|
1608
|
-
}
|
|
1609
|
-
return this.driveWorkflow(initialAction, observations, parentSessionId, runtime);
|
|
1610
|
-
}
|
|
1611
|
-
/**
|
|
1612
|
-
* M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`, at the safe
|
|
1613
|
-
* point (tool turn resolved → kernel in Reason). Each runs in THIS kernel (the kernel resumes the
|
|
1614
|
-
* reason loop on `workflow_completed`), then the outcome is injected as a user message and a fresh
|
|
1615
|
-
* `call_provider` is synthesized from the updated context (the workflow drive consumed its own
|
|
1616
|
-
* kernel actions — same re-render pattern as the reactive-compact retry path).
|
|
1617
|
-
*/
|
|
1618
|
-
async driveAuthoredWorkflows(runtime, action) {
|
|
1619
|
-
const specs = this.pendingAuthoredWorkflows;
|
|
1620
|
-
this.pendingAuthoredWorkflows = [];
|
|
1621
|
-
this.workflowContinuation = null;
|
|
1622
|
-
for (const spec of specs) {
|
|
1623
|
-
await this.bootstrapWorkflow(spec);
|
|
1624
|
-
}
|
|
1625
|
-
const continuation = this.workflowContinuation;
|
|
1626
|
-
if (!continuation) {
|
|
1627
|
-
throw new Error("authored workflow completed without a provider continuation");
|
|
1628
|
-
}
|
|
1629
|
-
return continuation;
|
|
1630
|
-
}
|
|
1631
1529
|
/**
|
|
1632
1530
|
* #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
|
|
1633
1531
|
* routes through the kernel (root in `SubAgentAwait` → preempt → `AgentPreempted` + tears the
|
|
@@ -1648,14 +1546,14 @@ export class RuntimeRunner {
|
|
|
1648
1546
|
continue;
|
|
1649
1547
|
}
|
|
1650
1548
|
const observationStart = this.pendingObservations.length;
|
|
1651
|
-
const signalAction = await this.consumeInboundSignal(delivery, claimed =>
|
|
1549
|
+
const signalAction = await this.consumeInboundSignal(delivery, claimed => this.commitKernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(claimed)));
|
|
1652
1550
|
if (signalAction) {
|
|
1653
1551
|
if (signalAction.kind !== "preempt_sub_agents") {
|
|
1654
1552
|
throw new Error(`workflow signal returned unexpected effect: ${signalAction.kind}`);
|
|
1655
1553
|
}
|
|
1656
1554
|
for (const id of signalAction.agentIds)
|
|
1657
1555
|
controllers.get(id)?.abort();
|
|
1658
|
-
const continuation =
|
|
1556
|
+
const continuation = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1659
1557
|
kind: "preempt_result", effect_id: signalAction.effectId,
|
|
1660
1558
|
});
|
|
1661
1559
|
if (continuation && continuation.kind !== "call_provider" && continuation.kind !== "done") {
|
|
@@ -1674,17 +1572,16 @@ export class RuntimeRunner {
|
|
|
1674
1572
|
return null;
|
|
1675
1573
|
}
|
|
1676
1574
|
/**
|
|
1677
|
-
* Shared workflow driver
|
|
1678
|
-
*
|
|
1679
|
-
* agent-submitted nodes first), and loop until the kernel reports the workflow complete.
|
|
1575
|
+
* Shared canonical workflow driver: run each kernel-emitted batch in parallel, feed completions
|
|
1576
|
+
* back, and loop until the kernel reports the workflow complete.
|
|
1680
1577
|
*/
|
|
1681
1578
|
async driveWorkflow(initialAction, initial, parentSessionId, runtime, seedOutputs) {
|
|
1682
1579
|
const observations = initial;
|
|
1683
1580
|
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
1684
1581
|
const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
|
|
1685
|
-
const acceptSpawn = (spawn) => {
|
|
1582
|
+
const acceptSpawn = async (spawn) => {
|
|
1686
1583
|
const observationStart = this.pendingObservations.length;
|
|
1687
|
-
const continuation =
|
|
1584
|
+
const continuation = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1688
1585
|
kind: "workflow_spawn_result",
|
|
1689
1586
|
effect_id: spawn.effectId,
|
|
1690
1587
|
started_agent_ids: spawn.nodes.map(node => String(node.agent_id ?? "")),
|
|
@@ -1712,7 +1609,7 @@ export class RuntimeRunner {
|
|
|
1712
1609
|
}
|
|
1713
1610
|
let nodes = initialAction.nodes;
|
|
1714
1611
|
let budget = initialAction.budget;
|
|
1715
|
-
acceptSpawn(initialAction);
|
|
1612
|
+
await acceptSpawn(initialAction);
|
|
1716
1613
|
// G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
|
|
1717
1614
|
// W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
|
|
1718
1615
|
// still see their (pre-crash) dependencies' outputs.
|
|
@@ -1720,6 +1617,13 @@ export class RuntimeRunner {
|
|
|
1720
1617
|
for (;;) {
|
|
1721
1618
|
if (nodes.length === 0)
|
|
1722
1619
|
return { nodeOutcomes: [], outputs: Object.fromEntries(outputs) };
|
|
1620
|
+
for (const node of nodes) {
|
|
1621
|
+
const dependencyOutputs = node.dependency_outputs ?? {};
|
|
1622
|
+
for (const [agentId, output] of Object.entries(dependencyOutputs)) {
|
|
1623
|
+
if (!outputs.has(agentId))
|
|
1624
|
+
outputs.set(agentId, output);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1723
1627
|
const roundBudget = budget;
|
|
1724
1628
|
// #2-B-ii: per-node abort controllers + a concurrent preemption monitor (see node runner).
|
|
1725
1629
|
const controllers = new Map(nodes.map(n => [n.agent_id, new AbortController()]));
|
|
@@ -1743,55 +1647,8 @@ export class RuntimeRunner {
|
|
|
1743
1647
|
const stableId = result.agentId.replace(/-i\d+$/, "");
|
|
1744
1648
|
if (stableId !== result.agentId)
|
|
1745
1649
|
outputs.set(stableId, outText);
|
|
1746
|
-
// R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
|
|
1747
|
-
// reporting the node's completion — the workflow is still active, so even a last-node
|
|
1748
|
-
// submission keeps the DAG alive.
|
|
1749
|
-
if (result.submittedNodes?.length) {
|
|
1750
|
-
// G1: stamp the submitting node's agent id so the kernel coerces a quarantined submitter's
|
|
1751
|
-
// nodes to quarantined (no topological privilege escalation).
|
|
1752
|
-
const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
|
|
1753
|
-
const observationStart = this.pendingObservations.length;
|
|
1754
|
-
const submitAction = kernelMaybeAction(runtime, this.pendingObservations, submitEvent);
|
|
1755
|
-
const subObs = this.pendingObservations.slice(observationStart);
|
|
1756
|
-
const nodesRejected = subObs.find(observation => observation.kind === "nodes_rejected");
|
|
1757
|
-
const rejected = controlRequestRejection(subObs, "submit_workflow_nodes")
|
|
1758
|
-
?? (nodesRejected
|
|
1759
|
-
? { operation: "submit_workflow_nodes", reason: String(nodesRejected.reason ?? "request denied") }
|
|
1760
|
-
: undefined);
|
|
1761
|
-
if (rejected) {
|
|
1762
|
-
const denial = `workflow node submission denied: ${rejected.reason}`;
|
|
1763
|
-
result.result = {
|
|
1764
|
-
...result.result,
|
|
1765
|
-
termination: "error",
|
|
1766
|
-
finalMessage: { role: "assistant", content: denial, toolCalls: [] },
|
|
1767
|
-
};
|
|
1768
|
-
outputs.set(result.agentId, denial);
|
|
1769
|
-
if (stableId !== result.agentId)
|
|
1770
|
-
outputs.set(stableId, denial);
|
|
1771
|
-
}
|
|
1772
|
-
if (submitAction?.kind === "spawn_workflow") {
|
|
1773
|
-
nextNodes.push(...submitAction.nodes);
|
|
1774
|
-
budget = submitAction.budget ?? budget;
|
|
1775
|
-
acceptSpawn(submitAction);
|
|
1776
|
-
}
|
|
1777
|
-
else if (submitAction) {
|
|
1778
|
-
throw new Error(`workflow node submission returned unexpected effect: ${submitAction.kind}`);
|
|
1779
|
-
}
|
|
1780
|
-
// R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
|
|
1781
|
-
// so resume can re-apply the batch at the exact original graph position. W-N3: also the
|
|
1782
|
-
// submitter, so resume drops batches whose submitter re-runs (it will re-submit).
|
|
1783
|
-
const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
|
|
1784
|
-
if (submitted) {
|
|
1785
|
-
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1786
|
-
turn: runtime.turn(),
|
|
1787
|
-
nodes: submitEvent.nodes ?? [],
|
|
1788
|
-
baseIndex: submitted.base,
|
|
1789
|
-
submitterAgentId: result.agentId,
|
|
1790
|
-
}));
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
1650
|
const observationStart = this.pendingObservations.length;
|
|
1794
|
-
const completionAction =
|
|
1651
|
+
const completionAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1795
1652
|
kind: "sub_agent_completed",
|
|
1796
1653
|
result: subAgentResultToKernel(result),
|
|
1797
1654
|
});
|
|
@@ -1799,14 +1656,31 @@ export class RuntimeRunner {
|
|
|
1799
1656
|
if (completionAction?.kind === "spawn_workflow") {
|
|
1800
1657
|
nextNodes.push(...completionAction.nodes);
|
|
1801
1658
|
budget = completionAction.budget ?? budget;
|
|
1802
|
-
obs = [...obs, ...acceptSpawn(completionAction)];
|
|
1659
|
+
obs = [...obs, ...await acceptSpawn(completionAction)];
|
|
1803
1660
|
}
|
|
1804
1661
|
else if (completionAction?.kind === "call_provider") {
|
|
1805
1662
|
this.workflowContinuation = completionAction;
|
|
1806
1663
|
}
|
|
1664
|
+
else if (completionAction?.kind === "done") {
|
|
1665
|
+
// The correlated child completion may terminalize the workflow in the same canonical
|
|
1666
|
+
// step. Its workflow_completed observation below remains the typed outcome source.
|
|
1667
|
+
}
|
|
1807
1668
|
else if (completionAction) {
|
|
1808
1669
|
throw new Error(`workflow completion returned unexpected effect: ${completionAction.kind}`);
|
|
1809
1670
|
}
|
|
1671
|
+
// Child-authored DAG additions are admitted only as part of the canonical child-completion
|
|
1672
|
+
// resolution. Persist the projection only after core reports the admitted base index.
|
|
1673
|
+
if (result.submittedNodes?.length) {
|
|
1674
|
+
const submitted = obs.find(o => o.kind === "workflow_nodes_submitted");
|
|
1675
|
+
if (submitted) {
|
|
1676
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1677
|
+
turn: runtime.turn(),
|
|
1678
|
+
nodes: result.submittedNodes.map(workflowNodeSpecToKernel),
|
|
1679
|
+
baseIndex: submitted.base,
|
|
1680
|
+
submitterAgentId: result.agentId,
|
|
1681
|
+
}));
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1810
1684
|
const d = findDone(obs);
|
|
1811
1685
|
if (d)
|
|
1812
1686
|
done = d;
|
|
@@ -1833,38 +1707,6 @@ export class RuntimeRunner {
|
|
|
1833
1707
|
nodes = nextNodes;
|
|
1834
1708
|
}
|
|
1835
1709
|
}
|
|
1836
|
-
/**
|
|
1837
|
-
* Resume a workflow from the parent session's completed nodes.
|
|
1838
|
-
* Reads the session log, extracts completed workflow node records (with their W-1 control
|
|
1839
|
-
* signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
|
|
1840
|
-
* flow (classify prune / loop stop), and the driver re-seeds its outputs map.
|
|
1841
|
-
*/
|
|
1842
|
-
async resumeWorkflow(spec, opts) {
|
|
1843
|
-
// Standalone resume: a stateless handler passes the prior `sessionId`; mid-run callers omit it.
|
|
1844
|
-
const sessionId = opts?.sessionId ?? this.currentSessionId;
|
|
1845
|
-
if (!sessionId) {
|
|
1846
|
-
throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
|
|
1847
|
-
}
|
|
1848
|
-
const events = await this.opts.sessionLog.read(sessionId);
|
|
1849
|
-
const resumedOutcomes = recoverWorkflowNodeOutcomes(events);
|
|
1850
|
-
const completedIds = new Set(resumedOutcomes.map(r => r.agentId));
|
|
1851
|
-
const recovered = recoverSubmittedWorkflowNodes(events);
|
|
1852
|
-
// W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
|
|
1853
|
-
// re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
|
|
1854
|
-
// Exact bases keep later graph indices stable while dropped slots remain inert placeholders.
|
|
1855
|
-
let { submissions, bases } = recovered;
|
|
1856
|
-
if (submissions.length > 0) {
|
|
1857
|
-
const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
|
|
1858
|
-
submissions = submissions.filter((_, i) => keep[i]);
|
|
1859
|
-
bases = bases.filter((_, i) => keep[i]);
|
|
1860
|
-
}
|
|
1861
|
-
return this.runWorkflow(spec, {
|
|
1862
|
-
resumedOutcomes,
|
|
1863
|
-
resumedSubmissions: submissions,
|
|
1864
|
-
resumedSubmissionBases: bases,
|
|
1865
|
-
sessionId,
|
|
1866
|
-
});
|
|
1867
|
-
}
|
|
1868
1710
|
async appendObservations(sessionId, runtime, nextArchiveStart) {
|
|
1869
1711
|
const turn = runtime.turn();
|
|
1870
1712
|
const preservedRefs = runtime.preservedRefs();
|
|
@@ -1920,7 +1762,7 @@ export class RuntimeRunner {
|
|
|
1920
1762
|
}
|
|
1921
1763
|
}
|
|
1922
1764
|
if (lines.length > 0) {
|
|
1923
|
-
|
|
1765
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1924
1766
|
kind: "add_history_message",
|
|
1925
1767
|
message: { role: "user", content: lines.join("\n") },
|
|
1926
1768
|
});
|
|
@@ -2104,72 +1946,6 @@ function parseUpdatePlanArgs(argsStr) {
|
|
|
2104
1946
|
: parsed.blocked_on,
|
|
2105
1947
|
};
|
|
2106
1948
|
}
|
|
2107
|
-
/** R3-1: parse `submit_workflow_nodes` tool args (`{ nodes: WorkflowNodeSpec[] }`). Node shapes are
|
|
2108
|
-
* trusted structurally; the kernel validates them on append. Malformed payload → no nodes. */
|
|
2109
|
-
function parseSubmitWorkflowNodesArgs(argsStr) {
|
|
2110
|
-
let parsed = {};
|
|
2111
|
-
try {
|
|
2112
|
-
parsed = JSON.parse(argsStr);
|
|
2113
|
-
}
|
|
2114
|
-
catch {
|
|
2115
|
-
// Ignore parse error → no nodes submitted.
|
|
2116
|
-
}
|
|
2117
|
-
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
2118
|
-
}
|
|
2119
|
-
/** M5 v1: parse `start_workflow` tool args (`{ spec: { nodes: WorkflowNodeSpec[] } }`) into the
|
|
2120
|
-
* spec's node batch — flattened onto the running workflow via the same append path. */
|
|
2121
|
-
function parseStartWorkflowArgs(argsStr) {
|
|
2122
|
-
let parsed = {};
|
|
2123
|
-
try {
|
|
2124
|
-
parsed = JSON.parse(argsStr);
|
|
2125
|
-
}
|
|
2126
|
-
catch {
|
|
2127
|
-
// Ignore parse error → no nodes.
|
|
2128
|
-
}
|
|
2129
|
-
const spec = parsed.spec;
|
|
2130
|
-
return Array.isArray(spec?.nodes) ? spec.nodes : [];
|
|
2131
|
-
}
|
|
2132
|
-
/** M5 v2.1: parse the full `WorkflowSpec` from a top-level `start_workflow` call for the auto-pivot
|
|
2133
|
-
* drive. Returns `undefined` on a malformed / empty payload (caller falls back to the flatten path). */
|
|
2134
|
-
function parseStartWorkflowSpec(argsStr) {
|
|
2135
|
-
try {
|
|
2136
|
-
const parsed = JSON.parse(argsStr);
|
|
2137
|
-
if (Array.isArray(parsed.spec?.nodes) && parsed.spec.nodes.length > 0) {
|
|
2138
|
-
return { nodes: parsed.spec.nodes };
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
catch {
|
|
2142
|
-
// Ignore parse error → undefined (fall back to flatten).
|
|
2143
|
-
}
|
|
2144
|
-
return undefined;
|
|
2145
|
-
}
|
|
2146
|
-
/** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
|
|
2147
|
-
* agent's context, so its next turn continues with the sub-workflow's results in view. */
|
|
2148
|
-
function recoveredOutputs(outcomes) {
|
|
2149
|
-
const outputs = new Map();
|
|
2150
|
-
for (const outcome of outcomes ?? []) {
|
|
2151
|
-
if (!outcome.output)
|
|
2152
|
-
continue;
|
|
2153
|
-
outputs.set(outcome.agentId, outcome.output.content);
|
|
2154
|
-
outputs.set(outcome.agentId.replace(/-i\d+$/, ""), outcome.output.content);
|
|
2155
|
-
}
|
|
2156
|
-
return outputs;
|
|
2157
|
-
}
|
|
2158
|
-
function authoredWorkflowOutcomeNote(outcome) {
|
|
2159
|
-
const counts = new Map();
|
|
2160
|
-
for (const node of outcome.nodeOutcomes)
|
|
2161
|
-
counts.set(node.status, (counts.get(node.status) ?? 0) + 1);
|
|
2162
|
-
const lines = [
|
|
2163
|
-
`[authored workflow result] ${outcome.nodeOutcomes.length} terminal node(s): ` +
|
|
2164
|
-
[...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ") + ".",
|
|
2165
|
-
];
|
|
2166
|
-
for (const node of outcome.nodeOutcomes) {
|
|
2167
|
-
const out = outcome.outputs[node.nodeId] ?? node.output?.content;
|
|
2168
|
-
if (out)
|
|
2169
|
-
lines.push(`- ${node.nodeId} (${node.status}): ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
|
|
2170
|
-
}
|
|
2171
|
-
return lines.join("\n");
|
|
2172
|
-
}
|
|
2173
1949
|
/** Lower a claimed signal delivery to the kernel's `deliver_signal` input event. Shared by the main
|
|
2174
1950
|
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
2175
1951
|
function signalToKernelEvent(delivery) {
|