@deepstrike/wasm 0.2.49 → 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 +72 -79
- package/dist/runtime/runner.js +424 -637
- 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 +21 -16
- package/dist/runtime/types/agent.js +21 -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,164 +433,161 @@ 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
|
-
|
|
541
|
-
|
|
542
|
-
//
|
|
543
|
-
// `capability_filter` (reuses the existing run_spec wire — no new ABI). Unset on both ⇒ no
|
|
544
|
-
// gating (铁律: no config = old behavior).
|
|
565
|
+
const startTask = { goal, criteria };
|
|
566
|
+
let startRunSpec;
|
|
567
|
+
// P0-A: lower an explicit `runSpec`, the `allowedToolIds` ceiling, and/or the `baselineToolIds`
|
|
568
|
+
// pre-activation surface to the kernel run spec (reuses the existing run_spec wire — no new
|
|
569
|
+
// ABI). Unset on all ⇒ no gating (铁律: no config = old behavior).
|
|
545
570
|
const allowedToolIds = this.opts.allowedToolIds;
|
|
546
571
|
const hasProfile = allowedToolIds !== undefined && allowedToolIds.length > 0;
|
|
547
|
-
|
|
572
|
+
// NOT the `length > 0` idiom above: `baselineToolIds: []` is the legitimate minimal surface
|
|
573
|
+
// (meta + stable-core only), so mere presence triggers the lowering.
|
|
574
|
+
const baselineToolIds = this.opts.baselineToolIds;
|
|
575
|
+
const hasBaseline = baselineToolIds !== undefined;
|
|
576
|
+
if (this.opts.runSpec || hasProfile || hasBaseline) {
|
|
548
577
|
const baseSpec = this.opts.runSpec ?? {
|
|
549
578
|
identity: { agentId: this.opts.agentId ?? "root", sessionId, isSubAgent: false },
|
|
550
579
|
role: "custom",
|
|
551
580
|
goal,
|
|
552
581
|
};
|
|
553
|
-
|
|
582
|
+
let spec = hasProfile
|
|
554
583
|
? { ...baseSpec, capabilityFilter: { ...baseSpec.capabilityFilter, allowedIds: allowedToolIds } }
|
|
555
584
|
: baseSpec;
|
|
556
|
-
|
|
585
|
+
if (hasBaseline)
|
|
586
|
+
spec = { ...spec, exposureBaseline: baselineToolIds };
|
|
587
|
+
startRunSpec = agentRunSpecToKernel(spec);
|
|
557
588
|
}
|
|
558
|
-
|
|
589
|
+
if (!resumeMidRun)
|
|
590
|
+
await this.applyKernelPolicies(runtime);
|
|
559
591
|
// I4: pre-fetch memory before the first LLM turn (mirrors Node). Strict dynamic context
|
|
560
592
|
// control: single-use retrieval content, not a stable skill — lands in `history` like an
|
|
561
593
|
// ordinary `memory` tool result, so it decays with the compression pyramid instead of
|
|
@@ -565,8 +597,8 @@ export class RuntimeRunner {
|
|
|
565
597
|
await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
566
598
|
}
|
|
567
599
|
let action = resumeMidRun
|
|
568
|
-
?
|
|
569
|
-
:
|
|
600
|
+
? runtime.resumeAction() ?? (() => { throw new Error("restored canonical operation has no pending effect or terminal"); })()
|
|
601
|
+
: await canonicalStartAgent(runtime, this.pendingObservations, startTask, startRunSpec);
|
|
570
602
|
// P0-C: the skill loaded and in effect going into the current turn → per-turn `activeSkill` metric.
|
|
571
603
|
let activeSkill;
|
|
572
604
|
// I0b: kernel-throw safety net — see Node runner for full rationale.
|
|
@@ -574,7 +606,7 @@ export class RuntimeRunner {
|
|
|
574
606
|
while (!runtime.isTerminal()) {
|
|
575
607
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
576
608
|
if (this.interrupted) {
|
|
577
|
-
action =
|
|
609
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
578
610
|
kind: "cancel_operation",
|
|
579
611
|
reason: this.cancellationReason ?? "user",
|
|
580
612
|
pending_call_ids: pendingCallIds(action),
|
|
@@ -584,7 +616,7 @@ export class RuntimeRunner {
|
|
|
584
616
|
if (this.opts.signalSource || this.injectedSignals.length > 0) {
|
|
585
617
|
const delivery = await this.nextInboundSignal();
|
|
586
618
|
if (delivery) {
|
|
587
|
-
const sigAction = await this.consumeInboundSignal(delivery, claimed =>
|
|
619
|
+
const sigAction = await this.consumeInboundSignal(delivery, claimed => this.commitKernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(claimed)));
|
|
588
620
|
if (sigAction)
|
|
589
621
|
action = sigAction;
|
|
590
622
|
// Critical attention/preemption is distinct from operation cancellation.
|
|
@@ -593,12 +625,6 @@ export class RuntimeRunner {
|
|
|
593
625
|
if (runtime.isTerminal())
|
|
594
626
|
break;
|
|
595
627
|
if (action.kind === "call_provider") {
|
|
596
|
-
// M5 v2.1: top-level auto-pivot at the safe point (kernel in Reason, not suspended). Loop-top
|
|
597
|
-
// placement catches every path to `call_provider` (incl. post-approval-resume), so a queued
|
|
598
|
-
// authored spec is never stranded. Drains the queue; fires once per authored batch.
|
|
599
|
-
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
600
|
-
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
601
|
-
}
|
|
602
628
|
const providerEffectId = action.effectId;
|
|
603
629
|
const finalToolCalls = [];
|
|
604
630
|
let finalText = "";
|
|
@@ -669,7 +695,7 @@ export class RuntimeRunner {
|
|
|
669
695
|
// compacted context, or `done` to terminate with an honest `ContextOverflow`. The
|
|
670
696
|
// classify + compact + retry + give-up policy lives in the kernel (one place), not
|
|
671
697
|
// duplicated across the four SDK runners.
|
|
672
|
-
action =
|
|
698
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
673
699
|
kind: "provider_error",
|
|
674
700
|
effect_id: providerEffectId,
|
|
675
701
|
message: formatToolError(err),
|
|
@@ -687,7 +713,7 @@ export class RuntimeRunner {
|
|
|
687
713
|
}
|
|
688
714
|
// #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
|
|
689
715
|
if (abortSignal?.aborted) {
|
|
690
|
-
action =
|
|
716
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
691
717
|
kind: "cancel_operation",
|
|
692
718
|
reason: this.cancellationReason ?? "user",
|
|
693
719
|
pending_call_ids: [providerEffectId],
|
|
@@ -706,10 +732,9 @@ export class RuntimeRunner {
|
|
|
706
732
|
message: messageToKernelMessage(assistantMessage),
|
|
707
733
|
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
708
734
|
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
709
|
-
now_ms: Date.now(),
|
|
710
735
|
...(turnStopReason ? { stop_reason: turnStopReason } : {}),
|
|
711
736
|
};
|
|
712
|
-
action =
|
|
737
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
|
|
713
738
|
const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
|
|
714
739
|
await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
|
|
715
740
|
turn: runtime.turn(),
|
|
@@ -749,7 +774,7 @@ export class RuntimeRunner {
|
|
|
749
774
|
const resolved = await this.resolveKernelSuspend(action.requests, runtime, sessionId);
|
|
750
775
|
for (const evt of resolved.events)
|
|
751
776
|
yield evt;
|
|
752
|
-
action =
|
|
777
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
753
778
|
kind: "approval_result",
|
|
754
779
|
effect_id: action.effectId,
|
|
755
780
|
approved_calls: resolved.approved,
|
|
@@ -766,7 +791,7 @@ export class RuntimeRunner {
|
|
|
766
791
|
catch (cause) {
|
|
767
792
|
error = formatToolError(cause);
|
|
768
793
|
}
|
|
769
|
-
action =
|
|
794
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
770
795
|
kind: "memory_persist_result",
|
|
771
796
|
effect_id: action.effectId,
|
|
772
797
|
...(error ? { error } : {}),
|
|
@@ -786,7 +811,7 @@ export class RuntimeRunner {
|
|
|
786
811
|
error = formatToolError(cause);
|
|
787
812
|
}
|
|
788
813
|
const obsStart = this.pendingObservations.length;
|
|
789
|
-
action =
|
|
814
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
790
815
|
kind: "memory_query_result",
|
|
791
816
|
effect_id: action.effectId,
|
|
792
817
|
hits,
|
|
@@ -802,23 +827,6 @@ export class RuntimeRunner {
|
|
|
802
827
|
}
|
|
803
828
|
}
|
|
804
829
|
}
|
|
805
|
-
else if (action.kind === "spool_large_result") {
|
|
806
|
-
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
807
|
-
let spoolRef;
|
|
808
|
-
let error;
|
|
809
|
-
try {
|
|
810
|
-
spoolRef = await spool.persistOutput(sessionId, action.callId, action.output);
|
|
811
|
-
}
|
|
812
|
-
catch (cause) {
|
|
813
|
-
error = formatToolError(cause);
|
|
814
|
-
}
|
|
815
|
-
action = kernelAction(runtime, this.pendingObservations, {
|
|
816
|
-
kind: "large_result_spool_result",
|
|
817
|
-
effect_id: action.effectId,
|
|
818
|
-
...(spoolRef ? { spool_ref: spoolRef } : {}),
|
|
819
|
-
...(error ? { error } : {}),
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
830
|
else if (action.kind === "archive_page_out") {
|
|
823
831
|
const archiveMeta = this.activePageOutArchive
|
|
824
832
|
?? this.pendingPageOutArchives.shift()
|
|
@@ -830,6 +838,10 @@ export class RuntimeRunner {
|
|
|
830
838
|
if (this.opts.compressionStore) {
|
|
831
839
|
archiveRef = await this.opts.compressionStore.write(sessionId, archiveMeta.archiveStart, action.archived);
|
|
832
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
|
+
}
|
|
833
845
|
}
|
|
834
846
|
catch (cause) {
|
|
835
847
|
error = formatToolError(cause);
|
|
@@ -839,7 +851,7 @@ export class RuntimeRunner {
|
|
|
839
851
|
const archiveTier = action.tier;
|
|
840
852
|
if (!error)
|
|
841
853
|
this.activePageOutArchive = undefined;
|
|
842
|
-
action =
|
|
854
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
843
855
|
kind: "page_out_archive_result",
|
|
844
856
|
effect_id: action.effectId,
|
|
845
857
|
...(archiveRef ? { archive_ref: archiveRef } : {}),
|
|
@@ -849,6 +861,25 @@ export class RuntimeRunner {
|
|
|
849
861
|
void this.archiveSemanticPageOut(archived, archiveAction);
|
|
850
862
|
}
|
|
851
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
|
+
}
|
|
852
883
|
else if (action.kind === "execute_tool") {
|
|
853
884
|
const toolEffectId = action.effectId;
|
|
854
885
|
const allCalls = action.calls;
|
|
@@ -863,55 +894,25 @@ export class RuntimeRunner {
|
|
|
863
894
|
onPermissionRequest: this.opts.onPermissionRequest,
|
|
864
895
|
};
|
|
865
896
|
const toolResults = [];
|
|
866
|
-
//
|
|
867
|
-
//
|
|
868
|
-
// the orchestrator collects them and `runWorkflow` sends them to the parent kernel.
|
|
869
|
-
// 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.
|
|
870
899
|
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
871
|
-
// O7: `read_result` re-fetches a tool output the kernel evicted from context. Content is
|
|
872
|
-
// host-resolved: (a) this turn's in-memory pending spool map, (b) the on-disk result spool,
|
|
873
|
-
// (c) a session-log scan for the original `tool_completed` event.
|
|
874
|
-
const readResultCalls = allCalls.filter(c => c.name === "read_result");
|
|
875
900
|
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
876
|
-
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"
|
|
877
902
|
&& c.name !== "update_plan");
|
|
878
903
|
// `update_plan` is a kernel meta-tool (exposed via `enablePlanTool`), not a registered
|
|
879
904
|
// plane tool — resolve it here as an `update_task` apply, mirroring the node/python runners.
|
|
880
905
|
for (const call of planCalls) {
|
|
881
906
|
const update = parseUpdatePlanArgs(call.arguments);
|
|
882
|
-
|
|
907
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
883
908
|
kind: "update_task",
|
|
884
909
|
update: taskUpdateToKernel(update),
|
|
885
910
|
});
|
|
886
911
|
toolResults.push({ callId: call.id, output: "success", isError: false });
|
|
887
912
|
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
888
913
|
}
|
|
889
|
-
for (const call of readResultCalls) {
|
|
890
|
-
const out = await this.resolveReadResult(sessionId, call.arguments);
|
|
891
|
-
toolResults.push({ callId: call.id, output: out.text, isError: out.isError });
|
|
892
|
-
yield { type: "tool_result", callId: call.id, content: out.text, isError: out.isError };
|
|
893
|
-
}
|
|
894
914
|
for (const call of submitCalls) {
|
|
895
|
-
|
|
896
|
-
// spec and AUTO-PIVOT once this tool turn resolves. A workflow-NODE's `start_workflow` (and
|
|
897
|
-
// every `submit_workflow_nodes`) instead FLATTENS for the parent `runWorkflow` to append.
|
|
898
|
-
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
899
|
-
const spec = parseStartWorkflowSpec(call.arguments);
|
|
900
|
-
if (spec) {
|
|
901
|
-
this.pendingAuthoredWorkflows.push(spec);
|
|
902
|
-
const out = "workflow submitted for governance adjudication";
|
|
903
|
-
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
904
|
-
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
const nodes = call.name === "start_workflow"
|
|
909
|
-
? parseStartWorkflowArgs(call.arguments)
|
|
910
|
-
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
911
|
-
yield { type: "workflow_nodes_submitted", nodes };
|
|
912
|
-
const out = "workflow nodes submitted for parent governance adjudication";
|
|
913
|
-
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
914
|
-
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`);
|
|
915
916
|
}
|
|
916
917
|
// O5 (PreToolUse-hook analog): stateful host veto over each kernel-approved call.
|
|
917
918
|
// A blocked call never executes; its reason reaches the model as a denied result.
|
|
@@ -1030,9 +1031,10 @@ export class RuntimeRunner {
|
|
|
1030
1031
|
token_count: r.tokenCount,
|
|
1031
1032
|
})),
|
|
1032
1033
|
});
|
|
1033
|
-
//
|
|
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.
|
|
1034
1036
|
//
|
|
1035
|
-
// Strict dynamic context control:
|
|
1037
|
+
// Strict dynamic context control: the skill text
|
|
1036
1038
|
// for the rest of the run, unlike a one-off memory/knowledge lookup (fact content, relevant
|
|
1037
1039
|
// for the moment it's used). So its text ALSO goes into the durable `knowledge` slot here
|
|
1038
1040
|
// (in addition to the ordinary tool_result already headed for `history`, where it will decay
|
|
@@ -1047,11 +1049,6 @@ export class RuntimeRunner {
|
|
|
1047
1049
|
const name = JSON.parse(call.arguments || "{}").name;
|
|
1048
1050
|
if (!name)
|
|
1049
1051
|
continue;
|
|
1050
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1051
|
-
kind: "skill_activated",
|
|
1052
|
-
name,
|
|
1053
|
-
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
1054
|
-
});
|
|
1055
1052
|
// With a lease configured, skip the Set optimization: an expired-then-reloaded skill
|
|
1056
1053
|
// must re-pin — only the kernel knows the lease state; its upsert dedupes anyway.
|
|
1057
1054
|
if (this.opts.skillLeaseTurns !== undefined || !this.knowledgePushedSkills.has(name)) {
|
|
@@ -1063,7 +1060,7 @@ export class RuntimeRunner {
|
|
|
1063
1060
|
catch { /* skip */ }
|
|
1064
1061
|
}
|
|
1065
1062
|
const entropyObsStart = this.pendingObservations.length;
|
|
1066
|
-
action =
|
|
1063
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1067
1064
|
kind: "tool_results",
|
|
1068
1065
|
effect_id: toolEffectId,
|
|
1069
1066
|
results: toolResults.map(toolResultToKernel),
|
|
@@ -1094,9 +1091,11 @@ export class RuntimeRunner {
|
|
|
1094
1091
|
}
|
|
1095
1092
|
}
|
|
1096
1093
|
else if (action.kind === "evaluate_milestone") {
|
|
1094
|
+
const milestoneEffectId = action.effectId;
|
|
1095
|
+
const milestonePhaseId = action.phaseId;
|
|
1097
1096
|
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
1098
1097
|
if (milestonePolicy === "auto_pass") {
|
|
1099
|
-
action =
|
|
1098
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1100
1099
|
kind: "milestone_result",
|
|
1101
1100
|
effect_id: action.effectId,
|
|
1102
1101
|
result: milestoneCheckResultToKernel(milestoneCheckPass(action.phaseId)),
|
|
@@ -1109,7 +1108,7 @@ export class RuntimeRunner {
|
|
|
1109
1108
|
criteria: action.criteria ?? [],
|
|
1110
1109
|
requiredEvidence: action.requiredEvidence ?? [],
|
|
1111
1110
|
});
|
|
1112
|
-
action =
|
|
1111
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1113
1112
|
kind: "milestone_result",
|
|
1114
1113
|
effect_id: action.effectId,
|
|
1115
1114
|
result: milestoneCheckResultToKernel(check),
|
|
@@ -1117,6 +1116,17 @@ export class RuntimeRunner {
|
|
|
1117
1116
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
1118
1117
|
}
|
|
1119
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
|
+
});
|
|
1120
1130
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
1121
1131
|
const turnsUsed = Math.max(1, runtime.turn());
|
|
1122
1132
|
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
@@ -1130,9 +1140,31 @@ export class RuntimeRunner {
|
|
|
1130
1140
|
return;
|
|
1131
1141
|
}
|
|
1132
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
|
+
}
|
|
1133
1155
|
else if (action.kind === "done") {
|
|
1134
1156
|
break;
|
|
1135
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
|
+
}
|
|
1136
1168
|
}
|
|
1137
1169
|
}
|
|
1138
1170
|
catch (err) {
|
|
@@ -1207,54 +1239,6 @@ export class RuntimeRunner {
|
|
|
1207
1239
|
this.activeKernel = null;
|
|
1208
1240
|
this.currentSessionId = null;
|
|
1209
1241
|
}
|
|
1210
|
-
async spawnSubAgent(spec) {
|
|
1211
|
-
if (!this.activeKernel || !this.currentSessionId) {
|
|
1212
|
-
throw new Error("spawnSubAgent requires an active parent run");
|
|
1213
|
-
}
|
|
1214
|
-
const parentSessionId = this.currentSessionId;
|
|
1215
|
-
const runtime = this.activeKernel;
|
|
1216
|
-
const observations = kernelApply(runtime, this.pendingObservations, {
|
|
1217
|
-
kind: "spawn_sub_agent",
|
|
1218
|
-
spec: agentRunSpecToKernel(spec),
|
|
1219
|
-
parent_session_id: parentSessionId,
|
|
1220
|
-
});
|
|
1221
|
-
this.nextArchiveStart = await this.appendObservations(parentSessionId, runtime, this.nextArchiveStart);
|
|
1222
|
-
const spawned = findSpawnProcessObservation(observations);
|
|
1223
|
-
if (!spawned) {
|
|
1224
|
-
const rejected = controlRequestRejection(observations, "spawn_sub_agent");
|
|
1225
|
-
if (rejected) {
|
|
1226
|
-
return {
|
|
1227
|
-
agentId: rejected.subject ?? spec.identity.agentId,
|
|
1228
|
-
result: {
|
|
1229
|
-
termination: "error",
|
|
1230
|
-
finalMessage: {
|
|
1231
|
-
role: "assistant",
|
|
1232
|
-
content: `spawn_sub_agent denied: ${rejected.reason}`,
|
|
1233
|
-
toolCalls: [],
|
|
1234
|
-
},
|
|
1235
|
-
turnsUsed: 0,
|
|
1236
|
-
totalTokensUsed: 0,
|
|
1237
|
-
},
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
throw new Error("spawn_sub_agent did not emit agent_process_changed");
|
|
1241
|
-
}
|
|
1242
|
-
const manifest = spawnObservationToManifest(spawned, spec, parentSessionId);
|
|
1243
|
-
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
1244
|
-
const result = await orchestrator.run({
|
|
1245
|
-
parentOpts: this.opts,
|
|
1246
|
-
parentSessionId,
|
|
1247
|
-
spec,
|
|
1248
|
-
manifest,
|
|
1249
|
-
sessionLog: this.opts.sessionLog,
|
|
1250
|
-
toolAccess: spec.toolAccess,
|
|
1251
|
-
});
|
|
1252
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1253
|
-
kind: "sub_agent_completed",
|
|
1254
|
-
result: subAgentResultToKernel(result),
|
|
1255
|
-
});
|
|
1256
|
-
return result;
|
|
1257
|
-
}
|
|
1258
1242
|
/**
|
|
1259
1243
|
* G3: run one workflow node, enforcing its `output_schema` (if any) by instructing the agent,
|
|
1260
1244
|
* validating its output (the supported JSON-Schema subset), and re-running once with the errors
|
|
@@ -1281,7 +1265,8 @@ export class RuntimeRunner {
|
|
|
1281
1265
|
spec: { ...baseSpec, goal: withBudget(goal) },
|
|
1282
1266
|
manifest,
|
|
1283
1267
|
sessionLog: this.opts.sessionLog,
|
|
1284
|
-
//
|
|
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.
|
|
1285
1270
|
isWorkflowNode: true,
|
|
1286
1271
|
// W-N1: trusted workflow nodes run on the parent's execution plane (they carry no grant list
|
|
1287
1272
|
// by design — filtering on the missing list ran every DAG node TOOL-LESS); quarantined nodes
|
|
@@ -1386,11 +1371,11 @@ export class RuntimeRunner {
|
|
|
1386
1371
|
*/
|
|
1387
1372
|
/**
|
|
1388
1373
|
* Lower the declarative governance / attention / scheduler-budget / resource-quota / memory policies
|
|
1389
|
-
* into a freshly-created kernel. Shared by `execute()` (full run) and `
|
|
1374
|
+
* into a freshly-created kernel. Shared by `execute()` (full run) and `initializeWorkflowKernel()`
|
|
1390
1375
|
* (standalone workflow) so a DAG's node spawns are gated and quota'd exactly as a mid-run spawn.
|
|
1391
|
-
* Must run
|
|
1376
|
+
* Must run before the canonical root start. No config ⇒ native-profile defaults.
|
|
1392
1377
|
*/
|
|
1393
|
-
applyKernelPolicies(runtime) {
|
|
1378
|
+
async applyKernelPolicies(runtime) {
|
|
1394
1379
|
const osProfile = assertNativeProfile(this.opts.osProfile ?? "native");
|
|
1395
1380
|
const signalPolicy = this.opts.signalPolicy ?? osProfile.signalPolicy;
|
|
1396
1381
|
const governancePolicy = this.opts.governancePolicy ?? osProfile.governancePolicy;
|
|
@@ -1420,21 +1405,7 @@ export class RuntimeRunner {
|
|
|
1420
1405
|
config.scheduler_policy = schedulerPolicyToKernel(this.opts.schedulerPolicy);
|
|
1421
1406
|
}
|
|
1422
1407
|
if (this.opts.kernelReliability) {
|
|
1423
|
-
|
|
1424
|
-
config.reliability = {
|
|
1425
|
-
...(r.eventReplayCapacity !== undefined ? { event_replay_capacity: r.eventReplayCapacity } : {}),
|
|
1426
|
-
...(r.completedEffectReplayCapacity !== undefined ? { completed_effect_replay_capacity: r.completedEffectReplayCapacity } : {}),
|
|
1427
|
-
...(r.providerRecoveryAttempts !== undefined ? { provider_recovery_attempts: r.providerRecoveryAttempts } : {}),
|
|
1428
|
-
...(r.outputRecoveryAttempts !== undefined ? { output_recovery_attempts: r.outputRecoveryAttempts } : {}),
|
|
1429
|
-
...(r.hostEffectRetryAttempts !== undefined ? { host_effect_retry_attempts: r.hostEffectRetryAttempts } : {}),
|
|
1430
|
-
...(r.spoolThresholdBytes !== undefined ? { spool_threshold_bytes: r.spoolThresholdBytes } : {}),
|
|
1431
|
-
...(r.spoolPreviewBytes !== undefined ? { spool_preview_bytes: r.spoolPreviewBytes } : {}),
|
|
1432
|
-
...(r.snapshotInputLimit !== undefined ? { snapshot_input_limit: r.snapshotInputLimit } : {}),
|
|
1433
|
-
...(r.maxInputBytes !== undefined ? { max_input_bytes: r.maxInputBytes } : {}),
|
|
1434
|
-
...(r.snapshotJournalBytesLimit !== undefined
|
|
1435
|
-
? { snapshot_journal_bytes_limit: r.snapshotJournalBytesLimit }
|
|
1436
|
-
: {}),
|
|
1437
|
-
};
|
|
1408
|
+
config.reliability = kernelReliabilityToKernel(this.opts.kernelReliability);
|
|
1438
1409
|
}
|
|
1439
1410
|
if (this.opts.resourceQuota) {
|
|
1440
1411
|
const q = this.opts.resourceQuota;
|
|
@@ -1458,6 +1429,11 @@ export class RuntimeRunner {
|
|
|
1458
1429
|
if (this.opts.criteriaGate !== undefined) {
|
|
1459
1430
|
config.criteria_gate = this.opts.criteriaGate;
|
|
1460
1431
|
}
|
|
1432
|
+
// P1: fail-closed dispatch selector (absent ⇒ kernel default "exposed"). "registered" is the
|
|
1433
|
+
// escape hatch back to permissive dispatch; the kernel rejects any other value.
|
|
1434
|
+
if (this.opts.toolDispatchGate !== undefined) {
|
|
1435
|
+
config.tool_dispatch_gate = this.opts.toolDispatchGate;
|
|
1436
|
+
}
|
|
1461
1437
|
// K2: knowledge budget ratio (absent ⇒ kernel default 0.25; 0 disables).
|
|
1462
1438
|
if (this.opts.knowledgeBudgetRatio !== undefined) {
|
|
1463
1439
|
config.knowledge_budget_ratio = this.opts.knowledgeBudgetRatio;
|
|
@@ -1473,54 +1449,30 @@ export class RuntimeRunner {
|
|
|
1473
1449
|
...(ew.notifyModel !== undefined ? { notify_model: ew.notifyModel } : {}),
|
|
1474
1450
|
};
|
|
1475
1451
|
}
|
|
1476
|
-
|
|
1452
|
+
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
1477
1453
|
if (this.opts.memoryPolicy) {
|
|
1478
|
-
|
|
1479
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1454
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1480
1455
|
kind: "set_memory_policy",
|
|
1481
|
-
...(
|
|
1482
|
-
...(m.staleWarningDays !== undefined ? { stale_warning_days: m.staleWarningDays } : {}),
|
|
1483
|
-
...(m.retrievalTopK !== undefined ? { retrieval_top_k: m.retrievalTopK } : {}),
|
|
1484
|
-
...(m.validationEnabled !== undefined ? { validation_enabled: m.validationEnabled } : {}),
|
|
1485
|
-
...(m.maxContentBytes !== undefined ? { max_content_bytes: m.maxContentBytes } : {}),
|
|
1486
|
-
...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
|
|
1456
|
+
...memoryPolicyToKernel(this.opts.memoryPolicy),
|
|
1487
1457
|
});
|
|
1488
1458
|
}
|
|
1489
1459
|
}
|
|
1490
1460
|
/**
|
|
1491
|
-
* Bootstrap a standalone kernel for a host-driven workflow with no active parent run
|
|
1492
|
-
*
|
|
1493
|
-
* `
|
|
1494
|
-
* 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.
|
|
1495
1464
|
*/
|
|
1496
|
-
async
|
|
1465
|
+
async initializeWorkflowKernel(sessionId, runId) {
|
|
1497
1466
|
this.interrupted = false;
|
|
1498
1467
|
this.cancellationReason = undefined;
|
|
1468
|
+
this.abortController = new AbortController();
|
|
1499
1469
|
this.pendingObservations = [];
|
|
1500
1470
|
this.pendingPageOutArchives = [];
|
|
1501
1471
|
this.activePageOutArchive = undefined;
|
|
1502
1472
|
this.currentSessionId = sessionId;
|
|
1503
|
-
const
|
|
1504
|
-
const runtime = new kernel.KernelRuntime({
|
|
1505
|
-
maxTokens: this.opts.maxTokens,
|
|
1506
|
-
maxTurns: this.opts.maxTurns ?? 25,
|
|
1507
|
-
...(this.opts.maxTotalTokens !== undefined ? { maxTotalTokens: this.opts.maxTotalTokens } : {}),
|
|
1508
|
-
timeoutMs: this.opts.timeoutMs,
|
|
1509
|
-
});
|
|
1473
|
+
const runtime = await this.createCanonicalRuntime(runId, sessionId);
|
|
1510
1474
|
this.activeKernel = runtime;
|
|
1511
|
-
|
|
1512
|
-
void Promise.resolve(this.opts.sessionLog.append(sessionId, {
|
|
1513
|
-
kind: "run_started",
|
|
1514
|
-
run_id: crypto.randomUUID(),
|
|
1515
|
-
goal,
|
|
1516
|
-
criteria: [],
|
|
1517
|
-
...(this.opts.agentId ? { agent_id: this.opts.agentId } : {}),
|
|
1518
|
-
})).catch(() => { });
|
|
1519
|
-
this.applyKernelPolicies(runtime);
|
|
1520
|
-
kernelAction(runtime, this.pendingObservations, {
|
|
1521
|
-
kind: "start_run",
|
|
1522
|
-
task: { goal: `workflow session ${sessionId}`, criteria: [] },
|
|
1523
|
-
});
|
|
1475
|
+
await this.applyKernelPolicies(runtime);
|
|
1524
1476
|
return runtime;
|
|
1525
1477
|
}
|
|
1526
1478
|
async runWorkflow(spec, opts) {
|
|
@@ -1529,94 +1481,51 @@ export class RuntimeRunner {
|
|
|
1529
1481
|
// Mid-run callers keep the original in-place behavior with no teardown.
|
|
1530
1482
|
const bootstrapped = !this.activeKernel || !this.currentSessionId;
|
|
1531
1483
|
if (bootstrapped) {
|
|
1532
|
-
|
|
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);
|
|
1533
1494
|
}
|
|
1534
1495
|
const parentSessionId = this.currentSessionId;
|
|
1535
1496
|
const runtime = this.activeKernel;
|
|
1536
1497
|
try {
|
|
1537
1498
|
const observationStart = this.pendingObservations.length;
|
|
1538
|
-
const initialAction =
|
|
1539
|
-
kind: "load_workflow",
|
|
1540
|
-
spec: workflowSpecToKernel(spec),
|
|
1541
|
-
parent_session_id: parentSessionId,
|
|
1542
|
-
// Exact typed terminal outcomes plus control-flow signals recovered from the journal.
|
|
1543
|
-
...(opts?.resumedOutcomes?.length
|
|
1544
|
-
? {
|
|
1545
|
-
resumed_outcomes: opts.resumedOutcomes.map(r => ({
|
|
1546
|
-
agent_id: r.agentId,
|
|
1547
|
-
status: r.status,
|
|
1548
|
-
termination: r.termination,
|
|
1549
|
-
...(r.output ? { output: messageToKernelMessage(r.output) } : {}),
|
|
1550
|
-
...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
|
|
1551
|
-
...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
|
|
1552
|
-
...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
|
|
1553
|
-
})),
|
|
1554
|
-
}
|
|
1555
|
-
: {}),
|
|
1556
|
-
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
1557
|
-
...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
1558
|
-
...(opts?.resumedSubmissionBases && opts.resumedSubmissionBases.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
|
|
1559
|
-
});
|
|
1499
|
+
const initialAction = await canonicalStartWorkflow(runtime, this.pendingObservations, workflowSpecToKernel(spec));
|
|
1560
1500
|
const observations = this.pendingObservations.slice(observationStart);
|
|
1561
|
-
|
|
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;
|
|
1562
1519
|
}
|
|
1563
1520
|
finally {
|
|
1564
1521
|
if (bootstrapped) {
|
|
1565
1522
|
this.activeKernel = null;
|
|
1566
1523
|
this.currentSessionId = null;
|
|
1524
|
+
this.abortController = null;
|
|
1567
1525
|
this.pendingObservations = [];
|
|
1568
1526
|
}
|
|
1569
1527
|
}
|
|
1570
1528
|
}
|
|
1571
|
-
/**
|
|
1572
|
-
* M5/G1: bootstrap an **agent-authored** workflow ("the model writes its own harness"). Routes the
|
|
1573
|
-
* spec through the agent-reachable `Syscall::LoadWorkflow` (`submit_workflow`): with no workflow
|
|
1574
|
-
* active the kernel bootstraps the DAG, else it flattens onto the running one (bootstrap-or-flatten —
|
|
1575
|
-
* one kernel, one quota). The same shared driver runs the resulting batches.
|
|
1576
|
-
*/
|
|
1577
|
-
async bootstrapWorkflow(spec, opts) {
|
|
1578
|
-
if (!this.activeKernel || !this.currentSessionId) {
|
|
1579
|
-
throw new Error("bootstrapWorkflow requires an active parent run");
|
|
1580
|
-
}
|
|
1581
|
-
const parentSessionId = this.currentSessionId;
|
|
1582
|
-
const runtime = this.activeKernel;
|
|
1583
|
-
const observationStart = this.pendingObservations.length;
|
|
1584
|
-
const initialAction = kernelMaybeAction(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
1585
|
-
const observations = this.pendingObservations.slice(observationStart);
|
|
1586
|
-
// W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
|
|
1587
|
-
// announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
|
|
1588
|
-
// had this spec, unlike the `runWorkflow` path.
|
|
1589
|
-
const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
|
|
1590
|
-
if (submitted) {
|
|
1591
|
-
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1592
|
-
turn: runtime.turn(),
|
|
1593
|
-
nodes: workflowSpecToKernel(spec).nodes ?? [],
|
|
1594
|
-
baseIndex: submitted.base,
|
|
1595
|
-
submitterAgentId: opts?.submitterAgentId,
|
|
1596
|
-
}));
|
|
1597
|
-
}
|
|
1598
|
-
return this.driveWorkflow(initialAction, observations, parentSessionId, runtime);
|
|
1599
|
-
}
|
|
1600
|
-
/**
|
|
1601
|
-
* M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`, at the safe
|
|
1602
|
-
* point (tool turn resolved → kernel in Reason). Each runs in THIS kernel (the kernel resumes the
|
|
1603
|
-
* reason loop on `workflow_completed`), then the outcome is injected as a user message and a fresh
|
|
1604
|
-
* `call_provider` is synthesized from the updated context (the workflow drive consumed its own
|
|
1605
|
-
* kernel actions — same re-render pattern as the reactive-compact retry path).
|
|
1606
|
-
*/
|
|
1607
|
-
async driveAuthoredWorkflows(runtime, action) {
|
|
1608
|
-
const specs = this.pendingAuthoredWorkflows;
|
|
1609
|
-
this.pendingAuthoredWorkflows = [];
|
|
1610
|
-
this.workflowContinuation = null;
|
|
1611
|
-
for (const spec of specs) {
|
|
1612
|
-
await this.bootstrapWorkflow(spec);
|
|
1613
|
-
}
|
|
1614
|
-
const continuation = this.workflowContinuation;
|
|
1615
|
-
if (!continuation) {
|
|
1616
|
-
throw new Error("authored workflow completed without a provider continuation");
|
|
1617
|
-
}
|
|
1618
|
-
return continuation;
|
|
1619
|
-
}
|
|
1620
1529
|
/**
|
|
1621
1530
|
* #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
|
|
1622
1531
|
* routes through the kernel (root in `SubAgentAwait` → preempt → `AgentPreempted` + tears the
|
|
@@ -1637,14 +1546,14 @@ export class RuntimeRunner {
|
|
|
1637
1546
|
continue;
|
|
1638
1547
|
}
|
|
1639
1548
|
const observationStart = this.pendingObservations.length;
|
|
1640
|
-
const signalAction = await this.consumeInboundSignal(delivery, claimed =>
|
|
1549
|
+
const signalAction = await this.consumeInboundSignal(delivery, claimed => this.commitKernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(claimed)));
|
|
1641
1550
|
if (signalAction) {
|
|
1642
1551
|
if (signalAction.kind !== "preempt_sub_agents") {
|
|
1643
1552
|
throw new Error(`workflow signal returned unexpected effect: ${signalAction.kind}`);
|
|
1644
1553
|
}
|
|
1645
1554
|
for (const id of signalAction.agentIds)
|
|
1646
1555
|
controllers.get(id)?.abort();
|
|
1647
|
-
const continuation =
|
|
1556
|
+
const continuation = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1648
1557
|
kind: "preempt_result", effect_id: signalAction.effectId,
|
|
1649
1558
|
});
|
|
1650
1559
|
if (continuation && continuation.kind !== "call_provider" && continuation.kind !== "done") {
|
|
@@ -1663,17 +1572,16 @@ export class RuntimeRunner {
|
|
|
1663
1572
|
return null;
|
|
1664
1573
|
}
|
|
1665
1574
|
/**
|
|
1666
|
-
* Shared workflow driver
|
|
1667
|
-
*
|
|
1668
|
-
* 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.
|
|
1669
1577
|
*/
|
|
1670
1578
|
async driveWorkflow(initialAction, initial, parentSessionId, runtime, seedOutputs) {
|
|
1671
1579
|
const observations = initial;
|
|
1672
1580
|
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
1673
1581
|
const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
|
|
1674
|
-
const acceptSpawn = (spawn) => {
|
|
1582
|
+
const acceptSpawn = async (spawn) => {
|
|
1675
1583
|
const observationStart = this.pendingObservations.length;
|
|
1676
|
-
const continuation =
|
|
1584
|
+
const continuation = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1677
1585
|
kind: "workflow_spawn_result",
|
|
1678
1586
|
effect_id: spawn.effectId,
|
|
1679
1587
|
started_agent_ids: spawn.nodes.map(node => String(node.agent_id ?? "")),
|
|
@@ -1701,7 +1609,7 @@ export class RuntimeRunner {
|
|
|
1701
1609
|
}
|
|
1702
1610
|
let nodes = initialAction.nodes;
|
|
1703
1611
|
let budget = initialAction.budget;
|
|
1704
|
-
acceptSpawn(initialAction);
|
|
1612
|
+
await acceptSpawn(initialAction);
|
|
1705
1613
|
// G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
|
|
1706
1614
|
// W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
|
|
1707
1615
|
// still see their (pre-crash) dependencies' outputs.
|
|
@@ -1709,6 +1617,13 @@ export class RuntimeRunner {
|
|
|
1709
1617
|
for (;;) {
|
|
1710
1618
|
if (nodes.length === 0)
|
|
1711
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
|
+
}
|
|
1712
1627
|
const roundBudget = budget;
|
|
1713
1628
|
// #2-B-ii: per-node abort controllers + a concurrent preemption monitor (see node runner).
|
|
1714
1629
|
const controllers = new Map(nodes.map(n => [n.agent_id, new AbortController()]));
|
|
@@ -1732,55 +1647,8 @@ export class RuntimeRunner {
|
|
|
1732
1647
|
const stableId = result.agentId.replace(/-i\d+$/, "");
|
|
1733
1648
|
if (stableId !== result.agentId)
|
|
1734
1649
|
outputs.set(stableId, outText);
|
|
1735
|
-
// R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
|
|
1736
|
-
// reporting the node's completion — the workflow is still active, so even a last-node
|
|
1737
|
-
// submission keeps the DAG alive.
|
|
1738
|
-
if (result.submittedNodes?.length) {
|
|
1739
|
-
// G1: stamp the submitting node's agent id so the kernel coerces a quarantined submitter's
|
|
1740
|
-
// nodes to quarantined (no topological privilege escalation).
|
|
1741
|
-
const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
|
|
1742
|
-
const observationStart = this.pendingObservations.length;
|
|
1743
|
-
const submitAction = kernelMaybeAction(runtime, this.pendingObservations, submitEvent);
|
|
1744
|
-
const subObs = this.pendingObservations.slice(observationStart);
|
|
1745
|
-
const nodesRejected = subObs.find(observation => observation.kind === "nodes_rejected");
|
|
1746
|
-
const rejected = controlRequestRejection(subObs, "submit_workflow_nodes")
|
|
1747
|
-
?? (nodesRejected
|
|
1748
|
-
? { operation: "submit_workflow_nodes", reason: String(nodesRejected.reason ?? "request denied") }
|
|
1749
|
-
: undefined);
|
|
1750
|
-
if (rejected) {
|
|
1751
|
-
const denial = `workflow node submission denied: ${rejected.reason}`;
|
|
1752
|
-
result.result = {
|
|
1753
|
-
...result.result,
|
|
1754
|
-
termination: "error",
|
|
1755
|
-
finalMessage: { role: "assistant", content: denial, toolCalls: [] },
|
|
1756
|
-
};
|
|
1757
|
-
outputs.set(result.agentId, denial);
|
|
1758
|
-
if (stableId !== result.agentId)
|
|
1759
|
-
outputs.set(stableId, denial);
|
|
1760
|
-
}
|
|
1761
|
-
if (submitAction?.kind === "spawn_workflow") {
|
|
1762
|
-
nextNodes.push(...submitAction.nodes);
|
|
1763
|
-
budget = submitAction.budget ?? budget;
|
|
1764
|
-
acceptSpawn(submitAction);
|
|
1765
|
-
}
|
|
1766
|
-
else if (submitAction) {
|
|
1767
|
-
throw new Error(`workflow node submission returned unexpected effect: ${submitAction.kind}`);
|
|
1768
|
-
}
|
|
1769
|
-
// R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
|
|
1770
|
-
// so resume can re-apply the batch at the exact original graph position. W-N3: also the
|
|
1771
|
-
// submitter, so resume drops batches whose submitter re-runs (it will re-submit).
|
|
1772
|
-
const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
|
|
1773
|
-
if (submitted) {
|
|
1774
|
-
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1775
|
-
turn: runtime.turn(),
|
|
1776
|
-
nodes: submitEvent.nodes ?? [],
|
|
1777
|
-
baseIndex: submitted.base,
|
|
1778
|
-
submitterAgentId: result.agentId,
|
|
1779
|
-
}));
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
1650
|
const observationStart = this.pendingObservations.length;
|
|
1783
|
-
const completionAction =
|
|
1651
|
+
const completionAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1784
1652
|
kind: "sub_agent_completed",
|
|
1785
1653
|
result: subAgentResultToKernel(result),
|
|
1786
1654
|
});
|
|
@@ -1788,14 +1656,31 @@ export class RuntimeRunner {
|
|
|
1788
1656
|
if (completionAction?.kind === "spawn_workflow") {
|
|
1789
1657
|
nextNodes.push(...completionAction.nodes);
|
|
1790
1658
|
budget = completionAction.budget ?? budget;
|
|
1791
|
-
obs = [...obs, ...acceptSpawn(completionAction)];
|
|
1659
|
+
obs = [...obs, ...await acceptSpawn(completionAction)];
|
|
1792
1660
|
}
|
|
1793
1661
|
else if (completionAction?.kind === "call_provider") {
|
|
1794
1662
|
this.workflowContinuation = completionAction;
|
|
1795
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
|
+
}
|
|
1796
1668
|
else if (completionAction) {
|
|
1797
1669
|
throw new Error(`workflow completion returned unexpected effect: ${completionAction.kind}`);
|
|
1798
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
|
+
}
|
|
1799
1684
|
const d = findDone(obs);
|
|
1800
1685
|
if (d)
|
|
1801
1686
|
done = d;
|
|
@@ -1822,38 +1707,6 @@ export class RuntimeRunner {
|
|
|
1822
1707
|
nodes = nextNodes;
|
|
1823
1708
|
}
|
|
1824
1709
|
}
|
|
1825
|
-
/**
|
|
1826
|
-
* Resume a workflow from the parent session's completed nodes.
|
|
1827
|
-
* Reads the session log, extracts completed workflow node records (with their W-1 control
|
|
1828
|
-
* signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
|
|
1829
|
-
* flow (classify prune / loop stop), and the driver re-seeds its outputs map.
|
|
1830
|
-
*/
|
|
1831
|
-
async resumeWorkflow(spec, opts) {
|
|
1832
|
-
// Standalone resume: a stateless handler passes the prior `sessionId`; mid-run callers omit it.
|
|
1833
|
-
const sessionId = opts?.sessionId ?? this.currentSessionId;
|
|
1834
|
-
if (!sessionId) {
|
|
1835
|
-
throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
|
|
1836
|
-
}
|
|
1837
|
-
const events = await this.opts.sessionLog.read(sessionId);
|
|
1838
|
-
const resumedOutcomes = recoverWorkflowNodeOutcomes(events);
|
|
1839
|
-
const completedIds = new Set(resumedOutcomes.map(r => r.agentId));
|
|
1840
|
-
const recovered = recoverSubmittedWorkflowNodes(events);
|
|
1841
|
-
// W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
|
|
1842
|
-
// re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
|
|
1843
|
-
// Exact bases keep later graph indices stable while dropped slots remain inert placeholders.
|
|
1844
|
-
let { submissions, bases } = recovered;
|
|
1845
|
-
if (submissions.length > 0) {
|
|
1846
|
-
const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
|
|
1847
|
-
submissions = submissions.filter((_, i) => keep[i]);
|
|
1848
|
-
bases = bases.filter((_, i) => keep[i]);
|
|
1849
|
-
}
|
|
1850
|
-
return this.runWorkflow(spec, {
|
|
1851
|
-
resumedOutcomes,
|
|
1852
|
-
resumedSubmissions: submissions,
|
|
1853
|
-
resumedSubmissionBases: bases,
|
|
1854
|
-
sessionId,
|
|
1855
|
-
});
|
|
1856
|
-
}
|
|
1857
1710
|
async appendObservations(sessionId, runtime, nextArchiveStart) {
|
|
1858
1711
|
const turn = runtime.turn();
|
|
1859
1712
|
const preservedRefs = runtime.preservedRefs();
|
|
@@ -1909,7 +1762,7 @@ export class RuntimeRunner {
|
|
|
1909
1762
|
}
|
|
1910
1763
|
}
|
|
1911
1764
|
if (lines.length > 0) {
|
|
1912
|
-
|
|
1765
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1913
1766
|
kind: "add_history_message",
|
|
1914
1767
|
message: { role: "user", content: lines.join("\n") },
|
|
1915
1768
|
});
|
|
@@ -2093,72 +1946,6 @@ function parseUpdatePlanArgs(argsStr) {
|
|
|
2093
1946
|
: parsed.blocked_on,
|
|
2094
1947
|
};
|
|
2095
1948
|
}
|
|
2096
|
-
/** R3-1: parse `submit_workflow_nodes` tool args (`{ nodes: WorkflowNodeSpec[] }`). Node shapes are
|
|
2097
|
-
* trusted structurally; the kernel validates them on append. Malformed payload → no nodes. */
|
|
2098
|
-
function parseSubmitWorkflowNodesArgs(argsStr) {
|
|
2099
|
-
let parsed = {};
|
|
2100
|
-
try {
|
|
2101
|
-
parsed = JSON.parse(argsStr);
|
|
2102
|
-
}
|
|
2103
|
-
catch {
|
|
2104
|
-
// Ignore parse error → no nodes submitted.
|
|
2105
|
-
}
|
|
2106
|
-
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
2107
|
-
}
|
|
2108
|
-
/** M5 v1: parse `start_workflow` tool args (`{ spec: { nodes: WorkflowNodeSpec[] } }`) into the
|
|
2109
|
-
* spec's node batch — flattened onto the running workflow via the same append path. */
|
|
2110
|
-
function parseStartWorkflowArgs(argsStr) {
|
|
2111
|
-
let parsed = {};
|
|
2112
|
-
try {
|
|
2113
|
-
parsed = JSON.parse(argsStr);
|
|
2114
|
-
}
|
|
2115
|
-
catch {
|
|
2116
|
-
// Ignore parse error → no nodes.
|
|
2117
|
-
}
|
|
2118
|
-
const spec = parsed.spec;
|
|
2119
|
-
return Array.isArray(spec?.nodes) ? spec.nodes : [];
|
|
2120
|
-
}
|
|
2121
|
-
/** M5 v2.1: parse the full `WorkflowSpec` from a top-level `start_workflow` call for the auto-pivot
|
|
2122
|
-
* drive. Returns `undefined` on a malformed / empty payload (caller falls back to the flatten path). */
|
|
2123
|
-
function parseStartWorkflowSpec(argsStr) {
|
|
2124
|
-
try {
|
|
2125
|
-
const parsed = JSON.parse(argsStr);
|
|
2126
|
-
if (Array.isArray(parsed.spec?.nodes) && parsed.spec.nodes.length > 0) {
|
|
2127
|
-
return { nodes: parsed.spec.nodes };
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
catch {
|
|
2131
|
-
// Ignore parse error → undefined (fall back to flatten).
|
|
2132
|
-
}
|
|
2133
|
-
return undefined;
|
|
2134
|
-
}
|
|
2135
|
-
/** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
|
|
2136
|
-
* agent's context, so its next turn continues with the sub-workflow's results in view. */
|
|
2137
|
-
function recoveredOutputs(outcomes) {
|
|
2138
|
-
const outputs = new Map();
|
|
2139
|
-
for (const outcome of outcomes ?? []) {
|
|
2140
|
-
if (!outcome.output)
|
|
2141
|
-
continue;
|
|
2142
|
-
outputs.set(outcome.agentId, outcome.output.content);
|
|
2143
|
-
outputs.set(outcome.agentId.replace(/-i\d+$/, ""), outcome.output.content);
|
|
2144
|
-
}
|
|
2145
|
-
return outputs;
|
|
2146
|
-
}
|
|
2147
|
-
function authoredWorkflowOutcomeNote(outcome) {
|
|
2148
|
-
const counts = new Map();
|
|
2149
|
-
for (const node of outcome.nodeOutcomes)
|
|
2150
|
-
counts.set(node.status, (counts.get(node.status) ?? 0) + 1);
|
|
2151
|
-
const lines = [
|
|
2152
|
-
`[authored workflow result] ${outcome.nodeOutcomes.length} terminal node(s): ` +
|
|
2153
|
-
[...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ") + ".",
|
|
2154
|
-
];
|
|
2155
|
-
for (const node of outcome.nodeOutcomes) {
|
|
2156
|
-
const out = outcome.outputs[node.nodeId] ?? node.output?.content;
|
|
2157
|
-
if (out)
|
|
2158
|
-
lines.push(`- ${node.nodeId} (${node.status}): ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
|
|
2159
|
-
}
|
|
2160
|
-
return lines.join("\n");
|
|
2161
|
-
}
|
|
2162
1949
|
/** Lower a claimed signal delivery to the kernel's `deliver_signal` input event. Shared by the main
|
|
2163
1950
|
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
2164
1951
|
function signalToKernelEvent(delivery) {
|