@deepstrike/sdk 0.2.50 → 0.2.52
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 +83 -60
- package/dist/index.d.ts +5 -7
- package/dist/index.js +3 -3
- package/dist/kernel.d.ts +61 -31
- package/dist/runtime/canonical-kernel-step.d.ts +152 -0
- package/dist/runtime/canonical-kernel-step.js +1483 -0
- package/dist/runtime/execution-plane.d.ts +0 -3
- package/dist/runtime/execution-plane.js +0 -24
- package/dist/runtime/facade.js +3 -0
- package/dist/runtime/kernel-event-log.js +7 -13
- package/dist/runtime/kernel-journal.d.ts +264 -0
- package/dist/runtime/kernel-journal.js +741 -0
- package/dist/runtime/kernel-primitives-dashboard.d.ts +0 -2
- package/dist/runtime/kernel-primitives-dashboard.js +1 -8
- package/dist/runtime/kernel-step.d.ts +29 -109
- package/dist/runtime/kernel-step.js +47 -317
- package/dist/runtime/os-snapshot.d.ts +2 -2
- package/dist/runtime/os-snapshot.js +2 -6
- package/dist/runtime/payload-store.d.ts +16 -0
- package/dist/runtime/payload-store.js +80 -0
- package/dist/runtime/runner.d.ts +31 -114
- package/dist/runtime/runner.js +689 -774
- package/dist/runtime/session-log.d.ts +34 -32
- package/dist/runtime/session-log.js +21 -131
- 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 +1 -1
- package/dist/runtime/sub-agent-orchestrator.js +42 -40
- package/dist/types/agent.d.ts +22 -19
- package/dist/types/agent.js +26 -42
- package/dist/workflow/public.d.ts +1 -1
- package/dist/workflow/public.js +1 -1
- package/package.json +2 -2
- package/dist/runtime/kernel-rebuild.d.ts +0 -13
- package/dist/runtime/kernel-rebuild.js +0 -75
- package/dist/runtime/kernel-transaction-log.d.ts +0 -61
- package/dist/runtime/kernel-transaction-log.js +0 -149
- package/dist/runtime/large-result-spool.d.ts +0 -93
- package/dist/runtime/large-result-spool.js +0 -214
package/dist/runtime/runner.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { extractSessionMemories } from "../memory/extraction.js";
|
|
2
3
|
import { resolvePermissionRequest } from "./execution-plane.js";
|
|
3
4
|
import { GroupBudgetScope } from "./run-group.js";
|
|
4
5
|
import { getKernel } from "../kernel.js";
|
|
5
6
|
import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-replay.js";
|
|
6
7
|
import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
7
|
-
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent,
|
|
8
|
+
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, } from "./session-repair.js";
|
|
8
9
|
import { KernelPrimitivesDashboard } from "./kernel-primitives-dashboard.js";
|
|
9
|
-
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, entropySampleFromObservation,
|
|
10
|
-
import {
|
|
10
|
+
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, entropySampleFromObservation, messageToKernelMessage, skillMetadataToKernel, taskUpdateToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
11
|
+
import { CanonicalKernelRejectedError, CanonicalRunnerRuntime, canonicalKernelAction, canonicalKernelApply, canonicalKernelMaybeAction, canonicalStartAgent, canonicalStartWorkflow, } from "./canonical-kernel-step.js";
|
|
12
|
+
import { agentRunSpecToKernel, MILESTONE_UNVERIFIED_REASON, milestoneCheckFail, milestoneCheckPass, milestoneCheckResultToKernel, subAgentResultToKernel, workflowBudgetNote, workflowNodeSpecToKernel, workflowNodeOutcomeFromKernel, workflowNodeStatusFromTermination, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "../types/agent.js";
|
|
11
13
|
import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
12
14
|
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
13
15
|
import { resolveReducer } from "./reducers.js";
|
|
@@ -15,7 +17,7 @@ import { loopInstruction, classifyInstruction, judgeGoal, dependencyOutputsNote,
|
|
|
15
17
|
import { governancePolicyToKernelEvent, governanceFilterSchema } from "../governance.js";
|
|
16
18
|
import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
|
|
17
19
|
import { assertNativeProfile } from "./os-profile.js";
|
|
18
|
-
import {
|
|
20
|
+
import { PayloadStore } from "./payload-store.js";
|
|
19
21
|
import { formatToolError } from "../tools/errors.js";
|
|
20
22
|
import { ManagedTaskScope } from "./reliability.js";
|
|
21
23
|
import { contextPolicyV1, normalizeContextPolicyV1, } from "./context-policy.js";
|
|
@@ -36,13 +38,45 @@ export function schedulerPolicyToKernel(policy) {
|
|
|
36
38
|
token_cost_weight: policy.tokenCostWeight,
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
function kernelReliabilityToKernel(policy) {
|
|
42
|
+
const allowed = new Set(["providerRecoveryAttempts", "outputRecoveryAttempts", "maxInputBytes"]);
|
|
43
|
+
const unknown = Object.keys(policy).filter(key => !allowed.has(key));
|
|
44
|
+
if (unknown.length > 0) {
|
|
45
|
+
throw new TypeError(`unknown kernel reliability field(s): ${unknown.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...(policy.providerRecoveryAttempts !== undefined
|
|
49
|
+
? { provider_recovery_attempts: policy.providerRecoveryAttempts }
|
|
50
|
+
: {}),
|
|
51
|
+
...(policy.outputRecoveryAttempts !== undefined
|
|
52
|
+
? { output_recovery_attempts: policy.outputRecoveryAttempts }
|
|
53
|
+
: {}),
|
|
54
|
+
...(policy.maxInputBytes !== undefined ? { max_input_bytes: policy.maxInputBytes } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function memoryPolicyToKernel(policy) {
|
|
58
|
+
const allowed = new Set([
|
|
59
|
+
"staleWarningDays",
|
|
60
|
+
"retrievalTopK",
|
|
61
|
+
"validationEnabled",
|
|
62
|
+
"maxContentBytes",
|
|
63
|
+
"maxNameLength",
|
|
64
|
+
"promotionRecallThreshold",
|
|
65
|
+
]);
|
|
66
|
+
const unknown = Object.keys(policy).filter(key => !allowed.has(key));
|
|
67
|
+
if (unknown.length > 0) {
|
|
68
|
+
throw new TypeError(`unknown memory policy field(s): ${unknown.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...(policy.staleWarningDays !== undefined ? { stale_warning_days: policy.staleWarningDays } : {}),
|
|
72
|
+
...(policy.retrievalTopK !== undefined ? { retrieval_top_k: policy.retrievalTopK } : {}),
|
|
73
|
+
...(policy.validationEnabled !== undefined ? { validation_enabled: policy.validationEnabled } : {}),
|
|
74
|
+
...(policy.maxContentBytes !== undefined ? { max_content_bytes: policy.maxContentBytes } : {}),
|
|
75
|
+
...(policy.maxNameLength !== undefined ? { max_name_length: policy.maxNameLength } : {}),
|
|
76
|
+
...(policy.promotionRecallThreshold !== undefined
|
|
77
|
+
? { promotion_recall_threshold: policy.promotionRecallThreshold }
|
|
78
|
+
: {}),
|
|
79
|
+
};
|
|
46
80
|
}
|
|
47
81
|
function controlRequestRejection(observations, operation) {
|
|
48
82
|
const rejected = observations.find(observation => observation.kind === "control_request_rejected"
|
|
@@ -71,6 +105,17 @@ function pendingCallIds(action) {
|
|
|
71
105
|
return "effectId" in action ? [action.effectId] : [];
|
|
72
106
|
}
|
|
73
107
|
}
|
|
108
|
+
function utf8Prefix(value, maxBytes) {
|
|
109
|
+
if (maxBytes <= 0)
|
|
110
|
+
return "";
|
|
111
|
+
const bytes = Buffer.from(value, "utf8");
|
|
112
|
+
if (bytes.byteLength <= maxBytes)
|
|
113
|
+
return value;
|
|
114
|
+
let end = maxBytes;
|
|
115
|
+
while (end > 0 && (bytes[end] & 0xC0) === 0x80)
|
|
116
|
+
end -= 1;
|
|
117
|
+
return bytes.subarray(0, end).toString("utf8");
|
|
118
|
+
}
|
|
74
119
|
export class RuntimeRunner {
|
|
75
120
|
opts;
|
|
76
121
|
interrupted = false;
|
|
@@ -81,6 +126,7 @@ export class RuntimeRunner {
|
|
|
81
126
|
activeGroupBudgetScope;
|
|
82
127
|
pendingObservations = [];
|
|
83
128
|
currentSessionId = null;
|
|
129
|
+
fallbackPayloadStore = null;
|
|
84
130
|
/** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
|
|
85
131
|
injectedSignals = [];
|
|
86
132
|
/** Skill names whose content has already been pushed into the durable `knowledge` slot this
|
|
@@ -92,9 +138,6 @@ export class RuntimeRunner {
|
|
|
92
138
|
activePageOutArchive;
|
|
93
139
|
/** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
|
|
94
140
|
currentGoal = "";
|
|
95
|
-
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
96
|
-
* at the next safe point (after the tool turn resolves, kernel back in Reason — not suspended). */
|
|
97
|
-
pendingAuthoredWorkflows = [];
|
|
98
141
|
workflowContinuation = null;
|
|
99
142
|
dashboard = null;
|
|
100
143
|
/** Most recent kernel entropy sample of the active/last run (see `latestEntropy`). */
|
|
@@ -110,6 +153,10 @@ export class RuntimeRunner {
|
|
|
110
153
|
if (!Number.isInteger(schemaAttempts) || schemaAttempts < 1 || schemaAttempts > 16) {
|
|
111
154
|
throw new RangeError("workflowSchemaValidationAttempts must be an integer between 1 and 16");
|
|
112
155
|
}
|
|
156
|
+
if (opts.kernelReliability)
|
|
157
|
+
kernelReliabilityToKernel(opts.kernelReliability);
|
|
158
|
+
if (opts.memoryPolicy)
|
|
159
|
+
memoryPolicyToKernel(opts.memoryPolicy);
|
|
113
160
|
this.composedSystemPrompt = composeSystemPrompt(opts.systemPrompt, opts.instructions);
|
|
114
161
|
if (opts.enableDiagnosticsDashboard) {
|
|
115
162
|
const originalAppend = opts.sessionLog.append.bind(opts.sessionLog);
|
|
@@ -148,14 +195,23 @@ export class RuntimeRunner {
|
|
|
148
195
|
throw new Error("durable kernel transitions require a session id");
|
|
149
196
|
return resolved;
|
|
150
197
|
}
|
|
151
|
-
async commitKernelApply(runtime, pending, event,
|
|
152
|
-
return
|
|
198
|
+
async commitKernelApply(runtime, pending, event, _sessionId) {
|
|
199
|
+
return canonicalKernelApply(runtime, pending, event);
|
|
153
200
|
}
|
|
154
|
-
async commitKernelMaybeAction(runtime, pending, event,
|
|
155
|
-
return
|
|
201
|
+
async commitKernelMaybeAction(runtime, pending, event, _sessionId) {
|
|
202
|
+
return canonicalKernelMaybeAction(runtime, pending, event);
|
|
156
203
|
}
|
|
157
|
-
async commitKernelAction(runtime, pending, event,
|
|
158
|
-
return
|
|
204
|
+
async commitKernelAction(runtime, pending, event, _sessionId) {
|
|
205
|
+
return canonicalKernelAction(runtime, pending, event);
|
|
206
|
+
}
|
|
207
|
+
async startKernelAgent(runtime, pending, task, runSpec) {
|
|
208
|
+
return canonicalStartAgent(runtime, pending, task, runSpec);
|
|
209
|
+
}
|
|
210
|
+
payloadStore() {
|
|
211
|
+
if (this.opts.payloadStore)
|
|
212
|
+
return this.opts.payloadStore;
|
|
213
|
+
this.fallbackPayloadStore ??= new PayloadStore();
|
|
214
|
+
return this.fallbackPayloadStore;
|
|
159
215
|
}
|
|
160
216
|
async persistMemoryToStore(memory, agentId) {
|
|
161
217
|
if (!this.opts.dreamStore)
|
|
@@ -169,31 +225,14 @@ export class RuntimeRunner {
|
|
|
169
225
|
.slice(0, requestedK);
|
|
170
226
|
}
|
|
171
227
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* `promotion_suggested`) from the routed hits — the store stays a pure query.
|
|
176
|
-
*
|
|
177
|
-
* `seenRecordIds` is one dedupe horizon: a record hit by several queries of the same
|
|
178
|
-
* prefetch is routed (recalled, injected) once. The kernel derives counts statelessly from
|
|
179
|
-
* each hit's payload, so host-side pre-filtering is the only place duplicates can be stopped.
|
|
180
|
-
*
|
|
181
|
-
* The recall lifecycle is consumed immediately rather than via the per-turn drain: the store
|
|
182
|
-
* must be up to date before any same-turn re-query, and a renewal prefetch fires inside the
|
|
183
|
-
* drain loop where queued observations would not be consumed until the next boundary.
|
|
184
|
-
* Non-memory observations are forwarded to `leftovers` (the run's pending queue) when given,
|
|
185
|
-
* and discarded for detached syscall runtimes (their kernel is throwaway).
|
|
228
|
+
* Route a host-originated renewal prefetch into canonical knowledge commands. This is not an
|
|
229
|
+
* agent syscall: the host selects records from its store, then the kernel owns the only mutation
|
|
230
|
+
* of live semantic context. `seenRecordIds` is the prefetch's dedupe horizon.
|
|
186
231
|
*/
|
|
187
|
-
async
|
|
188
|
-
const observations = [];
|
|
189
|
-
const action = await this.commitKernelAction(runtime, observations, { kind: "query_memory", query }, sessionId);
|
|
190
|
-
if (action.kind !== "query_memory") {
|
|
191
|
-
throw new Error(`query_memory returned unexpected kernel effect: ${action.kind}`);
|
|
192
|
-
}
|
|
232
|
+
async prefetchMemoryIntoKnowledge(runtime, query, agentId, sessionId, seenRecordIds, leftovers) {
|
|
193
233
|
let hits = [];
|
|
194
|
-
let ioError;
|
|
195
234
|
try {
|
|
196
|
-
hits = await this.retrieveMemoryFromStore(query,
|
|
235
|
+
hits = await this.retrieveMemoryFromStore(query, query.top_k, agentId);
|
|
197
236
|
if (seenRecordIds) {
|
|
198
237
|
hits = hits.filter(hit => {
|
|
199
238
|
const id = hit.record.record_id;
|
|
@@ -203,77 +242,94 @@ export class RuntimeRunner {
|
|
|
203
242
|
return true;
|
|
204
243
|
});
|
|
205
244
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// run kernel must adopt it (a query is a preload, not a detached side-channel).
|
|
214
|
-
const resumed = await this.commitKernelMaybeAction(runtime, observations, {
|
|
215
|
-
kind: "memory_query_result",
|
|
216
|
-
effect_id: action.effectId,
|
|
217
|
-
hits,
|
|
218
|
-
...(ioError ? { error: formatToolError(ioError) } : {}),
|
|
219
|
-
}, sessionId);
|
|
220
|
-
await this.consumeMemoryLifecycleObservations(sessionId, observations);
|
|
221
|
-
if (leftovers) {
|
|
222
|
-
for (const obs of observations) {
|
|
223
|
-
if (!isMemoryLifecycleObservation(obs))
|
|
224
|
-
leftovers.push(obs);
|
|
245
|
+
for (const hit of hits) {
|
|
246
|
+
await this.commitKernelApply(runtime, leftovers ?? [], {
|
|
247
|
+
kind: "add_knowledge_message",
|
|
248
|
+
key: `memory:${hit.record.record_id}`,
|
|
249
|
+
content: hit.record.content,
|
|
250
|
+
tokens: Math.max(1, Math.ceil(hit.record.content.length / 4)),
|
|
251
|
+
}, sessionId);
|
|
225
252
|
}
|
|
253
|
+
await this.applyHostMemoryRecallLifecycle(hits, agentId);
|
|
254
|
+
await this.logMemoryRetrievalResult(sessionId, hits);
|
|
226
255
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
256
|
+
catch {
|
|
257
|
+
return { hits: [], action: runtime.resumeAction() };
|
|
258
|
+
}
|
|
259
|
+
return { hits, action: runtime.resumeAction() };
|
|
230
260
|
}
|
|
231
261
|
async writeMemory(memory, opts = {}) {
|
|
232
262
|
const sessionId = opts.sessionId ?? this.currentSessionId;
|
|
233
263
|
const agentId = opts.agentId ?? this.opts.agentId;
|
|
234
264
|
if (!this.opts.dreamStore || !agentId)
|
|
235
265
|
return;
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
266
|
+
const policy = this.opts.memoryPolicy;
|
|
267
|
+
if (policy?.validationEnabled !== false) {
|
|
268
|
+
const error = !memory.name.trim()
|
|
269
|
+
? "memory name must not be empty"
|
|
270
|
+
: memory.name.length > (policy?.maxNameLength ?? 100)
|
|
271
|
+
? `memory name exceeds ${policy?.maxNameLength ?? 100} characters`
|
|
272
|
+
: Buffer.byteLength(memory.content, "utf8") > (policy?.maxContentBytes ?? 10_000)
|
|
273
|
+
? `memory content exceeds ${policy?.maxContentBytes ?? 10_000} bytes`
|
|
274
|
+
: undefined;
|
|
275
|
+
if (error) {
|
|
276
|
+
if (sessionId) {
|
|
277
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
278
|
+
kind: "memory_validation_failed",
|
|
279
|
+
turn: this.activeKernel?.turn() ?? 0,
|
|
280
|
+
record_id: memory.record_id,
|
|
281
|
+
error,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
246
286
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
await this.
|
|
287
|
+
await this.persistMemoryToStore(memory, agentId);
|
|
288
|
+
if (sessionId) {
|
|
289
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
290
|
+
kind: "memory_written",
|
|
291
|
+
turn: this.activeKernel?.turn() ?? 0,
|
|
292
|
+
record_id: memory.record_id,
|
|
293
|
+
scope: memory.scope,
|
|
294
|
+
memory_kind: memory.kind,
|
|
295
|
+
name: memory.name,
|
|
296
|
+
size_bytes: Buffer.byteLength(memory.content, "utf8"),
|
|
297
|
+
});
|
|
250
298
|
}
|
|
251
|
-
catch (cause) {
|
|
252
|
-
ioError = cause;
|
|
253
|
-
}
|
|
254
|
-
await this.commitKernelApply(runtime, observations, {
|
|
255
|
-
kind: "memory_persist_result",
|
|
256
|
-
effect_id: action.effectId,
|
|
257
|
-
...(ioError ? { error: formatToolError(ioError) } : {}),
|
|
258
|
-
}, durableSessionId);
|
|
259
|
-
await this.consumeMemoryLifecycleObservations(durableSessionId, observations);
|
|
260
|
-
if (ioError)
|
|
261
|
-
throw ioError;
|
|
262
299
|
}
|
|
263
300
|
async queryMemory(query, opts = {}) {
|
|
264
301
|
const sessionId = opts.sessionId ?? this.currentSessionId;
|
|
265
302
|
const agentId = opts.agentId ?? this.opts.agentId;
|
|
266
303
|
if (!this.opts.dreamStore || !agentId)
|
|
267
304
|
return [];
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// (T5 parity).
|
|
272
|
-
const runtime = this.createSyscallRuntime();
|
|
273
|
-
const { hits } = await this.queryMemoryThroughKernel(runtime, query, agentId, durableSessionId);
|
|
274
|
-
await this.logMemoryRetrievalResult(durableSessionId, hits);
|
|
305
|
+
const hits = await this.retrieveMemoryFromStore(query, query.top_k, agentId);
|
|
306
|
+
await this.applyHostMemoryRecallLifecycle(hits, agentId);
|
|
307
|
+
await this.logMemoryRetrievalResult(sessionId, hits);
|
|
275
308
|
return hits;
|
|
276
309
|
}
|
|
310
|
+
async applyHostMemoryRecallLifecycle(hits, agentId) {
|
|
311
|
+
if (hits.length === 0)
|
|
312
|
+
return;
|
|
313
|
+
const recalls = hits.map(hit => ({
|
|
314
|
+
record_id: hit.record.record_id,
|
|
315
|
+
recall_count: hit.record.recall_count + 1,
|
|
316
|
+
last_recalled_at: Date.now(),
|
|
317
|
+
}));
|
|
318
|
+
await this.opts.dreamStore?.recordRecall?.(agentId, recalls);
|
|
319
|
+
const threshold = this.opts.memoryPolicy?.promotionRecallThreshold;
|
|
320
|
+
if (threshold === undefined)
|
|
321
|
+
return;
|
|
322
|
+
for (let index = 0; index < hits.length; index += 1) {
|
|
323
|
+
const before = hits[index].record.recall_count;
|
|
324
|
+
const after = recalls[index].recall_count;
|
|
325
|
+
if (before < threshold && after >= threshold) {
|
|
326
|
+
this.opts.onPromotionSuggested?.({
|
|
327
|
+
recordId: recalls[index].record_id,
|
|
328
|
+
recallCount: after,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
277
333
|
async logMemoryRetrievalResult(sessionId, hits) {
|
|
278
334
|
if (!sessionId)
|
|
279
335
|
return;
|
|
@@ -284,16 +340,35 @@ export class RuntimeRunner {
|
|
|
284
340
|
hits,
|
|
285
341
|
});
|
|
286
342
|
}
|
|
287
|
-
|
|
288
|
-
const {
|
|
289
|
-
return new
|
|
290
|
-
|
|
343
|
+
createCanonicalRuntime(runId = crypto.randomUUID(), sessionId = this.durableSessionId()) {
|
|
344
|
+
const { CanonicalKernel } = getKernel();
|
|
345
|
+
return new CanonicalRunnerRuntime(new CanonicalKernel(), this.resolveKernelJournal(), `node-operation-${runId}`, {
|
|
346
|
+
maxContextTokens: this.opts.maxTokens,
|
|
291
347
|
maxTurns: this.opts.maxTurns,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
348
|
+
maxTotalTokens: this.opts.maxTotalTokens,
|
|
349
|
+
maxWallMs: this.opts.timeoutMs,
|
|
350
|
+
memoryBindingId: `node-memory-${this.opts.agentId ?? "root"}`,
|
|
351
|
+
persistPayload: async (callId, content, previewBytes) => {
|
|
352
|
+
const digest = `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
353
|
+
const payloadRef = `payload:${digest.slice("sha256:".length, "sha256:".length + 32)}`;
|
|
354
|
+
await this.payloadStore().persistPayload(sessionId, payloadRef, content);
|
|
355
|
+
return {
|
|
356
|
+
payloadRef,
|
|
357
|
+
digest,
|
|
358
|
+
originalSize: String(Buffer.byteLength(content, "utf8")),
|
|
359
|
+
preview: utf8Prefix(content, previewBytes),
|
|
360
|
+
};
|
|
361
|
+
},
|
|
295
362
|
});
|
|
296
363
|
}
|
|
364
|
+
resolveKernelJournal() {
|
|
365
|
+
const embedded = this.opts.sessionLog.kernelJournal;
|
|
366
|
+
const journal = this.opts.kernelJournal ?? embedded;
|
|
367
|
+
if (!journal) {
|
|
368
|
+
throw new Error("RuntimeOptions.kernelJournal is required when SessionLog has no canonical journal");
|
|
369
|
+
}
|
|
370
|
+
return journal;
|
|
371
|
+
}
|
|
297
372
|
groupBudgetRequest(includeTokens = true) {
|
|
298
373
|
const tokens = includeTokens ? this.opts.maxTotalTokens : undefined;
|
|
299
374
|
const subagents = this.opts.resourceQuota?.maxTotalSubagents;
|
|
@@ -313,7 +388,10 @@ export class RuntimeRunner {
|
|
|
313
388
|
};
|
|
314
389
|
}
|
|
315
390
|
async settleGroupBudget(scope, actual) {
|
|
316
|
-
const retries = this.opts.
|
|
391
|
+
const retries = this.opts.groupBudgetSettlementRetries ?? 3;
|
|
392
|
+
if (!Number.isInteger(retries) || retries < 0 || retries > 16) {
|
|
393
|
+
throw new RangeError("groupBudgetSettlementRetries must be an integer in 0..16");
|
|
394
|
+
}
|
|
317
395
|
for (let attempt = 0;; attempt += 1) {
|
|
318
396
|
try {
|
|
319
397
|
await scope.settle(actual);
|
|
@@ -327,9 +405,9 @@ export class RuntimeRunner {
|
|
|
327
405
|
}
|
|
328
406
|
/**
|
|
329
407
|
* Lower the declarative governance / attention / scheduler-budget / resource-quota policies into a
|
|
330
|
-
* freshly-created kernel. Shared by `execute()` (full agent run) and `
|
|
408
|
+
* freshly-created kernel. Shared by `execute()` (full agent run) and `initializeWorkflowKernel()`
|
|
331
409
|
* (standalone host-driven workflow) so a workflow's DAG-node spawns are gated, queued, and quota'd
|
|
332
|
-
* exactly as a mid-run spawn would be. Must run
|
|
410
|
+
* exactly as a mid-run spawn would be. Must run before the canonical root start so the gate enforces
|
|
333
411
|
* every policy from the first spawn. No config ⇒ the native-profile defaults (铁律: defaults only).
|
|
334
412
|
*/
|
|
335
413
|
async applyKernelPolicies(runtime, groupBudgetScope) {
|
|
@@ -348,39 +426,7 @@ export class RuntimeRunner {
|
|
|
348
426
|
config.context_policy = normalizeContextPolicyV1(contextPolicyV1(this.opts.contextPolicy));
|
|
349
427
|
}
|
|
350
428
|
if (this.opts.kernelReliability) {
|
|
351
|
-
|
|
352
|
-
config.reliability = {
|
|
353
|
-
...(reliability.eventReplayCapacity !== undefined
|
|
354
|
-
? { event_replay_capacity: reliability.eventReplayCapacity }
|
|
355
|
-
: {}),
|
|
356
|
-
...(reliability.completedEffectReplayCapacity !== undefined
|
|
357
|
-
? { completed_effect_replay_capacity: reliability.completedEffectReplayCapacity }
|
|
358
|
-
: {}),
|
|
359
|
-
...(reliability.providerRecoveryAttempts !== undefined
|
|
360
|
-
? { provider_recovery_attempts: reliability.providerRecoveryAttempts }
|
|
361
|
-
: {}),
|
|
362
|
-
...(reliability.outputRecoveryAttempts !== undefined
|
|
363
|
-
? { output_recovery_attempts: reliability.outputRecoveryAttempts }
|
|
364
|
-
: {}),
|
|
365
|
-
...(reliability.hostEffectRetryAttempts !== undefined
|
|
366
|
-
? { host_effect_retry_attempts: reliability.hostEffectRetryAttempts }
|
|
367
|
-
: {}),
|
|
368
|
-
...(reliability.spoolThresholdBytes !== undefined
|
|
369
|
-
? { spool_threshold_bytes: reliability.spoolThresholdBytes }
|
|
370
|
-
: {}),
|
|
371
|
-
...(reliability.spoolPreviewBytes !== undefined
|
|
372
|
-
? { spool_preview_bytes: reliability.spoolPreviewBytes }
|
|
373
|
-
: {}),
|
|
374
|
-
...(reliability.snapshotInputLimit !== undefined
|
|
375
|
-
? { snapshot_input_limit: reliability.snapshotInputLimit }
|
|
376
|
-
: {}),
|
|
377
|
-
...(reliability.maxInputBytes !== undefined
|
|
378
|
-
? { max_input_bytes: reliability.maxInputBytes }
|
|
379
|
-
: {}),
|
|
380
|
-
...(reliability.snapshotJournalBytesLimit !== undefined
|
|
381
|
-
? { snapshot_journal_bytes_limit: reliability.snapshotJournalBytesLimit }
|
|
382
|
-
: {}),
|
|
383
|
-
};
|
|
429
|
+
config.reliability = kernelReliabilityToKernel(this.opts.kernelReliability);
|
|
384
430
|
}
|
|
385
431
|
config.signal_policy = {
|
|
386
432
|
version: 1,
|
|
@@ -462,9 +508,7 @@ export class RuntimeRunner {
|
|
|
462
508
|
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
463
509
|
}
|
|
464
510
|
/**
|
|
465
|
-
* Mirror one
|
|
466
|
-
* Shared by the main run drain, the prefetch path, and the host memory syscalls so every
|
|
467
|
-
* query route has identical recall + promotion semantics (T5).
|
|
511
|
+
* Mirror one agent-syscall memory-lifecycle observation into the durable store / host callbacks.
|
|
468
512
|
*
|
|
469
513
|
* M3: `memory_recalled` carries the kernel-derived count — the runner never computes
|
|
470
514
|
* `recall_count + 1` itself. M4: `promotion_suggested` is advisory and already
|
|
@@ -484,19 +528,6 @@ export class RuntimeRunner {
|
|
|
484
528
|
});
|
|
485
529
|
}
|
|
486
530
|
}
|
|
487
|
-
async consumeMemoryLifecycleObservations(sessionId, observations) {
|
|
488
|
-
const turn = this.activeKernel?.turn() ?? 0;
|
|
489
|
-
for (const obs of observations) {
|
|
490
|
-
if (!isMemoryLifecycleObservation(obs))
|
|
491
|
-
continue;
|
|
492
|
-
await this.mirrorMemoryLifecycle(obs);
|
|
493
|
-
if (!sessionId)
|
|
494
|
-
continue;
|
|
495
|
-
const event = kernelObservationToSessionEvent(obs, turn);
|
|
496
|
-
if (event)
|
|
497
|
-
await this.opts.sessionLog.append(sessionId, event);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
531
|
/** Mount a tool capability on the currently-running kernel runtime. No-op if not running. */
|
|
501
532
|
async mountTool(schema) {
|
|
502
533
|
if (!this.activeKernel)
|
|
@@ -554,48 +585,6 @@ export class RuntimeRunner {
|
|
|
554
585
|
// Re-arm the SDK-side push guard so a re-activation re-pins the content.
|
|
555
586
|
this.knowledgePushedSkills.delete(name);
|
|
556
587
|
}
|
|
557
|
-
/**
|
|
558
|
-
* Spawn an isolated sub-agent via the kernel, run it on the host, and feed the result back.
|
|
559
|
-
* Requires an active parent run (`run()` / `wake()` in progress or paused at milestone).
|
|
560
|
-
*/
|
|
561
|
-
async *spawnSubAgent(spec) {
|
|
562
|
-
if (!this.activeKernel || !this.currentSessionId) {
|
|
563
|
-
throw new Error("spawnSubAgent requires an active parent run");
|
|
564
|
-
}
|
|
565
|
-
const parentSessionId = this.currentSessionId;
|
|
566
|
-
const runtime = this.activeKernel;
|
|
567
|
-
const observations = await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
568
|
-
kind: "spawn_sub_agent",
|
|
569
|
-
spec: agentRunSpecToKernel(spec),
|
|
570
|
-
parent_session_id: parentSessionId,
|
|
571
|
-
});
|
|
572
|
-
this.nextArchiveStart = await this.appendObservations(parentSessionId, runtime, this.nextArchiveStart);
|
|
573
|
-
const spawned = findSpawnProcessObservation(observations);
|
|
574
|
-
if (!spawned) {
|
|
575
|
-
const rejected = controlRequestRejection(observations, "spawn_sub_agent");
|
|
576
|
-
if (rejected) {
|
|
577
|
-
yield { type: "error", message: `spawn_sub_agent denied: ${rejected.reason}` };
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
throw new Error("spawn_sub_agent did not emit agent_process_changed");
|
|
581
|
-
}
|
|
582
|
-
const manifest = spawnObservationToManifest(spawned, spec, parentSessionId);
|
|
583
|
-
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
584
|
-
const result = await orchestrator.run({
|
|
585
|
-
parentOpts: this.opts,
|
|
586
|
-
parentSessionId,
|
|
587
|
-
spec,
|
|
588
|
-
manifest,
|
|
589
|
-
sessionLog: this.opts.sessionLog,
|
|
590
|
-
toolAccess: spec.toolAccess,
|
|
591
|
-
...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
|
|
592
|
-
});
|
|
593
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
594
|
-
kind: "sub_agent_completed",
|
|
595
|
-
result: subAgentResultToKernel(result),
|
|
596
|
-
});
|
|
597
|
-
yield { type: "done", iterations: result.result.turnsUsed, totalTokens: result.result.totalTokensUsed, status: result.result.termination };
|
|
598
|
-
}
|
|
599
588
|
/**
|
|
600
589
|
* G3: run one workflow node, enforcing its `output_schema` (if any). Without a schema this is a
|
|
601
590
|
* plain `orchestrator.run`. With one, the node's agent is instructed to emit conforming JSON, its
|
|
@@ -625,8 +614,8 @@ export class RuntimeRunner {
|
|
|
625
614
|
spec: { ...baseSpec, goal: withBudget(goal) },
|
|
626
615
|
manifest,
|
|
627
616
|
sessionLog: this.opts.sessionLog,
|
|
628
|
-
//
|
|
629
|
-
//
|
|
617
|
+
// This child is a workflow node, so capability resolution applies workflow-node quarantine
|
|
618
|
+
// semantics instead of treating it as an independently spawned agent.
|
|
630
619
|
isWorkflowNode: true,
|
|
631
620
|
// W-N1: trusted workflow nodes run on the parent's execution plane (they carry no grant list
|
|
632
621
|
// by design — filtering on the missing list ran every DAG node TOOL-LESS); quarantined nodes
|
|
@@ -736,7 +725,7 @@ export class RuntimeRunner {
|
|
|
736
725
|
*/
|
|
737
726
|
async runWorkflow(spec, opts) {
|
|
738
727
|
// Standalone entry: with no active parent run (e.g. a stateless HTTP handler), auto-bootstrap a
|
|
739
|
-
// kernel that owns the DAG —
|
|
728
|
+
// kernel that owns the DAG — canonical configure + root start with the same policies a full run
|
|
740
729
|
// gets — then tear it down on completion so the runner is reusable. Mid-run callers (activeKernel
|
|
741
730
|
// already set by an in-flight `run()`) keep the original in-place behavior with no teardown.
|
|
742
731
|
const bootstrapped = !this.activeKernel || !this.currentSessionId;
|
|
@@ -744,6 +733,7 @@ export class RuntimeRunner {
|
|
|
744
733
|
try {
|
|
745
734
|
if (bootstrapped) {
|
|
746
735
|
const sessionId = opts?.sessionId ?? `wf-${crypto.randomUUID()}`;
|
|
736
|
+
const runId = crypto.randomUUID();
|
|
747
737
|
// A standalone workflow reserves a bounded slice before its kernel schedules any node.
|
|
748
738
|
// Mid-run callers reuse their parent run's already-active reservation.
|
|
749
739
|
if (this.opts.runGroup) {
|
|
@@ -754,46 +744,56 @@ export class RuntimeRunner {
|
|
|
754
744
|
// Resume depends on this fact. Do not dispatch any node until it is durable.
|
|
755
745
|
await this.opts.sessionLog.append(sessionId, {
|
|
756
746
|
kind: "run_started",
|
|
757
|
-
run_id:
|
|
747
|
+
run_id: runId,
|
|
758
748
|
goal: `workflow:${spec.nodes.length} nodes`,
|
|
759
749
|
criteria: [],
|
|
760
750
|
agent_id: this.opts.agentId,
|
|
761
751
|
});
|
|
762
|
-
await this.
|
|
752
|
+
await this.initializeWorkflowKernel(sessionId, runId, groupBudgetScope);
|
|
763
753
|
}
|
|
764
754
|
const parentSessionId = this.currentSessionId;
|
|
765
755
|
const runtime = this.activeKernel;
|
|
766
756
|
const observationStart = this.pendingObservations.length;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
: {}),
|
|
785
|
-
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
786
|
-
...(opts?.resumedSubmissions?.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
787
|
-
...(opts?.resumedSubmissionBases?.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
|
|
788
|
-
});
|
|
757
|
+
let initialAction;
|
|
758
|
+
try {
|
|
759
|
+
initialAction = await canonicalStartWorkflow(runtime, this.pendingObservations, workflowSpecToKernel(spec));
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
if (!(error instanceof CanonicalKernelRejectedError))
|
|
763
|
+
throw error;
|
|
764
|
+
return {
|
|
765
|
+
nodeOutcomes: [],
|
|
766
|
+
outputs: {},
|
|
767
|
+
rejection: {
|
|
768
|
+
operation: "start_workflow",
|
|
769
|
+
reason: String(error.fault.message ?? error.message),
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
}
|
|
789
773
|
const observations = this.pendingObservations.slice(observationStart);
|
|
790
|
-
const outcome = await this.driveWorkflow(initialAction, observations, parentSessionId, runtime,
|
|
774
|
+
const outcome = await this.driveWorkflow(initialAction, observations, parentSessionId, runtime, new Map());
|
|
791
775
|
if (bootstrapped) {
|
|
792
|
-
|
|
776
|
+
let terminal = runtime.resumeAction();
|
|
777
|
+
if (!terminal)
|
|
778
|
+
throw new Error("completed canonical workflow has no terminal action");
|
|
779
|
+
if (terminal.kind !== "done" && this.interrupted) {
|
|
780
|
+
terminal = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
781
|
+
kind: "cancel_operation",
|
|
782
|
+
effect_id: terminal.effectId,
|
|
783
|
+
reason: this.cancellationReason ?? "user",
|
|
784
|
+
});
|
|
785
|
+
}
|
|
793
786
|
if (terminal.kind !== "done") {
|
|
794
|
-
throw new Error("
|
|
787
|
+
throw new Error("canonical workflow did not produce a terminal kernel action");
|
|
795
788
|
}
|
|
796
789
|
await this.appendObservations(parentSessionId, runtime, 0);
|
|
790
|
+
if (groupBudgetScope && !groupBudgetScope.isClosed) {
|
|
791
|
+
await this.settleGroupBudget(groupBudgetScope, {
|
|
792
|
+
tokens: terminal.result.totalTokensUsed,
|
|
793
|
+
subagents: runtime.localSubagentsSpawned(),
|
|
794
|
+
});
|
|
795
|
+
this.activeGroupBudgetScope = undefined;
|
|
796
|
+
}
|
|
797
797
|
}
|
|
798
798
|
return outcome;
|
|
799
799
|
}
|
|
@@ -816,81 +816,22 @@ export class RuntimeRunner {
|
|
|
816
816
|
/**
|
|
817
817
|
* Bootstrap a standalone kernel for a host-driven workflow with NO active parent run — the path a
|
|
818
818
|
* stateless request handler takes when it calls `runWorkflow(spec)` directly. Mirrors `execute()`'s
|
|
819
|
-
* pre-run kernel setup (governance / attention / quota via `applyKernelPolicies`, then
|
|
819
|
+
* pre-run kernel setup (governance / attention / quota via `applyKernelPolicies`, then root start)
|
|
820
820
|
* after `runWorkflow` has durably recorded `run_started`. Sets `activeKernel` / `currentSessionId`;
|
|
821
821
|
* `runWorkflow` is responsible for tearing them down.
|
|
822
822
|
*/
|
|
823
|
-
async
|
|
823
|
+
async initializeWorkflowKernel(sessionId, runId, groupBudgetScope) {
|
|
824
824
|
this.interrupted = false;
|
|
825
825
|
this.abortController = new AbortController();
|
|
826
826
|
this.pendingObservations = [];
|
|
827
827
|
this.pendingPageOutArchives = [];
|
|
828
828
|
this.activePageOutArchive = undefined;
|
|
829
829
|
this.currentSessionId = sessionId;
|
|
830
|
-
const runtime = this.
|
|
830
|
+
const runtime = this.createCanonicalRuntime(runId, sessionId);
|
|
831
831
|
this.activeKernel = runtime;
|
|
832
832
|
await this.applyKernelPolicies(runtime, groupBudgetScope);
|
|
833
|
-
// ABI v2 has one lifecycle: standalone workflows start a real run before loading their DAG.
|
|
834
|
-
// The initial provider effect is superseded by the workflow load; no self-bootstrap escape hatch.
|
|
835
|
-
await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
836
|
-
kind: "start_run",
|
|
837
|
-
task: { goal: `workflow session ${sessionId}`, criteria: [] },
|
|
838
|
-
});
|
|
839
833
|
return runtime;
|
|
840
834
|
}
|
|
841
|
-
/**
|
|
842
|
-
* M5/G1: bootstrap an **agent-authored** workflow ("the model writes its own harness"). Unlike
|
|
843
|
-
* `runWorkflow` (the host fires the privileged `load_workflow`), this routes the spec through the
|
|
844
|
-
* agent-reachable `Syscall::LoadWorkflow` (the `submit_workflow` event): with no workflow active the
|
|
845
|
-
* kernel **bootstraps** the DAG; if one is already active it **flattens** the spec's nodes onto it
|
|
846
|
-
* (bootstrap-or-flatten — one kernel, one quota, never a workflow stack). Gated by the same
|
|
847
|
-
* `max_workflow_nodes` backstop as runtime submission, so an authored harness can't overgrow the run.
|
|
848
|
-
* The resulting batches are driven by the same shared driver as `runWorkflow`.
|
|
849
|
-
*/
|
|
850
|
-
async bootstrapWorkflow(spec, opts) {
|
|
851
|
-
if (!this.activeKernel || !this.currentSessionId) {
|
|
852
|
-
throw new Error("bootstrapWorkflow requires an active parent run");
|
|
853
|
-
}
|
|
854
|
-
const parentSessionId = this.currentSessionId;
|
|
855
|
-
const runtime = this.activeKernel;
|
|
856
|
-
const observationStart = this.pendingObservations.length;
|
|
857
|
-
const initialAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
858
|
-
const observations = this.pendingObservations.slice(observationStart);
|
|
859
|
-
// W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
|
|
860
|
-
// announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
|
|
861
|
-
// had this spec, unlike the `runWorkflow` path.
|
|
862
|
-
const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
|
|
863
|
-
if (submitted) {
|
|
864
|
-
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
865
|
-
turn: runtime.turn(),
|
|
866
|
-
nodes: workflowSpecToKernel(spec).nodes ?? [],
|
|
867
|
-
baseIndex: submitted.base,
|
|
868
|
-
submitterAgentId: opts?.submitterAgentId,
|
|
869
|
-
}));
|
|
870
|
-
}
|
|
871
|
-
return this.driveWorkflow(initialAction, observations, parentSessionId, runtime);
|
|
872
|
-
}
|
|
873
|
-
/**
|
|
874
|
-
* M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`. Called at the
|
|
875
|
-
* verified-safe point (right after the tool turn resolved to `call_provider` — kernel in Reason, not
|
|
876
|
-
* suspended). For each authored spec: `bootstrapWorkflow` runs it in THIS kernel (the kernel resumes
|
|
877
|
-
* the agent reason loop on `workflow_completed` — `finish_workflow` sets phase=Reason), then the
|
|
878
|
-
* outcome is injected as a user message so the agent's next turn sees the result. Returns a fresh
|
|
879
|
-
* `call_provider` synthesized from the updated context (the workflow drive consumed its own kernel
|
|
880
|
-
* actions, so we re-render — the same pattern as the reactive-compact retry path).
|
|
881
|
-
*/
|
|
882
|
-
async driveAuthoredWorkflows(runtime, action) {
|
|
883
|
-
const specs = this.pendingAuthoredWorkflows;
|
|
884
|
-
this.pendingAuthoredWorkflows = [];
|
|
885
|
-
this.workflowContinuation = null;
|
|
886
|
-
for (const spec of specs) {
|
|
887
|
-
await this.bootstrapWorkflow(spec);
|
|
888
|
-
}
|
|
889
|
-
const continuation = this.workflowContinuation;
|
|
890
|
-
if (!continuation)
|
|
891
|
-
throw new Error("authored workflow completed without a provider continuation");
|
|
892
|
-
return continuation;
|
|
893
|
-
}
|
|
894
835
|
/**
|
|
895
836
|
* #2-B-ii: while a workflow batch is in flight, poll the signal source. A Critical `InterruptNow`
|
|
896
837
|
* routes through the kernel (which, with the root suspended in `SubAgentAwait`, preempts — marks the
|
|
@@ -901,9 +842,48 @@ export class RuntimeRunner {
|
|
|
901
842
|
*/
|
|
902
843
|
async monitorWorkflowPreemption(runtime, controllers, batchState) {
|
|
903
844
|
const source = this.opts.signalSource;
|
|
904
|
-
if (!source)
|
|
905
|
-
return null;
|
|
906
845
|
while (!batchState.settled) {
|
|
846
|
+
if (this.interrupted) {
|
|
847
|
+
const observationStart = this.pendingObservations.length;
|
|
848
|
+
let cancellation = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
849
|
+
kind: "cancel_operation",
|
|
850
|
+
reason: this.cancellationReason ?? "user",
|
|
851
|
+
});
|
|
852
|
+
let preemptedAgentIds = [...controllers.keys()];
|
|
853
|
+
if (cancellation.kind === "preempt_sub_agents") {
|
|
854
|
+
preemptedAgentIds = cancellation.agentIds;
|
|
855
|
+
for (const id of cancellation.agentIds) {
|
|
856
|
+
controllers.get(id)?.abort(this.cancellationReason ?? "user");
|
|
857
|
+
}
|
|
858
|
+
cancellation = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
859
|
+
kind: "preempt_result",
|
|
860
|
+
effect_id: cancellation.effectId,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
for (const controller of controllers.values()) {
|
|
865
|
+
controller.abort(this.cancellationReason ?? "user");
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (cancellation.kind !== "done") {
|
|
869
|
+
throw new Error(`workflow cancellation returned unexpected effect: ${cancellation.kind}`);
|
|
870
|
+
}
|
|
871
|
+
const observations = this.pendingObservations.slice(observationStart);
|
|
872
|
+
const completed = observations.find(o => o.kind === "workflow_completed");
|
|
873
|
+
if (completed) {
|
|
874
|
+
return (completed.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel);
|
|
875
|
+
}
|
|
876
|
+
const preempted = observations.find(o => o.kind === "agent_preempted");
|
|
877
|
+
return (preempted?.agent_ids ?? preemptedAgentIds).map(nodeId => ({
|
|
878
|
+
nodeId,
|
|
879
|
+
status: "failed",
|
|
880
|
+
termination: "user_abort",
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
if (!source) {
|
|
884
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
907
887
|
// O2: injected notes participate in the monitor too, so a host `injectNote` mid-batch is not
|
|
908
888
|
// stranded until the batch settles (the drain order matches `nextInboundSignal`).
|
|
909
889
|
const delivery = await this.nextInboundSignal();
|
|
@@ -936,6 +916,8 @@ export class RuntimeRunner {
|
|
|
936
916
|
}
|
|
937
917
|
const preempted = observations.find(o => o.kind === "agent_preempted");
|
|
938
918
|
if (preempted) {
|
|
919
|
+
this.interrupted = true;
|
|
920
|
+
this.cancellationReason ??= "user";
|
|
939
921
|
for (const id of preempted.agent_ids ?? [])
|
|
940
922
|
controllers.get(id)?.abort();
|
|
941
923
|
const wc = observations.find(o => o.kind === "workflow_completed");
|
|
@@ -945,10 +927,8 @@ export class RuntimeRunner {
|
|
|
945
927
|
return null;
|
|
946
928
|
}
|
|
947
929
|
/**
|
|
948
|
-
*
|
|
949
|
-
*
|
|
950
|
-
* batch in parallel, feed completions back (appending any agent-submitted nodes first), and loop
|
|
951
|
-
* until the kernel reports the workflow complete. Returns typed terminal node outcomes.
|
|
930
|
+
* Drive a canonical root or provider-authored workflow from kernel effects only: run each
|
|
931
|
+
* emitted batch, resolve its launch/completion/preemption effects, and stop at the kernel terminal.
|
|
952
932
|
*/
|
|
953
933
|
async driveWorkflow(initialAction, initial, parentSessionId, runtime, seedOutputs) {
|
|
954
934
|
let observations = initial;
|
|
@@ -993,9 +973,16 @@ export class RuntimeRunner {
|
|
|
993
973
|
// W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
|
|
994
974
|
// still see their (pre-crash) dependencies' outputs.
|
|
995
975
|
const outputs = new Map(seedOutputs ?? []);
|
|
976
|
+
const completedNodeOutcomes = [];
|
|
996
977
|
for (;;) {
|
|
997
978
|
if (nodes.length === 0)
|
|
998
979
|
return { nodeOutcomes: [], outputs: Object.fromEntries(outputs) }; // nothing to run (e.g. all gated)
|
|
980
|
+
for (const node of nodes) {
|
|
981
|
+
for (const [agentId, output] of Object.entries(node.dependency_outputs ?? {})) {
|
|
982
|
+
if (!outputs.has(agentId))
|
|
983
|
+
outputs.set(agentId, output);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
999
986
|
// Run the currently-runnable nodes in parallel — each is independent within a round.
|
|
1000
987
|
const roundBudget = budget;
|
|
1001
988
|
// #2-B-ii: per-node abort controllers + a concurrent preemption monitor. While the batch is in
|
|
@@ -1008,8 +995,9 @@ export class RuntimeRunner {
|
|
|
1008
995
|
const results = await Promise.all(nodes.map(node => this.runWorkflowNode(node, parentSessionId, orchestrator, roundBudget, outputs, controllers.get(node.agent_id)?.signal)));
|
|
1009
996
|
batchState.settled = true;
|
|
1010
997
|
const preempted = await monitor;
|
|
1011
|
-
if (preempted)
|
|
998
|
+
if (preempted !== null) {
|
|
1012
999
|
return { nodeOutcomes: preempted, outputs: Object.fromEntries(outputs) };
|
|
1000
|
+
}
|
|
1013
1001
|
// Feed completions back one at a time. The kernel's run-queue executor may spawn a node's
|
|
1014
1002
|
// dependents the moment *that* node completes (per-node unblock), so each feed can emit its
|
|
1015
1003
|
// own `workflow_batch_spawned`; ACCUMULATE them across the round rather than keeping only the
|
|
@@ -1023,61 +1011,17 @@ export class RuntimeRunner {
|
|
|
1023
1011
|
const outContent = result.result.finalMessage?.content;
|
|
1024
1012
|
const outText = typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "";
|
|
1025
1013
|
outputs.set(result.agentId, outText);
|
|
1014
|
+
completedNodeOutcomes.push({
|
|
1015
|
+
nodeId: result.agentId,
|
|
1016
|
+
status: workflowNodeStatusFromTermination(result.result.termination),
|
|
1017
|
+
termination: result.result.termination,
|
|
1018
|
+
...(result.result.finalMessage ? { output: result.result.finalMessage } : {}),
|
|
1019
|
+
});
|
|
1026
1020
|
// A loop iteration completes under `wf-node{N}-i{k}` but its dependents consume the STABLE
|
|
1027
1021
|
// node id `wf-node{N}` — alias it so the LAST iteration's output is what dependents see.
|
|
1028
1022
|
const stableId = result.agentId.replace(/-i\d+$/, "");
|
|
1029
1023
|
if (stableId !== result.agentId)
|
|
1030
1024
|
outputs.set(stableId, outText);
|
|
1031
|
-
// R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
|
|
1032
|
-
// reporting the node's completion — the workflow is still active (the kernel hasn't seen this
|
|
1033
|
-
// node finish), so even a submission from the last running node keeps the DAG alive. The
|
|
1034
|
-
// appended nodes' `workflow_batch_spawned` is collected into this round like any other.
|
|
1035
|
-
if (result.submittedNodes?.length) {
|
|
1036
|
-
// G1: stamp the submitting node's agent id so the kernel can coerce a quarantined
|
|
1037
|
-
// submitter's nodes to quarantined (no topological privilege escalation).
|
|
1038
|
-
const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
|
|
1039
|
-
const observationStart = this.pendingObservations.length;
|
|
1040
|
-
const submitAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, submitEvent);
|
|
1041
|
-
const subObs = this.pendingObservations.slice(observationStart);
|
|
1042
|
-
const rejected = controlRequestRejection(subObs, "submit_workflow_nodes")
|
|
1043
|
-
?? (subObs.find(o => o.kind === "nodes_rejected")
|
|
1044
|
-
? { operation: "submit_workflow_nodes", reason: String(subObs.find(o => o.kind === "nodes_rejected")?.reason ?? "request denied") }
|
|
1045
|
-
: undefined);
|
|
1046
|
-
if (rejected) {
|
|
1047
|
-
const denial = `workflow node submission denied: ${rejected.reason}`;
|
|
1048
|
-
result.result = {
|
|
1049
|
-
...result.result,
|
|
1050
|
-
termination: "error",
|
|
1051
|
-
finalMessage: { role: "assistant", content: denial, toolCalls: [] },
|
|
1052
|
-
};
|
|
1053
|
-
outputs.set(result.agentId, denial);
|
|
1054
|
-
if (stableId !== result.agentId)
|
|
1055
|
-
outputs.set(stableId, denial);
|
|
1056
|
-
}
|
|
1057
|
-
if (submitAction?.kind === "spawn_workflow") {
|
|
1058
|
-
nextNodes.push(...submitAction.nodes);
|
|
1059
|
-
budget = submitAction.budget ?? budget;
|
|
1060
|
-
const accepted = await acceptSpawn(submitAction);
|
|
1061
|
-
const submittedDone = findDone([...subObs, ...accepted]);
|
|
1062
|
-
if (submittedDone)
|
|
1063
|
-
done = submittedDone;
|
|
1064
|
-
}
|
|
1065
|
-
else if (submitAction) {
|
|
1066
|
-
throw new Error(`workflow node submission returned unexpected effect: ${submitAction.kind}`);
|
|
1067
|
-
}
|
|
1068
|
-
// R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
|
|
1069
|
-
// so resume can re-apply the batch at the exact original graph position. W-N3: also the
|
|
1070
|
-
// submitter, so resume drops batches whose submitter re-runs (it will re-submit).
|
|
1071
|
-
const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
|
|
1072
|
-
if (submitted) {
|
|
1073
|
-
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1074
|
-
turn: runtime.turn(),
|
|
1075
|
-
nodes: submitEvent.nodes ?? [],
|
|
1076
|
-
baseIndex: submitted.base,
|
|
1077
|
-
submitterAgentId: result.agentId,
|
|
1078
|
-
}));
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
1025
|
const observationStart = this.pendingObservations.length;
|
|
1082
1026
|
const completionAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1083
1027
|
kind: "sub_agent_completed",
|
|
@@ -1092,9 +1036,28 @@ export class RuntimeRunner {
|
|
|
1092
1036
|
else if (completionAction?.kind === "call_provider") {
|
|
1093
1037
|
this.workflowContinuation = completionAction;
|
|
1094
1038
|
}
|
|
1039
|
+
else if (completionAction?.kind === "done") {
|
|
1040
|
+
return {
|
|
1041
|
+
nodeOutcomes: completedNodeOutcomes,
|
|
1042
|
+
outputs: Object.fromEntries(outputs),
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1095
1045
|
else if (completionAction) {
|
|
1096
1046
|
throw new Error(`workflow completion returned unexpected effect: ${completionAction.kind}`);
|
|
1097
1047
|
}
|
|
1048
|
+
// ABI v3: child-authored DAG additions ride on ChildCompleted.parent_requests. Admission is
|
|
1049
|
+
// independent of the completion fact; only an admitted request emits this observation.
|
|
1050
|
+
if (result.submittedNodes?.length) {
|
|
1051
|
+
const submitted = obs.find(o => o.kind === "workflow_nodes_submitted");
|
|
1052
|
+
if (submitted) {
|
|
1053
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1054
|
+
turn: runtime.turn(),
|
|
1055
|
+
nodes: result.submittedNodes.map(workflowNodeSpecToKernel),
|
|
1056
|
+
baseIndex: submitted.base,
|
|
1057
|
+
submitterAgentId: result.agentId,
|
|
1058
|
+
}));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1098
1061
|
const d = findDone(obs);
|
|
1099
1062
|
if (d)
|
|
1100
1063
|
done = d;
|
|
@@ -1121,39 +1084,6 @@ export class RuntimeRunner {
|
|
|
1121
1084
|
nodes = nextNodes;
|
|
1122
1085
|
}
|
|
1123
1086
|
}
|
|
1124
|
-
/**
|
|
1125
|
-
* Resume a workflow from the parent session's completed nodes.
|
|
1126
|
-
* Reads the session log, extracts completed workflow node records (with their W-1 control
|
|
1127
|
-
* signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
|
|
1128
|
-
* flow (classify prune / loop stop), and the driver re-seeds its outputs map.
|
|
1129
|
-
*/
|
|
1130
|
-
async resumeWorkflow(spec, opts) {
|
|
1131
|
-
// Standalone resume: a stateless handler passes the prior `sessionId` to pick up an interrupted
|
|
1132
|
-
// workflow from the session log. Mid-run callers omit it and resume the active session.
|
|
1133
|
-
const sessionId = opts?.sessionId ?? this.currentSessionId;
|
|
1134
|
-
if (!sessionId) {
|
|
1135
|
-
throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
|
|
1136
|
-
}
|
|
1137
|
-
const events = await this.opts.sessionLog.read(sessionId);
|
|
1138
|
-
const resumedOutcomes = recoverWorkflowNodeOutcomes(events);
|
|
1139
|
-
const completedIds = new Set(resumedOutcomes.map(r => r.agentId));
|
|
1140
|
-
const recovered = recoverSubmittedWorkflowNodes(events);
|
|
1141
|
-
// W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
|
|
1142
|
-
// re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
|
|
1143
|
-
// Exact bases keep later graph indices stable while dropped slots remain inert placeholders.
|
|
1144
|
-
let { submissions, bases } = recovered;
|
|
1145
|
-
if (submissions.length > 0) {
|
|
1146
|
-
const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
|
|
1147
|
-
submissions = submissions.filter((_, i) => keep[i]);
|
|
1148
|
-
bases = bases.filter((_, i) => keep[i]);
|
|
1149
|
-
}
|
|
1150
|
-
return this.runWorkflow(spec, {
|
|
1151
|
-
resumedOutcomes,
|
|
1152
|
-
resumedSubmissions: submissions,
|
|
1153
|
-
resumedSubmissionBases: bases,
|
|
1154
|
-
sessionId,
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
1087
|
interrupt(reason = "user") {
|
|
1158
1088
|
this.interrupted = true;
|
|
1159
1089
|
this.cancellationReason = reason;
|
|
@@ -1218,7 +1148,7 @@ export class RuntimeRunner {
|
|
|
1218
1148
|
const dispositions = this.pendingObservations.slice(observationStart).filter(observation => observation.kind === "signal_delivery_disposed"
|
|
1219
1149
|
&& observation.delivery_id === delivery.deliveryId
|
|
1220
1150
|
&& observation.attempt === delivery.deliveryAttempt);
|
|
1221
|
-
if (dispositions.length
|
|
1151
|
+
if (dispositions.length > 1) {
|
|
1222
1152
|
throw new Error("kernel did not return the matching signal delivery disposition");
|
|
1223
1153
|
}
|
|
1224
1154
|
if (!await delivery.ack())
|
|
@@ -1232,8 +1162,24 @@ export class RuntimeRunner {
|
|
|
1232
1162
|
}
|
|
1233
1163
|
async *run(req) {
|
|
1234
1164
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
1235
|
-
const midRun = isMidRun(prior);
|
|
1236
1165
|
const resumedStart = [...prior].reverse().find(entry => entry.event.kind === "run_started");
|
|
1166
|
+
// SessionLog is an audit projection. A forged/stale run_terminal must not mint a new
|
|
1167
|
+
// operation while the canonical journal still has a live chain — same authority as wake().
|
|
1168
|
+
// Inherited parent events are transcript input for a fresh child operation, never recovery
|
|
1169
|
+
// evidence for the child's own canonical journal.
|
|
1170
|
+
let midRun = req.inheritEvents ? false : isMidRun(prior);
|
|
1171
|
+
if (!midRun
|
|
1172
|
+
&& resumedStart?.event.kind === "run_started"
|
|
1173
|
+
&& !req.inheritEvents) {
|
|
1174
|
+
const operationId = `node-operation-${resumedStart.event.run_id}`;
|
|
1175
|
+
const head = await this.resolveKernelJournal().head(operationId);
|
|
1176
|
+
if (head) {
|
|
1177
|
+
const authoritative = this.createCanonicalRuntime(resumedStart.event.run_id, req.sessionId);
|
|
1178
|
+
await authoritative.restore();
|
|
1179
|
+
if (!authoritative.isTerminal())
|
|
1180
|
+
midRun = true;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1237
1183
|
const runId = midRun && resumedStart?.event.kind === "run_started"
|
|
1238
1184
|
? resumedStart.event.run_id
|
|
1239
1185
|
: crypto.randomUUID();
|
|
@@ -1258,12 +1204,26 @@ export class RuntimeRunner {
|
|
|
1258
1204
|
}
|
|
1259
1205
|
async *wake(sessionId, extensions) {
|
|
1260
1206
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const startEntry = [...events].reverse().find(e => e.event.kind === "run_started");
|
|
1207
|
+
const startIndex = events.reduce((latest, entry, index) => entry.event.kind === "run_started" ? index : latest, -1);
|
|
1208
|
+
const startEntry = startIndex >= 0 ? events[startIndex] : undefined;
|
|
1264
1209
|
if (!startEntry)
|
|
1265
1210
|
throw new Error(`No run_started event for session: ${sessionId}`);
|
|
1266
1211
|
const start = startEntry.event;
|
|
1212
|
+
const projectedTerminal = events
|
|
1213
|
+
.slice(startIndex + 1)
|
|
1214
|
+
.some(e => e.event.kind === "run_terminal");
|
|
1215
|
+
if (projectedTerminal) {
|
|
1216
|
+
const operationId = `node-operation-${start.run_id}`;
|
|
1217
|
+
const journal = this.resolveKernelJournal();
|
|
1218
|
+
const head = await journal.head(operationId);
|
|
1219
|
+
if (!head) {
|
|
1220
|
+
throw new Error("run_terminal projection has no canonical journal");
|
|
1221
|
+
}
|
|
1222
|
+
const authoritative = this.createCanonicalRuntime(start.run_id, sessionId);
|
|
1223
|
+
await authoritative.restore();
|
|
1224
|
+
if (authoritative.isTerminal())
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1267
1227
|
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true, start.attachments, start.run_id);
|
|
1268
1228
|
}
|
|
1269
1229
|
/** Execute a kernel-owned approval effect and return the correlated decision lists. */
|
|
@@ -1344,62 +1304,6 @@ export class RuntimeRunner {
|
|
|
1344
1304
|
}
|
|
1345
1305
|
return { approved, denied, events };
|
|
1346
1306
|
}
|
|
1347
|
-
/**
|
|
1348
|
-
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
1349
|
-
* output. Resolution order: (a) the on-disk result spool committed by the explicit
|
|
1350
|
-
* `spool_large_result` host effect, then (b) a session-log scan for the original
|
|
1351
|
-
* `tool_completed` event carrying that `call_id`. Slices the
|
|
1352
|
-
* resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
1353
|
-
*/
|
|
1354
|
-
async resolveReadResult(sessionId, argsJson) {
|
|
1355
|
-
let callId = "";
|
|
1356
|
-
let offset = 0;
|
|
1357
|
-
let maxBytes = 4000;
|
|
1358
|
-
try {
|
|
1359
|
-
const args = JSON.parse(argsJson || "{}");
|
|
1360
|
-
callId = typeof args.call_id === "string" ? args.call_id : "";
|
|
1361
|
-
if (typeof args.offset === "number" && Number.isFinite(args.offset))
|
|
1362
|
-
offset = args.offset;
|
|
1363
|
-
if (typeof args.max_bytes === "number" && Number.isFinite(args.max_bytes))
|
|
1364
|
-
maxBytes = args.max_bytes;
|
|
1365
|
-
}
|
|
1366
|
-
catch {
|
|
1367
|
-
// malformed arguments — callId stays empty, falls through to "not found" below
|
|
1368
|
-
}
|
|
1369
|
-
let full;
|
|
1370
|
-
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
1371
|
-
try {
|
|
1372
|
-
full = await spool.findByCallId(sessionId, callId);
|
|
1373
|
-
}
|
|
1374
|
-
catch {
|
|
1375
|
-
full = undefined;
|
|
1376
|
-
}
|
|
1377
|
-
if (full === undefined) {
|
|
1378
|
-
try {
|
|
1379
|
-
const events = await this.opts.sessionLog.read(sessionId);
|
|
1380
|
-
for (const { event } of events) {
|
|
1381
|
-
if (event.kind !== "tool_completed")
|
|
1382
|
-
continue;
|
|
1383
|
-
const match = event.results.find(r => r.call_id === callId);
|
|
1384
|
-
if (match)
|
|
1385
|
-
full = match.output;
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
catch {
|
|
1389
|
-
full = undefined;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
if (full === undefined) {
|
|
1393
|
-
return { text: `no stored output for call_id "${callId}"`, isError: true };
|
|
1394
|
-
}
|
|
1395
|
-
const start = Math.max(0, offset);
|
|
1396
|
-
const end = Math.min(full.length, start + Math.max(0, maxBytes));
|
|
1397
|
-
const slice = full.slice(start, end);
|
|
1398
|
-
return {
|
|
1399
|
-
text: `[read_result ${callId}: chars ${start}–${end} of ${full.length}]\n${slice}`,
|
|
1400
|
-
isError: false,
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
1403
1307
|
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments, runId = crypto.randomUUID()) {
|
|
1404
1308
|
this.interrupted = false;
|
|
1405
1309
|
this.cancellationReason = undefined;
|
|
@@ -1411,7 +1315,6 @@ export class RuntimeRunner {
|
|
|
1411
1315
|
if (this.opts.enableDiagnosticsDashboard) {
|
|
1412
1316
|
this.dashboard = new KernelPrimitivesDashboard(sessionId);
|
|
1413
1317
|
}
|
|
1414
|
-
const kernel = getKernel();
|
|
1415
1318
|
const ext = { ...this.opts.extensions, ...(extensions ?? {}) };
|
|
1416
1319
|
const providerState = this.opts.provider.createRunState?.();
|
|
1417
1320
|
let nextCompressedArchiveStart = nextArchivedSeqStart(priorEvents);
|
|
@@ -1428,162 +1331,113 @@ export class RuntimeRunner {
|
|
|
1428
1331
|
const taskScope = new ManagedTaskScope(operation, this.opts.onBackgroundTaskError);
|
|
1429
1332
|
let groupBudgetScope;
|
|
1430
1333
|
try {
|
|
1431
|
-
const runtime =
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
timeoutMs: effectiveTimeoutMs !== undefined ? BigInt(effectiveTimeoutMs) : undefined,
|
|
1435
|
-
maxTotalTokens: this.opts.maxTotalTokens !== undefined ? BigInt(this.opts.maxTotalTokens) : undefined,
|
|
1436
|
-
});
|
|
1334
|
+
const runtime = this.createCanonicalRuntime(runId, sessionId);
|
|
1335
|
+
if (resumeMidRun)
|
|
1336
|
+
await runtime.restore();
|
|
1437
1337
|
this.activeKernel = runtime;
|
|
1438
1338
|
this.nextArchiveStart = nextCompressedArchiveStart;
|
|
1439
|
-
if (
|
|
1440
|
-
|
|
1441
|
-
kind: "set_tokenizer",
|
|
1442
|
-
name: this.opts.tokenizer,
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
if (this.opts.enablePlanTool !== undefined) {
|
|
1446
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1447
|
-
kind: "set_plan_tool_enabled",
|
|
1448
|
-
enabled: this.opts.enablePlanTool,
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1452
|
-
kind: "set_tools",
|
|
1453
|
-
tools: this.opts.executionPlane.schemas().map(toolSchemaToKernel),
|
|
1454
|
-
});
|
|
1455
|
-
if (this.composedSystemPrompt) {
|
|
1456
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1457
|
-
kind: "add_system_message",
|
|
1458
|
-
content: this.composedSystemPrompt,
|
|
1459
|
-
tokens: Math.max(1, Math.ceil(this.composedSystemPrompt.length / 4)),
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
if (this.opts.initialMemory) {
|
|
1463
|
-
for (const mem of this.opts.initialMemory) {
|
|
1339
|
+
if (!resumeMidRun) {
|
|
1340
|
+
if (this.opts.tokenizer) {
|
|
1464
1341
|
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1465
|
-
kind: "
|
|
1466
|
-
|
|
1467
|
-
|
|
1342
|
+
kind: "set_tokenizer",
|
|
1343
|
+
name: this.opts.tokenizer,
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
if (this.opts.enablePlanTool !== undefined) {
|
|
1347
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1348
|
+
kind: "set_plan_tool_enabled",
|
|
1349
|
+
enabled: this.opts.enablePlanTool,
|
|
1468
1350
|
});
|
|
1469
1351
|
}
|
|
1470
|
-
}
|
|
1471
|
-
if (this.opts.skillDir) {
|
|
1472
|
-
const { scanSkillDir } = await import("../skills/loader.js");
|
|
1473
|
-
const metas = await scanSkillDir(this.opts.skillDir);
|
|
1474
|
-
// S2 host-layer skill allowlist: keep only scanned skills named in `skillFilter` before feeding
|
|
1475
|
-
// the catalog. Absent ⇒ feed all (identical to the pre-feature message); empty ⇒ feed none. The
|
|
1476
|
-
// `set_available_skills` message is ALWAYS sent when a skillDir exists (shape preserved) — only
|
|
1477
|
-
// the list narrows; the no-skillDir path stays untouched.
|
|
1478
|
-
const filter = this.opts.skillFilter;
|
|
1479
|
-
const selected = filter === undefined ? metas : metas.filter(m => filter.includes(m.name));
|
|
1480
|
-
// P1-B: pass the full SkillMetadata (incl. `allowedTools`) straight through — re-mapping it
|
|
1481
|
-
// field-by-field previously dropped `allowedTools`.
|
|
1482
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1483
|
-
kind: "set_available_skills",
|
|
1484
|
-
skills: selected.map(m => skillMetadataToKernel(m)),
|
|
1485
|
-
});
|
|
1486
|
-
}
|
|
1487
|
-
// P1-B/D: configure the stable-core tool ids (always exposed under skill gating). Empty/absent
|
|
1488
|
-
// ⇒ skills narrow to exactly their declared tools + meta-tools.
|
|
1489
|
-
if (this.opts.stableCoreToolIds?.length) {
|
|
1490
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1491
|
-
kind: "set_stable_core_tools",
|
|
1492
|
-
tool_ids: this.opts.stableCoreToolIds,
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
if (this.opts.dreamStore && this.opts.agentId) {
|
|
1496
|
-
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_memory_enabled", enabled: true });
|
|
1497
|
-
}
|
|
1498
|
-
// Install optional memory policy. Maps the ergonomic camelCase option onto the kernel's
|
|
1499
|
-
// snake_case `set_memory_policy` event; omitted fields fall back to kernel defaults.
|
|
1500
|
-
if (this.opts.memoryPolicy) {
|
|
1501
|
-
const m = this.opts.memoryPolicy;
|
|
1502
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1503
|
-
kind: "set_memory_policy",
|
|
1504
|
-
...(m.memoryPath !== undefined ? { memory_path: m.memoryPath } : {}),
|
|
1505
|
-
...(m.staleWarningDays !== undefined ? { stale_warning_days: m.staleWarningDays } : {}),
|
|
1506
|
-
...(m.retrievalTopK !== undefined ? { retrieval_top_k: m.retrievalTopK } : {}),
|
|
1507
|
-
...(m.validationEnabled !== undefined ? { validation_enabled: m.validationEnabled } : {}),
|
|
1508
|
-
...(m.maxContentBytes !== undefined ? { max_content_bytes: m.maxContentBytes } : {}),
|
|
1509
|
-
...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
|
|
1510
|
-
...(m.promotionRecallThreshold !== undefined
|
|
1511
|
-
? { promotion_recall_threshold: m.promotionRecallThreshold }
|
|
1512
|
-
: {}),
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
if (this.opts.knowledgeSource) {
|
|
1516
|
-
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_knowledge_enabled", enabled: true });
|
|
1517
|
-
}
|
|
1518
|
-
if (this.opts.milestoneContract) {
|
|
1519
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1520
|
-
kind: "load_milestone_contract",
|
|
1521
|
-
contract: {
|
|
1522
|
-
phases: this.opts.milestoneContract.phases.map(p => ({
|
|
1523
|
-
id: p.id,
|
|
1524
|
-
criteria: p.criteria ?? [],
|
|
1525
|
-
unlocks: p.unlocks ?? [],
|
|
1526
|
-
required_evidence: p.requiredEvidence ?? [],
|
|
1527
|
-
...(p.verifier ? { verifier: p.verifier } : {}),
|
|
1528
|
-
})),
|
|
1529
|
-
},
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
const maxBytes = runtime.recoveryContentBytes();
|
|
1533
|
-
if (priorEvents && priorEvents.length > 0) {
|
|
1534
|
-
const repaired = repairEventsForRecovery(priorEvents, maxBytes);
|
|
1535
|
-
seedProviderReplayFromEvents(this.opts.provider, repaired);
|
|
1536
|
-
const loadArchive = this.opts.compressionStore
|
|
1537
|
-
? (ref) => this.opts.compressionStore.read(ref)
|
|
1538
|
-
: undefined;
|
|
1539
|
-
const replayed = await replayMessagesAsync(repaired, maxBytes, loadArchive);
|
|
1540
1352
|
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1541
|
-
kind: "
|
|
1542
|
-
|
|
1353
|
+
kind: "set_tools",
|
|
1354
|
+
tools: this.opts.executionPlane.schemas().map(toolSchemaToKernel),
|
|
1543
1355
|
});
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
for (const m of replayed) {
|
|
1551
|
-
for (const part of m.contentParts ?? []) {
|
|
1552
|
-
if (part.type === "tool_result")
|
|
1553
|
-
toolResultByCallId.set(part.callId, part.output);
|
|
1554
|
-
}
|
|
1356
|
+
if (this.composedSystemPrompt) {
|
|
1357
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1358
|
+
kind: "add_system_message",
|
|
1359
|
+
content: this.composedSystemPrompt,
|
|
1360
|
+
tokens: Math.max(1, Math.ceil(this.composedSystemPrompt.length / 4)),
|
|
1361
|
+
});
|
|
1555
1362
|
}
|
|
1556
|
-
|
|
1557
|
-
for (const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
continue;
|
|
1564
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1565
|
-
kind: "skill_activated",
|
|
1566
|
-
name,
|
|
1567
|
-
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
1568
|
-
});
|
|
1569
|
-
const output = toolResultByCallId.get(tc.id);
|
|
1570
|
-
if (output && !this.knowledgePushedSkills.has(name)) {
|
|
1571
|
-
this.knowledgePushedSkills.add(name);
|
|
1572
|
-
// K1: keyed — the kernel-side upsert is the authoritative dedup, so a wake re-push
|
|
1573
|
-
// of a skill already pinned live can never double-pin (the in-run Set resets with
|
|
1574
|
-
// each runner instance; the key does not).
|
|
1575
|
-
await this.pushKnowledge({ role: "system", content: output, toolCalls: [] }, undefined, { key: `skill:${name}` });
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
catch { /* malformed skill args — skip */ }
|
|
1363
|
+
if (this.opts.initialMemory) {
|
|
1364
|
+
for (const mem of this.opts.initialMemory) {
|
|
1365
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1366
|
+
kind: "add_knowledge_message",
|
|
1367
|
+
content: mem,
|
|
1368
|
+
tokens: Math.max(1, Math.ceil(mem.length / 4)),
|
|
1369
|
+
});
|
|
1579
1370
|
}
|
|
1580
1371
|
}
|
|
1372
|
+
if (this.opts.skillDir) {
|
|
1373
|
+
const { scanSkillDir } = await import("../skills/loader.js");
|
|
1374
|
+
const metas = await scanSkillDir(this.opts.skillDir);
|
|
1375
|
+
// S2 host-layer skill allowlist: keep only scanned skills named in `skillFilter` before feeding
|
|
1376
|
+
// the catalog. Absent ⇒ feed all (identical to the pre-feature message); empty ⇒ feed none. The
|
|
1377
|
+
// `set_available_skills` message is ALWAYS sent when a skillDir exists (shape preserved) — only
|
|
1378
|
+
// the list narrows; the no-skillDir path stays untouched.
|
|
1379
|
+
const filter = this.opts.skillFilter;
|
|
1380
|
+
const selected = filter === undefined ? metas : metas.filter(m => filter.includes(m.name));
|
|
1381
|
+
// P1-B: pass the full SkillMetadata (incl. `allowedTools`) straight through — re-mapping it
|
|
1382
|
+
// field-by-field previously dropped `allowedTools`.
|
|
1383
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1384
|
+
kind: "set_available_skills",
|
|
1385
|
+
skills: selected.map(m => skillMetadataToKernel(m)),
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
// P1-B/D: configure the stable-core tool ids (always exposed under skill gating). Empty/absent
|
|
1389
|
+
// ⇒ skills narrow to exactly their declared tools + meta-tools.
|
|
1390
|
+
if (this.opts.stableCoreToolIds?.length) {
|
|
1391
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1392
|
+
kind: "set_stable_core_tools",
|
|
1393
|
+
tool_ids: this.opts.stableCoreToolIds,
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
if (this.opts.dreamStore && this.opts.agentId) {
|
|
1397
|
+
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_memory_enabled", enabled: true });
|
|
1398
|
+
}
|
|
1399
|
+
// Install optional memory policy. Maps the ergonomic camelCase option onto the kernel's
|
|
1400
|
+
// snake_case `set_memory_policy` event; omitted fields fall back to kernel defaults.
|
|
1401
|
+
if (this.opts.memoryPolicy) {
|
|
1402
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1403
|
+
kind: "set_memory_policy",
|
|
1404
|
+
...memoryPolicyToKernel(this.opts.memoryPolicy),
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
if (this.opts.knowledgeSource) {
|
|
1408
|
+
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_knowledge_enabled", enabled: true });
|
|
1409
|
+
}
|
|
1410
|
+
if (this.opts.milestoneContract) {
|
|
1411
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1412
|
+
kind: "load_milestone_contract",
|
|
1413
|
+
contract: {
|
|
1414
|
+
phases: this.opts.milestoneContract.phases.map(p => ({
|
|
1415
|
+
id: p.id,
|
|
1416
|
+
criteria: p.criteria ?? [],
|
|
1417
|
+
unlocks: p.unlocks ?? [],
|
|
1418
|
+
required_evidence: p.requiredEvidence ?? [],
|
|
1419
|
+
...(p.verifier ? { verifier: p.verifier } : {}),
|
|
1420
|
+
})),
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (priorEvents && priorEvents.length > 0) {
|
|
1426
|
+
seedProviderReplayFromEvents(this.opts.provider, priorEvents);
|
|
1427
|
+
if (!resumeMidRun) {
|
|
1428
|
+
const loadArchive = this.opts.compressionStore
|
|
1429
|
+
? (ref) => this.opts.compressionStore.read(ref)
|
|
1430
|
+
: undefined;
|
|
1431
|
+
const replayed = await replayMessagesAsync(priorEvents, runtime.recoveryContentBytes(), loadArchive);
|
|
1432
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1433
|
+
kind: "preload_history",
|
|
1434
|
+
messages: replayed.map(messageToKernelMessage),
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1581
1437
|
}
|
|
1582
1438
|
const sessionStart = Date.now();
|
|
1583
|
-
const
|
|
1584
|
-
|
|
1585
|
-
task: { goal, criteria },
|
|
1586
|
-
};
|
|
1439
|
+
const startTask = { goal, criteria };
|
|
1440
|
+
let startRunSpec;
|
|
1587
1441
|
// P0-A: lower an explicit `runSpec`, the `allowedToolIds` ceiling, and/or the `baselineToolIds`
|
|
1588
1442
|
// pre-activation surface to the kernel run spec. Each augments an explicit spec, else
|
|
1589
1443
|
// synthesizes a minimal top-level spec carrying just the exposure config (reuses the existing
|
|
@@ -1595,7 +1449,8 @@ export class RuntimeRunner {
|
|
|
1595
1449
|
// (meta + stable-core only), so mere presence triggers the lowering.
|
|
1596
1450
|
const baselineToolIds = this.opts.baselineToolIds;
|
|
1597
1451
|
const hasBaseline = baselineToolIds !== undefined;
|
|
1598
|
-
|
|
1452
|
+
const hasMilestoneContract = this.opts.milestoneContract !== undefined;
|
|
1453
|
+
if (this.opts.runSpec || hasProfile || hasBaseline || hasMilestoneContract) {
|
|
1599
1454
|
const baseSpec = this.opts.runSpec ?? {
|
|
1600
1455
|
identity: { agentId: this.opts.agentId ?? "root", sessionId, isSubAgent: false },
|
|
1601
1456
|
role: "custom",
|
|
@@ -1606,29 +1461,37 @@ export class RuntimeRunner {
|
|
|
1606
1461
|
: baseSpec;
|
|
1607
1462
|
if (hasBaseline)
|
|
1608
1463
|
spec = { ...spec, exposureBaseline: baselineToolIds };
|
|
1609
|
-
|
|
1464
|
+
if (hasMilestoneContract && !spec.verificationContractId) {
|
|
1465
|
+
spec = { ...spec, verificationContractId: "node-default" };
|
|
1466
|
+
}
|
|
1467
|
+
startRunSpec = agentRunSpecToKernel(spec);
|
|
1610
1468
|
}
|
|
1611
|
-
// Reserve capacity before
|
|
1469
|
+
// Reserve capacity before the canonical root start. The kernel enforces only this vehicle's grant and reports
|
|
1612
1470
|
// exact terminal usage against the same opaque reservation identity. A nested vehicle joins for
|
|
1613
1471
|
// lineage/settlement only: it reserves no budget axes (group admission governs peer vehicles),
|
|
1614
1472
|
// so the parent's held reservation cannot squeeze the child's grant to zero.
|
|
1615
|
-
if (this.opts.runGroup) {
|
|
1473
|
+
if (!resumeMidRun && this.opts.runGroup) {
|
|
1616
1474
|
const g = this.opts.runGroup;
|
|
1617
1475
|
groupBudgetScope = await GroupBudgetScope.open(g, { sessionId, role: this.opts.agentId, kind: "vehicle" }, this.opts.nestedGroupVehicle ? { limits: {}, requested: {} } : this.groupBudgetRequest());
|
|
1618
1476
|
this.activeGroupBudgetScope = groupBudgetScope;
|
|
1619
1477
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1478
|
+
if (!resumeMidRun) {
|
|
1479
|
+
try {
|
|
1480
|
+
await this.applyKernelPolicies(runtime, groupBudgetScope);
|
|
1481
|
+
}
|
|
1482
|
+
catch (err) {
|
|
1483
|
+
// Admission failure (e.g. the kernel rejecting a zero-capacity grant): release the
|
|
1484
|
+
// reservation so it cannot linger in the group ledger, then surface the error.
|
|
1485
|
+
await groupBudgetScope?.release();
|
|
1486
|
+
this.activeGroupBudgetScope = undefined;
|
|
1487
|
+
throw err;
|
|
1488
|
+
}
|
|
1629
1489
|
}
|
|
1490
|
+
this.currentGoal = goal;
|
|
1491
|
+
if (!resumeMidRun)
|
|
1492
|
+
await this.prefetchMemoryIntoInitialContext(runtime);
|
|
1630
1493
|
// Multimodal upload: seed the user's attachments (images/audio) as a history
|
|
1631
|
-
// message before
|
|
1494
|
+
// message before root start pushes the "[TASK STATE]" anchor. init_task does not
|
|
1632
1495
|
// clear history, so order becomes [attachment user msg, "Proceed…"] — both land
|
|
1633
1496
|
// in the first render. On resume the message is already in the replayed history.
|
|
1634
1497
|
if (!resumeMidRun && attachments?.length) {
|
|
@@ -1637,24 +1500,14 @@ export class RuntimeRunner {
|
|
|
1637
1500
|
message: attachmentsToKernelMessage(attachments),
|
|
1638
1501
|
});
|
|
1639
1502
|
}
|
|
1640
|
-
|
|
1503
|
+
const resumedAction = resumeMidRun ? runtime.resumeAction() : null;
|
|
1641
1504
|
let action = resumeMidRun
|
|
1642
|
-
?
|
|
1643
|
-
: await this.
|
|
1644
|
-
// I4/T5: pre-fetch memory before
|
|
1645
|
-
//
|
|
1646
|
-
//
|
|
1647
|
-
//
|
|
1648
|
-
// kernel Running and start_run would fault. The resumed action from the last query
|
|
1649
|
-
// supersedes start_run's (same pull contract; it renders the injected hits). Hits land in
|
|
1650
|
-
// `history` as ordinary turns — single-use retrieval content that decays with the
|
|
1651
|
-
// compression pyramid, never pinned into `knowledge`. Skipped on resumes (already in
|
|
1652
|
-
// prior context) and when dreamStore/agentId is absent.
|
|
1653
|
-
if (!resumeMidRun) {
|
|
1654
|
-
const resumed = await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
1655
|
-
if (resumed)
|
|
1656
|
-
action = resumed;
|
|
1657
|
-
}
|
|
1505
|
+
? resumedAction ?? (() => { throw new Error("restored canonical operation has no pending effect or terminal"); })()
|
|
1506
|
+
: await this.startKernelAgent(runtime, this.pendingObservations, startTask, startRunSpec);
|
|
1507
|
+
// I4/T5: pre-fetch memory before root start so the model sees it on turn 1 instead of
|
|
1508
|
+
// discovering it via the `memory` tool later. Accepted hits enter `initial_context.messages`
|
|
1509
|
+
// and are therefore frozen into the canonical start record. Skipped on restore (the checkpoint
|
|
1510
|
+
// or journal already owns them) and when dreamStore/agentId is absent.
|
|
1658
1511
|
// P0-C: the skill loaded and in effect going into the current turn (updated when the model's
|
|
1659
1512
|
// `skill` tool call resolves). Drives the per-turn `activeSkill` metric → dwell measurement.
|
|
1660
1513
|
let activeSkill;
|
|
@@ -1691,15 +1544,6 @@ export class RuntimeRunner {
|
|
|
1691
1544
|
if (runtime.isTerminal())
|
|
1692
1545
|
break;
|
|
1693
1546
|
if (action.kind === "call_provider") {
|
|
1694
|
-
// M5 v2.1: top-level auto-pivot at the safe point. If the agent authored sub-workflow(s) via
|
|
1695
|
-
// `start_workflow`, drive each in THIS kernel now (the kernel is in Reason / `call_provider`,
|
|
1696
|
-
// NOT suspended — driving mid-suspend would clobber the single-slot suspend state), inject the
|
|
1697
|
-
// outcome into context, and re-render. Loop-top placement (vs only after `tool_results`) catches
|
|
1698
|
-
// EVERY path to `call_provider` — including resuming after an approval gate — so a queued spec
|
|
1699
|
-
// is never stranded. Drains the queue; fires once per authored batch.
|
|
1700
|
-
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
1701
|
-
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
1702
|
-
}
|
|
1703
1547
|
const providerEffectId = action.effectId;
|
|
1704
1548
|
const finalToolCalls = [];
|
|
1705
1549
|
let finalText = "";
|
|
@@ -1802,10 +1646,28 @@ export class RuntimeRunner {
|
|
|
1802
1646
|
});
|
|
1803
1647
|
break;
|
|
1804
1648
|
}
|
|
1649
|
+
const canonicalToolCalls = this.opts.skillLeaseTurns === undefined
|
|
1650
|
+
? finalToolCalls
|
|
1651
|
+
: finalToolCalls.map(call => {
|
|
1652
|
+
if (call.name !== "skill")
|
|
1653
|
+
return call;
|
|
1654
|
+
try {
|
|
1655
|
+
return {
|
|
1656
|
+
...call,
|
|
1657
|
+
arguments: JSON.stringify({
|
|
1658
|
+
...JSON.parse(call.arguments || "{}"),
|
|
1659
|
+
lease_turns: this.opts.skillLeaseTurns,
|
|
1660
|
+
}),
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
catch {
|
|
1664
|
+
return call;
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1805
1667
|
const assistantMessage = {
|
|
1806
1668
|
role: "assistant",
|
|
1807
1669
|
content: finalText,
|
|
1808
|
-
toolCalls:
|
|
1670
|
+
toolCalls: canonicalToolCalls,
|
|
1809
1671
|
tokenCount: turnOutputTokens || turnTokens || undefined,
|
|
1810
1672
|
};
|
|
1811
1673
|
const providerEvent = {
|
|
@@ -1814,9 +1676,36 @@ export class RuntimeRunner {
|
|
|
1814
1676
|
message: messageToKernelMessage(assistantMessage),
|
|
1815
1677
|
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
1816
1678
|
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
1817
|
-
now_ms: Date.now(),
|
|
1818
1679
|
...(turnStopReason ? { stop_reason: turnStopReason } : {}),
|
|
1819
1680
|
};
|
|
1681
|
+
if (this.opts.skillDir) {
|
|
1682
|
+
const skillCalls = finalToolCalls.filter(call => call.name === "skill");
|
|
1683
|
+
if (skillCalls.length > 0) {
|
|
1684
|
+
const { readSkillFile } = await import("../skills/loader.js");
|
|
1685
|
+
for (const call of skillCalls) {
|
|
1686
|
+
try {
|
|
1687
|
+
const name = String(JSON.parse(call.arguments || "{}").name ?? "");
|
|
1688
|
+
if (!name)
|
|
1689
|
+
continue;
|
|
1690
|
+
if (this.opts.skillFilter && !this.opts.skillFilter.includes(name))
|
|
1691
|
+
continue;
|
|
1692
|
+
const content = await readSkillFile(this.opts.skillDir, name);
|
|
1693
|
+
if (!content)
|
|
1694
|
+
continue;
|
|
1695
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1696
|
+
kind: "add_knowledge_message",
|
|
1697
|
+
key: `skill:${name}`,
|
|
1698
|
+
content,
|
|
1699
|
+
tokens: Math.max(1, Math.ceil(content.length / 4)),
|
|
1700
|
+
pinned: true,
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
catch {
|
|
1704
|
+
// A missing or malformed skill stays a model-visible syscall rejection.
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1820
1709
|
action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
|
|
1821
1710
|
const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
|
|
1822
1711
|
await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
|
|
@@ -1868,10 +1757,32 @@ export class RuntimeRunner {
|
|
|
1868
1757
|
else if (action.kind === "persist_memory") {
|
|
1869
1758
|
let error;
|
|
1870
1759
|
const agentId = this.opts.agentId;
|
|
1760
|
+
const canonicalMemory = action.memory;
|
|
1761
|
+
const record = {
|
|
1762
|
+
record_id: `memory:${crypto.randomUUID()}`,
|
|
1763
|
+
scope: this.opts.memoryScope ?? { tenant_id: "default", namespace: agentId ?? "default" },
|
|
1764
|
+
name: String(canonicalMemory.name ?? ""),
|
|
1765
|
+
kind: String(canonicalMemory.kind ?? "reference"),
|
|
1766
|
+
content: String(canonicalMemory.content ?? ""),
|
|
1767
|
+
description: String(canonicalMemory.description ?? ""),
|
|
1768
|
+
provenance: {
|
|
1769
|
+
author: "model",
|
|
1770
|
+
trust: "untrusted",
|
|
1771
|
+
evidence_refs: Array.isArray(canonicalMemory.evidence_refs)
|
|
1772
|
+
? canonicalMemory.evidence_refs.map(String)
|
|
1773
|
+
: [],
|
|
1774
|
+
},
|
|
1775
|
+
created_at: Number(canonicalMemory.accepted_at_ms ?? Date.now()),
|
|
1776
|
+
updated_at: Number(canonicalMemory.accepted_at_ms ?? Date.now()),
|
|
1777
|
+
recall_count: 0,
|
|
1778
|
+
confidence: 1,
|
|
1779
|
+
links: [],
|
|
1780
|
+
pinned: false,
|
|
1781
|
+
};
|
|
1871
1782
|
try {
|
|
1872
1783
|
if (!agentId)
|
|
1873
1784
|
throw new Error("memory persistence requires RuntimeOptions.agentId");
|
|
1874
|
-
await this.persistMemoryToStore(
|
|
1785
|
+
await this.persistMemoryToStore(record, agentId);
|
|
1875
1786
|
}
|
|
1876
1787
|
catch (cause) {
|
|
1877
1788
|
error = formatToolError(cause);
|
|
@@ -1879,11 +1790,22 @@ export class RuntimeRunner {
|
|
|
1879
1790
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1880
1791
|
kind: "memory_persist_result",
|
|
1881
1792
|
effect_id: action.effectId,
|
|
1793
|
+
record_ref: record.record_id,
|
|
1882
1794
|
...(error ? { error } : {}),
|
|
1883
1795
|
});
|
|
1884
1796
|
}
|
|
1885
1797
|
else if (action.kind === "query_memory") {
|
|
1886
|
-
const query =
|
|
1798
|
+
const query = {
|
|
1799
|
+
scope: this.opts.memoryScope ?? {
|
|
1800
|
+
tenant_id: "default",
|
|
1801
|
+
namespace: this.opts.agentId ?? "default",
|
|
1802
|
+
},
|
|
1803
|
+
query: String(action.query.text ?? ""),
|
|
1804
|
+
top_k: action.requestedK,
|
|
1805
|
+
kinds: Array.isArray(action.query.kinds)
|
|
1806
|
+
? action.query.kinds.map(String)
|
|
1807
|
+
: [],
|
|
1808
|
+
};
|
|
1887
1809
|
let hits = [];
|
|
1888
1810
|
let error;
|
|
1889
1811
|
const agentId = this.opts.agentId;
|
|
@@ -1904,23 +1826,6 @@ export class RuntimeRunner {
|
|
|
1904
1826
|
if (!error)
|
|
1905
1827
|
await this.logMemoryRetrievalResult(sessionId, hits);
|
|
1906
1828
|
}
|
|
1907
|
-
else if (action.kind === "spool_large_result") {
|
|
1908
|
-
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
1909
|
-
let spoolRef;
|
|
1910
|
-
let error;
|
|
1911
|
-
try {
|
|
1912
|
-
spoolRef = await spool.persistOutput(sessionId, action.callId, action.output);
|
|
1913
|
-
}
|
|
1914
|
-
catch (cause) {
|
|
1915
|
-
error = formatToolError(cause);
|
|
1916
|
-
}
|
|
1917
|
-
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1918
|
-
kind: "large_result_spool_result",
|
|
1919
|
-
effect_id: action.effectId,
|
|
1920
|
-
...(spoolRef ? { spool_ref: spoolRef } : {}),
|
|
1921
|
-
...(error ? { error } : {}),
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
1829
|
else if (action.kind === "archive_page_out") {
|
|
1925
1830
|
const archiveMeta = this.activePageOutArchive
|
|
1926
1831
|
?? this.pendingPageOutArchives.shift()
|
|
@@ -1932,8 +1837,15 @@ export class RuntimeRunner {
|
|
|
1932
1837
|
let archiveRef;
|
|
1933
1838
|
let error;
|
|
1934
1839
|
try {
|
|
1935
|
-
if (
|
|
1936
|
-
|
|
1840
|
+
if (action.payload && action.handleId) {
|
|
1841
|
+
archiveRef = this.opts.compressionStore
|
|
1842
|
+
? await this.opts.compressionStore.write(sessionId, archiveMeta.archiveStart, action.archived ?? [])
|
|
1843
|
+
: undefined;
|
|
1844
|
+
archiveRef ??= `payload:${String(action.payload.digest).replace(/^sha256:/, "").slice(0, 32)}`;
|
|
1845
|
+
await this.payloadStore().persistPayload(sessionId, archiveRef, action.payload.content);
|
|
1846
|
+
}
|
|
1847
|
+
else if (this.opts.compressionStore) {
|
|
1848
|
+
const ref = await this.opts.compressionStore.write(sessionId, archiveMeta.archiveStart, action.archived ?? []);
|
|
1937
1849
|
if (ref)
|
|
1938
1850
|
archiveRef = ref;
|
|
1939
1851
|
}
|
|
@@ -1941,7 +1853,7 @@ export class RuntimeRunner {
|
|
|
1941
1853
|
catch (cause) {
|
|
1942
1854
|
error = formatToolError(cause);
|
|
1943
1855
|
}
|
|
1944
|
-
const archived = action.archived;
|
|
1856
|
+
const archived = action.archived ?? [];
|
|
1945
1857
|
const archiveAction = compressionAction(action.action) ?? "auto_compact";
|
|
1946
1858
|
const archiveTier = action.tier;
|
|
1947
1859
|
const compressedSeq = archiveMeta.compressedSeq;
|
|
@@ -1950,6 +1862,7 @@ export class RuntimeRunner {
|
|
|
1950
1862
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1951
1863
|
kind: "page_out_archive_result",
|
|
1952
1864
|
effect_id: action.effectId,
|
|
1865
|
+
...(archiveRef ? { payload_ref: archiveRef } : {}),
|
|
1953
1866
|
...(archiveRef ? { archive_ref: archiveRef } : {}),
|
|
1954
1867
|
...(error ? { error } : {}),
|
|
1955
1868
|
});
|
|
@@ -1963,6 +1876,39 @@ export class RuntimeRunner {
|
|
|
1963
1876
|
}
|
|
1964
1877
|
}
|
|
1965
1878
|
}
|
|
1879
|
+
else if (action.kind === "load_payload") {
|
|
1880
|
+
let content;
|
|
1881
|
+
let error;
|
|
1882
|
+
try {
|
|
1883
|
+
content = await this.payloadStore().loadPayload(sessionId, action.payloadRef);
|
|
1884
|
+
if (content === undefined) {
|
|
1885
|
+
throw new Error(`payload not found for opaque locator ${action.payloadRef}`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
catch (cause) {
|
|
1889
|
+
error = formatToolError(cause);
|
|
1890
|
+
}
|
|
1891
|
+
if (error || content === undefined) {
|
|
1892
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1893
|
+
kind: "payload_load_failed",
|
|
1894
|
+
effect_id: action.effectId,
|
|
1895
|
+
error,
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
else {
|
|
1899
|
+
const digestBytes = await crypto.subtle.digest("SHA-256", Buffer.from(content));
|
|
1900
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1901
|
+
kind: "payload_loaded",
|
|
1902
|
+
effect_id: action.effectId,
|
|
1903
|
+
handle_id: action.handleId,
|
|
1904
|
+
payload: {
|
|
1905
|
+
content,
|
|
1906
|
+
digest: `sha256:${Buffer.from(digestBytes).toString("hex")}`,
|
|
1907
|
+
original_size: String(Buffer.byteLength(content, "utf8")),
|
|
1908
|
+
},
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1966
1912
|
else if (action.kind === "execute_tool") {
|
|
1967
1913
|
const toolEffectId = action.effectId;
|
|
1968
1914
|
const allCalls = action.calls;
|
|
@@ -1976,18 +1922,13 @@ export class RuntimeRunner {
|
|
|
1976
1922
|
knowledgeSource: this.opts.knowledgeSource,
|
|
1977
1923
|
onToolSuspend: this.opts.onToolSuspend,
|
|
1978
1924
|
onPermissionRequest: this.opts.onPermissionRequest,
|
|
1979
|
-
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
1980
1925
|
};
|
|
1981
1926
|
const toolResults = [];
|
|
1982
|
-
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow"
|
|
1983
|
-
&& c.name !== "read_result");
|
|
1927
|
+
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow");
|
|
1984
1928
|
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
1985
|
-
//
|
|
1986
|
-
//
|
|
1929
|
+
// Syscall tools are consumed by core from the provider result. If one reaches this host
|
|
1930
|
+
// effect projection, the canonical boundary has drifted and must fail closed.
|
|
1987
1931
|
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
1988
|
-
// O7: `read_result` re-fetches a tool output the kernel evicted from context. Content is
|
|
1989
|
-
// host-resolved from the effect-committed spool, then from the durable session log.
|
|
1990
|
-
const readResultCalls = allCalls.filter(c => c.name === "read_result");
|
|
1991
1932
|
for (const call of planCalls) {
|
|
1992
1933
|
const update = parseUpdatePlanArgs(call.arguments);
|
|
1993
1934
|
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
@@ -1998,39 +1939,8 @@ export class RuntimeRunner {
|
|
|
1998
1939
|
toolResults.push(result);
|
|
1999
1940
|
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
2000
1941
|
}
|
|
2001
|
-
for (const call of readResultCalls) {
|
|
2002
|
-
const out = await this.resolveReadResult(sessionId, call.arguments);
|
|
2003
|
-
toolResults.push({ callId: call.id, output: out.text, isError: out.isError });
|
|
2004
|
-
yield { type: "tool_result", callId: call.id, content: out.text, isError: out.isError };
|
|
2005
|
-
}
|
|
2006
|
-
// R3-1: `submit_workflow_nodes` cannot be applied to this runner's kernel — when this runner
|
|
2007
|
-
// is a workflow node, the workflow lives in the *parent* kernel. Surface the requested nodes
|
|
2008
|
-
// as a stream event; the orchestrator collects them onto the node's result and `runWorkflow`
|
|
2009
|
-
// sends `submit_workflow_nodes` to the parent kernel. (When not a workflow node, the event is
|
|
2010
|
-
// simply unconsumed — a no-op.)
|
|
2011
1942
|
for (const call of submitCalls) {
|
|
2012
|
-
|
|
2013
|
-
// full spec and AUTO-PIVOT once this tool turn resolves (the loop drives it in this kernel and
|
|
2014
|
-
// injects the outcome). A workflow-NODE's `start_workflow` (and every `submit_workflow_nodes`)
|
|
2015
|
-
// instead FLATTENS: the batch is surfaced for the parent `runWorkflow` to append.
|
|
2016
|
-
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
2017
|
-
const spec = parseStartWorkflowSpec(call.arguments);
|
|
2018
|
-
if (spec) {
|
|
2019
|
-
this.pendingAuthoredWorkflows.push(spec);
|
|
2020
|
-
const out = "workflow submitted for governance adjudication";
|
|
2021
|
-
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
2022
|
-
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
2023
|
-
continue;
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
// `start_workflow` wraps the batch as `{ spec: { nodes } }`; `submit_workflow_nodes` is `{ nodes }`.
|
|
2027
|
-
const nodes = call.name === "start_workflow"
|
|
2028
|
-
? parseStartWorkflowArgs(call.arguments)
|
|
2029
|
-
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
2030
|
-
yield { type: "workflow_nodes_submitted", nodes };
|
|
2031
|
-
const result = { callId: call.id, output: "workflow nodes submitted for parent governance adjudication", isError: false };
|
|
2032
|
-
toolResults.push(result);
|
|
2033
|
-
yield { type: "tool_result", callId: call.id, content: result.output, isError: false };
|
|
1943
|
+
throw new Error(`canonical kernel published model syscall ${call.name} as a host tool effect`);
|
|
2034
1944
|
}
|
|
2035
1945
|
// O5 (PreToolUse-hook analog): give the host a STATEFUL veto over each kernel-approved
|
|
2036
1946
|
// call. A blocked call never executes; its reason reaches the model as a committed
|
|
@@ -2161,11 +2071,11 @@ export class RuntimeRunner {
|
|
|
2161
2071
|
token_count: r.tokenCount,
|
|
2162
2072
|
})),
|
|
2163
2073
|
});
|
|
2164
|
-
//
|
|
2165
|
-
//
|
|
2166
|
-
//
|
|
2074
|
+
// The canonical provider resolution already activates a successfully resolved `skill` call.
|
|
2075
|
+
// The host's remaining responsibility is to pin the resolved METHOD content — how to do
|
|
2076
|
+
// something — for reuse throughout the run, unlike a one-off memory/knowledge lookup.
|
|
2167
2077
|
//
|
|
2168
|
-
// Strict dynamic context control:
|
|
2078
|
+
// Strict dynamic context control: the skill text
|
|
2169
2079
|
// for the rest of the run, unlike a one-off memory/knowledge lookup (fact content, relevant
|
|
2170
2080
|
// for the moment it's used). So its text ALSO goes into the durable `knowledge` slot here
|
|
2171
2081
|
// (in addition to the ordinary tool_result already headed for `history`, where it will decay
|
|
@@ -2181,11 +2091,6 @@ export class RuntimeRunner {
|
|
|
2181
2091
|
const name = JSON.parse(call.arguments || "{}").name;
|
|
2182
2092
|
if (!name)
|
|
2183
2093
|
continue;
|
|
2184
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
2185
|
-
kind: "skill_activated",
|
|
2186
|
-
name,
|
|
2187
|
-
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
2188
|
-
});
|
|
2189
2094
|
// K1: keyed `skill:<name>` — the kernel-side upsert dedupes across runner instances
|
|
2190
2095
|
// (wake re-push of an already-pinned skill upserts instead of duplicating). With a
|
|
2191
2096
|
// lease configured, the Set optimization is skipped: an expired-then-reloaded skill
|
|
@@ -2195,7 +2100,7 @@ export class RuntimeRunner {
|
|
|
2195
2100
|
await this.pushKnowledge({ role: "system", content: res.output, toolCalls: [] }, undefined, { key: `skill:${name}` });
|
|
2196
2101
|
}
|
|
2197
2102
|
}
|
|
2198
|
-
catch { /* malformed skill args — skip
|
|
2103
|
+
catch { /* malformed skill args — skip the knowledge pin */ }
|
|
2199
2104
|
}
|
|
2200
2105
|
const entropyObsStart = this.pendingObservations.length;
|
|
2201
2106
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
@@ -2222,6 +2127,7 @@ export class RuntimeRunner {
|
|
|
2222
2127
|
}
|
|
2223
2128
|
else if (action.kind === "evaluate_milestone") {
|
|
2224
2129
|
const milestoneEffectId = action.effectId;
|
|
2130
|
+
const milestonePhaseId = action.phaseId;
|
|
2225
2131
|
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
2226
2132
|
if (milestonePolicy === "auto_pass") {
|
|
2227
2133
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
@@ -2245,6 +2151,17 @@ export class RuntimeRunner {
|
|
|
2245
2151
|
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart, taskScope);
|
|
2246
2152
|
}
|
|
2247
2153
|
else {
|
|
2154
|
+
// R-B27: resolve the effect before ending the run. Nothing here can attest the phase, but
|
|
2155
|
+
// the kernel is holding this milestone in its pending-effect table and only a matching
|
|
2156
|
+
// result removes it — a bare `return` leaves a dangling effect that a logical-checkpoint
|
|
2157
|
+
// recovery cannot resolve. Feed back the conservative "unverified" resolution: the wire
|
|
2158
|
+
// has no error field yet, so it rides as `passed: false`, which keeps the phase where it
|
|
2159
|
+
// is (fail-closed, no unlocks mounted). The run still ends as `milestone_pending`.
|
|
2160
|
+
await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
2161
|
+
kind: "milestone_result",
|
|
2162
|
+
effect_id: milestoneEffectId,
|
|
2163
|
+
result: milestoneCheckResultToKernel(milestoneCheckFail(milestonePhaseId, MILESTONE_UNVERIFIED_REASON)),
|
|
2164
|
+
});
|
|
2248
2165
|
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart, taskScope);
|
|
2249
2166
|
const turnsUsed = Math.max(1, runtime.turn());
|
|
2250
2167
|
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
@@ -2260,9 +2177,31 @@ export class RuntimeRunner {
|
|
|
2260
2177
|
return;
|
|
2261
2178
|
}
|
|
2262
2179
|
}
|
|
2180
|
+
else if (action.kind === "spawn_workflow") {
|
|
2181
|
+
await this.driveWorkflow(action, [], sessionId, runtime, new Map());
|
|
2182
|
+
action = runtime.resumeAction()
|
|
2183
|
+
?? (() => { throw new Error("canonical workflow completed without a terminal or continuation"); })();
|
|
2184
|
+
}
|
|
2185
|
+
else if (action.kind === "unsupported_effect") {
|
|
2186
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
2187
|
+
kind: "unsupported_effect",
|
|
2188
|
+
effect_id: action.effectId,
|
|
2189
|
+
effect_kind: action.effectKind,
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2263
2192
|
else if (action.kind === "done") {
|
|
2264
2193
|
break;
|
|
2265
2194
|
}
|
|
2195
|
+
else {
|
|
2196
|
+
// R-B28: fail-closed backstop. Without it an effect that reaches this position but has no
|
|
2197
|
+
// branch here (`spawn_workflow` / `preempt_sub_agents` are only driven inside the workflow
|
|
2198
|
+
// driver, and any effect kind a newer kernel adds) leaves `action` unreplaced and no event
|
|
2199
|
+
// in flight — `while (!runtime.isTerminal())` re-enters immediately and the run pins a core
|
|
2200
|
+
// at 100% forever while the kernel waits for a result that will never come. Terminating
|
|
2201
|
+
// through the loop's existing throw path makes the protocol mismatch visible instead
|
|
2202
|
+
// (run_terminal `error` + an `error` event) and cannot busy-wait.
|
|
2203
|
+
throw new Error(`unhandled kernel effect ${action.kind} in the main run loop`);
|
|
2204
|
+
}
|
|
2266
2205
|
}
|
|
2267
2206
|
}
|
|
2268
2207
|
catch (err) {
|
|
@@ -2310,7 +2249,12 @@ export class RuntimeRunner {
|
|
|
2310
2249
|
totalTokens,
|
|
2311
2250
|
}));
|
|
2312
2251
|
if (groupBudgetScope && !groupBudgetScope.isClosed) {
|
|
2313
|
-
|
|
2252
|
+
await this.settleGroupBudget(groupBudgetScope, {
|
|
2253
|
+
tokens: totalTokens,
|
|
2254
|
+
subagents: runtime.localSubagentsSpawned(),
|
|
2255
|
+
...(this.opts.runSpec?.loopRound ? { rounds: 1 } : {}),
|
|
2256
|
+
});
|
|
2257
|
+
this.activeGroupBudgetScope = undefined;
|
|
2314
2258
|
}
|
|
2315
2259
|
if (this.opts.dreamStore && this.opts.agentId) {
|
|
2316
2260
|
const newMsgs = runtime.drainNewMessages().map(m => ({
|
|
@@ -2386,9 +2330,8 @@ export class RuntimeRunner {
|
|
|
2386
2330
|
runSpec: this.opts.runSpec,
|
|
2387
2331
|
phase,
|
|
2388
2332
|
});
|
|
2389
|
-
//
|
|
2390
|
-
//
|
|
2391
|
-
// recall lifecycle (recordRecall / promotion) fires exactly like an in-run query.
|
|
2333
|
+
// Renewal prefetch is a host policy, not an agent syscall. Store selection stays host-side;
|
|
2334
|
+
// each accepted hit enters the live run through a canonical seed-knowledge command.
|
|
2392
2335
|
//
|
|
2393
2336
|
// One prefetch = one dedupe horizon: a record hit by several short queries recalls and
|
|
2394
2337
|
// injects once. A renewal prefetch starts a fresh horizon — renewal dropped the earlier
|
|
@@ -2398,16 +2341,57 @@ export class RuntimeRunner {
|
|
|
2398
2341
|
for (const q of queries ?? []) {
|
|
2399
2342
|
if (!q.query.trim())
|
|
2400
2343
|
continue;
|
|
2401
|
-
const { action } = await this.
|
|
2344
|
+
const { action } = await this.prefetchMemoryIntoKnowledge(runtime, q, this.opts.agentId, this.durableSessionId(this.currentSessionId), seenRecordIds, this.pendingObservations);
|
|
2402
2345
|
resumed = action ?? resumed;
|
|
2403
2346
|
}
|
|
2404
|
-
//
|
|
2405
|
-
// action; the caller must continue from the LAST one (it renders the injected hits).
|
|
2347
|
+
// Every seed command leaves the pending provider action authoritative; use the last view.
|
|
2406
2348
|
return resumed;
|
|
2407
2349
|
}
|
|
2408
2350
|
catch { /* errs-open — a faulty pre-fetch never breaks the run */ }
|
|
2409
2351
|
return undefined;
|
|
2410
2352
|
}
|
|
2353
|
+
async prefetchMemoryIntoInitialContext(runtime) {
|
|
2354
|
+
if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
|
|
2355
|
+
return;
|
|
2356
|
+
const preQuery = this.opts.preQueryMemory
|
|
2357
|
+
?? ((ctx) => [{
|
|
2358
|
+
scope: this.opts.memoryScope,
|
|
2359
|
+
query: ctx.goal,
|
|
2360
|
+
top_k: 5,
|
|
2361
|
+
kinds: [],
|
|
2362
|
+
}]);
|
|
2363
|
+
try {
|
|
2364
|
+
const queries = await preQuery({
|
|
2365
|
+
goal: this.currentGoal,
|
|
2366
|
+
runSpec: this.opts.runSpec,
|
|
2367
|
+
phase: "initial",
|
|
2368
|
+
});
|
|
2369
|
+
const seen = new Set();
|
|
2370
|
+
const accepted = [];
|
|
2371
|
+
for (const query of queries ?? []) {
|
|
2372
|
+
if (!query.query.trim())
|
|
2373
|
+
continue;
|
|
2374
|
+
const hits = await this.retrieveMemoryFromStore(query, query.top_k, this.opts.agentId);
|
|
2375
|
+
for (const hit of hits) {
|
|
2376
|
+
if (seen.has(hit.record.record_id))
|
|
2377
|
+
continue;
|
|
2378
|
+
seen.add(hit.record.record_id);
|
|
2379
|
+
accepted.push(hit);
|
|
2380
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
2381
|
+
kind: "add_history_message",
|
|
2382
|
+
message: {
|
|
2383
|
+
role: "user",
|
|
2384
|
+
content: `[MEMORY record_id=${hit.record.record_id} kind=${hit.record.kind}] ${hit.record.content}`,
|
|
2385
|
+
},
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
await this.applyHostMemoryRecallLifecycle(accepted, this.opts.agentId);
|
|
2390
|
+
}
|
|
2391
|
+
catch {
|
|
2392
|
+
// Prefetch is advisory; store/config failure never blocks the operation.
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2411
2395
|
async appendObservations(sessionId, runtime, nextArchiveStart, _taskScope) {
|
|
2412
2396
|
const turn = runtime.turn();
|
|
2413
2397
|
const preservedRefs = runtime.preservedRefs();
|
|
@@ -2833,75 +2817,6 @@ function parseUpdatePlanArgs(argsStr) {
|
|
|
2833
2817
|
: parsed.blocked_on,
|
|
2834
2818
|
};
|
|
2835
2819
|
}
|
|
2836
|
-
/** R3-1: parse the `submit_workflow_nodes` tool arguments (`{ nodes: WorkflowNodeSpec[] }`). Node
|
|
2837
|
-
* shapes are trusted structurally here; the kernel validates them (dep range, quarantine, quota) on
|
|
2838
|
-
* append. A malformed payload yields no nodes rather than throwing. */
|
|
2839
|
-
function parseSubmitWorkflowNodesArgs(argsStr) {
|
|
2840
|
-
let parsed = {};
|
|
2841
|
-
try {
|
|
2842
|
-
parsed = JSON.parse(argsStr);
|
|
2843
|
-
}
|
|
2844
|
-
catch {
|
|
2845
|
-
// Ignore parse error → no nodes submitted.
|
|
2846
|
-
}
|
|
2847
|
-
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
2848
|
-
}
|
|
2849
|
-
/** M5 v1: parse the `start_workflow` tool arguments (`{ spec: { nodes: WorkflowNodeSpec[] } }`) into
|
|
2850
|
-
* the spec's node batch — flattened onto the running workflow via the same append path. A malformed
|
|
2851
|
-
* payload yields no nodes rather than throwing. */
|
|
2852
|
-
function parseStartWorkflowArgs(argsStr) {
|
|
2853
|
-
let parsed = {};
|
|
2854
|
-
try {
|
|
2855
|
-
parsed = JSON.parse(argsStr);
|
|
2856
|
-
}
|
|
2857
|
-
catch {
|
|
2858
|
-
// Ignore parse error → no nodes.
|
|
2859
|
-
}
|
|
2860
|
-
const spec = parsed.spec;
|
|
2861
|
-
return Array.isArray(spec?.nodes) ? spec.nodes : [];
|
|
2862
|
-
}
|
|
2863
|
-
/** M5 v2.1: parse the full `WorkflowSpec` from a top-level `start_workflow` call, for auto-pivot drive
|
|
2864
|
-
* (vs `parseStartWorkflowArgs`, which returns only the node batch for the flatten path). Returns
|
|
2865
|
-
* `undefined` on a malformed / empty payload so the caller falls back to the flatten path. */
|
|
2866
|
-
function parseStartWorkflowSpec(argsStr) {
|
|
2867
|
-
try {
|
|
2868
|
-
const parsed = JSON.parse(argsStr);
|
|
2869
|
-
if (Array.isArray(parsed.spec?.nodes) && parsed.spec.nodes.length > 0) {
|
|
2870
|
-
return { nodes: parsed.spec.nodes };
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
catch {
|
|
2874
|
-
// Ignore parse error → undefined (fall back to flatten).
|
|
2875
|
-
}
|
|
2876
|
-
return undefined;
|
|
2877
|
-
}
|
|
2878
|
-
/** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
|
|
2879
|
-
* agent's context, so the agent's next turn continues with the sub-workflow's results in view. */
|
|
2880
|
-
function recoveredOutputs(outcomes) {
|
|
2881
|
-
const outputs = new Map();
|
|
2882
|
-
for (const outcome of outcomes ?? []) {
|
|
2883
|
-
if (!outcome.output)
|
|
2884
|
-
continue;
|
|
2885
|
-
outputs.set(outcome.agentId, outcome.output.content);
|
|
2886
|
-
outputs.set(outcome.agentId.replace(/-i\d+$/, ""), outcome.output.content);
|
|
2887
|
-
}
|
|
2888
|
-
return outputs;
|
|
2889
|
-
}
|
|
2890
|
-
function authoredWorkflowOutcomeNote(outcome) {
|
|
2891
|
-
const counts = new Map();
|
|
2892
|
-
for (const node of outcome.nodeOutcomes)
|
|
2893
|
-
counts.set(node.status, (counts.get(node.status) ?? 0) + 1);
|
|
2894
|
-
const lines = [
|
|
2895
|
-
`[authored workflow result] ${outcome.nodeOutcomes.length} terminal node(s): ` +
|
|
2896
|
-
[...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ") + ".",
|
|
2897
|
-
];
|
|
2898
|
-
for (const node of outcome.nodeOutcomes) {
|
|
2899
|
-
const out = outcome.outputs[node.nodeId] ?? node.output?.content;
|
|
2900
|
-
if (out)
|
|
2901
|
-
lines.push(`- ${node.nodeId} (${node.status}): ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
|
|
2902
|
-
}
|
|
2903
|
-
return lines.join("\n");
|
|
2904
|
-
}
|
|
2905
2820
|
/** Lower a host `RuntimeSignal` to the kernel's snake_case `signal` input event. Shared by the main
|
|
2906
2821
|
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
2907
2822
|
function signalToKernelEvent(delivery) {
|