@deepstrike/wasm 0.2.38 → 0.2.40
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 +14 -13
- package/dist/harness/index.d.ts +127 -61
- package/dist/harness/index.js +212 -83
- package/dist/index.d.ts +9 -6
- package/dist/index.js +4 -2
- package/dist/memory/extraction.d.ts +4 -0
- package/dist/memory/extraction.js +48 -0
- package/dist/memory/in-memory-store.d.ts +19 -9
- package/dist/memory/in-memory-store.js +68 -23
- package/dist/memory/index.d.ts +52 -24
- package/dist/memory/ranking.d.ts +33 -0
- package/dist/memory/ranking.js +77 -0
- package/dist/memory/retention.d.ts +17 -0
- package/dist/memory/retention.js +54 -0
- package/dist/providers/base.d.ts +5 -0
- package/dist/providers/base.js +32 -0
- package/dist/runtime/context-policy.d.ts +35 -0
- package/dist/runtime/context-policy.js +66 -0
- package/dist/runtime/eval.d.ts +2 -0
- package/dist/runtime/execution-plane.d.ts +2 -1
- package/dist/runtime/execution-plane.js +3 -3
- package/dist/runtime/facade.js +2 -1
- package/dist/runtime/index.d.ts +11 -4
- package/dist/runtime/index.js +5 -1
- package/dist/runtime/kernel-event-log.js +46 -13
- package/dist/runtime/kernel-rebuild.d.ts +8 -0
- package/dist/runtime/kernel-rebuild.js +65 -0
- package/dist/runtime/kernel-step.d.ts +139 -7
- package/dist/runtime/kernel-step.js +110 -10
- package/dist/runtime/kernel-transaction-log.d.ts +61 -0
- package/dist/runtime/kernel-transaction-log.js +140 -0
- package/dist/runtime/os-profile.d.ts +9 -10
- package/dist/runtime/os-profile.js +14 -10
- package/dist/runtime/os-snapshot.d.ts +19 -0
- package/dist/runtime/os-snapshot.js +16 -3
- package/dist/runtime/runner.d.ts +73 -41
- package/dist/runtime/runner.js +562 -290
- package/dist/runtime/session-log.d.ts +55 -10
- package/dist/runtime/session-log.js +60 -3
- package/dist/runtime/session-repair.d.ts +11 -7
- package/dist/runtime/session-repair.js +11 -8
- package/dist/runtime/sub-agent-orchestrator.js +1 -1
- package/dist/runtime/types/agent.d.ts +31 -0
- package/dist/runtime/types/agent.js +35 -0
- package/dist/signals/index.d.ts +21 -1
- package/dist/signals/index.js +1 -0
- package/dist/types.d.ts +7 -2
- package/package.json +2 -2
package/dist/runtime/runner.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { extractSessionMemories } from "../memory/extraction.js";
|
|
1
2
|
import { resolvePermissionRequest } from "./execution-plane.js";
|
|
2
3
|
import { governancePolicyToKernelEvent, governanceFilterSchema } from "../governance.js";
|
|
3
4
|
import { getKernel } from "./kernel.js";
|
|
4
5
|
import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-replay.js";
|
|
5
6
|
import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
6
7
|
import { formatToolError } from "../tools/errors.js";
|
|
7
|
-
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent,
|
|
8
|
+
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverWorkflowNodeOutcomes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
|
|
8
9
|
import { kernelAction, kernelApply, kernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
9
|
-
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
|
|
10
|
+
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeOutcomeFromKernel, workflowNodeStatusFromTermination, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
|
|
10
11
|
import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
11
12
|
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
12
13
|
import { resolveReducer } from "./reducers.js";
|
|
@@ -14,9 +15,36 @@ import { loopInstruction, classifyInstruction, judgeGoal, dependencyOutputsNote,
|
|
|
14
15
|
import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
|
|
15
16
|
import { assertNativeProfile } from "./os-profile.js";
|
|
16
17
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
18
|
+
import { contextPolicyV1, normalizeContextPolicyV1, } from "./context-policy.js";
|
|
19
|
+
export function schedulerPolicyToKernel(policy) {
|
|
20
|
+
const allowed = new Set([
|
|
21
|
+
"version", "criticalPathWeight", "fanoutWeight", "ageWeight", "tokenCostWeight",
|
|
22
|
+
]);
|
|
23
|
+
const unknown = Object.keys(policy).filter(key => !allowed.has(key));
|
|
24
|
+
if (unknown.length > 0)
|
|
25
|
+
throw new TypeError(`unknown scheduler policy field(s): ${unknown.join(", ")}`);
|
|
26
|
+
return {
|
|
27
|
+
version: policy.version,
|
|
28
|
+
critical_path_weight: policy.criticalPathWeight,
|
|
29
|
+
fanout_weight: policy.fanoutWeight,
|
|
30
|
+
age_weight: policy.ageWeight,
|
|
31
|
+
token_cost_weight: policy.tokenCostWeight,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function pendingCallIds(action) {
|
|
35
|
+
switch (action.kind) {
|
|
36
|
+
case "call_provider": return [action.effectId];
|
|
37
|
+
case "execute_tool": return action.calls.map(call => call.id);
|
|
38
|
+
case "request_approval": return action.requests.map(request => request.callId);
|
|
39
|
+
case "spawn_workflow": return action.nodes.map(node => String(node.agent_id ?? "")).filter(Boolean);
|
|
40
|
+
case "preempt_sub_agents": return action.agentIds;
|
|
41
|
+
default: return "effectId" in action ? [action.effectId] : [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
17
44
|
export class RuntimeRunner {
|
|
18
45
|
opts;
|
|
19
46
|
interrupted = false;
|
|
47
|
+
cancellationReason;
|
|
20
48
|
/** #2-B-ii: aborts the in-flight provider stream on interrupt/preempt. Recreated per `execute`. */
|
|
21
49
|
abortController = null;
|
|
22
50
|
pendingObservations = [];
|
|
@@ -33,18 +61,28 @@ export class RuntimeRunner {
|
|
|
33
61
|
/** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
|
|
34
62
|
currentGoal = "";
|
|
35
63
|
nextArchiveStart = 0;
|
|
64
|
+
pendingPageOutArchives = [];
|
|
65
|
+
activePageOutArchive;
|
|
36
66
|
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
37
67
|
* at the next safe point (after the tool turn resolves, kernel back in Reason). */
|
|
38
68
|
pendingAuthoredWorkflows = [];
|
|
39
|
-
|
|
69
|
+
workflowContinuation = null;
|
|
40
70
|
constructor(opts) {
|
|
41
71
|
this.opts = opts;
|
|
72
|
+
const schemaAttempts = opts.workflowSchemaValidationAttempts ?? 2;
|
|
73
|
+
if (!Number.isInteger(schemaAttempts) || schemaAttempts < 1 || schemaAttempts > 16) {
|
|
74
|
+
throw new RangeError("workflowSchemaValidationAttempts must be an integer between 1 and 16");
|
|
75
|
+
}
|
|
42
76
|
}
|
|
43
77
|
get hostOptions() { return this.opts; }
|
|
44
|
-
interrupt(
|
|
78
|
+
interrupt(reason = "user") {
|
|
79
|
+
this.interrupted = true;
|
|
80
|
+
this.cancellationReason = reason;
|
|
81
|
+
this.abortController?.abort(reason);
|
|
82
|
+
}
|
|
45
83
|
/** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
|
|
46
|
-
* the next turn boundary, routes through the kernel attention policy, and
|
|
47
|
-
*
|
|
84
|
+
* the next turn boundary, routes through the kernel attention policy, and renders once as a
|
|
85
|
+
* `[SIGNAL] <text>` line in the volatile state turn. `urgency` maps to
|
|
48
86
|
* the kernel disposition ladder: `"normal"` queues (default), `"high"` soft-interrupts, `"critical"`
|
|
49
87
|
* preempts. */
|
|
50
88
|
injectNote(text, urgency = "normal") {
|
|
@@ -60,10 +98,51 @@ export class RuntimeRunner {
|
|
|
60
98
|
async nextInboundSignal() {
|
|
61
99
|
const injected = this.injectedSignals.shift();
|
|
62
100
|
if (injected)
|
|
63
|
-
return
|
|
101
|
+
return {
|
|
102
|
+
signalId: crypto.randomUUID(),
|
|
103
|
+
deliveryId: `injected-${crypto.randomUUID()}`,
|
|
104
|
+
deliveryAttempt: 1,
|
|
105
|
+
signal: injected,
|
|
106
|
+
ack: async () => true,
|
|
107
|
+
nack: async () => true,
|
|
108
|
+
};
|
|
64
109
|
if (!this.opts.signalSource)
|
|
65
110
|
return null;
|
|
66
|
-
|
|
111
|
+
const source = this.opts.signalSource;
|
|
112
|
+
const claim = await source.claimSignal();
|
|
113
|
+
if (!claim)
|
|
114
|
+
return null;
|
|
115
|
+
const receipt = {
|
|
116
|
+
deliveryId: claim.deliveryId,
|
|
117
|
+
leaseToken: claim.leaseToken,
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
signalId: claim.signalId,
|
|
121
|
+
deliveryId: claim.deliveryId,
|
|
122
|
+
deliveryAttempt: claim.deliveryAttempt,
|
|
123
|
+
signal: claim.signal,
|
|
124
|
+
ack: () => source.ackSignal(receipt),
|
|
125
|
+
nack: () => source.nackSignal(receipt),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async consumeInboundSignal(delivery, consume) {
|
|
129
|
+
try {
|
|
130
|
+
const observationStart = this.pendingObservations.length;
|
|
131
|
+
const result = consume(delivery);
|
|
132
|
+
const dispositions = this.pendingObservations.slice(observationStart).filter(observation => observation.kind === "signal_delivery_disposed"
|
|
133
|
+
&& observation.delivery_id === delivery.deliveryId
|
|
134
|
+
&& observation.attempt === delivery.deliveryAttempt);
|
|
135
|
+
if (dispositions.length !== 1) {
|
|
136
|
+
throw new Error("kernel did not return the matching signal delivery disposition");
|
|
137
|
+
}
|
|
138
|
+
if (!await delivery.ack())
|
|
139
|
+
throw new Error("signal lease was lost before acknowledgement");
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
catch (cause) {
|
|
143
|
+
await delivery.nack();
|
|
144
|
+
throw cause;
|
|
145
|
+
}
|
|
67
146
|
}
|
|
68
147
|
async *run(req) {
|
|
69
148
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
@@ -76,9 +155,10 @@ export class RuntimeRunner {
|
|
|
76
155
|
criteria: req.criteria ?? [],
|
|
77
156
|
agent_id: this.opts.agentId,
|
|
78
157
|
system_prompt: this.opts.systemPrompt,
|
|
158
|
+
...(req.attachments?.length ? { attachments: req.attachments } : {}),
|
|
79
159
|
});
|
|
80
160
|
}
|
|
81
|
-
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun);
|
|
161
|
+
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, req.attachments);
|
|
82
162
|
}
|
|
83
163
|
async *wake(sessionId, extensions) {
|
|
84
164
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
@@ -88,7 +168,47 @@ export class RuntimeRunner {
|
|
|
88
168
|
if (!startEntry)
|
|
89
169
|
throw new Error(`No run_started event for session: ${sessionId}`);
|
|
90
170
|
const start = startEntry.event;
|
|
91
|
-
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true);
|
|
171
|
+
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true, start.attachments);
|
|
172
|
+
}
|
|
173
|
+
async writeMemory(memory, sessionId) {
|
|
174
|
+
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
175
|
+
return;
|
|
176
|
+
const kernel = await getKernel();
|
|
177
|
+
const runtime = new kernel.KernelRuntime({ maxTokens: this.opts.maxTokens, maxTurns: this.opts.maxTurns ?? 25 });
|
|
178
|
+
const observations = [];
|
|
179
|
+
const action = kernelMaybeAction(runtime, observations, { kind: "write_memory", memory });
|
|
180
|
+
if (!action) {
|
|
181
|
+
await this.appendMemorySyscallObservations(sessionId, observations);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (action.kind !== "persist_memory")
|
|
185
|
+
throw new Error(`write_memory returned unexpected kernel effect: ${action.kind}`);
|
|
186
|
+
let error;
|
|
187
|
+
try {
|
|
188
|
+
await this.opts.dreamStore.upsert(this.opts.agentId, action.memory);
|
|
189
|
+
}
|
|
190
|
+
catch (cause) {
|
|
191
|
+
error = formatToolError(cause);
|
|
192
|
+
}
|
|
193
|
+
kernelApply(runtime, observations, {
|
|
194
|
+
kind: "memory_persist_result",
|
|
195
|
+
effect_id: action.effectId,
|
|
196
|
+
...(error ? { error } : {}),
|
|
197
|
+
});
|
|
198
|
+
await this.appendMemorySyscallObservations(sessionId, observations);
|
|
199
|
+
if (error)
|
|
200
|
+
throw new Error(error);
|
|
201
|
+
}
|
|
202
|
+
async appendMemorySyscallObservations(sessionId, observations) {
|
|
203
|
+
if (!sessionId)
|
|
204
|
+
return;
|
|
205
|
+
for (const observation of observations) {
|
|
206
|
+
if (!["memory_written", "memory_queried", "memory_validation_failed"].includes(observation.kind))
|
|
207
|
+
continue;
|
|
208
|
+
const event = kernelObservationToSessionEvent(observation, 0);
|
|
209
|
+
if (event)
|
|
210
|
+
await this.opts.sessionLog.append(sessionId, event);
|
|
211
|
+
}
|
|
92
212
|
}
|
|
93
213
|
/** Push content into Slot 2 (system_knowledge) via add_knowledge_message.
|
|
94
214
|
* K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
|
|
@@ -120,26 +240,25 @@ export class RuntimeRunner {
|
|
|
120
240
|
kernelApply(this.activeKernel, this.pendingObservations, { kind: "skill_deactivated", name });
|
|
121
241
|
this.knowledgePushedSkills.delete(name);
|
|
122
242
|
}
|
|
123
|
-
async resolveKernelSuspend(runtime, sessionId) {
|
|
124
|
-
const gated = this.pendingObservations.filter((o) => o.kind === "tool_gated" && typeof o.call_id === "string" && typeof o.tool === "string");
|
|
243
|
+
async resolveKernelSuspend(requests, runtime, sessionId) {
|
|
125
244
|
const approved = [];
|
|
126
245
|
const denied = [];
|
|
127
246
|
const events = [];
|
|
128
247
|
const runCtx = { onPermissionRequest: this.opts.onPermissionRequest };
|
|
129
|
-
for (const
|
|
248
|
+
for (const requestAction of requests) {
|
|
130
249
|
const request = {
|
|
131
250
|
type: "permission_request",
|
|
132
|
-
callId:
|
|
133
|
-
toolName:
|
|
134
|
-
arguments:
|
|
135
|
-
reason:
|
|
251
|
+
callId: requestAction.callId,
|
|
252
|
+
toolName: requestAction.tool,
|
|
253
|
+
arguments: requestAction.arguments,
|
|
254
|
+
reason: requestAction.reason,
|
|
136
255
|
};
|
|
137
256
|
events.push(request);
|
|
138
257
|
const decision = await resolvePermissionRequest(request, runCtx);
|
|
139
258
|
events.push({
|
|
140
259
|
type: "permission_resolved",
|
|
141
|
-
callId:
|
|
142
|
-
toolName:
|
|
260
|
+
callId: requestAction.callId,
|
|
261
|
+
toolName: requestAction.tool,
|
|
143
262
|
approved: decision.approved,
|
|
144
263
|
responder: decision.responder ?? "host",
|
|
145
264
|
...(decision.reason ? { reason: decision.reason } : {}),
|
|
@@ -147,8 +266,8 @@ export class RuntimeRunner {
|
|
|
147
266
|
await this.opts.sessionLog.append(sessionId, {
|
|
148
267
|
kind: "permission_requested",
|
|
149
268
|
turn: runtime.turn(),
|
|
150
|
-
tool:
|
|
151
|
-
arguments:
|
|
269
|
+
tool: requestAction.tool,
|
|
270
|
+
arguments: requestAction.arguments,
|
|
152
271
|
reason: request.reason,
|
|
153
272
|
});
|
|
154
273
|
await this.opts.sessionLog.append(sessionId, {
|
|
@@ -158,21 +277,21 @@ export class RuntimeRunner {
|
|
|
158
277
|
responder: decision.responder ?? "host",
|
|
159
278
|
});
|
|
160
279
|
if (decision.approved) {
|
|
161
|
-
approved.push(
|
|
280
|
+
approved.push(requestAction.callId);
|
|
162
281
|
}
|
|
163
282
|
else {
|
|
164
|
-
denied.push(
|
|
283
|
+
denied.push(requestAction.callId);
|
|
165
284
|
const denyReason = decision.reason ?? "permission denied";
|
|
166
285
|
events.push({
|
|
167
286
|
type: "tool_denied",
|
|
168
|
-
callId:
|
|
169
|
-
toolName:
|
|
287
|
+
callId: requestAction.callId,
|
|
288
|
+
toolName: requestAction.tool,
|
|
170
289
|
reason: denyReason,
|
|
171
290
|
});
|
|
172
291
|
events.push({
|
|
173
292
|
type: "tool_result",
|
|
174
|
-
callId:
|
|
175
|
-
name:
|
|
293
|
+
callId: requestAction.callId,
|
|
294
|
+
name: requestAction.tool,
|
|
176
295
|
content: `permission denied: ${denyReason}`,
|
|
177
296
|
isError: true,
|
|
178
297
|
errorKind: "governance_denied",
|
|
@@ -180,15 +299,15 @@ export class RuntimeRunner {
|
|
|
180
299
|
await this.opts.sessionLog.append(sessionId, {
|
|
181
300
|
kind: "tool_denied",
|
|
182
301
|
turn: runtime.turn(),
|
|
183
|
-
call_id:
|
|
184
|
-
tool_name:
|
|
302
|
+
call_id: requestAction.callId,
|
|
303
|
+
tool_name: requestAction.tool,
|
|
185
304
|
reason: denyReason,
|
|
186
305
|
});
|
|
187
306
|
await this.opts.sessionLog.append(sessionId, {
|
|
188
307
|
kind: "tool_completed",
|
|
189
308
|
turn: runtime.turn(),
|
|
190
309
|
results: [{
|
|
191
|
-
call_id:
|
|
310
|
+
call_id: requestAction.callId,
|
|
192
311
|
output: `permission denied: ${denyReason}`,
|
|
193
312
|
is_error: true,
|
|
194
313
|
error_kind: "governance_denied",
|
|
@@ -200,10 +319,9 @@ export class RuntimeRunner {
|
|
|
200
319
|
}
|
|
201
320
|
/**
|
|
202
321
|
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
203
|
-
* output. Resolution order: (a)
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
322
|
+
* output. Resolution order: (a) the result spool committed by `large_result_spool_result`,
|
|
323
|
+
* (b) a session-log scan for the original `tool_completed` event carrying that `call_id`.
|
|
324
|
+
* Slices the resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
207
325
|
*/
|
|
208
326
|
async resolveReadResult(sessionId, argsJson) {
|
|
209
327
|
let callId = "";
|
|
@@ -220,7 +338,7 @@ export class RuntimeRunner {
|
|
|
220
338
|
catch {
|
|
221
339
|
// malformed arguments — callId stays empty, falls through to "not found" below
|
|
222
340
|
}
|
|
223
|
-
let full
|
|
341
|
+
let full;
|
|
224
342
|
if (full === undefined && this.opts.resultSpool) {
|
|
225
343
|
try {
|
|
226
344
|
full = await this.opts.resultSpool.findByCallId(callId);
|
|
@@ -255,71 +373,13 @@ export class RuntimeRunner {
|
|
|
255
373
|
isError: false,
|
|
256
374
|
};
|
|
257
375
|
}
|
|
258
|
-
async
|
|
259
|
-
if (!this.opts.dreamStore)
|
|
260
|
-
throw new Error("dreamStore not configured");
|
|
261
|
-
const kernel = await getKernel();
|
|
262
|
-
const sessions = await this.opts.dreamStore.loadSessions(agentId);
|
|
263
|
-
const existingMemories = await this.opts.dreamStore.loadMemories(agentId);
|
|
264
|
-
if (!sessions.length)
|
|
265
|
-
return { sessionsProcessed: 0, insightsExtracted: 0, entriesAdded: 0, entriesRemoved: 0 };
|
|
266
|
-
const pipeline = new kernel.IdlePipeline(agentId);
|
|
267
|
-
const action1 = pipeline.feedTrigger(sessions.map(s => ({
|
|
268
|
-
sessionId: s.sessionId, agentId: s.agentId,
|
|
269
|
-
messages: s.messages.map(m => ({
|
|
270
|
-
role: m.role, content: m.content, tokenCount: m.tokenCount,
|
|
271
|
-
toolCalls: (m.toolCalls ?? []).map(tc => ({ id: tc.id, name: tc.name, arguments: tc.arguments })),
|
|
272
|
-
})),
|
|
273
|
-
metadata: JSON.stringify(s.metadata ?? null),
|
|
274
|
-
createdAtMs: s.createdAtMs, updatedAtMs: s.updatedAtMs,
|
|
275
|
-
})), existingMemories.map(e => ({ text: e.text, score: e.score, metadata: JSON.stringify(e.metadata ?? null) })), nowMs);
|
|
276
|
-
if (action1.kind === "noop" || action1.kind === "aborted") {
|
|
277
|
-
return { sessionsProcessed: 0, insightsExtracted: 0, entriesAdded: 0, entriesRemoved: 0 };
|
|
278
|
-
}
|
|
279
|
-
if (action1.kind !== "synthesize_insights")
|
|
280
|
-
throw new Error(`unexpected: ${action1.kind}`);
|
|
281
|
-
let synthesisText = "";
|
|
282
|
-
const dreamProvider = this.opts.dreamProvider ?? this.opts.provider;
|
|
283
|
-
const providerState = dreamProvider.createRunState?.();
|
|
284
|
-
const synthMsgs = (action1.messages ?? []);
|
|
285
|
-
const synthContext = {
|
|
286
|
-
systemText: synthMsgs.filter(m => m.role === "system").map(m => m.content).join("\n\n"),
|
|
287
|
-
turns: synthMsgs.filter(m => m.role !== "system"),
|
|
288
|
-
};
|
|
289
|
-
for await (const evt of dreamProvider.stream(synthContext, [], undefined, providerState)) {
|
|
290
|
-
if (evt.type === "text_delta")
|
|
291
|
-
synthesisText += evt.delta;
|
|
292
|
-
}
|
|
293
|
-
const action2 = pipeline.feedSynthesisResult(synthesisText);
|
|
294
|
-
if (action2.kind !== "commit_memories")
|
|
295
|
-
throw new Error(`unexpected: ${action2.kind}`);
|
|
296
|
-
const cr = action2.curationResult;
|
|
297
|
-
const rr = action2.runResult;
|
|
298
|
-
const dsResult = {
|
|
299
|
-
toAdd: (cr.toAdd ?? []).map((e) => ({
|
|
300
|
-
text: e.text, score: e.score, metadata: tryParseJson(e.metadata),
|
|
301
|
-
})),
|
|
302
|
-
toRemoveIndices: (cr.toRemoveIndices ?? []).map(Number),
|
|
303
|
-
stats: {
|
|
304
|
-
insightsProcessed: cr.stats?.insightsProcessed ?? 0,
|
|
305
|
-
duplicatesRemoved: cr.stats?.duplicatesRemoved ?? 0,
|
|
306
|
-
conflictsResolved: cr.stats?.conflictsResolved ?? 0,
|
|
307
|
-
entriesAdded: cr.stats?.entriesAdded ?? 0,
|
|
308
|
-
},
|
|
309
|
-
};
|
|
310
|
-
await this.opts.dreamStore.commit(agentId, dsResult, existingMemories);
|
|
311
|
-
return {
|
|
312
|
-
sessionsProcessed: rr.sessionsProcessed,
|
|
313
|
-
insightsExtracted: rr.insightsExtracted,
|
|
314
|
-
entriesAdded: cr.stats?.entriesAdded ?? 0,
|
|
315
|
-
entriesRemoved: (cr.toRemoveIndices ?? []).length,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false) {
|
|
376
|
+
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments) {
|
|
319
377
|
this.interrupted = false;
|
|
378
|
+
this.cancellationReason = undefined;
|
|
320
379
|
this.abortController = new AbortController();
|
|
321
380
|
this.pendingObservations = [];
|
|
322
|
-
this.
|
|
381
|
+
this.pendingPageOutArchives = [];
|
|
382
|
+
this.activePageOutArchive = undefined;
|
|
323
383
|
this.currentSessionId = sessionId;
|
|
324
384
|
const kernel = await getKernel();
|
|
325
385
|
const ext = { ...this.opts.extensions, ...(extensions ?? {}) };
|
|
@@ -333,7 +393,7 @@ export class RuntimeRunner {
|
|
|
333
393
|
maxTurns: effectiveMaxTurns,
|
|
334
394
|
// M4/G5: per-node token cap → child run's cumulative token budget (wasm LoopPolicy.maxTotalTokens is f64).
|
|
335
395
|
...(this.opts.maxTotalTokens !== undefined ? { maxTotalTokens: this.opts.maxTotalTokens } : {}),
|
|
336
|
-
timeoutMs: effectiveTimeoutMs
|
|
396
|
+
timeoutMs: effectiveTimeoutMs,
|
|
337
397
|
});
|
|
338
398
|
this.activeKernel = runtime;
|
|
339
399
|
if (this.opts.tokenizer) {
|
|
@@ -410,7 +470,7 @@ export class RuntimeRunner {
|
|
|
410
470
|
if (priorEvents && priorEvents.length > 0) {
|
|
411
471
|
const repaired = repairEventsForRecovery(priorEvents, maxBytes);
|
|
412
472
|
seedProviderReplayFromEvents(this.opts.provider, repaired);
|
|
413
|
-
const replayed = replayMessages(repaired, maxBytes);
|
|
473
|
+
const replayed = await replayMessages(repaired, maxBytes, this.opts.compressionStore);
|
|
414
474
|
kernelApply(runtime, this.pendingObservations, {
|
|
415
475
|
kind: "preload_history",
|
|
416
476
|
messages: replayed.map(messageToKernelMessage),
|
|
@@ -450,6 +510,13 @@ export class RuntimeRunner {
|
|
|
450
510
|
}
|
|
451
511
|
}
|
|
452
512
|
}
|
|
513
|
+
// Multimodal upload: seed attachments before start_run (parity with Node/Python).
|
|
514
|
+
if (!resumeMidRun && attachments?.length) {
|
|
515
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
516
|
+
kind: "add_history_message",
|
|
517
|
+
message: attachmentsToKernelMessage(attachments),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
453
520
|
const sessionStart = Date.now();
|
|
454
521
|
const startPayload = {
|
|
455
522
|
kind: "start_run",
|
|
@@ -490,18 +557,20 @@ export class RuntimeRunner {
|
|
|
490
557
|
while (!runtime.isTerminal()) {
|
|
491
558
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
492
559
|
if (this.interrupted) {
|
|
493
|
-
action = kernelAction(runtime, this.pendingObservations, {
|
|
560
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
561
|
+
kind: "cancel_operation",
|
|
562
|
+
reason: this.cancellationReason ?? "user",
|
|
563
|
+
pending_call_ids: pendingCallIds(action),
|
|
564
|
+
});
|
|
494
565
|
break;
|
|
495
566
|
}
|
|
496
567
|
if (this.opts.signalSource || this.injectedSignals.length > 0) {
|
|
497
|
-
const
|
|
498
|
-
if (
|
|
499
|
-
const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(
|
|
568
|
+
const delivery = await this.nextInboundSignal();
|
|
569
|
+
if (delivery) {
|
|
570
|
+
const sigAction = await this.consumeInboundSignal(delivery, claimed => kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(claimed)));
|
|
500
571
|
if (sigAction)
|
|
501
572
|
action = sigAction;
|
|
502
|
-
//
|
|
503
|
-
if (sig.urgency === "critical")
|
|
504
|
-
this.interrupted = true;
|
|
573
|
+
// Critical attention/preemption is distinct from operation cancellation.
|
|
505
574
|
}
|
|
506
575
|
}
|
|
507
576
|
if (runtime.isTerminal())
|
|
@@ -513,6 +582,7 @@ export class RuntimeRunner {
|
|
|
513
582
|
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
514
583
|
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
515
584
|
}
|
|
585
|
+
const providerEffectId = action.effectId;
|
|
516
586
|
const finalToolCalls = [];
|
|
517
587
|
let finalText = "";
|
|
518
588
|
// I5: governance schema-level pre-filter — see Node runner for full rationale.
|
|
@@ -574,6 +644,7 @@ export class RuntimeRunner {
|
|
|
574
644
|
// interrupt (the post-stream `aborted` check below converts it to a clean
|
|
575
645
|
// timeout/UserAbort), not a crash or a provider error.
|
|
576
646
|
this.interrupted = true;
|
|
647
|
+
this.cancellationReason ??= "user";
|
|
577
648
|
}
|
|
578
649
|
else {
|
|
579
650
|
// Reactive recovery is now a kernel decision. Forward the raw provider error and
|
|
@@ -583,6 +654,7 @@ export class RuntimeRunner {
|
|
|
583
654
|
// duplicated across the four SDK runners.
|
|
584
655
|
action = kernelAction(runtime, this.pendingObservations, {
|
|
585
656
|
kind: "provider_error",
|
|
657
|
+
effect_id: providerEffectId,
|
|
586
658
|
message: formatToolError(err),
|
|
587
659
|
});
|
|
588
660
|
// Withholding (query.ts parity): surface the raw provider error only when the kernel
|
|
@@ -598,7 +670,11 @@ export class RuntimeRunner {
|
|
|
598
670
|
}
|
|
599
671
|
// #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
|
|
600
672
|
if (abortSignal?.aborted) {
|
|
601
|
-
action = kernelAction(runtime, this.pendingObservations, {
|
|
673
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
674
|
+
kind: "cancel_operation",
|
|
675
|
+
reason: this.cancellationReason ?? "user",
|
|
676
|
+
pending_call_ids: [providerEffectId],
|
|
677
|
+
});
|
|
602
678
|
break;
|
|
603
679
|
}
|
|
604
680
|
const assistantMessage = {
|
|
@@ -609,25 +685,14 @@ export class RuntimeRunner {
|
|
|
609
685
|
};
|
|
610
686
|
const providerEvent = {
|
|
611
687
|
kind: "provider_result",
|
|
688
|
+
effect_id: providerEffectId,
|
|
612
689
|
message: messageToKernelMessage(assistantMessage),
|
|
613
690
|
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
614
691
|
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
615
692
|
now_ms: Date.now(),
|
|
616
693
|
...(turnStopReason ? { stop_reason: turnStopReason } : {}),
|
|
617
694
|
};
|
|
618
|
-
|
|
619
|
-
const hasSuspended = this.pendingObservations.some(o => o.kind === "suspended");
|
|
620
|
-
if (!nextAction && hasSuspended) {
|
|
621
|
-
const resolved = await this.resolveKernelSuspend(runtime, sessionId);
|
|
622
|
-
for (const evt of resolved.events)
|
|
623
|
-
yield evt;
|
|
624
|
-
nextAction = kernelAction(runtime, this.pendingObservations, {
|
|
625
|
-
kind: "resume",
|
|
626
|
-
approved_calls: resolved.approved,
|
|
627
|
-
denied_calls: resolved.denied,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
action = nextAction ?? kernelAction(runtime, this.pendingObservations, providerEvent);
|
|
695
|
+
action = kernelAction(runtime, this.pendingObservations, providerEvent);
|
|
631
696
|
const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
|
|
632
697
|
await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
|
|
633
698
|
turn: runtime.turn(),
|
|
@@ -663,11 +728,117 @@ export class RuntimeRunner {
|
|
|
663
728
|
catch { /* malformed skill args — leave activeSkill unchanged */ }
|
|
664
729
|
}
|
|
665
730
|
}
|
|
731
|
+
else if (action.kind === "request_approval") {
|
|
732
|
+
const resolved = await this.resolveKernelSuspend(action.requests, runtime, sessionId);
|
|
733
|
+
for (const evt of resolved.events)
|
|
734
|
+
yield evt;
|
|
735
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
736
|
+
kind: "approval_result",
|
|
737
|
+
effect_id: action.effectId,
|
|
738
|
+
approved_calls: resolved.approved,
|
|
739
|
+
denied_calls: resolved.denied,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
else if (action.kind === "persist_memory") {
|
|
743
|
+
let error;
|
|
744
|
+
try {
|
|
745
|
+
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
746
|
+
throw new Error("WASM memory persistence requires dreamStore and agentId");
|
|
747
|
+
await this.opts.dreamStore.upsert(this.opts.agentId, action.memory);
|
|
748
|
+
}
|
|
749
|
+
catch (cause) {
|
|
750
|
+
error = formatToolError(cause);
|
|
751
|
+
}
|
|
752
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
753
|
+
kind: "memory_persist_result",
|
|
754
|
+
effect_id: action.effectId,
|
|
755
|
+
...(error ? { error } : {}),
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
else if (action.kind === "query_memory") {
|
|
759
|
+
let hits = [];
|
|
760
|
+
let error;
|
|
761
|
+
try {
|
|
762
|
+
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
763
|
+
throw new Error("WASM memory queries require dreamStore and agentId");
|
|
764
|
+
hits = await this.opts.dreamStore.search(this.opts.agentId, {
|
|
765
|
+
...action.query, top_k: action.requestedK,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
catch (cause) {
|
|
769
|
+
error = formatToolError(cause);
|
|
770
|
+
}
|
|
771
|
+
const obsStart = this.pendingObservations.length;
|
|
772
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
773
|
+
kind: "memory_query_result",
|
|
774
|
+
effect_id: action.effectId,
|
|
775
|
+
hits,
|
|
776
|
+
...(error ? { error } : {}),
|
|
777
|
+
});
|
|
778
|
+
// M3/M4: mirror the kernel's journaled recall lifecycle + surface promotion suggestions.
|
|
779
|
+
for (const obs of this.pendingObservations.slice(obsStart)) {
|
|
780
|
+
if (obs.kind === "memory_recalled" && obs.recalls?.length && this.opts.agentId) {
|
|
781
|
+
await this.opts.dreamStore?.recordRecall?.(this.opts.agentId, obs.recalls);
|
|
782
|
+
}
|
|
783
|
+
if (obs.kind === "promotion_suggested" && obs.record_id) {
|
|
784
|
+
this.opts.onPromotionSuggested?.({ recordId: obs.record_id, recallCount: obs.recall_count ?? 0 });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else if (action.kind === "spool_large_result") {
|
|
789
|
+
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
790
|
+
let spoolRef;
|
|
791
|
+
let error;
|
|
792
|
+
try {
|
|
793
|
+
spoolRef = await spool.persistOutput(action.callId, action.output);
|
|
794
|
+
}
|
|
795
|
+
catch (cause) {
|
|
796
|
+
error = formatToolError(cause);
|
|
797
|
+
}
|
|
798
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
799
|
+
kind: "large_result_spool_result",
|
|
800
|
+
effect_id: action.effectId,
|
|
801
|
+
...(spoolRef ? { spool_ref: spoolRef } : {}),
|
|
802
|
+
...(error ? { error } : {}),
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
else if (action.kind === "archive_page_out") {
|
|
806
|
+
const archiveMeta = this.activePageOutArchive
|
|
807
|
+
?? this.pendingPageOutArchives.shift()
|
|
808
|
+
?? { archiveStart: this.nextArchiveStart, compressedSeq: await this.opts.sessionLog.latestSeq(sessionId) };
|
|
809
|
+
this.activePageOutArchive = archiveMeta;
|
|
810
|
+
let archiveRef;
|
|
811
|
+
let error;
|
|
812
|
+
try {
|
|
813
|
+
if (this.opts.compressionStore) {
|
|
814
|
+
archiveRef = await this.opts.compressionStore.write(sessionId, archiveMeta.archiveStart, action.archived);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
catch (cause) {
|
|
818
|
+
error = formatToolError(cause);
|
|
819
|
+
}
|
|
820
|
+
const archived = action.archived;
|
|
821
|
+
const archiveAction = compressionAction(action.action) ?? "auto_compact";
|
|
822
|
+
const archiveTier = action.tier;
|
|
823
|
+
if (!error)
|
|
824
|
+
this.activePageOutArchive = undefined;
|
|
825
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
826
|
+
kind: "page_out_archive_result",
|
|
827
|
+
effect_id: action.effectId,
|
|
828
|
+
...(archiveRef ? { archive_ref: archiveRef } : {}),
|
|
829
|
+
...(error ? { error } : {}),
|
|
830
|
+
});
|
|
831
|
+
if (!error && archiveTier === "semantic" && archived.length > 0) {
|
|
832
|
+
void this.archiveSemanticPageOut(archived, archiveAction);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
666
835
|
else if (action.kind === "execute_tool") {
|
|
836
|
+
const toolEffectId = action.effectId;
|
|
667
837
|
const allCalls = action.calls;
|
|
668
838
|
await this.opts.sessionLog.append(sessionId, { kind: "tool_requested", turn: runtime.turn(), calls: allCalls });
|
|
669
839
|
const runCtx = {
|
|
670
840
|
agentId: this.opts.agentId,
|
|
841
|
+
memoryScope: this.opts.memoryScope,
|
|
671
842
|
skillContentMap: this.opts.skillContentMap,
|
|
672
843
|
dreamStore: this.opts.dreamStore,
|
|
673
844
|
knowledgeSource: this.opts.knowledgeSource,
|
|
@@ -828,12 +999,6 @@ export class RuntimeRunner {
|
|
|
828
999
|
token_count: r.tokenCount,
|
|
829
1000
|
})),
|
|
830
1001
|
});
|
|
831
|
-
for (const call of allCalls) {
|
|
832
|
-
const result = toolResults.find(r => r.callId === call.id);
|
|
833
|
-
if (result) {
|
|
834
|
-
this.pendingSpoolOutputs.set(call.id, { tool: call.name, output: result.output });
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
1002
|
// P1-B B3: a successfully-resolved `skill` call activates that skill for the next turn.
|
|
838
1003
|
//
|
|
839
1004
|
// Strict dynamic context control: a skill is METHOD content — how to do something — reused
|
|
@@ -869,6 +1034,7 @@ export class RuntimeRunner {
|
|
|
869
1034
|
const entropyObsStart = this.pendingObservations.length;
|
|
870
1035
|
action = kernelAction(runtime, this.pendingObservations, {
|
|
871
1036
|
kind: "tool_results",
|
|
1037
|
+
effect_id: toolEffectId,
|
|
872
1038
|
results: toolResults.map(toolResultToKernel),
|
|
873
1039
|
});
|
|
874
1040
|
// Surface the boundary's entropy measurement live (the heartbeat watch source).
|
|
@@ -901,6 +1067,7 @@ export class RuntimeRunner {
|
|
|
901
1067
|
if (milestonePolicy === "auto_pass") {
|
|
902
1068
|
action = kernelAction(runtime, this.pendingObservations, {
|
|
903
1069
|
kind: "milestone_result",
|
|
1070
|
+
effect_id: action.effectId,
|
|
904
1071
|
result: milestoneCheckResultToKernel(milestoneCheckPass(action.phaseId)),
|
|
905
1072
|
});
|
|
906
1073
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
@@ -913,6 +1080,7 @@ export class RuntimeRunner {
|
|
|
913
1080
|
});
|
|
914
1081
|
action = kernelAction(runtime, this.pendingObservations, {
|
|
915
1082
|
kind: "milestone_result",
|
|
1083
|
+
effect_id: action.effectId,
|
|
916
1084
|
result: milestoneCheckResultToKernel(check),
|
|
917
1085
|
});
|
|
918
1086
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
@@ -961,7 +1129,7 @@ export class RuntimeRunner {
|
|
|
961
1129
|
}
|
|
962
1130
|
const result = action.kind === "done" ? action.result : undefined;
|
|
963
1131
|
// I0a: preserve preempt intent when loop exits without clean kernel-done (see Node runner for full rationale).
|
|
964
|
-
const status = result?.termination ??
|
|
1132
|
+
const status = result?.termination ?? "error";
|
|
965
1133
|
const turnsUsed = result ? Math.max(1, result.turnsUsed) : runtime.turn() || 0;
|
|
966
1134
|
const totalTokens = result?.totalTokensUsed ?? 0;
|
|
967
1135
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
@@ -979,14 +1147,20 @@ export class RuntimeRunner {
|
|
|
979
1147
|
}));
|
|
980
1148
|
if (newMsgs.length > 0) {
|
|
981
1149
|
try {
|
|
982
|
-
|
|
983
|
-
sessionId
|
|
1150
|
+
const completedSession = {
|
|
1151
|
+
sessionId,
|
|
984
1152
|
agentId: this.opts.agentId,
|
|
985
1153
|
messages: newMsgs,
|
|
986
1154
|
metadata: null,
|
|
987
1155
|
createdAtMs: sessionStart,
|
|
988
1156
|
updatedAtMs: Date.now(),
|
|
989
|
-
}
|
|
1157
|
+
};
|
|
1158
|
+
await this.opts.dreamStore.saveSession(completedSession);
|
|
1159
|
+
if (this.opts.memoryScope) {
|
|
1160
|
+
const extracted = await extractSessionMemories(this.opts.dreamProvider ?? this.opts.provider, completedSession, this.opts.memoryScope, this.opts.dreamSystemPrompt);
|
|
1161
|
+
for (const memory of extracted)
|
|
1162
|
+
await this.writeMemory(memory, sessionId);
|
|
1163
|
+
}
|
|
990
1164
|
}
|
|
991
1165
|
catch { /* non-fatal */ }
|
|
992
1166
|
}
|
|
@@ -1108,10 +1282,10 @@ export class RuntimeRunner {
|
|
|
1108
1282
|
const schema = node.output_schema;
|
|
1109
1283
|
if (!schema)
|
|
1110
1284
|
return orchestrator.run(mkCtx(baseSpec.goal));
|
|
1111
|
-
const
|
|
1285
|
+
const maxAttempts = this.opts.workflowSchemaValidationAttempts ?? 2;
|
|
1112
1286
|
let last;
|
|
1113
1287
|
let lastErrors = [];
|
|
1114
|
-
for (let attempt = 1; attempt <=
|
|
1288
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1115
1289
|
const goal = attempt === 1
|
|
1116
1290
|
? `${baseSpec.goal}\n\n${schemaInstruction(schema)}`
|
|
1117
1291
|
: `${baseSpec.goal}\n\n${schemaRetryInstruction(schema, lastErrors)}`;
|
|
@@ -1124,7 +1298,7 @@ export class RuntimeRunner {
|
|
|
1124
1298
|
last = result;
|
|
1125
1299
|
lastErrors = v.errors;
|
|
1126
1300
|
}
|
|
1127
|
-
const reason = `output_schema validation failed after ${
|
|
1301
|
+
const reason = `output_schema validation failed after ${maxAttempts} attempts: ${lastErrors.join("; ")}`;
|
|
1128
1302
|
const fallback = last;
|
|
1129
1303
|
return {
|
|
1130
1304
|
...fallback,
|
|
@@ -1169,17 +1343,49 @@ export class RuntimeRunner {
|
|
|
1169
1343
|
*/
|
|
1170
1344
|
applyKernelPolicies(runtime) {
|
|
1171
1345
|
const osProfile = assertNativeProfile(this.opts.osProfile ?? "native");
|
|
1172
|
-
const
|
|
1346
|
+
const signalPolicy = this.opts.signalPolicy ?? osProfile.signalPolicy;
|
|
1173
1347
|
const governancePolicy = this.opts.governancePolicy ?? osProfile.governancePolicy;
|
|
1174
1348
|
// K2: lower governance / attention / scheduler / quota in ONE `configure_run` event (the 0.2.30
|
|
1175
1349
|
// core applies each present field via its granular path). `set_memory_policy` stays separate below.
|
|
1176
1350
|
const { kind: _govKind, ...governance } = governancePolicyToKernelEvent(governancePolicy);
|
|
1177
1351
|
const config = { governance };
|
|
1178
|
-
if (
|
|
1179
|
-
config.
|
|
1352
|
+
if (this.opts.contextPolicy) {
|
|
1353
|
+
config.context_policy = normalizeContextPolicyV1(contextPolicyV1(this.opts.contextPolicy));
|
|
1354
|
+
}
|
|
1355
|
+
config.signal_policy = {
|
|
1356
|
+
version: 1,
|
|
1357
|
+
queue_max: signalPolicy.queueMax,
|
|
1358
|
+
...(signalPolicy.ttlMs !== undefined ? { ttl_ms: signalPolicy.ttlMs } : {}),
|
|
1359
|
+
...(signalPolicy.deadlineEscalation !== undefined
|
|
1360
|
+
? { deadline_escalation: signalPolicy.deadlineEscalation }
|
|
1361
|
+
: {}),
|
|
1362
|
+
};
|
|
1363
|
+
if (this.opts.promptBudget) {
|
|
1364
|
+
config.prompt_budget = {
|
|
1365
|
+
prompt_overhead_tokens: this.opts.promptBudget.promptOverheadTokens,
|
|
1366
|
+
output_reserve_tokens: this.opts.promptBudget.outputReserveTokens,
|
|
1367
|
+
safety_margin_tokens: this.opts.promptBudget.safetyMarginTokens,
|
|
1368
|
+
};
|
|
1180
1369
|
}
|
|
1181
|
-
if (this.opts.
|
|
1182
|
-
config.
|
|
1370
|
+
if (this.opts.schedulerPolicy) {
|
|
1371
|
+
config.scheduler_policy = schedulerPolicyToKernel(this.opts.schedulerPolicy);
|
|
1372
|
+
}
|
|
1373
|
+
if (this.opts.kernelReliability) {
|
|
1374
|
+
const r = this.opts.kernelReliability;
|
|
1375
|
+
config.reliability = {
|
|
1376
|
+
...(r.eventReplayCapacity !== undefined ? { event_replay_capacity: r.eventReplayCapacity } : {}),
|
|
1377
|
+
...(r.completedEffectReplayCapacity !== undefined ? { completed_effect_replay_capacity: r.completedEffectReplayCapacity } : {}),
|
|
1378
|
+
...(r.providerRecoveryAttempts !== undefined ? { provider_recovery_attempts: r.providerRecoveryAttempts } : {}),
|
|
1379
|
+
...(r.outputRecoveryAttempts !== undefined ? { output_recovery_attempts: r.outputRecoveryAttempts } : {}),
|
|
1380
|
+
...(r.hostEffectRetryAttempts !== undefined ? { host_effect_retry_attempts: r.hostEffectRetryAttempts } : {}),
|
|
1381
|
+
...(r.spoolThresholdBytes !== undefined ? { spool_threshold_bytes: r.spoolThresholdBytes } : {}),
|
|
1382
|
+
...(r.spoolPreviewBytes !== undefined ? { spool_preview_bytes: r.spoolPreviewBytes } : {}),
|
|
1383
|
+
...(r.snapshotInputLimit !== undefined ? { snapshot_input_limit: r.snapshotInputLimit } : {}),
|
|
1384
|
+
...(r.maxInputBytes !== undefined ? { max_input_bytes: r.maxInputBytes } : {}),
|
|
1385
|
+
...(r.snapshotJournalBytesLimit !== undefined
|
|
1386
|
+
? { snapshot_journal_bytes_limit: r.snapshotJournalBytesLimit }
|
|
1387
|
+
: {}),
|
|
1388
|
+
};
|
|
1183
1389
|
}
|
|
1184
1390
|
if (this.opts.resourceQuota) {
|
|
1185
1391
|
const q = this.opts.resourceQuota;
|
|
@@ -1239,15 +1445,17 @@ export class RuntimeRunner {
|
|
|
1239
1445
|
*/
|
|
1240
1446
|
async bootstrapWorkflowKernel(sessionId, spec) {
|
|
1241
1447
|
this.interrupted = false;
|
|
1448
|
+
this.cancellationReason = undefined;
|
|
1242
1449
|
this.pendingObservations = [];
|
|
1243
|
-
this.
|
|
1450
|
+
this.pendingPageOutArchives = [];
|
|
1451
|
+
this.activePageOutArchive = undefined;
|
|
1244
1452
|
this.currentSessionId = sessionId;
|
|
1245
1453
|
const kernel = await getKernel();
|
|
1246
1454
|
const runtime = new kernel.KernelRuntime({
|
|
1247
1455
|
maxTokens: this.opts.maxTokens,
|
|
1248
1456
|
maxTurns: this.opts.maxTurns ?? 25,
|
|
1249
1457
|
...(this.opts.maxTotalTokens !== undefined ? { maxTotalTokens: this.opts.maxTotalTokens } : {}),
|
|
1250
|
-
timeoutMs: this.opts.timeoutMs
|
|
1458
|
+
timeoutMs: this.opts.timeoutMs,
|
|
1251
1459
|
});
|
|
1252
1460
|
this.activeKernel = runtime;
|
|
1253
1461
|
const goal = `workflow:${spec.nodes.length} nodes`;
|
|
@@ -1259,9 +1467,10 @@ export class RuntimeRunner {
|
|
|
1259
1467
|
...(this.opts.agentId ? { agent_id: this.opts.agentId } : {}),
|
|
1260
1468
|
})).catch(() => { });
|
|
1261
1469
|
this.applyKernelPolicies(runtime);
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1470
|
+
kernelAction(runtime, this.pendingObservations, {
|
|
1471
|
+
kind: "start_run",
|
|
1472
|
+
task: { goal: `workflow session ${sessionId}`, criteria: [] },
|
|
1473
|
+
});
|
|
1265
1474
|
return runtime;
|
|
1266
1475
|
}
|
|
1267
1476
|
async runWorkflow(spec, opts) {
|
|
@@ -1275,17 +1484,19 @@ export class RuntimeRunner {
|
|
|
1275
1484
|
const parentSessionId = this.currentSessionId;
|
|
1276
1485
|
const runtime = this.activeKernel;
|
|
1277
1486
|
try {
|
|
1278
|
-
const
|
|
1487
|
+
const observationStart = this.pendingObservations.length;
|
|
1488
|
+
const initialAction = kernelMaybeAction(runtime, this.pendingObservations, {
|
|
1279
1489
|
kind: "load_workflow",
|
|
1280
1490
|
spec: workflowSpecToKernel(spec),
|
|
1281
1491
|
parent_session_id: parentSessionId,
|
|
1282
|
-
//
|
|
1283
|
-
...(opts?.
|
|
1284
|
-
// W-1: signal-carrying completion records (classify branch / loop stop replay).
|
|
1285
|
-
...(opts?.resumedResults?.length
|
|
1492
|
+
// Exact typed terminal outcomes plus control-flow signals recovered from the journal.
|
|
1493
|
+
...(opts?.resumedOutcomes?.length
|
|
1286
1494
|
? {
|
|
1287
|
-
|
|
1495
|
+
resumed_outcomes: opts.resumedOutcomes.map(r => ({
|
|
1288
1496
|
agent_id: r.agentId,
|
|
1497
|
+
status: r.status,
|
|
1498
|
+
termination: r.termination,
|
|
1499
|
+
...(r.output ? { output: messageToKernelMessage(r.output) } : {}),
|
|
1289
1500
|
...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
|
|
1290
1501
|
...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
|
|
1291
1502
|
...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
|
|
@@ -1296,7 +1507,8 @@ export class RuntimeRunner {
|
|
|
1296
1507
|
...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
1297
1508
|
...(opts?.resumedSubmissionBases && opts.resumedSubmissionBases.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
|
|
1298
1509
|
});
|
|
1299
|
-
|
|
1510
|
+
const observations = this.pendingObservations.slice(observationStart);
|
|
1511
|
+
return await this.driveWorkflow(initialAction, observations, parentSessionId, runtime, recoveredOutputs(opts?.resumedOutcomes));
|
|
1300
1512
|
}
|
|
1301
1513
|
finally {
|
|
1302
1514
|
if (bootstrapped) {
|
|
@@ -1318,7 +1530,9 @@ export class RuntimeRunner {
|
|
|
1318
1530
|
}
|
|
1319
1531
|
const parentSessionId = this.currentSessionId;
|
|
1320
1532
|
const runtime = this.activeKernel;
|
|
1321
|
-
const
|
|
1533
|
+
const observationStart = this.pendingObservations.length;
|
|
1534
|
+
const initialAction = kernelMaybeAction(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
1535
|
+
const observations = this.pendingObservations.slice(observationStart);
|
|
1322
1536
|
// W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
|
|
1323
1537
|
// announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
|
|
1324
1538
|
// had this spec, unlike the `runWorkflow` path.
|
|
@@ -1331,7 +1545,7 @@ export class RuntimeRunner {
|
|
|
1331
1545
|
submitterAgentId: opts?.submitterAgentId,
|
|
1332
1546
|
}));
|
|
1333
1547
|
}
|
|
1334
|
-
return this.driveWorkflow(observations, parentSessionId, runtime);
|
|
1548
|
+
return this.driveWorkflow(initialAction, observations, parentSessionId, runtime);
|
|
1335
1549
|
}
|
|
1336
1550
|
/**
|
|
1337
1551
|
* M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`, at the safe
|
|
@@ -1343,14 +1557,15 @@ export class RuntimeRunner {
|
|
|
1343
1557
|
async driveAuthoredWorkflows(runtime, action) {
|
|
1344
1558
|
const specs = this.pendingAuthoredWorkflows;
|
|
1345
1559
|
this.pendingAuthoredWorkflows = [];
|
|
1560
|
+
this.workflowContinuation = null;
|
|
1346
1561
|
for (const spec of specs) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1562
|
+
await this.bootstrapWorkflow(spec);
|
|
1563
|
+
}
|
|
1564
|
+
const continuation = this.workflowContinuation;
|
|
1565
|
+
if (!continuation) {
|
|
1566
|
+
throw new Error("authored workflow completed without a provider continuation");
|
|
1352
1567
|
}
|
|
1353
|
-
return
|
|
1568
|
+
return continuation;
|
|
1354
1569
|
}
|
|
1355
1570
|
/**
|
|
1356
1571
|
* #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
|
|
@@ -1360,24 +1575,39 @@ export class RuntimeRunner {
|
|
|
1360
1575
|
*/
|
|
1361
1576
|
async monitorWorkflowPreemption(runtime, controllers, batchState) {
|
|
1362
1577
|
const source = this.opts.signalSource;
|
|
1363
|
-
if (!source)
|
|
1578
|
+
if (!source && this.injectedSignals.length === 0)
|
|
1364
1579
|
return null;
|
|
1365
1580
|
while (!batchState.settled) {
|
|
1366
1581
|
// O2: injected notes participate in the monitor too (drain order matches nextInboundSignal).
|
|
1367
|
-
const
|
|
1582
|
+
const delivery = await this.nextInboundSignal();
|
|
1368
1583
|
if (batchState.settled)
|
|
1369
1584
|
break;
|
|
1370
|
-
if (!
|
|
1585
|
+
if (!delivery) {
|
|
1371
1586
|
await new Promise(resolve => setTimeout(resolve, 5));
|
|
1372
1587
|
continue;
|
|
1373
1588
|
}
|
|
1374
|
-
const
|
|
1589
|
+
const observationStart = this.pendingObservations.length;
|
|
1590
|
+
const signalAction = await this.consumeInboundSignal(delivery, claimed => kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(claimed)));
|
|
1591
|
+
if (signalAction) {
|
|
1592
|
+
if (signalAction.kind !== "preempt_sub_agents") {
|
|
1593
|
+
throw new Error(`workflow signal returned unexpected effect: ${signalAction.kind}`);
|
|
1594
|
+
}
|
|
1595
|
+
for (const id of signalAction.agentIds)
|
|
1596
|
+
controllers.get(id)?.abort();
|
|
1597
|
+
const continuation = kernelMaybeAction(runtime, this.pendingObservations, {
|
|
1598
|
+
kind: "preempt_result", effect_id: signalAction.effectId,
|
|
1599
|
+
});
|
|
1600
|
+
if (continuation && continuation.kind !== "call_provider" && continuation.kind !== "done") {
|
|
1601
|
+
throw new Error(`workflow preemption returned unexpected effect: ${continuation.kind}`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const obs = this.pendingObservations.slice(observationStart);
|
|
1375
1605
|
const preempted = obs.find(o => o.kind === "agent_preempted");
|
|
1376
1606
|
if (preempted) {
|
|
1377
1607
|
for (const id of preempted.agent_ids ?? [])
|
|
1378
1608
|
controllers.get(id)?.abort();
|
|
1379
1609
|
const wc = obs.find(o => o.kind === "workflow_completed");
|
|
1380
|
-
return
|
|
1610
|
+
return (wc?.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel);
|
|
1381
1611
|
}
|
|
1382
1612
|
}
|
|
1383
1613
|
return null;
|
|
@@ -1387,25 +1617,43 @@ export class RuntimeRunner {
|
|
|
1387
1617
|
* `submit_workflow`): run each kernel-emitted batch in parallel, feed completions back (appending any
|
|
1388
1618
|
* agent-submitted nodes first), and loop until the kernel reports the workflow complete.
|
|
1389
1619
|
*/
|
|
1390
|
-
async driveWorkflow(initial, parentSessionId, runtime, seedOutputs) {
|
|
1620
|
+
async driveWorkflow(initialAction, initial, parentSessionId, runtime, seedOutputs) {
|
|
1391
1621
|
const observations = initial;
|
|
1392
1622
|
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
1393
|
-
const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.nodes ?? [];
|
|
1394
|
-
// G4: the batch observation carries the workflow's remaining budget; track the latest.
|
|
1395
|
-
const collectBudget = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.budget;
|
|
1396
1623
|
const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
|
|
1624
|
+
const acceptSpawn = (spawn) => {
|
|
1625
|
+
const observationStart = this.pendingObservations.length;
|
|
1626
|
+
const continuation = kernelMaybeAction(runtime, this.pendingObservations, {
|
|
1627
|
+
kind: "workflow_spawn_result",
|
|
1628
|
+
effect_id: spawn.effectId,
|
|
1629
|
+
started_agent_ids: spawn.nodes.map(node => String(node.agent_id ?? "")),
|
|
1630
|
+
failures: [],
|
|
1631
|
+
});
|
|
1632
|
+
if (continuation)
|
|
1633
|
+
throw new Error(`workflow spawn acknowledgement returned unexpected effect: ${continuation.kind}`);
|
|
1634
|
+
return this.pendingObservations.slice(observationStart);
|
|
1635
|
+
};
|
|
1397
1636
|
let done = findDone(observations);
|
|
1398
|
-
if (done)
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1637
|
+
if (done) {
|
|
1638
|
+
if (initialAction?.kind === "call_provider")
|
|
1639
|
+
this.workflowContinuation = initialAction;
|
|
1640
|
+
return { nodeOutcomes: (done.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel), outputs: {} };
|
|
1641
|
+
}
|
|
1642
|
+
if (!initialAction)
|
|
1643
|
+
return { nodeOutcomes: [], outputs: {} };
|
|
1644
|
+
if (initialAction.kind !== "spawn_workflow") {
|
|
1645
|
+
throw new Error(`workflow load returned unexpected kernel effect: ${initialAction.kind}`);
|
|
1646
|
+
}
|
|
1647
|
+
let nodes = initialAction.nodes;
|
|
1648
|
+
let budget = initialAction.budget;
|
|
1649
|
+
acceptSpawn(initialAction);
|
|
1402
1650
|
// G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
|
|
1403
1651
|
// W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
|
|
1404
1652
|
// still see their (pre-crash) dependencies' outputs.
|
|
1405
1653
|
const outputs = new Map(seedOutputs ?? []);
|
|
1406
1654
|
for (;;) {
|
|
1407
1655
|
if (nodes.length === 0)
|
|
1408
|
-
return {
|
|
1656
|
+
return { nodeOutcomes: [], outputs: Object.fromEntries(outputs) };
|
|
1409
1657
|
const roundBudget = budget;
|
|
1410
1658
|
// #2-B-ii: per-node abort controllers + a concurrent preemption monitor (see node runner).
|
|
1411
1659
|
const controllers = new Map(nodes.map(n => [n.agent_id, new AbortController()]));
|
|
@@ -1415,7 +1663,7 @@ export class RuntimeRunner {
|
|
|
1415
1663
|
batchState.settled = true;
|
|
1416
1664
|
const preempted = await monitor;
|
|
1417
1665
|
if (preempted)
|
|
1418
|
-
return {
|
|
1666
|
+
return { nodeOutcomes: preempted, outputs: Object.fromEntries(outputs) };
|
|
1419
1667
|
// Accumulate next-batch nodes across feeds (per-node unblock can spawn dependents per feed).
|
|
1420
1668
|
const nextNodes = [];
|
|
1421
1669
|
done = undefined;
|
|
@@ -1436,26 +1684,47 @@ export class RuntimeRunner {
|
|
|
1436
1684
|
// G1: stamp the submitting node's agent id so the kernel coerces a quarantined submitter's
|
|
1437
1685
|
// nodes to quarantined (no topological privilege escalation).
|
|
1438
1686
|
const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1687
|
+
const observationStart = this.pendingObservations.length;
|
|
1688
|
+
const submitAction = kernelMaybeAction(runtime, this.pendingObservations, submitEvent);
|
|
1689
|
+
const subObs = this.pendingObservations.slice(observationStart);
|
|
1690
|
+
if (submitAction?.kind === "spawn_workflow") {
|
|
1691
|
+
nextNodes.push(...submitAction.nodes);
|
|
1692
|
+
budget = submitAction.budget ?? budget;
|
|
1693
|
+
acceptSpawn(submitAction);
|
|
1694
|
+
}
|
|
1695
|
+
else if (submitAction) {
|
|
1696
|
+
throw new Error(`workflow node submission returned unexpected effect: ${submitAction.kind}`);
|
|
1697
|
+
}
|
|
1442
1698
|
// R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
|
|
1443
1699
|
// so resume can re-apply the batch at the exact original graph position. W-N3: also the
|
|
1444
1700
|
// submitter, so resume drops batches whose submitter re-runs (it will re-submit).
|
|
1445
1701
|
const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1702
|
+
if (submitted) {
|
|
1703
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1704
|
+
turn: runtime.turn(),
|
|
1705
|
+
nodes: submitEvent.nodes ?? [],
|
|
1706
|
+
baseIndex: submitted.base,
|
|
1707
|
+
submitterAgentId: result.agentId,
|
|
1708
|
+
}));
|
|
1709
|
+
}
|
|
1452
1710
|
}
|
|
1453
|
-
const
|
|
1711
|
+
const observationStart = this.pendingObservations.length;
|
|
1712
|
+
const completionAction = kernelMaybeAction(runtime, this.pendingObservations, {
|
|
1454
1713
|
kind: "sub_agent_completed",
|
|
1455
1714
|
result: subAgentResultToKernel(result),
|
|
1456
1715
|
});
|
|
1457
|
-
|
|
1458
|
-
|
|
1716
|
+
let obs = this.pendingObservations.slice(observationStart);
|
|
1717
|
+
if (completionAction?.kind === "spawn_workflow") {
|
|
1718
|
+
nextNodes.push(...completionAction.nodes);
|
|
1719
|
+
budget = completionAction.budget ?? budget;
|
|
1720
|
+
obs = [...obs, ...acceptSpawn(completionAction)];
|
|
1721
|
+
}
|
|
1722
|
+
else if (completionAction?.kind === "call_provider") {
|
|
1723
|
+
this.workflowContinuation = completionAction;
|
|
1724
|
+
}
|
|
1725
|
+
else if (completionAction) {
|
|
1726
|
+
throw new Error(`workflow completion returned unexpected effect: ${completionAction.kind}`);
|
|
1727
|
+
}
|
|
1459
1728
|
const d = findDone(obs);
|
|
1460
1729
|
if (d)
|
|
1461
1730
|
done = d;
|
|
@@ -1465,15 +1734,19 @@ export class RuntimeRunner {
|
|
|
1465
1734
|
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
|
|
1466
1735
|
turn: runtime.turn(),
|
|
1467
1736
|
agentId: result.agentId,
|
|
1737
|
+
status: workflowNodeStatusFromTermination(result.result.termination),
|
|
1468
1738
|
termination: result.result.termination,
|
|
1469
1739
|
classifyBranch: result.result.classifyBranch,
|
|
1470
1740
|
tournamentWinner: result.result.tournamentWinner,
|
|
1471
1741
|
loopContinue: result.result.loopContinue,
|
|
1472
|
-
...(result.result.
|
|
1742
|
+
...(result.result.finalMessage ? { output: result.result.finalMessage } : {}),
|
|
1473
1743
|
}));
|
|
1474
1744
|
}
|
|
1475
1745
|
if (done && nextNodes.length === 0) {
|
|
1476
|
-
return {
|
|
1746
|
+
return {
|
|
1747
|
+
nodeOutcomes: (done.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel),
|
|
1748
|
+
outputs: Object.fromEntries(outputs),
|
|
1749
|
+
};
|
|
1477
1750
|
}
|
|
1478
1751
|
nodes = nextNodes;
|
|
1479
1752
|
}
|
|
@@ -1491,32 +1764,22 @@ export class RuntimeRunner {
|
|
|
1491
1764
|
throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
|
|
1492
1765
|
}
|
|
1493
1766
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
1494
|
-
const
|
|
1495
|
-
const completedIds = new Set(
|
|
1767
|
+
const resumedOutcomes = recoverWorkflowNodeOutcomes(events);
|
|
1768
|
+
const completedIds = new Set(resumedOutcomes.map(r => r.agentId));
|
|
1496
1769
|
const recovered = recoverSubmittedWorkflowNodes(events);
|
|
1497
1770
|
// W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
|
|
1498
1771
|
// re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
|
|
1499
|
-
//
|
|
1500
|
-
// order-only log keeps every batch, since dropping would shift all later indices.
|
|
1772
|
+
// Exact bases keep later graph indices stable while dropped slots remain inert placeholders.
|
|
1501
1773
|
let { submissions, bases } = recovered;
|
|
1502
|
-
if (
|
|
1774
|
+
if (submissions.length > 0) {
|
|
1503
1775
|
const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
|
|
1504
1776
|
submissions = submissions.filter((_, i) => keep[i]);
|
|
1505
1777
|
bases = bases.filter((_, i) => keep[i]);
|
|
1506
1778
|
}
|
|
1507
|
-
const resumedOutputs = new Map(resumedResults.filter(r => r.output).map(r => [r.agentId, r.output]));
|
|
1508
|
-
// Alias loop iterations onto their stable node id (last iteration wins) — dependents consume
|
|
1509
|
-
// `wf-node{N}`, not `wf-node{N}-i{k}`.
|
|
1510
|
-
for (const r of resumedResults) {
|
|
1511
|
-
const stableId = r.agentId.replace(/-i\d+$/, "");
|
|
1512
|
-
if (stableId !== r.agentId && r.output)
|
|
1513
|
-
resumedOutputs.set(stableId, r.output);
|
|
1514
|
-
}
|
|
1515
1779
|
return this.runWorkflow(spec, {
|
|
1516
|
-
|
|
1780
|
+
resumedOutcomes,
|
|
1517
1781
|
resumedSubmissions: submissions,
|
|
1518
1782
|
resumedSubmissionBases: bases,
|
|
1519
|
-
resumedOutputs,
|
|
1520
1783
|
sessionId,
|
|
1521
1784
|
});
|
|
1522
1785
|
}
|
|
@@ -1524,56 +1787,24 @@ export class RuntimeRunner {
|
|
|
1524
1787
|
const turn = runtime.turn();
|
|
1525
1788
|
const preservedRefs = runtime.preservedRefs();
|
|
1526
1789
|
const observations = this.pendingObservations.splice(0);
|
|
1527
|
-
for (
|
|
1790
|
+
for (const obs of observations) {
|
|
1528
1791
|
if (obs.kind === "page_in_requested")
|
|
1529
1792
|
continue;
|
|
1530
|
-
let spoolRef;
|
|
1531
|
-
if (obs.kind === "large_result_spooled") {
|
|
1532
|
-
const pending = this.pendingSpoolOutputs.get(obs.call_id ?? "");
|
|
1533
|
-
if (pending) {
|
|
1534
|
-
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
1535
|
-
try {
|
|
1536
|
-
spoolRef = await spool.persistOutput(obs.call_id ?? "", pending.output);
|
|
1537
|
-
}
|
|
1538
|
-
catch {
|
|
1539
|
-
// non-fatal
|
|
1540
|
-
}
|
|
1541
|
-
if (!obs.tool && pending.tool) {
|
|
1542
|
-
obs = { ...obs, tool: pending.tool };
|
|
1543
|
-
}
|
|
1544
|
-
this.pendingSpoolOutputs.delete(obs.call_id ?? "");
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
1793
|
const latest = obs.kind === "compressed" ? await this.opts.sessionLog.latestSeq(sessionId) : undefined;
|
|
1548
1794
|
const event = kernelObservationToSessionEvent(obs, turn, {
|
|
1549
1795
|
nextArchiveStart,
|
|
1550
1796
|
latestSeq: latest,
|
|
1551
1797
|
preservedRefs,
|
|
1552
|
-
spoolRef,
|
|
1553
1798
|
compressionAction,
|
|
1554
1799
|
});
|
|
1555
1800
|
if (!event)
|
|
1556
1801
|
continue;
|
|
1557
1802
|
const compressedSeq = await this.opts.sessionLog.append(sessionId, event);
|
|
1558
1803
|
if (event.kind === "compressed") {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
// semantic-archive branch) is DERIVED from Compressed.tier_hint, preserving the
|
|
1562
|
-
// session-log format and OsSnapshot page_out_count.
|
|
1563
|
-
const archived = obs.archived;
|
|
1564
|
-
if (obs.tier_hint && Array.isArray(archived) && archived.length > 0) {
|
|
1565
|
-
await this.opts.sessionLog.append(sessionId, {
|
|
1566
|
-
kind: "page_out",
|
|
1567
|
-
turn: obs.turn ?? turn,
|
|
1568
|
-
action: compressionAction(obs.action),
|
|
1569
|
-
summary: obs.summary,
|
|
1570
|
-
tier_hint: obs.tier_hint ?? "durable",
|
|
1571
|
-
message_count: archived.length,
|
|
1572
|
-
});
|
|
1573
|
-
if (obs.tier_hint === "semantic") {
|
|
1574
|
-
void this.archiveSemanticPageOut(archived, compressionAction(obs.action));
|
|
1575
|
-
}
|
|
1804
|
+
if ((obs.archived_count ?? 0) > 0) {
|
|
1805
|
+
this.pendingPageOutArchives.push({ archiveStart: nextArchiveStart, compressedSeq });
|
|
1576
1806
|
}
|
|
1807
|
+
nextArchiveStart = compressedSeq + 1;
|
|
1577
1808
|
}
|
|
1578
1809
|
// K4: a sprint renewal dropped the old history — including any earlier memory hits — so
|
|
1579
1810
|
// re-run the preQueryMemory prefetch for the new sprint (live observations only).
|
|
@@ -1589,21 +1820,21 @@ export class RuntimeRunner {
|
|
|
1589
1820
|
* re-fired after each sprint renewal (renewal drops the old history INCLUDING earlier memory
|
|
1590
1821
|
* hits). Errs-open throughout. */
|
|
1591
1822
|
async prefetchMemoryIntoHistory(runtime, phase) {
|
|
1592
|
-
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
1823
|
+
if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
|
|
1593
1824
|
return;
|
|
1594
1825
|
// P10: recall is default-on (CC session-start recall) — with no hook configured,
|
|
1595
1826
|
// the goal itself is the query. preQueryMemory stays as the targeting override.
|
|
1596
1827
|
const preQuery = this.opts.preQueryMemory
|
|
1597
|
-
?? ((ctx) => [ctx.goal]);
|
|
1828
|
+
?? ((ctx) => [{ scope: this.opts.memoryScope, query: ctx.goal, top_k: 5, kinds: [] }]);
|
|
1598
1829
|
try {
|
|
1599
1830
|
const queries = await preQuery({ goal: this.currentGoal, phase });
|
|
1600
1831
|
const lines = [];
|
|
1601
1832
|
for (const q of queries ?? []) {
|
|
1602
|
-
if (
|
|
1833
|
+
if (!q.query.trim())
|
|
1603
1834
|
continue;
|
|
1604
|
-
const hits = await this.opts.dreamStore.search(this.opts.agentId, q
|
|
1835
|
+
const hits = await this.opts.dreamStore.search(this.opts.agentId, q);
|
|
1605
1836
|
for (const hit of hits) {
|
|
1606
|
-
lines.push(`[memory score=${hit.score.toFixed(3)}] ${hit.
|
|
1837
|
+
lines.push(`[memory record_id=${hit.record.record_id} trust=${hit.record.provenance.trust} score=${hit.score.toFixed(3)}] ${hit.record.content}`);
|
|
1607
1838
|
}
|
|
1608
1839
|
}
|
|
1609
1840
|
if (lines.length > 0) {
|
|
@@ -1616,46 +1847,27 @@ export class RuntimeRunner {
|
|
|
1616
1847
|
catch { /* errs-open */ }
|
|
1617
1848
|
}
|
|
1618
1849
|
async archiveSemanticPageOut(archived, action) {
|
|
1619
|
-
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
1850
|
+
if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
|
|
1620
1851
|
return;
|
|
1621
1852
|
try {
|
|
1622
1853
|
const summary = this.opts.dreamSummarizer
|
|
1623
1854
|
? await this.opts.dreamSummarizer.summarize(archived, { action })
|
|
1624
1855
|
: await summarizeForLongTermMemory(this.opts.dreamProvider ?? this.opts.provider, archived, this.opts.dreamSystemPrompt);
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
insightsProcessed: 1,
|
|
1635
|
-
duplicatesRemoved: 0,
|
|
1636
|
-
conflictsResolved: 0,
|
|
1637
|
-
entriesAdded: 1,
|
|
1638
|
-
},
|
|
1639
|
-
}, existing);
|
|
1856
|
+
const now = Date.now();
|
|
1857
|
+
const name = `page-out-${now}`;
|
|
1858
|
+
await this.writeMemory({
|
|
1859
|
+
record_id: `${this.opts.memoryScope.tenant_id}:${this.opts.memoryScope.namespace}:project:${name}`,
|
|
1860
|
+
scope: this.opts.memoryScope, name, kind: "project", content: summary,
|
|
1861
|
+
description: `auto summary of ${action ?? "compaction"} archive`,
|
|
1862
|
+
provenance: { author: "extraction", trust: "untrusted", evidence_refs: [] },
|
|
1863
|
+
created_at: now, updated_at: now, recall_count: 0, confidence: 0.6, links: [], pinned: false,
|
|
1864
|
+
}, this.currentSessionId ?? undefined);
|
|
1640
1865
|
}
|
|
1641
1866
|
catch {
|
|
1642
1867
|
// non-fatal
|
|
1643
1868
|
}
|
|
1644
1869
|
}
|
|
1645
1870
|
}
|
|
1646
|
-
/** Word-set jaccard similarity — the curator's dedup rule at the write funnel. */
|
|
1647
|
-
function jaccardSimilarity(a, b) {
|
|
1648
|
-
const sa = new Set(a.split(/\s+/).filter(Boolean));
|
|
1649
|
-
const sb = new Set(b.split(/\s+/).filter(Boolean));
|
|
1650
|
-
if (sa.size === 0 && sb.size === 0)
|
|
1651
|
-
return 1;
|
|
1652
|
-
let inter = 0;
|
|
1653
|
-
for (const w of sa)
|
|
1654
|
-
if (sb.has(w))
|
|
1655
|
-
inter++;
|
|
1656
|
-
const union = sa.size + sb.size - inter;
|
|
1657
|
-
return union === 0 ? 0 : inter / union;
|
|
1658
|
-
}
|
|
1659
1871
|
async function summarizeForLongTermMemory(provider, archived, systemPrompt) {
|
|
1660
1872
|
const transcript = archived
|
|
1661
1873
|
.map(m => `${m.role}: ${m.content}`)
|
|
@@ -1687,7 +1899,7 @@ function compressionAction(action) {
|
|
|
1687
1899
|
}
|
|
1688
1900
|
return undefined;
|
|
1689
1901
|
}
|
|
1690
|
-
function replayMessages(events, maxBytes) {
|
|
1902
|
+
async function replayMessages(events, maxBytes, archiveStore) {
|
|
1691
1903
|
// Build upgraded-summary index: compressed_seq -> upgraded summary
|
|
1692
1904
|
const upgradedSummaries = new Map();
|
|
1693
1905
|
for (const { event: e } of events) {
|
|
@@ -1695,19 +1907,29 @@ function replayMessages(events, maxBytes) {
|
|
|
1695
1907
|
upgradedSummaries.set(e.compressed_seq, e.summary);
|
|
1696
1908
|
}
|
|
1697
1909
|
const messages = [];
|
|
1910
|
+
const archivedTurns = new Set(events.flatMap(({ event }) => event.kind === "page_out" && event.archive_ref && archiveStore?.read ? [event.turn] : []));
|
|
1698
1911
|
for (const { seq, event: e } of events) {
|
|
1699
1912
|
if (e.kind === "run_started") {
|
|
1700
1913
|
const userText = e.criteria.length
|
|
1701
1914
|
? `${e.goal}\n\nCriteria:\n${e.criteria.map((c, i) => `${i + 1}. ${c}`).join("\n")}`
|
|
1702
1915
|
: e.goal;
|
|
1916
|
+
// Multimodal parity: the live seed of `attachments` is gated behind `!resumeMidRun`, so on
|
|
1917
|
+
// resume the image/audio must be recovered from the persisted run_started event or it is lost.
|
|
1918
|
+
const attachments = (e.attachments ?? []);
|
|
1919
|
+
const contentParts = attachments.length
|
|
1920
|
+
? [...(userText ? [{ type: "text", text: userText }] : []), ...attachments]
|
|
1921
|
+
: undefined;
|
|
1703
1922
|
messages.push({
|
|
1704
1923
|
role: "user",
|
|
1705
1924
|
content: userText,
|
|
1925
|
+
...(contentParts ? { contentParts } : {}),
|
|
1706
1926
|
toolCalls: [],
|
|
1707
1927
|
tokenCount: Math.max(1, Math.ceil(userText.length / 4)),
|
|
1708
1928
|
});
|
|
1709
1929
|
}
|
|
1710
1930
|
else if (e.kind === "compressed") {
|
|
1931
|
+
if (archivedTurns.has(e.turn))
|
|
1932
|
+
continue;
|
|
1711
1933
|
const summary = upgradedSummaries.get(seq) ?? e.summary;
|
|
1712
1934
|
if (summary) {
|
|
1713
1935
|
const systemText = `[Compressed context: turn ${e.turn}]\n${summary}`;
|
|
@@ -1719,6 +1941,24 @@ function replayMessages(events, maxBytes) {
|
|
|
1719
1941
|
});
|
|
1720
1942
|
}
|
|
1721
1943
|
}
|
|
1944
|
+
else if (e.kind === "page_out" && e.archive_ref && archiveStore?.read) {
|
|
1945
|
+
try {
|
|
1946
|
+
const archived = await archiveStore.read(e.archive_ref);
|
|
1947
|
+
messages.push(...archived.map(message => ({
|
|
1948
|
+
...message,
|
|
1949
|
+
content: sanitizeReplayText(message.content, maxBytes),
|
|
1950
|
+
})));
|
|
1951
|
+
}
|
|
1952
|
+
catch {
|
|
1953
|
+
if (e.summary) {
|
|
1954
|
+
const systemText = `[Compressed context: turn ${e.turn}]\n${e.summary}`;
|
|
1955
|
+
messages.push({
|
|
1956
|
+
role: "system", content: systemText, toolCalls: [],
|
|
1957
|
+
tokenCount: Math.max(1, Math.ceil(systemText.length / 4)),
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1722
1962
|
else if (e.kind === "llm_completed") {
|
|
1723
1963
|
messages.push({
|
|
1724
1964
|
role: "assistant",
|
|
@@ -1754,14 +1994,6 @@ function nextArchivedSeqStart(events) {
|
|
|
1754
1994
|
}
|
|
1755
1995
|
return next;
|
|
1756
1996
|
}
|
|
1757
|
-
function tryParseJson(s) {
|
|
1758
|
-
try {
|
|
1759
|
-
return JSON.parse(s);
|
|
1760
|
-
}
|
|
1761
|
-
catch {
|
|
1762
|
-
return null;
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
1997
|
export async function collectText(stream) {
|
|
1766
1998
|
let text = "";
|
|
1767
1999
|
for await (const evt of stream) {
|
|
@@ -1811,32 +2043,72 @@ function parseStartWorkflowSpec(argsStr) {
|
|
|
1811
2043
|
}
|
|
1812
2044
|
/** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
|
|
1813
2045
|
* agent's context, so its next turn continues with the sub-workflow's results in view. */
|
|
2046
|
+
function recoveredOutputs(outcomes) {
|
|
2047
|
+
const outputs = new Map();
|
|
2048
|
+
for (const outcome of outcomes ?? []) {
|
|
2049
|
+
if (!outcome.output)
|
|
2050
|
+
continue;
|
|
2051
|
+
outputs.set(outcome.agentId, outcome.output.content);
|
|
2052
|
+
outputs.set(outcome.agentId.replace(/-i\d+$/, ""), outcome.output.content);
|
|
2053
|
+
}
|
|
2054
|
+
return outputs;
|
|
2055
|
+
}
|
|
1814
2056
|
function authoredWorkflowOutcomeNote(outcome) {
|
|
2057
|
+
const counts = new Map();
|
|
2058
|
+
for (const node of outcome.nodeOutcomes)
|
|
2059
|
+
counts.set(node.status, (counts.get(node.status) ?? 0) + 1);
|
|
1815
2060
|
const lines = [
|
|
1816
|
-
`[authored workflow result] ${outcome.
|
|
1817
|
-
(
|
|
2061
|
+
`[authored workflow result] ${outcome.nodeOutcomes.length} terminal node(s): ` +
|
|
2062
|
+
[...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ") + ".",
|
|
1818
2063
|
];
|
|
1819
|
-
for (const
|
|
1820
|
-
const out = outcome.outputs[
|
|
2064
|
+
for (const node of outcome.nodeOutcomes) {
|
|
2065
|
+
const out = outcome.outputs[node.nodeId] ?? node.output?.content;
|
|
1821
2066
|
if (out)
|
|
1822
|
-
lines.push(`- ${
|
|
2067
|
+
lines.push(`- ${node.nodeId} (${node.status}): ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
|
|
1823
2068
|
}
|
|
1824
2069
|
return lines.join("\n");
|
|
1825
2070
|
}
|
|
1826
|
-
/** Lower a
|
|
2071
|
+
/** Lower a claimed signal delivery to the kernel's `deliver_signal` input event. Shared by the main
|
|
1827
2072
|
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
1828
|
-
function signalToKernelEvent(
|
|
2073
|
+
function signalToKernelEvent(delivery) {
|
|
2074
|
+
const sig = delivery.signal;
|
|
1829
2075
|
return {
|
|
1830
|
-
kind: "
|
|
2076
|
+
kind: "deliver_signal",
|
|
2077
|
+
delivery_id: delivery.deliveryId,
|
|
2078
|
+
attempt: delivery.deliveryAttempt,
|
|
1831
2079
|
signal: {
|
|
1832
|
-
id:
|
|
2080
|
+
id: delivery.signalId,
|
|
1833
2081
|
source: sig.source ?? "custom",
|
|
1834
2082
|
signal_type: sig.signalType ?? "event",
|
|
1835
2083
|
urgency: sig.urgency ?? "normal",
|
|
1836
2084
|
summary: String(sig.payload?.goal ?? "signal"),
|
|
1837
2085
|
payload: sig.payload ?? {},
|
|
1838
2086
|
...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
|
|
2087
|
+
...(sig.recipient ? { recipient: sig.recipient } : {}),
|
|
2088
|
+
...(sig.deadlineMs !== undefined ? { deadline_ms: sig.deadlineMs } : {}),
|
|
2089
|
+
...(sig.coalesceKey ? { coalesce_key: sig.coalesceKey } : {}),
|
|
2090
|
+
coalesced_count: Math.max(1, sig.coalescedCount ?? 1),
|
|
1839
2091
|
timestamp_ms: Date.now(),
|
|
1840
2092
|
},
|
|
1841
2093
|
};
|
|
1842
2094
|
}
|
|
2095
|
+
/** Convert SDK ContentParts (camelCase mediaType) to kernel serde shape (media_type). */
|
|
2096
|
+
function attachmentsToKernelMessage(parts) {
|
|
2097
|
+
const content = parts.map(p => {
|
|
2098
|
+
if (p.type === "image") {
|
|
2099
|
+
return {
|
|
2100
|
+
type: "image",
|
|
2101
|
+
...(p.url ? { url: p.url } : {}),
|
|
2102
|
+
...(p.data ? { data: p.data } : {}),
|
|
2103
|
+
...(p.mediaType ? { media_type: p.mediaType } : {}),
|
|
2104
|
+
...(p.detail ? { detail: p.detail } : {}),
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
if (p.type === "audio")
|
|
2108
|
+
return { type: "audio", data: p.data, media_type: p.mediaType };
|
|
2109
|
+
if (p.type === "text")
|
|
2110
|
+
return { type: "text", text: p.text };
|
|
2111
|
+
return { type: "text", text: "" };
|
|
2112
|
+
});
|
|
2113
|
+
return { role: "user", content };
|
|
2114
|
+
}
|