@deepstrike/sdk 0.2.49 → 0.2.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -60
- package/dist/harness/manifest.d.ts +1 -1
- package/dist/harness/manifest.js +43 -29
- 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 +143 -0
- package/dist/runtime/canonical-kernel-step.js +1444 -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 +80 -119
- package/dist/runtime/runner.js +706 -779
- 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 +31 -19
- package/dist/types/agent.js +31 -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,
|
|
@@ -438,6 +484,11 @@ export class RuntimeRunner {
|
|
|
438
484
|
if (this.opts.criteriaGate !== undefined) {
|
|
439
485
|
config.criteria_gate = this.opts.criteriaGate;
|
|
440
486
|
}
|
|
487
|
+
// P1: fail-closed dispatch selector (absent ⇒ kernel default "exposed"). "registered" is the
|
|
488
|
+
// escape hatch back to permissive dispatch; the kernel rejects any other value.
|
|
489
|
+
if (this.opts.toolDispatchGate !== undefined) {
|
|
490
|
+
config.tool_dispatch_gate = this.opts.toolDispatchGate;
|
|
491
|
+
}
|
|
441
492
|
// K2: knowledge budget ratio (absent ⇒ kernel default 0.25; 0 disables).
|
|
442
493
|
if (this.opts.knowledgeBudgetRatio !== undefined) {
|
|
443
494
|
config.knowledge_budget_ratio = this.opts.knowledgeBudgetRatio;
|
|
@@ -457,9 +508,7 @@ export class RuntimeRunner {
|
|
|
457
508
|
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
458
509
|
}
|
|
459
510
|
/**
|
|
460
|
-
* Mirror one
|
|
461
|
-
* Shared by the main run drain, the prefetch path, and the host memory syscalls so every
|
|
462
|
-
* query route has identical recall + promotion semantics (T5).
|
|
511
|
+
* Mirror one agent-syscall memory-lifecycle observation into the durable store / host callbacks.
|
|
463
512
|
*
|
|
464
513
|
* M3: `memory_recalled` carries the kernel-derived count — the runner never computes
|
|
465
514
|
* `recall_count + 1` itself. M4: `promotion_suggested` is advisory and already
|
|
@@ -479,19 +528,6 @@ export class RuntimeRunner {
|
|
|
479
528
|
});
|
|
480
529
|
}
|
|
481
530
|
}
|
|
482
|
-
async consumeMemoryLifecycleObservations(sessionId, observations) {
|
|
483
|
-
const turn = this.activeKernel?.turn() ?? 0;
|
|
484
|
-
for (const obs of observations) {
|
|
485
|
-
if (!isMemoryLifecycleObservation(obs))
|
|
486
|
-
continue;
|
|
487
|
-
await this.mirrorMemoryLifecycle(obs);
|
|
488
|
-
if (!sessionId)
|
|
489
|
-
continue;
|
|
490
|
-
const event = kernelObservationToSessionEvent(obs, turn);
|
|
491
|
-
if (event)
|
|
492
|
-
await this.opts.sessionLog.append(sessionId, event);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
531
|
/** Mount a tool capability on the currently-running kernel runtime. No-op if not running. */
|
|
496
532
|
async mountTool(schema) {
|
|
497
533
|
if (!this.activeKernel)
|
|
@@ -549,48 +585,6 @@ export class RuntimeRunner {
|
|
|
549
585
|
// Re-arm the SDK-side push guard so a re-activation re-pins the content.
|
|
550
586
|
this.knowledgePushedSkills.delete(name);
|
|
551
587
|
}
|
|
552
|
-
/**
|
|
553
|
-
* Spawn an isolated sub-agent via the kernel, run it on the host, and feed the result back.
|
|
554
|
-
* Requires an active parent run (`run()` / `wake()` in progress or paused at milestone).
|
|
555
|
-
*/
|
|
556
|
-
async *spawnSubAgent(spec) {
|
|
557
|
-
if (!this.activeKernel || !this.currentSessionId) {
|
|
558
|
-
throw new Error("spawnSubAgent requires an active parent run");
|
|
559
|
-
}
|
|
560
|
-
const parentSessionId = this.currentSessionId;
|
|
561
|
-
const runtime = this.activeKernel;
|
|
562
|
-
const observations = await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
563
|
-
kind: "spawn_sub_agent",
|
|
564
|
-
spec: agentRunSpecToKernel(spec),
|
|
565
|
-
parent_session_id: parentSessionId,
|
|
566
|
-
});
|
|
567
|
-
this.nextArchiveStart = await this.appendObservations(parentSessionId, runtime, this.nextArchiveStart);
|
|
568
|
-
const spawned = findSpawnProcessObservation(observations);
|
|
569
|
-
if (!spawned) {
|
|
570
|
-
const rejected = controlRequestRejection(observations, "spawn_sub_agent");
|
|
571
|
-
if (rejected) {
|
|
572
|
-
yield { type: "error", message: `spawn_sub_agent denied: ${rejected.reason}` };
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
throw new Error("spawn_sub_agent did not emit agent_process_changed");
|
|
576
|
-
}
|
|
577
|
-
const manifest = spawnObservationToManifest(spawned, spec, parentSessionId);
|
|
578
|
-
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
579
|
-
const result = await orchestrator.run({
|
|
580
|
-
parentOpts: this.opts,
|
|
581
|
-
parentSessionId,
|
|
582
|
-
spec,
|
|
583
|
-
manifest,
|
|
584
|
-
sessionLog: this.opts.sessionLog,
|
|
585
|
-
toolAccess: spec.toolAccess,
|
|
586
|
-
...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
|
|
587
|
-
});
|
|
588
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
589
|
-
kind: "sub_agent_completed",
|
|
590
|
-
result: subAgentResultToKernel(result),
|
|
591
|
-
});
|
|
592
|
-
yield { type: "done", iterations: result.result.turnsUsed, totalTokens: result.result.totalTokensUsed, status: result.result.termination };
|
|
593
|
-
}
|
|
594
588
|
/**
|
|
595
589
|
* G3: run one workflow node, enforcing its `output_schema` (if any). Without a schema this is a
|
|
596
590
|
* plain `orchestrator.run`. With one, the node's agent is instructed to emit conforming JSON, its
|
|
@@ -620,8 +614,8 @@ export class RuntimeRunner {
|
|
|
620
614
|
spec: { ...baseSpec, goal: withBudget(goal) },
|
|
621
615
|
manifest,
|
|
622
616
|
sessionLog: this.opts.sessionLog,
|
|
623
|
-
//
|
|
624
|
-
//
|
|
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.
|
|
625
619
|
isWorkflowNode: true,
|
|
626
620
|
// W-N1: trusted workflow nodes run on the parent's execution plane (they carry no grant list
|
|
627
621
|
// by design — filtering on the missing list ran every DAG node TOOL-LESS); quarantined nodes
|
|
@@ -731,7 +725,7 @@ export class RuntimeRunner {
|
|
|
731
725
|
*/
|
|
732
726
|
async runWorkflow(spec, opts) {
|
|
733
727
|
// Standalone entry: with no active parent run (e.g. a stateless HTTP handler), auto-bootstrap a
|
|
734
|
-
// kernel that owns the DAG —
|
|
728
|
+
// kernel that owns the DAG — canonical configure + root start with the same policies a full run
|
|
735
729
|
// gets — then tear it down on completion so the runner is reusable. Mid-run callers (activeKernel
|
|
736
730
|
// already set by an in-flight `run()`) keep the original in-place behavior with no teardown.
|
|
737
731
|
const bootstrapped = !this.activeKernel || !this.currentSessionId;
|
|
@@ -739,6 +733,7 @@ export class RuntimeRunner {
|
|
|
739
733
|
try {
|
|
740
734
|
if (bootstrapped) {
|
|
741
735
|
const sessionId = opts?.sessionId ?? `wf-${crypto.randomUUID()}`;
|
|
736
|
+
const runId = crypto.randomUUID();
|
|
742
737
|
// A standalone workflow reserves a bounded slice before its kernel schedules any node.
|
|
743
738
|
// Mid-run callers reuse their parent run's already-active reservation.
|
|
744
739
|
if (this.opts.runGroup) {
|
|
@@ -749,46 +744,56 @@ export class RuntimeRunner {
|
|
|
749
744
|
// Resume depends on this fact. Do not dispatch any node until it is durable.
|
|
750
745
|
await this.opts.sessionLog.append(sessionId, {
|
|
751
746
|
kind: "run_started",
|
|
752
|
-
run_id:
|
|
747
|
+
run_id: runId,
|
|
753
748
|
goal: `workflow:${spec.nodes.length} nodes`,
|
|
754
749
|
criteria: [],
|
|
755
750
|
agent_id: this.opts.agentId,
|
|
756
751
|
});
|
|
757
|
-
await this.
|
|
752
|
+
await this.initializeWorkflowKernel(sessionId, runId, groupBudgetScope);
|
|
758
753
|
}
|
|
759
754
|
const parentSessionId = this.currentSessionId;
|
|
760
755
|
const runtime = this.activeKernel;
|
|
761
756
|
const observationStart = this.pendingObservations.length;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
: {}),
|
|
780
|
-
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
781
|
-
...(opts?.resumedSubmissions?.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
782
|
-
...(opts?.resumedSubmissionBases?.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
|
|
783
|
-
});
|
|
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
|
+
}
|
|
784
773
|
const observations = this.pendingObservations.slice(observationStart);
|
|
785
|
-
const outcome = await this.driveWorkflow(initialAction, observations, parentSessionId, runtime,
|
|
774
|
+
const outcome = await this.driveWorkflow(initialAction, observations, parentSessionId, runtime, new Map());
|
|
786
775
|
if (bootstrapped) {
|
|
787
|
-
|
|
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
|
+
}
|
|
788
786
|
if (terminal.kind !== "done") {
|
|
789
|
-
throw new Error("
|
|
787
|
+
throw new Error("canonical workflow did not produce a terminal kernel action");
|
|
790
788
|
}
|
|
791
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
|
+
}
|
|
792
797
|
}
|
|
793
798
|
return outcome;
|
|
794
799
|
}
|
|
@@ -811,81 +816,22 @@ export class RuntimeRunner {
|
|
|
811
816
|
/**
|
|
812
817
|
* Bootstrap a standalone kernel for a host-driven workflow with NO active parent run — the path a
|
|
813
818
|
* stateless request handler takes when it calls `runWorkflow(spec)` directly. Mirrors `execute()`'s
|
|
814
|
-
* pre-run kernel setup (governance / attention / quota via `applyKernelPolicies`, then
|
|
819
|
+
* pre-run kernel setup (governance / attention / quota via `applyKernelPolicies`, then root start)
|
|
815
820
|
* after `runWorkflow` has durably recorded `run_started`. Sets `activeKernel` / `currentSessionId`;
|
|
816
821
|
* `runWorkflow` is responsible for tearing them down.
|
|
817
822
|
*/
|
|
818
|
-
async
|
|
823
|
+
async initializeWorkflowKernel(sessionId, runId, groupBudgetScope) {
|
|
819
824
|
this.interrupted = false;
|
|
820
825
|
this.abortController = new AbortController();
|
|
821
826
|
this.pendingObservations = [];
|
|
822
827
|
this.pendingPageOutArchives = [];
|
|
823
828
|
this.activePageOutArchive = undefined;
|
|
824
829
|
this.currentSessionId = sessionId;
|
|
825
|
-
const runtime = this.
|
|
830
|
+
const runtime = this.createCanonicalRuntime(runId, sessionId);
|
|
826
831
|
this.activeKernel = runtime;
|
|
827
832
|
await this.applyKernelPolicies(runtime, groupBudgetScope);
|
|
828
|
-
// ABI v2 has one lifecycle: standalone workflows start a real run before loading their DAG.
|
|
829
|
-
// The initial provider effect is superseded by the workflow load; no self-bootstrap escape hatch.
|
|
830
|
-
await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
831
|
-
kind: "start_run",
|
|
832
|
-
task: { goal: `workflow session ${sessionId}`, criteria: [] },
|
|
833
|
-
});
|
|
834
833
|
return runtime;
|
|
835
834
|
}
|
|
836
|
-
/**
|
|
837
|
-
* M5/G1: bootstrap an **agent-authored** workflow ("the model writes its own harness"). Unlike
|
|
838
|
-
* `runWorkflow` (the host fires the privileged `load_workflow`), this routes the spec through the
|
|
839
|
-
* agent-reachable `Syscall::LoadWorkflow` (the `submit_workflow` event): with no workflow active the
|
|
840
|
-
* kernel **bootstraps** the DAG; if one is already active it **flattens** the spec's nodes onto it
|
|
841
|
-
* (bootstrap-or-flatten — one kernel, one quota, never a workflow stack). Gated by the same
|
|
842
|
-
* `max_workflow_nodes` backstop as runtime submission, so an authored harness can't overgrow the run.
|
|
843
|
-
* The resulting batches are driven by the same shared driver as `runWorkflow`.
|
|
844
|
-
*/
|
|
845
|
-
async bootstrapWorkflow(spec, opts) {
|
|
846
|
-
if (!this.activeKernel || !this.currentSessionId) {
|
|
847
|
-
throw new Error("bootstrapWorkflow requires an active parent run");
|
|
848
|
-
}
|
|
849
|
-
const parentSessionId = this.currentSessionId;
|
|
850
|
-
const runtime = this.activeKernel;
|
|
851
|
-
const observationStart = this.pendingObservations.length;
|
|
852
|
-
const initialAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
853
|
-
const observations = this.pendingObservations.slice(observationStart);
|
|
854
|
-
// W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
|
|
855
|
-
// announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
|
|
856
|
-
// had this spec, unlike the `runWorkflow` path.
|
|
857
|
-
const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
|
|
858
|
-
if (submitted) {
|
|
859
|
-
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
860
|
-
turn: runtime.turn(),
|
|
861
|
-
nodes: workflowSpecToKernel(spec).nodes ?? [],
|
|
862
|
-
baseIndex: submitted.base,
|
|
863
|
-
submitterAgentId: opts?.submitterAgentId,
|
|
864
|
-
}));
|
|
865
|
-
}
|
|
866
|
-
return this.driveWorkflow(initialAction, observations, parentSessionId, runtime);
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`. Called at the
|
|
870
|
-
* verified-safe point (right after the tool turn resolved to `call_provider` — kernel in Reason, not
|
|
871
|
-
* suspended). For each authored spec: `bootstrapWorkflow` runs it in THIS kernel (the kernel resumes
|
|
872
|
-
* the agent reason loop on `workflow_completed` — `finish_workflow` sets phase=Reason), then the
|
|
873
|
-
* outcome is injected as a user message so the agent's next turn sees the result. Returns a fresh
|
|
874
|
-
* `call_provider` synthesized from the updated context (the workflow drive consumed its own kernel
|
|
875
|
-
* actions, so we re-render — the same pattern as the reactive-compact retry path).
|
|
876
|
-
*/
|
|
877
|
-
async driveAuthoredWorkflows(runtime, action) {
|
|
878
|
-
const specs = this.pendingAuthoredWorkflows;
|
|
879
|
-
this.pendingAuthoredWorkflows = [];
|
|
880
|
-
this.workflowContinuation = null;
|
|
881
|
-
for (const spec of specs) {
|
|
882
|
-
await this.bootstrapWorkflow(spec);
|
|
883
|
-
}
|
|
884
|
-
const continuation = this.workflowContinuation;
|
|
885
|
-
if (!continuation)
|
|
886
|
-
throw new Error("authored workflow completed without a provider continuation");
|
|
887
|
-
return continuation;
|
|
888
|
-
}
|
|
889
835
|
/**
|
|
890
836
|
* #2-B-ii: while a workflow batch is in flight, poll the signal source. A Critical `InterruptNow`
|
|
891
837
|
* routes through the kernel (which, with the root suspended in `SubAgentAwait`, preempts — marks the
|
|
@@ -896,9 +842,48 @@ export class RuntimeRunner {
|
|
|
896
842
|
*/
|
|
897
843
|
async monitorWorkflowPreemption(runtime, controllers, batchState) {
|
|
898
844
|
const source = this.opts.signalSource;
|
|
899
|
-
if (!source)
|
|
900
|
-
return null;
|
|
901
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
|
+
}
|
|
902
887
|
// O2: injected notes participate in the monitor too, so a host `injectNote` mid-batch is not
|
|
903
888
|
// stranded until the batch settles (the drain order matches `nextInboundSignal`).
|
|
904
889
|
const delivery = await this.nextInboundSignal();
|
|
@@ -931,6 +916,8 @@ export class RuntimeRunner {
|
|
|
931
916
|
}
|
|
932
917
|
const preempted = observations.find(o => o.kind === "agent_preempted");
|
|
933
918
|
if (preempted) {
|
|
919
|
+
this.interrupted = true;
|
|
920
|
+
this.cancellationReason ??= "user";
|
|
934
921
|
for (const id of preempted.agent_ids ?? [])
|
|
935
922
|
controllers.get(id)?.abort();
|
|
936
923
|
const wc = observations.find(o => o.kind === "workflow_completed");
|
|
@@ -940,10 +927,8 @@ export class RuntimeRunner {
|
|
|
940
927
|
return null;
|
|
941
928
|
}
|
|
942
929
|
/**
|
|
943
|
-
*
|
|
944
|
-
*
|
|
945
|
-
* batch in parallel, feed completions back (appending any agent-submitted nodes first), and loop
|
|
946
|
-
* 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.
|
|
947
932
|
*/
|
|
948
933
|
async driveWorkflow(initialAction, initial, parentSessionId, runtime, seedOutputs) {
|
|
949
934
|
let observations = initial;
|
|
@@ -988,9 +973,16 @@ export class RuntimeRunner {
|
|
|
988
973
|
// W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
|
|
989
974
|
// still see their (pre-crash) dependencies' outputs.
|
|
990
975
|
const outputs = new Map(seedOutputs ?? []);
|
|
976
|
+
const completedNodeOutcomes = [];
|
|
991
977
|
for (;;) {
|
|
992
978
|
if (nodes.length === 0)
|
|
993
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
|
+
}
|
|
994
986
|
// Run the currently-runnable nodes in parallel — each is independent within a round.
|
|
995
987
|
const roundBudget = budget;
|
|
996
988
|
// #2-B-ii: per-node abort controllers + a concurrent preemption monitor. While the batch is in
|
|
@@ -1003,8 +995,9 @@ export class RuntimeRunner {
|
|
|
1003
995
|
const results = await Promise.all(nodes.map(node => this.runWorkflowNode(node, parentSessionId, orchestrator, roundBudget, outputs, controllers.get(node.agent_id)?.signal)));
|
|
1004
996
|
batchState.settled = true;
|
|
1005
997
|
const preempted = await monitor;
|
|
1006
|
-
if (preempted)
|
|
998
|
+
if (preempted !== null) {
|
|
1007
999
|
return { nodeOutcomes: preempted, outputs: Object.fromEntries(outputs) };
|
|
1000
|
+
}
|
|
1008
1001
|
// Feed completions back one at a time. The kernel's run-queue executor may spawn a node's
|
|
1009
1002
|
// dependents the moment *that* node completes (per-node unblock), so each feed can emit its
|
|
1010
1003
|
// own `workflow_batch_spawned`; ACCUMULATE them across the round rather than keeping only the
|
|
@@ -1018,61 +1011,17 @@ export class RuntimeRunner {
|
|
|
1018
1011
|
const outContent = result.result.finalMessage?.content;
|
|
1019
1012
|
const outText = typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "";
|
|
1020
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
|
+
});
|
|
1021
1020
|
// A loop iteration completes under `wf-node{N}-i{k}` but its dependents consume the STABLE
|
|
1022
1021
|
// node id `wf-node{N}` — alias it so the LAST iteration's output is what dependents see.
|
|
1023
1022
|
const stableId = result.agentId.replace(/-i\d+$/, "");
|
|
1024
1023
|
if (stableId !== result.agentId)
|
|
1025
1024
|
outputs.set(stableId, outText);
|
|
1026
|
-
// R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
|
|
1027
|
-
// reporting the node's completion — the workflow is still active (the kernel hasn't seen this
|
|
1028
|
-
// node finish), so even a submission from the last running node keeps the DAG alive. The
|
|
1029
|
-
// appended nodes' `workflow_batch_spawned` is collected into this round like any other.
|
|
1030
|
-
if (result.submittedNodes?.length) {
|
|
1031
|
-
// G1: stamp the submitting node's agent id so the kernel can coerce a quarantined
|
|
1032
|
-
// submitter's nodes to quarantined (no topological privilege escalation).
|
|
1033
|
-
const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
|
|
1034
|
-
const observationStart = this.pendingObservations.length;
|
|
1035
|
-
const submitAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, submitEvent);
|
|
1036
|
-
const subObs = this.pendingObservations.slice(observationStart);
|
|
1037
|
-
const rejected = controlRequestRejection(subObs, "submit_workflow_nodes")
|
|
1038
|
-
?? (subObs.find(o => o.kind === "nodes_rejected")
|
|
1039
|
-
? { operation: "submit_workflow_nodes", reason: String(subObs.find(o => o.kind === "nodes_rejected")?.reason ?? "request denied") }
|
|
1040
|
-
: undefined);
|
|
1041
|
-
if (rejected) {
|
|
1042
|
-
const denial = `workflow node submission denied: ${rejected.reason}`;
|
|
1043
|
-
result.result = {
|
|
1044
|
-
...result.result,
|
|
1045
|
-
termination: "error",
|
|
1046
|
-
finalMessage: { role: "assistant", content: denial, toolCalls: [] },
|
|
1047
|
-
};
|
|
1048
|
-
outputs.set(result.agentId, denial);
|
|
1049
|
-
if (stableId !== result.agentId)
|
|
1050
|
-
outputs.set(stableId, denial);
|
|
1051
|
-
}
|
|
1052
|
-
if (submitAction?.kind === "spawn_workflow") {
|
|
1053
|
-
nextNodes.push(...submitAction.nodes);
|
|
1054
|
-
budget = submitAction.budget ?? budget;
|
|
1055
|
-
const accepted = await acceptSpawn(submitAction);
|
|
1056
|
-
const submittedDone = findDone([...subObs, ...accepted]);
|
|
1057
|
-
if (submittedDone)
|
|
1058
|
-
done = submittedDone;
|
|
1059
|
-
}
|
|
1060
|
-
else if (submitAction) {
|
|
1061
|
-
throw new Error(`workflow node submission returned unexpected effect: ${submitAction.kind}`);
|
|
1062
|
-
}
|
|
1063
|
-
// R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
|
|
1064
|
-
// so resume can re-apply the batch at the exact original graph position. W-N3: also the
|
|
1065
|
-
// submitter, so resume drops batches whose submitter re-runs (it will re-submit).
|
|
1066
|
-
const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
|
|
1067
|
-
if (submitted) {
|
|
1068
|
-
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1069
|
-
turn: runtime.turn(),
|
|
1070
|
-
nodes: submitEvent.nodes ?? [],
|
|
1071
|
-
baseIndex: submitted.base,
|
|
1072
|
-
submitterAgentId: result.agentId,
|
|
1073
|
-
}));
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
1025
|
const observationStart = this.pendingObservations.length;
|
|
1077
1026
|
const completionAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1078
1027
|
kind: "sub_agent_completed",
|
|
@@ -1087,9 +1036,28 @@ export class RuntimeRunner {
|
|
|
1087
1036
|
else if (completionAction?.kind === "call_provider") {
|
|
1088
1037
|
this.workflowContinuation = completionAction;
|
|
1089
1038
|
}
|
|
1039
|
+
else if (completionAction?.kind === "done") {
|
|
1040
|
+
return {
|
|
1041
|
+
nodeOutcomes: completedNodeOutcomes,
|
|
1042
|
+
outputs: Object.fromEntries(outputs),
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1090
1045
|
else if (completionAction) {
|
|
1091
1046
|
throw new Error(`workflow completion returned unexpected effect: ${completionAction.kind}`);
|
|
1092
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
|
+
}
|
|
1093
1061
|
const d = findDone(obs);
|
|
1094
1062
|
if (d)
|
|
1095
1063
|
done = d;
|
|
@@ -1116,39 +1084,6 @@ export class RuntimeRunner {
|
|
|
1116
1084
|
nodes = nextNodes;
|
|
1117
1085
|
}
|
|
1118
1086
|
}
|
|
1119
|
-
/**
|
|
1120
|
-
* Resume a workflow from the parent session's completed nodes.
|
|
1121
|
-
* Reads the session log, extracts completed workflow node records (with their W-1 control
|
|
1122
|
-
* signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
|
|
1123
|
-
* flow (classify prune / loop stop), and the driver re-seeds its outputs map.
|
|
1124
|
-
*/
|
|
1125
|
-
async resumeWorkflow(spec, opts) {
|
|
1126
|
-
// Standalone resume: a stateless handler passes the prior `sessionId` to pick up an interrupted
|
|
1127
|
-
// workflow from the session log. Mid-run callers omit it and resume the active session.
|
|
1128
|
-
const sessionId = opts?.sessionId ?? this.currentSessionId;
|
|
1129
|
-
if (!sessionId) {
|
|
1130
|
-
throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
|
|
1131
|
-
}
|
|
1132
|
-
const events = await this.opts.sessionLog.read(sessionId);
|
|
1133
|
-
const resumedOutcomes = recoverWorkflowNodeOutcomes(events);
|
|
1134
|
-
const completedIds = new Set(resumedOutcomes.map(r => r.agentId));
|
|
1135
|
-
const recovered = recoverSubmittedWorkflowNodes(events);
|
|
1136
|
-
// W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
|
|
1137
|
-
// re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
|
|
1138
|
-
// Exact bases keep later graph indices stable while dropped slots remain inert placeholders.
|
|
1139
|
-
let { submissions, bases } = recovered;
|
|
1140
|
-
if (submissions.length > 0) {
|
|
1141
|
-
const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
|
|
1142
|
-
submissions = submissions.filter((_, i) => keep[i]);
|
|
1143
|
-
bases = bases.filter((_, i) => keep[i]);
|
|
1144
|
-
}
|
|
1145
|
-
return this.runWorkflow(spec, {
|
|
1146
|
-
resumedOutcomes,
|
|
1147
|
-
resumedSubmissions: submissions,
|
|
1148
|
-
resumedSubmissionBases: bases,
|
|
1149
|
-
sessionId,
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
1087
|
interrupt(reason = "user") {
|
|
1153
1088
|
this.interrupted = true;
|
|
1154
1089
|
this.cancellationReason = reason;
|
|
@@ -1213,7 +1148,7 @@ export class RuntimeRunner {
|
|
|
1213
1148
|
const dispositions = this.pendingObservations.slice(observationStart).filter(observation => observation.kind === "signal_delivery_disposed"
|
|
1214
1149
|
&& observation.delivery_id === delivery.deliveryId
|
|
1215
1150
|
&& observation.attempt === delivery.deliveryAttempt);
|
|
1216
|
-
if (dispositions.length
|
|
1151
|
+
if (dispositions.length > 1) {
|
|
1217
1152
|
throw new Error("kernel did not return the matching signal delivery disposition");
|
|
1218
1153
|
}
|
|
1219
1154
|
if (!await delivery.ack())
|
|
@@ -1227,8 +1162,24 @@ export class RuntimeRunner {
|
|
|
1227
1162
|
}
|
|
1228
1163
|
async *run(req) {
|
|
1229
1164
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
1230
|
-
const midRun = isMidRun(prior);
|
|
1231
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
|
+
}
|
|
1232
1183
|
const runId = midRun && resumedStart?.event.kind === "run_started"
|
|
1233
1184
|
? resumedStart.event.run_id
|
|
1234
1185
|
: crypto.randomUUID();
|
|
@@ -1253,12 +1204,26 @@ export class RuntimeRunner {
|
|
|
1253
1204
|
}
|
|
1254
1205
|
async *wake(sessionId, extensions) {
|
|
1255
1206
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
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;
|
|
1259
1209
|
if (!startEntry)
|
|
1260
1210
|
throw new Error(`No run_started event for session: ${sessionId}`);
|
|
1261
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
|
+
}
|
|
1262
1227
|
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true, start.attachments, start.run_id);
|
|
1263
1228
|
}
|
|
1264
1229
|
/** Execute a kernel-owned approval effect and return the correlated decision lists. */
|
|
@@ -1339,62 +1304,6 @@ export class RuntimeRunner {
|
|
|
1339
1304
|
}
|
|
1340
1305
|
return { approved, denied, events };
|
|
1341
1306
|
}
|
|
1342
|
-
/**
|
|
1343
|
-
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
1344
|
-
* output. Resolution order: (a) the on-disk result spool committed by the explicit
|
|
1345
|
-
* `spool_large_result` host effect, then (b) a session-log scan for the original
|
|
1346
|
-
* `tool_completed` event carrying that `call_id`. Slices the
|
|
1347
|
-
* resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
1348
|
-
*/
|
|
1349
|
-
async resolveReadResult(sessionId, argsJson) {
|
|
1350
|
-
let callId = "";
|
|
1351
|
-
let offset = 0;
|
|
1352
|
-
let maxBytes = 4000;
|
|
1353
|
-
try {
|
|
1354
|
-
const args = JSON.parse(argsJson || "{}");
|
|
1355
|
-
callId = typeof args.call_id === "string" ? args.call_id : "";
|
|
1356
|
-
if (typeof args.offset === "number" && Number.isFinite(args.offset))
|
|
1357
|
-
offset = args.offset;
|
|
1358
|
-
if (typeof args.max_bytes === "number" && Number.isFinite(args.max_bytes))
|
|
1359
|
-
maxBytes = args.max_bytes;
|
|
1360
|
-
}
|
|
1361
|
-
catch {
|
|
1362
|
-
// malformed arguments — callId stays empty, falls through to "not found" below
|
|
1363
|
-
}
|
|
1364
|
-
let full;
|
|
1365
|
-
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
1366
|
-
try {
|
|
1367
|
-
full = await spool.findByCallId(sessionId, callId);
|
|
1368
|
-
}
|
|
1369
|
-
catch {
|
|
1370
|
-
full = undefined;
|
|
1371
|
-
}
|
|
1372
|
-
if (full === undefined) {
|
|
1373
|
-
try {
|
|
1374
|
-
const events = await this.opts.sessionLog.read(sessionId);
|
|
1375
|
-
for (const { event } of events) {
|
|
1376
|
-
if (event.kind !== "tool_completed")
|
|
1377
|
-
continue;
|
|
1378
|
-
const match = event.results.find(r => r.call_id === callId);
|
|
1379
|
-
if (match)
|
|
1380
|
-
full = match.output;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
catch {
|
|
1384
|
-
full = undefined;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
if (full === undefined) {
|
|
1388
|
-
return { text: `no stored output for call_id "${callId}"`, isError: true };
|
|
1389
|
-
}
|
|
1390
|
-
const start = Math.max(0, offset);
|
|
1391
|
-
const end = Math.min(full.length, start + Math.max(0, maxBytes));
|
|
1392
|
-
const slice = full.slice(start, end);
|
|
1393
|
-
return {
|
|
1394
|
-
text: `[read_result ${callId}: chars ${start}–${end} of ${full.length}]\n${slice}`,
|
|
1395
|
-
isError: false,
|
|
1396
|
-
};
|
|
1397
|
-
}
|
|
1398
1307
|
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments, runId = crypto.randomUUID()) {
|
|
1399
1308
|
this.interrupted = false;
|
|
1400
1309
|
this.cancellationReason = undefined;
|
|
@@ -1406,7 +1315,6 @@ export class RuntimeRunner {
|
|
|
1406
1315
|
if (this.opts.enableDiagnosticsDashboard) {
|
|
1407
1316
|
this.dashboard = new KernelPrimitivesDashboard(sessionId);
|
|
1408
1317
|
}
|
|
1409
|
-
const kernel = getKernel();
|
|
1410
1318
|
const ext = { ...this.opts.extensions, ...(extensions ?? {}) };
|
|
1411
1319
|
const providerState = this.opts.provider.createRunState?.();
|
|
1412
1320
|
let nextCompressedArchiveStart = nextArchivedSeqStart(priorEvents);
|
|
@@ -1423,200 +1331,167 @@ export class RuntimeRunner {
|
|
|
1423
1331
|
const taskScope = new ManagedTaskScope(operation, this.opts.onBackgroundTaskError);
|
|
1424
1332
|
let groupBudgetScope;
|
|
1425
1333
|
try {
|
|
1426
|
-
const runtime =
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
timeoutMs: effectiveTimeoutMs !== undefined ? BigInt(effectiveTimeoutMs) : undefined,
|
|
1430
|
-
maxTotalTokens: this.opts.maxTotalTokens !== undefined ? BigInt(this.opts.maxTotalTokens) : undefined,
|
|
1431
|
-
});
|
|
1334
|
+
const runtime = this.createCanonicalRuntime(runId, sessionId);
|
|
1335
|
+
if (resumeMidRun)
|
|
1336
|
+
await runtime.restore();
|
|
1432
1337
|
this.activeKernel = runtime;
|
|
1433
1338
|
this.nextArchiveStart = nextCompressedArchiveStart;
|
|
1434
|
-
if (
|
|
1435
|
-
|
|
1436
|
-
kind: "set_tokenizer",
|
|
1437
|
-
name: this.opts.tokenizer,
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
|
-
if (this.opts.enablePlanTool !== undefined) {
|
|
1441
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1442
|
-
kind: "set_plan_tool_enabled",
|
|
1443
|
-
enabled: this.opts.enablePlanTool,
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1447
|
-
kind: "set_tools",
|
|
1448
|
-
tools: this.opts.executionPlane.schemas().map(toolSchemaToKernel),
|
|
1449
|
-
});
|
|
1450
|
-
if (this.composedSystemPrompt) {
|
|
1451
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1452
|
-
kind: "add_system_message",
|
|
1453
|
-
content: this.composedSystemPrompt,
|
|
1454
|
-
tokens: Math.max(1, Math.ceil(this.composedSystemPrompt.length / 4)),
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
if (this.opts.initialMemory) {
|
|
1458
|
-
for (const mem of this.opts.initialMemory) {
|
|
1339
|
+
if (!resumeMidRun) {
|
|
1340
|
+
if (this.opts.tokenizer) {
|
|
1459
1341
|
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1460
|
-
kind: "
|
|
1461
|
-
|
|
1462
|
-
|
|
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,
|
|
1463
1350
|
});
|
|
1464
1351
|
}
|
|
1465
|
-
}
|
|
1466
|
-
if (this.opts.skillDir) {
|
|
1467
|
-
const { scanSkillDir } = await import("../skills/loader.js");
|
|
1468
|
-
const metas = await scanSkillDir(this.opts.skillDir);
|
|
1469
|
-
// S2 host-layer skill allowlist: keep only scanned skills named in `skillFilter` before feeding
|
|
1470
|
-
// the catalog. Absent ⇒ feed all (identical to the pre-feature message); empty ⇒ feed none. The
|
|
1471
|
-
// `set_available_skills` message is ALWAYS sent when a skillDir exists (shape preserved) — only
|
|
1472
|
-
// the list narrows; the no-skillDir path stays untouched.
|
|
1473
|
-
const filter = this.opts.skillFilter;
|
|
1474
|
-
const selected = filter === undefined ? metas : metas.filter(m => filter.includes(m.name));
|
|
1475
|
-
// P1-B: pass the full SkillMetadata (incl. `allowedTools`) straight through — re-mapping it
|
|
1476
|
-
// field-by-field previously dropped `allowedTools`.
|
|
1477
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1478
|
-
kind: "set_available_skills",
|
|
1479
|
-
skills: selected.map(m => skillMetadataToKernel(m)),
|
|
1480
|
-
});
|
|
1481
|
-
}
|
|
1482
|
-
// P1-B/D: configure the stable-core tool ids (always exposed under skill gating). Empty/absent
|
|
1483
|
-
// ⇒ skills narrow to exactly their declared tools + meta-tools.
|
|
1484
|
-
if (this.opts.stableCoreToolIds?.length) {
|
|
1485
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1486
|
-
kind: "set_stable_core_tools",
|
|
1487
|
-
tool_ids: this.opts.stableCoreToolIds,
|
|
1488
|
-
});
|
|
1489
|
-
}
|
|
1490
|
-
if (this.opts.dreamStore && this.opts.agentId) {
|
|
1491
|
-
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_memory_enabled", enabled: true });
|
|
1492
|
-
}
|
|
1493
|
-
// Install optional memory policy. Maps the ergonomic camelCase option onto the kernel's
|
|
1494
|
-
// snake_case `set_memory_policy` event; omitted fields fall back to kernel defaults.
|
|
1495
|
-
if (this.opts.memoryPolicy) {
|
|
1496
|
-
const m = this.opts.memoryPolicy;
|
|
1497
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1498
|
-
kind: "set_memory_policy",
|
|
1499
|
-
...(m.memoryPath !== undefined ? { memory_path: m.memoryPath } : {}),
|
|
1500
|
-
...(m.staleWarningDays !== undefined ? { stale_warning_days: m.staleWarningDays } : {}),
|
|
1501
|
-
...(m.retrievalTopK !== undefined ? { retrieval_top_k: m.retrievalTopK } : {}),
|
|
1502
|
-
...(m.validationEnabled !== undefined ? { validation_enabled: m.validationEnabled } : {}),
|
|
1503
|
-
...(m.maxContentBytes !== undefined ? { max_content_bytes: m.maxContentBytes } : {}),
|
|
1504
|
-
...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
|
|
1505
|
-
...(m.promotionRecallThreshold !== undefined
|
|
1506
|
-
? { promotion_recall_threshold: m.promotionRecallThreshold }
|
|
1507
|
-
: {}),
|
|
1508
|
-
});
|
|
1509
|
-
}
|
|
1510
|
-
if (this.opts.knowledgeSource) {
|
|
1511
|
-
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_knowledge_enabled", enabled: true });
|
|
1512
|
-
}
|
|
1513
|
-
if (this.opts.milestoneContract) {
|
|
1514
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1515
|
-
kind: "load_milestone_contract",
|
|
1516
|
-
contract: {
|
|
1517
|
-
phases: this.opts.milestoneContract.phases.map(p => ({
|
|
1518
|
-
id: p.id,
|
|
1519
|
-
criteria: p.criteria ?? [],
|
|
1520
|
-
unlocks: p.unlocks ?? [],
|
|
1521
|
-
required_evidence: p.requiredEvidence ?? [],
|
|
1522
|
-
...(p.verifier ? { verifier: p.verifier } : {}),
|
|
1523
|
-
})),
|
|
1524
|
-
},
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
const maxBytes = runtime.recoveryContentBytes();
|
|
1528
|
-
if (priorEvents && priorEvents.length > 0) {
|
|
1529
|
-
const repaired = repairEventsForRecovery(priorEvents, maxBytes);
|
|
1530
|
-
seedProviderReplayFromEvents(this.opts.provider, repaired);
|
|
1531
|
-
const loadArchive = this.opts.compressionStore
|
|
1532
|
-
? (ref) => this.opts.compressionStore.read(ref)
|
|
1533
|
-
: undefined;
|
|
1534
|
-
const replayed = await replayMessagesAsync(repaired, maxBytes, loadArchive);
|
|
1535
1352
|
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1536
|
-
kind: "
|
|
1537
|
-
|
|
1353
|
+
kind: "set_tools",
|
|
1354
|
+
tools: this.opts.executionPlane.schemas().map(toolSchemaToKernel),
|
|
1538
1355
|
});
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
for (const m of replayed) {
|
|
1546
|
-
for (const part of m.contentParts ?? []) {
|
|
1547
|
-
if (part.type === "tool_result")
|
|
1548
|
-
toolResultByCallId.set(part.callId, part.output);
|
|
1549
|
-
}
|
|
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
|
+
});
|
|
1550
1362
|
}
|
|
1551
|
-
|
|
1552
|
-
for (const
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
continue;
|
|
1559
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1560
|
-
kind: "skill_activated",
|
|
1561
|
-
name,
|
|
1562
|
-
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
1563
|
-
});
|
|
1564
|
-
const output = toolResultByCallId.get(tc.id);
|
|
1565
|
-
if (output && !this.knowledgePushedSkills.has(name)) {
|
|
1566
|
-
this.knowledgePushedSkills.add(name);
|
|
1567
|
-
// K1: keyed — the kernel-side upsert is the authoritative dedup, so a wake re-push
|
|
1568
|
-
// of a skill already pinned live can never double-pin (the in-run Set resets with
|
|
1569
|
-
// each runner instance; the key does not).
|
|
1570
|
-
await this.pushKnowledge({ role: "system", content: output, toolCalls: [] }, undefined, { key: `skill:${name}` });
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
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
|
+
});
|
|
1574
1370
|
}
|
|
1575
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
|
+
}
|
|
1576
1437
|
}
|
|
1577
1438
|
const sessionStart = Date.now();
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
1584
|
-
//
|
|
1585
|
-
// new ABI). Unset on both ⇒ no run_spec ⇒ no gating (铁律: no config = old behavior).
|
|
1439
|
+
const startTask = { goal, criteria };
|
|
1440
|
+
let startRunSpec;
|
|
1441
|
+
// P0-A: lower an explicit `runSpec`, the `allowedToolIds` ceiling, and/or the `baselineToolIds`
|
|
1442
|
+
// pre-activation surface to the kernel run spec. Each augments an explicit spec, else
|
|
1443
|
+
// synthesizes a minimal top-level spec carrying just the exposure config (reuses the existing
|
|
1444
|
+
// run_spec wire — no new ABI). Unset on all ⇒ no run_spec ⇒ no gating (铁律: no config = old
|
|
1445
|
+
// behavior).
|
|
1586
1446
|
const allowedToolIds = this.opts.allowedToolIds;
|
|
1587
1447
|
const hasProfile = allowedToolIds !== undefined && allowedToolIds.length > 0;
|
|
1588
|
-
|
|
1448
|
+
// NOT the `length > 0` idiom above: `baselineToolIds: []` is the legitimate minimal surface
|
|
1449
|
+
// (meta + stable-core only), so mere presence triggers the lowering.
|
|
1450
|
+
const baselineToolIds = this.opts.baselineToolIds;
|
|
1451
|
+
const hasBaseline = baselineToolIds !== undefined;
|
|
1452
|
+
const hasMilestoneContract = this.opts.milestoneContract !== undefined;
|
|
1453
|
+
if (this.opts.runSpec || hasProfile || hasBaseline || hasMilestoneContract) {
|
|
1589
1454
|
const baseSpec = this.opts.runSpec ?? {
|
|
1590
1455
|
identity: { agentId: this.opts.agentId ?? "root", sessionId, isSubAgent: false },
|
|
1591
1456
|
role: "custom",
|
|
1592
1457
|
goal,
|
|
1593
1458
|
};
|
|
1594
|
-
|
|
1459
|
+
let spec = hasProfile
|
|
1595
1460
|
? { ...baseSpec, capabilityFilter: { ...baseSpec.capabilityFilter, allowedIds: allowedToolIds } }
|
|
1596
1461
|
: baseSpec;
|
|
1597
|
-
|
|
1462
|
+
if (hasBaseline)
|
|
1463
|
+
spec = { ...spec, exposureBaseline: baselineToolIds };
|
|
1464
|
+
if (hasMilestoneContract && !spec.verificationContractId) {
|
|
1465
|
+
spec = { ...spec, verificationContractId: "node-default" };
|
|
1466
|
+
}
|
|
1467
|
+
startRunSpec = agentRunSpecToKernel(spec);
|
|
1598
1468
|
}
|
|
1599
|
-
// Reserve capacity before
|
|
1469
|
+
// Reserve capacity before the canonical root start. The kernel enforces only this vehicle's grant and reports
|
|
1600
1470
|
// exact terminal usage against the same opaque reservation identity. A nested vehicle joins for
|
|
1601
1471
|
// lineage/settlement only: it reserves no budget axes (group admission governs peer vehicles),
|
|
1602
1472
|
// so the parent's held reservation cannot squeeze the child's grant to zero.
|
|
1603
|
-
if (this.opts.runGroup) {
|
|
1473
|
+
if (!resumeMidRun && this.opts.runGroup) {
|
|
1604
1474
|
const g = this.opts.runGroup;
|
|
1605
1475
|
groupBudgetScope = await GroupBudgetScope.open(g, { sessionId, role: this.opts.agentId, kind: "vehicle" }, this.opts.nestedGroupVehicle ? { limits: {}, requested: {} } : this.groupBudgetRequest());
|
|
1606
1476
|
this.activeGroupBudgetScope = groupBudgetScope;
|
|
1607
1477
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
+
}
|
|
1617
1489
|
}
|
|
1490
|
+
this.currentGoal = goal;
|
|
1491
|
+
if (!resumeMidRun)
|
|
1492
|
+
await this.prefetchMemoryIntoInitialContext(runtime);
|
|
1618
1493
|
// Multimodal upload: seed the user's attachments (images/audio) as a history
|
|
1619
|
-
// message before
|
|
1494
|
+
// message before root start pushes the "[TASK STATE]" anchor. init_task does not
|
|
1620
1495
|
// clear history, so order becomes [attachment user msg, "Proceed…"] — both land
|
|
1621
1496
|
// in the first render. On resume the message is already in the replayed history.
|
|
1622
1497
|
if (!resumeMidRun && attachments?.length) {
|
|
@@ -1625,24 +1500,14 @@ export class RuntimeRunner {
|
|
|
1625
1500
|
message: attachmentsToKernelMessage(attachments),
|
|
1626
1501
|
});
|
|
1627
1502
|
}
|
|
1628
|
-
|
|
1503
|
+
const resumedAction = resumeMidRun ? runtime.resumeAction() : null;
|
|
1629
1504
|
let action = resumeMidRun
|
|
1630
|
-
?
|
|
1631
|
-
: await this.
|
|
1632
|
-
// I4/T5: pre-fetch memory before
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
//
|
|
1636
|
-
// kernel Running and start_run would fault. The resumed action from the last query
|
|
1637
|
-
// supersedes start_run's (same pull contract; it renders the injected hits). Hits land in
|
|
1638
|
-
// `history` as ordinary turns — single-use retrieval content that decays with the
|
|
1639
|
-
// compression pyramid, never pinned into `knowledge`. Skipped on resumes (already in
|
|
1640
|
-
// prior context) and when dreamStore/agentId is absent.
|
|
1641
|
-
if (!resumeMidRun) {
|
|
1642
|
-
const resumed = await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
1643
|
-
if (resumed)
|
|
1644
|
-
action = resumed;
|
|
1645
|
-
}
|
|
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.
|
|
1646
1511
|
// P0-C: the skill loaded and in effect going into the current turn (updated when the model's
|
|
1647
1512
|
// `skill` tool call resolves). Drives the per-turn `activeSkill` metric → dwell measurement.
|
|
1648
1513
|
let activeSkill;
|
|
@@ -1679,15 +1544,6 @@ export class RuntimeRunner {
|
|
|
1679
1544
|
if (runtime.isTerminal())
|
|
1680
1545
|
break;
|
|
1681
1546
|
if (action.kind === "call_provider") {
|
|
1682
|
-
// M5 v2.1: top-level auto-pivot at the safe point. If the agent authored sub-workflow(s) via
|
|
1683
|
-
// `start_workflow`, drive each in THIS kernel now (the kernel is in Reason / `call_provider`,
|
|
1684
|
-
// NOT suspended — driving mid-suspend would clobber the single-slot suspend state), inject the
|
|
1685
|
-
// outcome into context, and re-render. Loop-top placement (vs only after `tool_results`) catches
|
|
1686
|
-
// EVERY path to `call_provider` — including resuming after an approval gate — so a queued spec
|
|
1687
|
-
// is never stranded. Drains the queue; fires once per authored batch.
|
|
1688
|
-
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
1689
|
-
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
1690
|
-
}
|
|
1691
1547
|
const providerEffectId = action.effectId;
|
|
1692
1548
|
const finalToolCalls = [];
|
|
1693
1549
|
let finalText = "";
|
|
@@ -1790,10 +1646,28 @@ export class RuntimeRunner {
|
|
|
1790
1646
|
});
|
|
1791
1647
|
break;
|
|
1792
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
|
+
});
|
|
1793
1667
|
const assistantMessage = {
|
|
1794
1668
|
role: "assistant",
|
|
1795
1669
|
content: finalText,
|
|
1796
|
-
toolCalls:
|
|
1670
|
+
toolCalls: canonicalToolCalls,
|
|
1797
1671
|
tokenCount: turnOutputTokens || turnTokens || undefined,
|
|
1798
1672
|
};
|
|
1799
1673
|
const providerEvent = {
|
|
@@ -1802,9 +1676,36 @@ export class RuntimeRunner {
|
|
|
1802
1676
|
message: messageToKernelMessage(assistantMessage),
|
|
1803
1677
|
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
1804
1678
|
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
1805
|
-
now_ms: Date.now(),
|
|
1806
1679
|
...(turnStopReason ? { stop_reason: turnStopReason } : {}),
|
|
1807
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
|
+
}
|
|
1808
1709
|
action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
|
|
1809
1710
|
const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
|
|
1810
1711
|
await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
|
|
@@ -1856,10 +1757,32 @@ export class RuntimeRunner {
|
|
|
1856
1757
|
else if (action.kind === "persist_memory") {
|
|
1857
1758
|
let error;
|
|
1858
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
|
+
};
|
|
1859
1782
|
try {
|
|
1860
1783
|
if (!agentId)
|
|
1861
1784
|
throw new Error("memory persistence requires RuntimeOptions.agentId");
|
|
1862
|
-
await this.persistMemoryToStore(
|
|
1785
|
+
await this.persistMemoryToStore(record, agentId);
|
|
1863
1786
|
}
|
|
1864
1787
|
catch (cause) {
|
|
1865
1788
|
error = formatToolError(cause);
|
|
@@ -1867,11 +1790,22 @@ export class RuntimeRunner {
|
|
|
1867
1790
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1868
1791
|
kind: "memory_persist_result",
|
|
1869
1792
|
effect_id: action.effectId,
|
|
1793
|
+
record_ref: record.record_id,
|
|
1870
1794
|
...(error ? { error } : {}),
|
|
1871
1795
|
});
|
|
1872
1796
|
}
|
|
1873
1797
|
else if (action.kind === "query_memory") {
|
|
1874
|
-
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
|
+
};
|
|
1875
1809
|
let hits = [];
|
|
1876
1810
|
let error;
|
|
1877
1811
|
const agentId = this.opts.agentId;
|
|
@@ -1892,23 +1826,6 @@ export class RuntimeRunner {
|
|
|
1892
1826
|
if (!error)
|
|
1893
1827
|
await this.logMemoryRetrievalResult(sessionId, hits);
|
|
1894
1828
|
}
|
|
1895
|
-
else if (action.kind === "spool_large_result") {
|
|
1896
|
-
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
1897
|
-
let spoolRef;
|
|
1898
|
-
let error;
|
|
1899
|
-
try {
|
|
1900
|
-
spoolRef = await spool.persistOutput(sessionId, action.callId, action.output);
|
|
1901
|
-
}
|
|
1902
|
-
catch (cause) {
|
|
1903
|
-
error = formatToolError(cause);
|
|
1904
|
-
}
|
|
1905
|
-
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1906
|
-
kind: "large_result_spool_result",
|
|
1907
|
-
effect_id: action.effectId,
|
|
1908
|
-
...(spoolRef ? { spool_ref: spoolRef } : {}),
|
|
1909
|
-
...(error ? { error } : {}),
|
|
1910
|
-
});
|
|
1911
|
-
}
|
|
1912
1829
|
else if (action.kind === "archive_page_out") {
|
|
1913
1830
|
const archiveMeta = this.activePageOutArchive
|
|
1914
1831
|
?? this.pendingPageOutArchives.shift()
|
|
@@ -1920,8 +1837,15 @@ export class RuntimeRunner {
|
|
|
1920
1837
|
let archiveRef;
|
|
1921
1838
|
let error;
|
|
1922
1839
|
try {
|
|
1923
|
-
if (
|
|
1924
|
-
|
|
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 ?? []);
|
|
1925
1849
|
if (ref)
|
|
1926
1850
|
archiveRef = ref;
|
|
1927
1851
|
}
|
|
@@ -1929,7 +1853,7 @@ export class RuntimeRunner {
|
|
|
1929
1853
|
catch (cause) {
|
|
1930
1854
|
error = formatToolError(cause);
|
|
1931
1855
|
}
|
|
1932
|
-
const archived = action.archived;
|
|
1856
|
+
const archived = action.archived ?? [];
|
|
1933
1857
|
const archiveAction = compressionAction(action.action) ?? "auto_compact";
|
|
1934
1858
|
const archiveTier = action.tier;
|
|
1935
1859
|
const compressedSeq = archiveMeta.compressedSeq;
|
|
@@ -1938,6 +1862,7 @@ export class RuntimeRunner {
|
|
|
1938
1862
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1939
1863
|
kind: "page_out_archive_result",
|
|
1940
1864
|
effect_id: action.effectId,
|
|
1865
|
+
...(archiveRef ? { payload_ref: archiveRef } : {}),
|
|
1941
1866
|
...(archiveRef ? { archive_ref: archiveRef } : {}),
|
|
1942
1867
|
...(error ? { error } : {}),
|
|
1943
1868
|
});
|
|
@@ -1951,6 +1876,39 @@ export class RuntimeRunner {
|
|
|
1951
1876
|
}
|
|
1952
1877
|
}
|
|
1953
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
|
+
}
|
|
1954
1912
|
else if (action.kind === "execute_tool") {
|
|
1955
1913
|
const toolEffectId = action.effectId;
|
|
1956
1914
|
const allCalls = action.calls;
|
|
@@ -1964,18 +1922,13 @@ export class RuntimeRunner {
|
|
|
1964
1922
|
knowledgeSource: this.opts.knowledgeSource,
|
|
1965
1923
|
onToolSuspend: this.opts.onToolSuspend,
|
|
1966
1924
|
onPermissionRequest: this.opts.onPermissionRequest,
|
|
1967
|
-
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
1968
1925
|
};
|
|
1969
1926
|
const toolResults = [];
|
|
1970
|
-
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow"
|
|
1971
|
-
&& c.name !== "read_result");
|
|
1927
|
+
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow");
|
|
1972
1928
|
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
1973
|
-
//
|
|
1974
|
-
//
|
|
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.
|
|
1975
1931
|
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
1976
|
-
// O7: `read_result` re-fetches a tool output the kernel evicted from context. Content is
|
|
1977
|
-
// host-resolved from the effect-committed spool, then from the durable session log.
|
|
1978
|
-
const readResultCalls = allCalls.filter(c => c.name === "read_result");
|
|
1979
1932
|
for (const call of planCalls) {
|
|
1980
1933
|
const update = parseUpdatePlanArgs(call.arguments);
|
|
1981
1934
|
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
@@ -1986,39 +1939,8 @@ export class RuntimeRunner {
|
|
|
1986
1939
|
toolResults.push(result);
|
|
1987
1940
|
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
1988
1941
|
}
|
|
1989
|
-
for (const call of readResultCalls) {
|
|
1990
|
-
const out = await this.resolveReadResult(sessionId, call.arguments);
|
|
1991
|
-
toolResults.push({ callId: call.id, output: out.text, isError: out.isError });
|
|
1992
|
-
yield { type: "tool_result", callId: call.id, content: out.text, isError: out.isError };
|
|
1993
|
-
}
|
|
1994
|
-
// R3-1: `submit_workflow_nodes` cannot be applied to this runner's kernel — when this runner
|
|
1995
|
-
// is a workflow node, the workflow lives in the *parent* kernel. Surface the requested nodes
|
|
1996
|
-
// as a stream event; the orchestrator collects them onto the node's result and `runWorkflow`
|
|
1997
|
-
// sends `submit_workflow_nodes` to the parent kernel. (When not a workflow node, the event is
|
|
1998
|
-
// simply unconsumed — a no-op.)
|
|
1999
1942
|
for (const call of submitCalls) {
|
|
2000
|
-
|
|
2001
|
-
// full spec and AUTO-PIVOT once this tool turn resolves (the loop drives it in this kernel and
|
|
2002
|
-
// injects the outcome). A workflow-NODE's `start_workflow` (and every `submit_workflow_nodes`)
|
|
2003
|
-
// instead FLATTENS: the batch is surfaced for the parent `runWorkflow` to append.
|
|
2004
|
-
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
2005
|
-
const spec = parseStartWorkflowSpec(call.arguments);
|
|
2006
|
-
if (spec) {
|
|
2007
|
-
this.pendingAuthoredWorkflows.push(spec);
|
|
2008
|
-
const out = "workflow submitted for governance adjudication";
|
|
2009
|
-
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
2010
|
-
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
2011
|
-
continue;
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
// `start_workflow` wraps the batch as `{ spec: { nodes } }`; `submit_workflow_nodes` is `{ nodes }`.
|
|
2015
|
-
const nodes = call.name === "start_workflow"
|
|
2016
|
-
? parseStartWorkflowArgs(call.arguments)
|
|
2017
|
-
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
2018
|
-
yield { type: "workflow_nodes_submitted", nodes };
|
|
2019
|
-
const result = { callId: call.id, output: "workflow nodes submitted for parent governance adjudication", isError: false };
|
|
2020
|
-
toolResults.push(result);
|
|
2021
|
-
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`);
|
|
2022
1944
|
}
|
|
2023
1945
|
// O5 (PreToolUse-hook analog): give the host a STATEFUL veto over each kernel-approved
|
|
2024
1946
|
// call. A blocked call never executes; its reason reaches the model as a committed
|
|
@@ -2149,11 +2071,11 @@ export class RuntimeRunner {
|
|
|
2149
2071
|
token_count: r.tokenCount,
|
|
2150
2072
|
})),
|
|
2151
2073
|
});
|
|
2152
|
-
//
|
|
2153
|
-
//
|
|
2154
|
-
//
|
|
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.
|
|
2155
2077
|
//
|
|
2156
|
-
// Strict dynamic context control:
|
|
2078
|
+
// Strict dynamic context control: the skill text
|
|
2157
2079
|
// for the rest of the run, unlike a one-off memory/knowledge lookup (fact content, relevant
|
|
2158
2080
|
// for the moment it's used). So its text ALSO goes into the durable `knowledge` slot here
|
|
2159
2081
|
// (in addition to the ordinary tool_result already headed for `history`, where it will decay
|
|
@@ -2169,11 +2091,6 @@ export class RuntimeRunner {
|
|
|
2169
2091
|
const name = JSON.parse(call.arguments || "{}").name;
|
|
2170
2092
|
if (!name)
|
|
2171
2093
|
continue;
|
|
2172
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
2173
|
-
kind: "skill_activated",
|
|
2174
|
-
name,
|
|
2175
|
-
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
2176
|
-
});
|
|
2177
2094
|
// K1: keyed `skill:<name>` — the kernel-side upsert dedupes across runner instances
|
|
2178
2095
|
// (wake re-push of an already-pinned skill upserts instead of duplicating). With a
|
|
2179
2096
|
// lease configured, the Set optimization is skipped: an expired-then-reloaded skill
|
|
@@ -2183,7 +2100,7 @@ export class RuntimeRunner {
|
|
|
2183
2100
|
await this.pushKnowledge({ role: "system", content: res.output, toolCalls: [] }, undefined, { key: `skill:${name}` });
|
|
2184
2101
|
}
|
|
2185
2102
|
}
|
|
2186
|
-
catch { /* malformed skill args — skip
|
|
2103
|
+
catch { /* malformed skill args — skip the knowledge pin */ }
|
|
2187
2104
|
}
|
|
2188
2105
|
const entropyObsStart = this.pendingObservations.length;
|
|
2189
2106
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
@@ -2210,6 +2127,7 @@ export class RuntimeRunner {
|
|
|
2210
2127
|
}
|
|
2211
2128
|
else if (action.kind === "evaluate_milestone") {
|
|
2212
2129
|
const milestoneEffectId = action.effectId;
|
|
2130
|
+
const milestonePhaseId = action.phaseId;
|
|
2213
2131
|
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
2214
2132
|
if (milestonePolicy === "auto_pass") {
|
|
2215
2133
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
@@ -2233,6 +2151,17 @@ export class RuntimeRunner {
|
|
|
2233
2151
|
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart, taskScope);
|
|
2234
2152
|
}
|
|
2235
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
|
+
});
|
|
2236
2165
|
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart, taskScope);
|
|
2237
2166
|
const turnsUsed = Math.max(1, runtime.turn());
|
|
2238
2167
|
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
@@ -2248,9 +2177,31 @@ export class RuntimeRunner {
|
|
|
2248
2177
|
return;
|
|
2249
2178
|
}
|
|
2250
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
|
+
}
|
|
2251
2192
|
else if (action.kind === "done") {
|
|
2252
2193
|
break;
|
|
2253
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
|
+
}
|
|
2254
2205
|
}
|
|
2255
2206
|
}
|
|
2256
2207
|
catch (err) {
|
|
@@ -2298,7 +2249,12 @@ export class RuntimeRunner {
|
|
|
2298
2249
|
totalTokens,
|
|
2299
2250
|
}));
|
|
2300
2251
|
if (groupBudgetScope && !groupBudgetScope.isClosed) {
|
|
2301
|
-
|
|
2252
|
+
await this.settleGroupBudget(groupBudgetScope, {
|
|
2253
|
+
tokens: totalTokens,
|
|
2254
|
+
subagents: runtime.localSubagentsSpawned(),
|
|
2255
|
+
...(this.opts.runSpec?.loopRound ? { rounds: 1 } : {}),
|
|
2256
|
+
});
|
|
2257
|
+
this.activeGroupBudgetScope = undefined;
|
|
2302
2258
|
}
|
|
2303
2259
|
if (this.opts.dreamStore && this.opts.agentId) {
|
|
2304
2260
|
const newMsgs = runtime.drainNewMessages().map(m => ({
|
|
@@ -2374,9 +2330,8 @@ export class RuntimeRunner {
|
|
|
2374
2330
|
runSpec: this.opts.runSpec,
|
|
2375
2331
|
phase,
|
|
2376
2332
|
});
|
|
2377
|
-
//
|
|
2378
|
-
//
|
|
2379
|
-
// 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.
|
|
2380
2335
|
//
|
|
2381
2336
|
// One prefetch = one dedupe horizon: a record hit by several short queries recalls and
|
|
2382
2337
|
// injects once. A renewal prefetch starts a fresh horizon — renewal dropped the earlier
|
|
@@ -2386,16 +2341,57 @@ export class RuntimeRunner {
|
|
|
2386
2341
|
for (const q of queries ?? []) {
|
|
2387
2342
|
if (!q.query.trim())
|
|
2388
2343
|
continue;
|
|
2389
|
-
const { action } = await this.
|
|
2344
|
+
const { action } = await this.prefetchMemoryIntoKnowledge(runtime, q, this.opts.agentId, this.durableSessionId(this.currentSessionId), seenRecordIds, this.pendingObservations);
|
|
2390
2345
|
resumed = action ?? resumed;
|
|
2391
2346
|
}
|
|
2392
|
-
//
|
|
2393
|
-
// 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.
|
|
2394
2348
|
return resumed;
|
|
2395
2349
|
}
|
|
2396
2350
|
catch { /* errs-open — a faulty pre-fetch never breaks the run */ }
|
|
2397
2351
|
return undefined;
|
|
2398
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
|
+
}
|
|
2399
2395
|
async appendObservations(sessionId, runtime, nextArchiveStart, _taskScope) {
|
|
2400
2396
|
const turn = runtime.turn();
|
|
2401
2397
|
const preservedRefs = runtime.preservedRefs();
|
|
@@ -2821,75 +2817,6 @@ function parseUpdatePlanArgs(argsStr) {
|
|
|
2821
2817
|
: parsed.blocked_on,
|
|
2822
2818
|
};
|
|
2823
2819
|
}
|
|
2824
|
-
/** R3-1: parse the `submit_workflow_nodes` tool arguments (`{ nodes: WorkflowNodeSpec[] }`). Node
|
|
2825
|
-
* shapes are trusted structurally here; the kernel validates them (dep range, quarantine, quota) on
|
|
2826
|
-
* append. A malformed payload yields no nodes rather than throwing. */
|
|
2827
|
-
function parseSubmitWorkflowNodesArgs(argsStr) {
|
|
2828
|
-
let parsed = {};
|
|
2829
|
-
try {
|
|
2830
|
-
parsed = JSON.parse(argsStr);
|
|
2831
|
-
}
|
|
2832
|
-
catch {
|
|
2833
|
-
// Ignore parse error → no nodes submitted.
|
|
2834
|
-
}
|
|
2835
|
-
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
2836
|
-
}
|
|
2837
|
-
/** M5 v1: parse the `start_workflow` tool arguments (`{ spec: { nodes: WorkflowNodeSpec[] } }`) into
|
|
2838
|
-
* the spec's node batch — flattened onto the running workflow via the same append path. A malformed
|
|
2839
|
-
* payload yields no nodes rather than throwing. */
|
|
2840
|
-
function parseStartWorkflowArgs(argsStr) {
|
|
2841
|
-
let parsed = {};
|
|
2842
|
-
try {
|
|
2843
|
-
parsed = JSON.parse(argsStr);
|
|
2844
|
-
}
|
|
2845
|
-
catch {
|
|
2846
|
-
// Ignore parse error → no nodes.
|
|
2847
|
-
}
|
|
2848
|
-
const spec = parsed.spec;
|
|
2849
|
-
return Array.isArray(spec?.nodes) ? spec.nodes : [];
|
|
2850
|
-
}
|
|
2851
|
-
/** M5 v2.1: parse the full `WorkflowSpec` from a top-level `start_workflow` call, for auto-pivot drive
|
|
2852
|
-
* (vs `parseStartWorkflowArgs`, which returns only the node batch for the flatten path). Returns
|
|
2853
|
-
* `undefined` on a malformed / empty payload so the caller falls back to the flatten path. */
|
|
2854
|
-
function parseStartWorkflowSpec(argsStr) {
|
|
2855
|
-
try {
|
|
2856
|
-
const parsed = JSON.parse(argsStr);
|
|
2857
|
-
if (Array.isArray(parsed.spec?.nodes) && parsed.spec.nodes.length > 0) {
|
|
2858
|
-
return { nodes: parsed.spec.nodes };
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
catch {
|
|
2862
|
-
// Ignore parse error → undefined (fall back to flatten).
|
|
2863
|
-
}
|
|
2864
|
-
return undefined;
|
|
2865
|
-
}
|
|
2866
|
-
/** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
|
|
2867
|
-
* agent's context, so the agent's next turn continues with the sub-workflow's results in view. */
|
|
2868
|
-
function recoveredOutputs(outcomes) {
|
|
2869
|
-
const outputs = new Map();
|
|
2870
|
-
for (const outcome of outcomes ?? []) {
|
|
2871
|
-
if (!outcome.output)
|
|
2872
|
-
continue;
|
|
2873
|
-
outputs.set(outcome.agentId, outcome.output.content);
|
|
2874
|
-
outputs.set(outcome.agentId.replace(/-i\d+$/, ""), outcome.output.content);
|
|
2875
|
-
}
|
|
2876
|
-
return outputs;
|
|
2877
|
-
}
|
|
2878
|
-
function authoredWorkflowOutcomeNote(outcome) {
|
|
2879
|
-
const counts = new Map();
|
|
2880
|
-
for (const node of outcome.nodeOutcomes)
|
|
2881
|
-
counts.set(node.status, (counts.get(node.status) ?? 0) + 1);
|
|
2882
|
-
const lines = [
|
|
2883
|
-
`[authored workflow result] ${outcome.nodeOutcomes.length} terminal node(s): ` +
|
|
2884
|
-
[...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ") + ".",
|
|
2885
|
-
];
|
|
2886
|
-
for (const node of outcome.nodeOutcomes) {
|
|
2887
|
-
const out = outcome.outputs[node.nodeId] ?? node.output?.content;
|
|
2888
|
-
if (out)
|
|
2889
|
-
lines.push(`- ${node.nodeId} (${node.status}): ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
|
|
2890
|
-
}
|
|
2891
|
-
return lines.join("\n");
|
|
2892
|
-
}
|
|
2893
2820
|
/** Lower a host `RuntimeSignal` to the kernel's snake_case `signal` input event. Shared by the main
|
|
2894
2821
|
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
2895
2822
|
function signalToKernelEvent(delivery) {
|