@deepstrike/sdk 0.2.39 → 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 +61 -31
- package/dist/collaboration/contract.d.ts +2 -2
- package/dist/collaboration/contract.js +2 -2
- package/dist/collaboration/handoff.d.ts +2 -14
- package/dist/collaboration/handoff.js +1 -17
- package/dist/collaboration/harness.d.ts +16 -47
- package/dist/collaboration/harness.js +57 -158
- package/dist/collaboration/index.d.ts +2 -2
- package/dist/collaboration/index.js +1 -2
- package/dist/collaboration/modes/creator-verifier.js +42 -6
- package/dist/collaboration/pool.d.ts +8 -0
- package/dist/collaboration/pool.js +56 -3
- package/dist/harness/harness.d.ts +97 -97
- package/dist/harness/harness.js +190 -144
- package/dist/harness/judge.d.ts +3 -2
- package/dist/harness/judge.js +7 -3
- package/dist/harness/public.d.ts +5 -3
- package/dist/harness/public.js +3 -2
- package/dist/index.d.ts +22 -7
- package/dist/index.js +8 -1
- package/dist/kernel.d.ts +12 -39
- package/dist/memory/agent.d.ts +8 -41
- package/dist/memory/agent.js +34 -117
- package/dist/memory/extraction.d.ts +4 -0
- package/dist/memory/extraction.js +79 -0
- package/dist/memory/in-memory-store.d.ts +28 -15
- package/dist/memory/in-memory-store.js +74 -25
- package/dist/memory/protocols.d.ts +56 -56
- package/dist/memory/protocols.js +1 -0
- package/dist/memory/public.d.ts +3 -1
- package/dist/memory/public.js +1 -0
- 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/os/public.d.ts +3 -3
- package/dist/os/public.js +1 -1
- package/dist/providers/base.d.ts +8 -0
- package/dist/providers/base.js +0 -0
- package/dist/providers/gemini.js +12 -0
- package/dist/providers/ollama.js +3 -1
- package/dist/providers/openai-responses.js +9 -2
- package/dist/providers/profiles.d.ts +5 -3
- package/dist/providers/profiles.js +3 -3
- package/dist/runtime/context-policy.d.ts +35 -0
- package/dist/runtime/context-policy.js +66 -0
- package/dist/runtime/eval.d.ts +6 -2
- package/dist/runtime/eval.js +2 -2
- package/dist/runtime/event-stream.d.ts +9 -0
- package/dist/runtime/event-stream.js +25 -11
- package/dist/runtime/execution-plane.d.ts +5 -1
- package/dist/runtime/execution-plane.js +9 -3
- package/dist/runtime/facade.js +2 -1
- package/dist/runtime/kernel-event-log.d.ts +0 -2
- package/dist/runtime/kernel-event-log.js +61 -13
- package/dist/runtime/kernel-primitives-dashboard.js +1 -1
- package/dist/runtime/kernel-rebuild.d.ts +13 -0
- package/dist/runtime/kernel-rebuild.js +75 -0
- package/dist/runtime/kernel-step.d.ts +157 -8
- package/dist/runtime/kernel-step.js +220 -7
- package/dist/runtime/kernel-transaction-log.d.ts +61 -0
- package/dist/runtime/kernel-transaction-log.js +149 -0
- package/dist/runtime/large-result-spool.d.ts +3 -1
- package/dist/runtime/large-result-spool.js +24 -5
- package/dist/runtime/loop-driver.d.ts +1 -1
- package/dist/runtime/loop-driver.js +2 -7
- package/dist/runtime/mcp-proxy-plane.d.ts +1 -0
- package/dist/runtime/mcp-proxy-plane.js +23 -6
- 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 +33 -3
- package/dist/runtime/process-sandbox-plane.js +16 -11
- package/dist/runtime/reaction-checkpoint.d.ts +51 -0
- package/dist/runtime/reaction-checkpoint.js +83 -0
- package/dist/runtime/reactive-session.d.ts +9 -3
- package/dist/runtime/reactive-session.js +44 -14
- package/dist/runtime/reliability.d.ts +48 -0
- package/dist/runtime/reliability.js +86 -0
- package/dist/runtime/remote-vpc-plane.js +4 -3
- package/dist/runtime/run-group.d.ts +38 -35
- package/dist/runtime/run-group.js +97 -54
- package/dist/runtime/runner.d.ts +117 -78
- package/dist/runtime/runner.js +1516 -1197
- package/dist/runtime/session-log.d.ts +71 -12
- package/dist/runtime/session-log.js +192 -32
- package/dist/runtime/session-repair.d.ts +11 -7
- package/dist/runtime/session-repair.js +11 -8
- package/dist/runtime/sub-agent-orchestrator.d.ts +6 -2
- package/dist/runtime/sub-agent-orchestrator.js +45 -18
- package/dist/signals/gateway.d.ts +23 -10
- package/dist/signals/gateway.js +81 -17
- package/dist/signals/scheduled.js +0 -1
- package/dist/signals/types.d.ts +21 -13
- package/dist/skills/loader.js +12 -2
- package/dist/tools/index.d.ts +2 -0
- package/dist/types/agent.d.ts +40 -1
- package/dist/types/agent.js +61 -1
- package/dist/types.d.ts +6 -1
- package/dist/workflow/public.d.ts +2 -3
- package/dist/workflow/public.js +0 -1
- package/package.json +2 -2
package/dist/runtime/runner.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { extractSessionMemories } from "../memory/extraction.js";
|
|
2
2
|
import { resolvePermissionRequest } from "./execution-plane.js";
|
|
3
|
+
import { GroupBudgetScope } from "./run-group.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
|
-
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent,
|
|
7
|
+
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverWorkflowNodeOutcomes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
|
|
7
8
|
import { KernelPrimitivesDashboard } from "./kernel-primitives-dashboard.js";
|
|
8
|
-
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, entropySampleFromObservation,
|
|
9
|
-
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "../types/agent.js";
|
|
9
|
+
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, entropySampleFromObservation, durableKernelAction, durableKernelApply, durableKernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, taskUpdateToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.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";
|
|
@@ -16,13 +17,47 @@ import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
|
|
|
16
17
|
import { assertNativeProfile } from "./os-profile.js";
|
|
17
18
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
18
19
|
import { formatToolError } from "../tools/errors.js";
|
|
20
|
+
import { ManagedTaskScope } from "./reliability.js";
|
|
21
|
+
import { contextPolicyV1, normalizeContextPolicyV1, } from "./context-policy.js";
|
|
22
|
+
export function schedulerPolicyToKernel(policy) {
|
|
23
|
+
const allowed = new Set([
|
|
24
|
+
"version", "criticalPathWeight", "fanoutWeight", "ageWeight", "tokenCostWeight",
|
|
25
|
+
]);
|
|
26
|
+
const unknown = Object.keys(policy).filter(key => !allowed.has(key));
|
|
27
|
+
if (unknown.length > 0)
|
|
28
|
+
throw new TypeError(`unknown scheduler policy field(s): ${unknown.join(", ")}`);
|
|
29
|
+
return {
|
|
30
|
+
version: policy.version,
|
|
31
|
+
critical_path_weight: policy.criticalPathWeight,
|
|
32
|
+
fanout_weight: policy.fanoutWeight,
|
|
33
|
+
age_weight: policy.ageWeight,
|
|
34
|
+
token_cost_weight: policy.tokenCostWeight,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function pendingCallIds(action) {
|
|
38
|
+
switch (action.kind) {
|
|
39
|
+
case "call_provider":
|
|
40
|
+
return [action.effectId];
|
|
41
|
+
case "execute_tool":
|
|
42
|
+
return action.calls.map(call => call.id);
|
|
43
|
+
case "request_approval":
|
|
44
|
+
return action.requests.map(request => request.callId);
|
|
45
|
+
case "spawn_workflow":
|
|
46
|
+
return action.nodes.map(node => String(node.agent_id ?? "")).filter(Boolean);
|
|
47
|
+
case "preempt_sub_agents":
|
|
48
|
+
return action.agentIds;
|
|
49
|
+
default:
|
|
50
|
+
return "effectId" in action ? [action.effectId] : [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
19
53
|
export class RuntimeRunner {
|
|
20
54
|
opts;
|
|
21
55
|
interrupted = false;
|
|
22
|
-
|
|
23
|
-
|
|
56
|
+
cancellationReason;
|
|
57
|
+
/** Aborts host-owned provider I/O before `cancel_operation` commits the kernel terminal fact. */
|
|
24
58
|
abortController = null;
|
|
25
59
|
activeKernel = null;
|
|
60
|
+
activeGroupBudgetScope;
|
|
26
61
|
pendingObservations = [];
|
|
27
62
|
currentSessionId = null;
|
|
28
63
|
/** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
|
|
@@ -32,18 +67,23 @@ export class RuntimeRunner {
|
|
|
32
67
|
* an already-active skill (loading is idempotent; the knowledge push should be too). */
|
|
33
68
|
knowledgePushedSkills = new Set();
|
|
34
69
|
nextArchiveStart = 0;
|
|
70
|
+
pendingPageOutArchives = [];
|
|
71
|
+
activePageOutArchive;
|
|
35
72
|
/** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
|
|
36
73
|
currentGoal = "";
|
|
37
|
-
/** Full tool outputs keyed by call_id until Layer-1 spool observations are logged. */
|
|
38
|
-
pendingSpoolOutputs = new Map();
|
|
39
74
|
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
40
75
|
* at the next safe point (after the tool turn resolves, kernel back in Reason — not suspended). */
|
|
41
76
|
pendingAuthoredWorkflows = [];
|
|
77
|
+
workflowContinuation = null;
|
|
42
78
|
dashboard = null;
|
|
43
79
|
/** Most recent kernel entropy sample of the active/last run (see `latestEntropy`). */
|
|
44
80
|
lastEntropySample = null;
|
|
45
81
|
constructor(opts) {
|
|
46
82
|
this.opts = opts;
|
|
83
|
+
const schemaAttempts = opts.workflowSchemaValidationAttempts ?? 2;
|
|
84
|
+
if (!Number.isInteger(schemaAttempts) || schemaAttempts < 1 || schemaAttempts > 16) {
|
|
85
|
+
throw new RangeError("workflowSchemaValidationAttempts must be an integer between 1 and 16");
|
|
86
|
+
}
|
|
47
87
|
if (opts.enableDiagnosticsDashboard) {
|
|
48
88
|
const originalAppend = opts.sessionLog.append.bind(opts.sessionLog);
|
|
49
89
|
opts.sessionLog.append = async (sessionId, event) => {
|
|
@@ -60,83 +100,104 @@ export class RuntimeRunner {
|
|
|
60
100
|
get hostOptions() {
|
|
61
101
|
return this.opts;
|
|
62
102
|
}
|
|
103
|
+
durableSessionId(sessionId) {
|
|
104
|
+
const resolved = sessionId ?? this.currentSessionId;
|
|
105
|
+
if (!resolved)
|
|
106
|
+
throw new Error("durable kernel transitions require a session id");
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
|
109
|
+
async commitKernelApply(runtime, pending, event, sessionId) {
|
|
110
|
+
return durableKernelApply(runtime, this.opts.sessionLog, this.durableSessionId(sessionId), pending, event);
|
|
111
|
+
}
|
|
112
|
+
async commitKernelMaybeAction(runtime, pending, event, sessionId) {
|
|
113
|
+
return durableKernelMaybeAction(runtime, this.opts.sessionLog, this.durableSessionId(sessionId), pending, event);
|
|
114
|
+
}
|
|
115
|
+
async commitKernelAction(runtime, pending, event, sessionId) {
|
|
116
|
+
return durableKernelAction(runtime, this.opts.sessionLog, this.durableSessionId(sessionId), pending, event);
|
|
117
|
+
}
|
|
118
|
+
async persistMemoryToStore(memory, agentId) {
|
|
119
|
+
if (!this.opts.dreamStore)
|
|
120
|
+
throw new Error("memory persistence requires dreamStore");
|
|
121
|
+
await this.opts.dreamStore.upsert(agentId, memory);
|
|
122
|
+
}
|
|
123
|
+
async retrieveMemoryFromStore(query, requestedK, agentId) {
|
|
124
|
+
if (!this.opts.dreamStore)
|
|
125
|
+
throw new Error("memory queries require dreamStore");
|
|
126
|
+
return (await this.opts.dreamStore.search(agentId, { ...query, top_k: requestedK }))
|
|
127
|
+
.slice(0, requestedK);
|
|
128
|
+
}
|
|
63
129
|
async writeMemory(memory, opts = {}) {
|
|
64
130
|
const sessionId = opts.sessionId ?? this.currentSessionId;
|
|
65
131
|
const agentId = opts.agentId ?? this.opts.agentId;
|
|
66
132
|
if (!this.opts.dreamStore || !agentId)
|
|
67
133
|
return;
|
|
134
|
+
const durableSessionId = this.durableSessionId(sessionId);
|
|
68
135
|
const observations = [];
|
|
69
|
-
const runtime = this.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!event) {
|
|
136
|
+
const runtime = this.createSyscallRuntime();
|
|
137
|
+
const action = await this.commitKernelMaybeAction(runtime, observations, { kind: "write_memory", memory }, durableSessionId);
|
|
138
|
+
if (!action) {
|
|
73
139
|
await this.appendMemorySyscallObservations(sessionId, observations);
|
|
74
140
|
return;
|
|
75
141
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// existing entry is dropped (the observation is still logged for audit).
|
|
79
|
-
const isDuplicate = existing.some(e => jaccardSimilarity(e.text, memory.content) >= 0.9);
|
|
80
|
-
if (!isDuplicate) {
|
|
81
|
-
const meta = memory.metadata;
|
|
82
|
-
const score = typeof meta?.score === "number" ? meta.score : 1.0;
|
|
83
|
-
await this.opts.dreamStore.commit(agentId, {
|
|
84
|
-
toAdd: [{
|
|
85
|
-
text: memory.content,
|
|
86
|
-
score,
|
|
87
|
-
metadata: {
|
|
88
|
-
...memory.metadata,
|
|
89
|
-
source: meta?.source ?? "write_memory_syscall",
|
|
90
|
-
},
|
|
91
|
-
}],
|
|
92
|
-
toRemoveIndices: [],
|
|
93
|
-
stats: {
|
|
94
|
-
insightsProcessed: 1,
|
|
95
|
-
duplicatesRemoved: 0,
|
|
96
|
-
conflictsResolved: 0,
|
|
97
|
-
entriesAdded: 1,
|
|
98
|
-
},
|
|
99
|
-
}, existing);
|
|
142
|
+
if (action.kind !== "persist_memory") {
|
|
143
|
+
throw new Error(`write_memory returned unexpected kernel effect: ${action.kind}`);
|
|
100
144
|
}
|
|
101
|
-
|
|
145
|
+
let ioError;
|
|
146
|
+
try {
|
|
147
|
+
await this.persistMemoryToStore(action.memory, agentId);
|
|
148
|
+
}
|
|
149
|
+
catch (cause) {
|
|
150
|
+
ioError = cause;
|
|
151
|
+
}
|
|
152
|
+
await this.commitKernelApply(runtime, observations, {
|
|
153
|
+
kind: "memory_persist_result",
|
|
154
|
+
effect_id: action.effectId,
|
|
155
|
+
...(ioError ? { error: formatToolError(ioError) } : {}),
|
|
156
|
+
}, durableSessionId);
|
|
157
|
+
await this.appendMemorySyscallObservations(durableSessionId, observations);
|
|
158
|
+
if (ioError)
|
|
159
|
+
throw ioError;
|
|
102
160
|
}
|
|
103
161
|
async queryMemory(query, opts = {}) {
|
|
104
162
|
const sessionId = opts.sessionId ?? this.currentSessionId;
|
|
105
163
|
const agentId = opts.agentId ?? this.opts.agentId;
|
|
106
164
|
if (!this.opts.dreamStore || !agentId)
|
|
107
165
|
return [];
|
|
166
|
+
const durableSessionId = this.durableSessionId(sessionId);
|
|
108
167
|
const observations = [];
|
|
109
|
-
const runtime = this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
let hits;
|
|
114
|
-
if (retrieval.selected_memory_ids.length > 0) {
|
|
115
|
-
const selected = new Set(retrieval.selected_memory_ids);
|
|
116
|
-
hits = allMemories
|
|
117
|
-
.filter(m => selected.has(String(m.metadata?.name ?? "")))
|
|
118
|
-
.slice(0, query.top_k);
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
hits = await this.opts.dreamStore.search(agentId, query.current_context, query.top_k);
|
|
122
|
-
if (hits.length > 0 && retrieval.selection_rationale === "No candidates after filtering") {
|
|
123
|
-
retrieval.selected_memory_ids = hits.map(h => String(h.metadata?.name ?? h.text.slice(0, 32)));
|
|
124
|
-
retrieval.selection_rationale = `DreamStore.search returned ${hits.length} hit(s)`;
|
|
125
|
-
}
|
|
168
|
+
const runtime = this.createSyscallRuntime();
|
|
169
|
+
const action = await this.commitKernelAction(runtime, observations, { kind: "query_memory", query }, durableSessionId);
|
|
170
|
+
if (action.kind !== "query_memory") {
|
|
171
|
+
throw new Error(`query_memory returned unexpected kernel effect: ${action.kind}`);
|
|
126
172
|
}
|
|
127
|
-
|
|
128
|
-
|
|
173
|
+
let hits = [];
|
|
174
|
+
let ioError;
|
|
175
|
+
try {
|
|
176
|
+
hits = await this.retrieveMemoryFromStore(query, action.requestedK, agentId);
|
|
177
|
+
}
|
|
178
|
+
catch (cause) {
|
|
179
|
+
ioError = cause;
|
|
180
|
+
}
|
|
181
|
+
await this.commitKernelApply(runtime, observations, {
|
|
182
|
+
kind: "memory_query_result",
|
|
183
|
+
effect_id: action.effectId,
|
|
184
|
+
hits,
|
|
185
|
+
...(ioError ? { error: formatToolError(ioError) } : {}),
|
|
186
|
+
}, durableSessionId);
|
|
187
|
+
await this.appendMemorySyscallObservations(durableSessionId, observations);
|
|
188
|
+
if (ioError)
|
|
189
|
+
throw ioError;
|
|
190
|
+
await this.logMemoryRetrievalResult(durableSessionId, hits);
|
|
129
191
|
return hits;
|
|
130
192
|
}
|
|
131
|
-
async logMemoryRetrievalResult(sessionId,
|
|
193
|
+
async logMemoryRetrievalResult(sessionId, hits) {
|
|
132
194
|
if (!sessionId)
|
|
133
195
|
return;
|
|
134
196
|
// The session-log record is the durable audit artifact; the kernel needs no
|
|
135
197
|
// acknowledgment (the former kernel event was a no-op and was removed).
|
|
136
198
|
await this.opts.sessionLog.append(sessionId, {
|
|
137
199
|
kind: "memory_retrieval_result",
|
|
138
|
-
|
|
139
|
-
selection_rationale: retrieval.selection_rationale,
|
|
200
|
+
hits,
|
|
140
201
|
});
|
|
141
202
|
}
|
|
142
203
|
createSyscallRuntime() {
|
|
@@ -149,6 +210,37 @@ export class RuntimeRunner {
|
|
|
149
210
|
timeoutMs: this.opts.timeoutMs !== undefined ? BigInt(this.opts.timeoutMs) : undefined,
|
|
150
211
|
});
|
|
151
212
|
}
|
|
213
|
+
groupBudgetRequest(includeTokens = true) {
|
|
214
|
+
const tokens = includeTokens ? this.opts.maxTotalTokens : undefined;
|
|
215
|
+
const subagents = this.opts.resourceQuota?.maxTotalSubagents;
|
|
216
|
+
const rounds = this.opts.runSpec?.loopRound ? 1 : undefined;
|
|
217
|
+
const roundLimit = this.opts.runSpec?.loopRound?.maxRounds;
|
|
218
|
+
return {
|
|
219
|
+
limits: {
|
|
220
|
+
...(tokens !== undefined ? { tokens } : {}),
|
|
221
|
+
...(subagents !== undefined ? { subagents } : {}),
|
|
222
|
+
...(roundLimit !== undefined ? { rounds: roundLimit } : {}),
|
|
223
|
+
},
|
|
224
|
+
requested: {
|
|
225
|
+
...(tokens !== undefined ? { tokens } : {}),
|
|
226
|
+
...(subagents !== undefined ? { subagents } : {}),
|
|
227
|
+
...(rounds !== undefined ? { rounds } : {}),
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async settleGroupBudget(scope, actual) {
|
|
232
|
+
const retries = this.opts.kernelReliability?.hostEffectRetryAttempts ?? 3;
|
|
233
|
+
for (let attempt = 0;; attempt += 1) {
|
|
234
|
+
try {
|
|
235
|
+
await scope.settle(actual);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (attempt >= retries)
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
152
244
|
/**
|
|
153
245
|
* Lower the declarative governance / attention / scheduler-budget / resource-quota policies into a
|
|
154
246
|
* freshly-created kernel. Shared by `execute()` (full agent run) and `bootstrapWorkflowKernel()`
|
|
@@ -156,23 +248,73 @@ export class RuntimeRunner {
|
|
|
156
248
|
* exactly as a mid-run spawn would be. Must run BEFORE `start_run` so the in-kernel gate enforces
|
|
157
249
|
* every policy from the first spawn. No config ⇒ the native-profile defaults (铁律: defaults only).
|
|
158
250
|
*/
|
|
159
|
-
applyKernelPolicies(runtime,
|
|
251
|
+
async applyKernelPolicies(runtime, groupBudgetScope) {
|
|
160
252
|
// K2: lower governance / attention / scheduler / quota in ONE `configure_run` event instead of
|
|
161
253
|
// the previous 2–4 separate `set_*` / `load_governance_policy` events. The kernel applies each
|
|
162
254
|
// present field via the same path its granular event uses; absent fields are left untouched.
|
|
163
255
|
// (Requires the 0.2.30 core that ships `configure_run`.)
|
|
164
256
|
const osProfile = assertNativeProfile(this.opts.osProfile ?? "native");
|
|
165
|
-
const
|
|
257
|
+
const signalPolicy = this.opts.signalPolicy ?? osProfile.signalPolicy;
|
|
166
258
|
const governancePolicy = this.opts.governancePolicy ?? osProfile.governancePolicy;
|
|
167
259
|
// Strip the event `kind` off the governance event — `configure_run.config.governance` carries the
|
|
168
260
|
// bare policy fields (default_action / rules / vetoed_tools / rate_limits / constraints).
|
|
169
261
|
const { kind: _govKind, ...governance } = governancePolicyToKernelEvent(governancePolicy);
|
|
170
262
|
const config = { governance };
|
|
171
|
-
if (
|
|
172
|
-
config.
|
|
263
|
+
if (this.opts.contextPolicy) {
|
|
264
|
+
config.context_policy = normalizeContextPolicyV1(contextPolicyV1(this.opts.contextPolicy));
|
|
265
|
+
}
|
|
266
|
+
if (this.opts.kernelReliability) {
|
|
267
|
+
const reliability = this.opts.kernelReliability;
|
|
268
|
+
config.reliability = {
|
|
269
|
+
...(reliability.eventReplayCapacity !== undefined
|
|
270
|
+
? { event_replay_capacity: reliability.eventReplayCapacity }
|
|
271
|
+
: {}),
|
|
272
|
+
...(reliability.completedEffectReplayCapacity !== undefined
|
|
273
|
+
? { completed_effect_replay_capacity: reliability.completedEffectReplayCapacity }
|
|
274
|
+
: {}),
|
|
275
|
+
...(reliability.providerRecoveryAttempts !== undefined
|
|
276
|
+
? { provider_recovery_attempts: reliability.providerRecoveryAttempts }
|
|
277
|
+
: {}),
|
|
278
|
+
...(reliability.outputRecoveryAttempts !== undefined
|
|
279
|
+
? { output_recovery_attempts: reliability.outputRecoveryAttempts }
|
|
280
|
+
: {}),
|
|
281
|
+
...(reliability.hostEffectRetryAttempts !== undefined
|
|
282
|
+
? { host_effect_retry_attempts: reliability.hostEffectRetryAttempts }
|
|
283
|
+
: {}),
|
|
284
|
+
...(reliability.spoolThresholdBytes !== undefined
|
|
285
|
+
? { spool_threshold_bytes: reliability.spoolThresholdBytes }
|
|
286
|
+
: {}),
|
|
287
|
+
...(reliability.spoolPreviewBytes !== undefined
|
|
288
|
+
? { spool_preview_bytes: reliability.spoolPreviewBytes }
|
|
289
|
+
: {}),
|
|
290
|
+
...(reliability.snapshotInputLimit !== undefined
|
|
291
|
+
? { snapshot_input_limit: reliability.snapshotInputLimit }
|
|
292
|
+
: {}),
|
|
293
|
+
...(reliability.maxInputBytes !== undefined
|
|
294
|
+
? { max_input_bytes: reliability.maxInputBytes }
|
|
295
|
+
: {}),
|
|
296
|
+
...(reliability.snapshotJournalBytesLimit !== undefined
|
|
297
|
+
? { snapshot_journal_bytes_limit: reliability.snapshotJournalBytesLimit }
|
|
298
|
+
: {}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
config.signal_policy = {
|
|
302
|
+
version: 1,
|
|
303
|
+
queue_max: signalPolicy.queueMax,
|
|
304
|
+
...(signalPolicy.ttlMs !== undefined ? { ttl_ms: signalPolicy.ttlMs } : {}),
|
|
305
|
+
...(signalPolicy.deadlineEscalation !== undefined
|
|
306
|
+
? { deadline_escalation: signalPolicy.deadlineEscalation }
|
|
307
|
+
: {}),
|
|
308
|
+
};
|
|
309
|
+
if (this.opts.promptBudget) {
|
|
310
|
+
config.prompt_budget = {
|
|
311
|
+
prompt_overhead_tokens: this.opts.promptBudget.promptOverheadTokens,
|
|
312
|
+
output_reserve_tokens: this.opts.promptBudget.outputReserveTokens,
|
|
313
|
+
safety_margin_tokens: this.opts.promptBudget.safetyMarginTokens,
|
|
314
|
+
};
|
|
173
315
|
}
|
|
174
|
-
if (this.opts.
|
|
175
|
-
config.
|
|
316
|
+
if (this.opts.schedulerPolicy) {
|
|
317
|
+
config.scheduler_policy = schedulerPolicyToKernel(this.opts.schedulerPolicy);
|
|
176
318
|
}
|
|
177
319
|
if (this.opts.resourceQuota) {
|
|
178
320
|
const q = this.opts.resourceQuota;
|
|
@@ -185,15 +327,19 @@ export class RuntimeRunner {
|
|
|
185
327
|
: {}),
|
|
186
328
|
};
|
|
187
329
|
}
|
|
188
|
-
if (
|
|
189
|
-
config.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
330
|
+
if (groupBudgetScope) {
|
|
331
|
+
config.budget_grant = {
|
|
332
|
+
reservation_id: groupBudgetScope.reservationId,
|
|
333
|
+
...(groupBudgetScope.granted.tokens !== undefined
|
|
334
|
+
? { tokens: groupBudgetScope.granted.tokens }
|
|
335
|
+
: {}),
|
|
336
|
+
...(groupBudgetScope.granted.subagents !== undefined
|
|
337
|
+
? { subagents: groupBudgetScope.granted.subagents }
|
|
338
|
+
: {}),
|
|
339
|
+
...(groupBudgetScope.granted.rounds !== undefined
|
|
340
|
+
? { rounds: groupBudgetScope.granted.rounds }
|
|
341
|
+
: {}),
|
|
342
|
+
};
|
|
197
343
|
}
|
|
198
344
|
// O6: tune/disable the in-kernel repeat fuse. `false` disables; an object overrides thresholds.
|
|
199
345
|
// Absent ⇒ kernel defaults (enabled, deny_after=5, terminate_after=8).
|
|
@@ -223,7 +369,7 @@ export class RuntimeRunner {
|
|
|
223
369
|
...(ew.notifyModel !== undefined ? { notify_model: ew.notifyModel } : {}),
|
|
224
370
|
};
|
|
225
371
|
}
|
|
226
|
-
|
|
372
|
+
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
227
373
|
}
|
|
228
374
|
async appendMemorySyscallObservations(sessionId, observations) {
|
|
229
375
|
if (!sessionId)
|
|
@@ -240,37 +386,37 @@ export class RuntimeRunner {
|
|
|
240
386
|
}
|
|
241
387
|
}
|
|
242
388
|
/** Mount a tool capability on the currently-running kernel runtime. No-op if not running. */
|
|
243
|
-
mountTool(schema) {
|
|
389
|
+
async mountTool(schema) {
|
|
244
390
|
if (!this.activeKernel)
|
|
245
391
|
return;
|
|
246
|
-
|
|
392
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, capabilityCommandMount(capabilityTool(schema)));
|
|
247
393
|
}
|
|
248
394
|
/** Mount a skill capability on the currently-running kernel runtime. No-op if not running. */
|
|
249
|
-
mountSkill(name, description) {
|
|
395
|
+
async mountSkill(name, description) {
|
|
250
396
|
if (!this.activeKernel)
|
|
251
397
|
return;
|
|
252
|
-
|
|
398
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, capabilityCommandMount(capabilitySkill({ name, description, estimatedTokens: 0 })));
|
|
253
399
|
}
|
|
254
400
|
/** Mount a generic marker capability (e.g. MCP server, agent) on the active run. No-op if not running. */
|
|
255
|
-
mountMarker(kind, id, description) {
|
|
401
|
+
async mountMarker(kind, id, description) {
|
|
256
402
|
if (!this.activeKernel)
|
|
257
403
|
return;
|
|
258
|
-
|
|
404
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, capabilityCommandMount(capabilityMarker(kind, id, description)));
|
|
259
405
|
}
|
|
260
406
|
/** Unmount a capability by kind + id from the active run. No-op if not running. */
|
|
261
|
-
unmountCapability(kind, id) {
|
|
407
|
+
async unmountCapability(kind, id) {
|
|
262
408
|
if (!this.activeKernel)
|
|
263
409
|
return;
|
|
264
|
-
|
|
410
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, capabilityCommandUnmount(kind, id));
|
|
265
411
|
}
|
|
266
412
|
/** Push content into the Knowledge slot (memory retrievals, skill definitions, artifacts).
|
|
267
413
|
* K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
|
|
268
414
|
* compaction/renewal boundary, where the cached system[1] block is rewritten anyway) instead
|
|
269
415
|
* of appending a duplicate. `opts.pinned` exempts the entry from the knowledge-budget sweep. */
|
|
270
|
-
pushKnowledge(message, tokens, opts) {
|
|
416
|
+
async pushKnowledge(message, tokens, opts) {
|
|
271
417
|
if (!this.activeKernel)
|
|
272
418
|
return;
|
|
273
|
-
|
|
419
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, {
|
|
274
420
|
kind: "add_knowledge_message",
|
|
275
421
|
content: message.content ?? "",
|
|
276
422
|
tokens: tokens ?? Math.max(1, Math.ceil((message.content?.length ?? 0) / 4)),
|
|
@@ -280,19 +426,19 @@ export class RuntimeRunner {
|
|
|
280
426
|
}
|
|
281
427
|
/** K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
|
|
282
428
|
* Errs-open: an unknown key is a kernel-side no-op. */
|
|
283
|
-
removeKnowledge(key) {
|
|
429
|
+
async removeKnowledge(key) {
|
|
284
430
|
if (!this.activeKernel)
|
|
285
431
|
return;
|
|
286
|
-
|
|
432
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, { kind: "remove_knowledge", key });
|
|
287
433
|
}
|
|
288
434
|
/** K3: host-driven skill deactivation (there is deliberately no model-facing unload — it
|
|
289
435
|
* invites thrash). The toolset re-widens at the next provider call; the skill's knowledge pin
|
|
290
436
|
* drops at the next compaction/renewal boundary. A later `skill(name)` call re-activates and
|
|
291
437
|
* re-pins fresh content. Errs-open: not-active is a kernel-side no-op. */
|
|
292
|
-
deactivateSkill(name) {
|
|
438
|
+
async deactivateSkill(name) {
|
|
293
439
|
if (!this.activeKernel)
|
|
294
440
|
return;
|
|
295
|
-
|
|
441
|
+
await this.commitKernelApply(this.activeKernel, this.pendingObservations, { kind: "skill_deactivated", name });
|
|
296
442
|
// Re-arm the SDK-side push guard so a re-activation re-pins the content.
|
|
297
443
|
this.knowledgePushedSkills.delete(name);
|
|
298
444
|
}
|
|
@@ -306,7 +452,7 @@ export class RuntimeRunner {
|
|
|
306
452
|
}
|
|
307
453
|
const parentSessionId = this.currentSessionId;
|
|
308
454
|
const runtime = this.activeKernel;
|
|
309
|
-
const observations =
|
|
455
|
+
const observations = await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
310
456
|
kind: "spawn_sub_agent",
|
|
311
457
|
spec: agentRunSpecToKernel(spec),
|
|
312
458
|
parent_session_id: parentSessionId,
|
|
@@ -325,7 +471,7 @@ export class RuntimeRunner {
|
|
|
325
471
|
sessionLog: this.opts.sessionLog,
|
|
326
472
|
...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
|
|
327
473
|
});
|
|
328
|
-
|
|
474
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
329
475
|
kind: "sub_agent_completed",
|
|
330
476
|
result: subAgentResultToKernel(result),
|
|
331
477
|
});
|
|
@@ -414,10 +560,10 @@ export class RuntimeRunner {
|
|
|
414
560
|
const schema = node.output_schema;
|
|
415
561
|
if (!schema)
|
|
416
562
|
return orchestrator.run(mkCtx(baseSpec.goal));
|
|
417
|
-
const
|
|
563
|
+
const maxAttempts = this.opts.workflowSchemaValidationAttempts ?? 2;
|
|
418
564
|
let last;
|
|
419
565
|
let lastErrors = [];
|
|
420
|
-
for (let attempt = 1; attempt <=
|
|
566
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
421
567
|
const goal = attempt === 1
|
|
422
568
|
? `${baseSpec.goal}\n\n${schemaInstruction(schema)}`
|
|
423
569
|
: `${baseSpec.goal}\n\n${schemaRetryInstruction(schema, lastErrors)}`;
|
|
@@ -430,7 +576,7 @@ export class RuntimeRunner {
|
|
|
430
576
|
last = result;
|
|
431
577
|
lastErrors = v.errors;
|
|
432
578
|
}
|
|
433
|
-
const reason = `output_schema validation failed after ${
|
|
579
|
+
const reason = `output_schema validation failed after ${maxAttempts} attempts: ${lastErrors.join("; ")}`;
|
|
434
580
|
const fallback = last;
|
|
435
581
|
return {
|
|
436
582
|
...fallback,
|
|
@@ -467,7 +613,7 @@ export class RuntimeRunner {
|
|
|
467
613
|
* W0-ABI: run a declarative workflow DAG. The kernel owns the DAG and gates every node spawn
|
|
468
614
|
* through the syscall trap; this driver runs each kernel-emitted batch of nodes in parallel,
|
|
469
615
|
* feeds their results back, and loops until the kernel reports the workflow complete.
|
|
470
|
-
* Returns
|
|
616
|
+
* Returns one typed terminal outcome for every node in the DAG.
|
|
471
617
|
*/
|
|
472
618
|
async runWorkflow(spec, opts) {
|
|
473
619
|
// Standalone entry: with no active parent run (e.g. a stateless HTTP handler), auto-bootstrap a
|
|
@@ -475,34 +621,42 @@ export class RuntimeRunner {
|
|
|
475
621
|
// gets — then tear it down on completion so the runner is reusable. Mid-run callers (activeKernel
|
|
476
622
|
// already set by an in-flight `run()`) keep the original in-place behavior with no teardown.
|
|
477
623
|
const bootstrapped = !this.activeKernel || !this.currentSessionId;
|
|
478
|
-
|
|
479
|
-
const sessionId = opts?.sessionId ?? `wf-${crypto.randomUUID()}`;
|
|
480
|
-
// L1: a standalone workflow is a member of its runner's governance domain too. Seed the
|
|
481
|
-
// bootstrap kernel with the group's cumulative spend (so the cumulative spawn/token cap bites
|
|
482
|
-
// while scheduling DAG nodes) and register membership — mirroring `execute()`. Mid-run callers
|
|
483
|
-
// skip this: their parent `run()` already seeds + counts the nodes via `localSubagentsSpawned()`.
|
|
484
|
-
let groupLedger;
|
|
485
|
-
if (this.opts.runGroup) {
|
|
486
|
-
const g = this.opts.runGroup;
|
|
487
|
-
groupLedger = await g.budgetStore.read(g.id);
|
|
488
|
-
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId, kind: "vehicle" });
|
|
489
|
-
}
|
|
490
|
-
this.bootstrapWorkflowKernel(sessionId, spec, groupLedger?.tokensSpent, groupLedger?.subagentsSpawned);
|
|
491
|
-
}
|
|
492
|
-
const parentSessionId = this.currentSessionId;
|
|
493
|
-
const runtime = this.activeKernel;
|
|
624
|
+
let groupBudgetScope;
|
|
494
625
|
try {
|
|
495
|
-
|
|
626
|
+
if (bootstrapped) {
|
|
627
|
+
const sessionId = opts?.sessionId ?? `wf-${crypto.randomUUID()}`;
|
|
628
|
+
// A standalone workflow reserves a bounded slice before its kernel schedules any node.
|
|
629
|
+
// Mid-run callers reuse their parent run's already-active reservation.
|
|
630
|
+
if (this.opts.runGroup) {
|
|
631
|
+
const g = this.opts.runGroup;
|
|
632
|
+
groupBudgetScope = await GroupBudgetScope.open(g, { sessionId, role: this.opts.agentId, kind: "vehicle" }, this.groupBudgetRequest(false));
|
|
633
|
+
this.activeGroupBudgetScope = groupBudgetScope;
|
|
634
|
+
}
|
|
635
|
+
// Resume depends on this fact. Do not dispatch any node until it is durable.
|
|
636
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
637
|
+
kind: "run_started",
|
|
638
|
+
run_id: crypto.randomUUID(),
|
|
639
|
+
goal: `workflow:${spec.nodes.length} nodes`,
|
|
640
|
+
criteria: [],
|
|
641
|
+
agent_id: this.opts.agentId,
|
|
642
|
+
});
|
|
643
|
+
await this.bootstrapWorkflowKernel(sessionId, groupBudgetScope);
|
|
644
|
+
}
|
|
645
|
+
const parentSessionId = this.currentSessionId;
|
|
646
|
+
const runtime = this.activeKernel;
|
|
647
|
+
const observationStart = this.pendingObservations.length;
|
|
648
|
+
const initialAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
496
649
|
kind: "load_workflow",
|
|
497
650
|
spec: workflowSpecToKernel(spec),
|
|
498
651
|
parent_session_id: parentSessionId,
|
|
499
|
-
//
|
|
500
|
-
...(opts?.
|
|
501
|
-
// W-1: signal-carrying completion records (classify branch / loop stop replay).
|
|
502
|
-
...(opts?.resumedResults?.length
|
|
652
|
+
// Exact typed terminal outcomes plus control-flow signals recovered from the journal.
|
|
653
|
+
...(opts?.resumedOutcomes?.length
|
|
503
654
|
? {
|
|
504
|
-
|
|
655
|
+
resumed_outcomes: opts.resumedOutcomes.map(r => ({
|
|
505
656
|
agent_id: r.agentId,
|
|
657
|
+
status: r.status,
|
|
658
|
+
termination: r.termination,
|
|
659
|
+
...(r.output ? { output: messageToKernelMessage(r.output) } : {}),
|
|
506
660
|
...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
|
|
507
661
|
...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
|
|
508
662
|
...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
|
|
@@ -513,24 +667,30 @@ export class RuntimeRunner {
|
|
|
513
667
|
...(opts?.resumedSubmissions?.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
514
668
|
...(opts?.resumedSubmissionBases?.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
|
|
515
669
|
});
|
|
516
|
-
|
|
670
|
+
const observations = this.pendingObservations.slice(observationStart);
|
|
671
|
+
const outcome = await this.driveWorkflow(initialAction, observations, parentSessionId, runtime, recoveredOutputs(opts?.resumedOutcomes));
|
|
672
|
+
if (bootstrapped) {
|
|
673
|
+
const terminal = await this.commitKernelAction(runtime, this.pendingObservations, { kind: "complete_run" });
|
|
674
|
+
if (terminal.kind !== "done") {
|
|
675
|
+
throw new Error("complete_run did not produce a terminal kernel action");
|
|
676
|
+
}
|
|
677
|
+
await this.appendObservations(parentSessionId, runtime, 0);
|
|
678
|
+
}
|
|
679
|
+
return outcome;
|
|
517
680
|
}
|
|
518
681
|
finally {
|
|
519
682
|
if (bootstrapped) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
// group. The envelope kernel's TaskTable holds one proc per scheduled node, so
|
|
524
|
-
// `localSubagentsSpawned()` is exactly that node count (the envelope itself burns no tokens).
|
|
525
|
-
if (this.opts.runGroup) {
|
|
526
|
-
const subagents = runtime.localSubagentsSpawned?.() ?? 0;
|
|
527
|
-
if (subagents > 0) {
|
|
528
|
-
await this.opts.runGroup.budgetStore.charge(this.opts.runGroup.id, { subagents });
|
|
683
|
+
try {
|
|
684
|
+
if (groupBudgetScope && !groupBudgetScope.isClosed) {
|
|
685
|
+
await groupBudgetScope.release();
|
|
529
686
|
}
|
|
530
687
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
688
|
+
finally {
|
|
689
|
+
this.activeKernel = null;
|
|
690
|
+
this.currentSessionId = null;
|
|
691
|
+
this.pendingObservations = [];
|
|
692
|
+
this.activeGroupBudgetScope = undefined;
|
|
693
|
+
}
|
|
534
694
|
}
|
|
535
695
|
}
|
|
536
696
|
}
|
|
@@ -538,31 +698,25 @@ export class RuntimeRunner {
|
|
|
538
698
|
* Bootstrap a standalone kernel for a host-driven workflow with NO active parent run — the path a
|
|
539
699
|
* stateless request handler takes when it calls `runWorkflow(spec)` directly. Mirrors `execute()`'s
|
|
540
700
|
* pre-run kernel setup (governance / attention / quota via `applyKernelPolicies`, then `start_run`)
|
|
541
|
-
*
|
|
542
|
-
* `
|
|
701
|
+
* after `runWorkflow` has durably recorded `run_started`. Sets `activeKernel` / `currentSessionId`;
|
|
702
|
+
* `runWorkflow` is responsible for tearing them down.
|
|
543
703
|
*/
|
|
544
|
-
bootstrapWorkflowKernel(sessionId,
|
|
704
|
+
async bootstrapWorkflowKernel(sessionId, groupBudgetScope) {
|
|
545
705
|
this.interrupted = false;
|
|
546
706
|
this.abortController = new AbortController();
|
|
547
707
|
this.pendingObservations = [];
|
|
548
|
-
this.
|
|
708
|
+
this.pendingPageOutArchives = [];
|
|
709
|
+
this.activePageOutArchive = undefined;
|
|
549
710
|
this.currentSessionId = sessionId;
|
|
550
711
|
const runtime = this.createSyscallRuntime();
|
|
551
712
|
this.activeKernel = runtime;
|
|
552
|
-
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
goal,
|
|
560
|
-
criteria: [],
|
|
561
|
-
agent_id: this.opts.agentId,
|
|
562
|
-
}).catch(() => { });
|
|
563
|
-
this.applyKernelPolicies(runtime, groupTokensBase, groupSpawnsBase);
|
|
564
|
-
// K1: no explicit `start_run` — the host `load_workflow` (fired next by `runWorkflow`) self-bootstraps
|
|
565
|
-
// the run on the 0.2.30 core, matching the agent-reachable `submit_workflow` path.
|
|
713
|
+
await this.applyKernelPolicies(runtime, groupBudgetScope);
|
|
714
|
+
// ABI v2 has one lifecycle: standalone workflows start a real run before loading their DAG.
|
|
715
|
+
// The initial provider effect is superseded by the workflow load; no self-bootstrap escape hatch.
|
|
716
|
+
await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
717
|
+
kind: "start_run",
|
|
718
|
+
task: { goal: `workflow session ${sessionId}`, criteria: [] },
|
|
719
|
+
});
|
|
566
720
|
return runtime;
|
|
567
721
|
}
|
|
568
722
|
/**
|
|
@@ -580,7 +734,9 @@ export class RuntimeRunner {
|
|
|
580
734
|
}
|
|
581
735
|
const parentSessionId = this.currentSessionId;
|
|
582
736
|
const runtime = this.activeKernel;
|
|
583
|
-
const
|
|
737
|
+
const observationStart = this.pendingObservations.length;
|
|
738
|
+
const initialAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
739
|
+
const observations = this.pendingObservations.slice(observationStart);
|
|
584
740
|
// W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
|
|
585
741
|
// announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
|
|
586
742
|
// had this spec, unlike the `runWorkflow` path.
|
|
@@ -593,7 +749,7 @@ export class RuntimeRunner {
|
|
|
593
749
|
submitterAgentId: opts?.submitterAgentId,
|
|
594
750
|
}));
|
|
595
751
|
}
|
|
596
|
-
return this.driveWorkflow(observations, parentSessionId, runtime);
|
|
752
|
+
return this.driveWorkflow(initialAction, observations, parentSessionId, runtime);
|
|
597
753
|
}
|
|
598
754
|
/**
|
|
599
755
|
* M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`. Called at the
|
|
@@ -607,14 +763,14 @@ export class RuntimeRunner {
|
|
|
607
763
|
async driveAuthoredWorkflows(runtime, action) {
|
|
608
764
|
const specs = this.pendingAuthoredWorkflows;
|
|
609
765
|
this.pendingAuthoredWorkflows = [];
|
|
766
|
+
this.workflowContinuation = null;
|
|
610
767
|
for (const spec of specs) {
|
|
611
|
-
|
|
612
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
613
|
-
kind: "add_history_message",
|
|
614
|
-
message: messageToKernelMessage({ role: "user", content: authoredWorkflowOutcomeNote(outcome) }),
|
|
615
|
-
});
|
|
768
|
+
await this.bootstrapWorkflow(spec);
|
|
616
769
|
}
|
|
617
|
-
|
|
770
|
+
const continuation = this.workflowContinuation;
|
|
771
|
+
if (!continuation)
|
|
772
|
+
throw new Error("authored workflow completed without a provider continuation");
|
|
773
|
+
return continuation;
|
|
618
774
|
}
|
|
619
775
|
/**
|
|
620
776
|
* #2-B-ii: while a workflow batch is in flight, poll the signal source. A Critical `InterruptNow`
|
|
@@ -631,21 +787,40 @@ export class RuntimeRunner {
|
|
|
631
787
|
while (!batchState.settled) {
|
|
632
788
|
// O2: injected notes participate in the monitor too, so a host `injectNote` mid-batch is not
|
|
633
789
|
// stranded until the batch settles (the drain order matches `nextInboundSignal`).
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
790
|
+
const delivery = await this.nextInboundSignal();
|
|
791
|
+
if (batchState.settled) {
|
|
792
|
+
await delivery?.nack();
|
|
637
793
|
break;
|
|
638
|
-
|
|
794
|
+
}
|
|
795
|
+
if (!delivery) {
|
|
639
796
|
await new Promise(resolve => setTimeout(resolve, 5));
|
|
640
797
|
continue;
|
|
641
798
|
}
|
|
642
|
-
const
|
|
643
|
-
const
|
|
799
|
+
const observationStart = this.pendingObservations.length;
|
|
800
|
+
const signalAction = await this.consumeInboundSignal(delivery, sig => this.commitKernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig)));
|
|
801
|
+
let observations = this.pendingObservations.slice(observationStart);
|
|
802
|
+
if (signalAction?.kind === "preempt_sub_agents") {
|
|
803
|
+
for (const id of signalAction.agentIds)
|
|
804
|
+
controllers.get(id)?.abort();
|
|
805
|
+
const resultStart = this.pendingObservations.length;
|
|
806
|
+
const continuation = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
807
|
+
kind: "preempt_result",
|
|
808
|
+
effect_id: signalAction.effectId,
|
|
809
|
+
});
|
|
810
|
+
if (continuation && continuation.kind !== "call_provider" && continuation.kind !== "done") {
|
|
811
|
+
throw new Error(`workflow preemption returned unexpected effect: ${continuation.kind}`);
|
|
812
|
+
}
|
|
813
|
+
observations = [...observations, ...this.pendingObservations.slice(resultStart)];
|
|
814
|
+
}
|
|
815
|
+
else if (signalAction) {
|
|
816
|
+
throw new Error(`workflow signal returned unexpected effect: ${signalAction.kind}`);
|
|
817
|
+
}
|
|
818
|
+
const preempted = observations.find(o => o.kind === "agent_preempted");
|
|
644
819
|
if (preempted) {
|
|
645
820
|
for (const id of preempted.agent_ids ?? [])
|
|
646
821
|
controllers.get(id)?.abort();
|
|
647
|
-
const wc =
|
|
648
|
-
return
|
|
822
|
+
const wc = observations.find(o => o.kind === "workflow_completed");
|
|
823
|
+
return (wc?.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel);
|
|
649
824
|
}
|
|
650
825
|
}
|
|
651
826
|
return null;
|
|
@@ -654,22 +829,40 @@ export class RuntimeRunner {
|
|
|
654
829
|
* Shared workflow driver for `runWorkflow` (host `load_workflow`) and `bootstrapWorkflow` (agent
|
|
655
830
|
* `submit_workflow`): given the observations from the initial load/bootstrap, run each kernel-emitted
|
|
656
831
|
* batch in parallel, feed completions back (appending any agent-submitted nodes first), and loop
|
|
657
|
-
* until the kernel reports the workflow complete. Returns
|
|
832
|
+
* until the kernel reports the workflow complete. Returns typed terminal node outcomes.
|
|
658
833
|
*/
|
|
659
|
-
async driveWorkflow(initial, parentSessionId, runtime, seedOutputs) {
|
|
834
|
+
async driveWorkflow(initialAction, initial, parentSessionId, runtime, seedOutputs) {
|
|
660
835
|
let observations = initial;
|
|
661
836
|
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
662
|
-
const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")
|
|
663
|
-
?.nodes ?? [];
|
|
664
|
-
// G4: the batch observation also carries the workflow's remaining budget; track the latest so a
|
|
665
|
-
// coordinator node's prompt reflects current headroom when it decides how much to submit.
|
|
666
|
-
const collectBudget = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.budget;
|
|
667
837
|
const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
|
|
838
|
+
const acceptSpawn = async (spawn) => {
|
|
839
|
+
const observationStart = this.pendingObservations.length;
|
|
840
|
+
const continuation = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
841
|
+
kind: "workflow_spawn_result",
|
|
842
|
+
effect_id: spawn.effectId,
|
|
843
|
+
started_agent_ids: spawn.nodes.map(node => String(node.agent_id ?? "")),
|
|
844
|
+
failures: [],
|
|
845
|
+
});
|
|
846
|
+
if (continuation) {
|
|
847
|
+
throw new Error(`workflow spawn acknowledgement returned unexpected effect: ${continuation.kind}`);
|
|
848
|
+
}
|
|
849
|
+
return this.pendingObservations.slice(observationStart);
|
|
850
|
+
};
|
|
668
851
|
let done = findDone(observations);
|
|
669
|
-
if (done)
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
852
|
+
if (done) {
|
|
853
|
+
if (initialAction?.kind === "call_provider")
|
|
854
|
+
this.workflowContinuation = initialAction;
|
|
855
|
+
return { nodeOutcomes: (done.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel), outputs: {} };
|
|
856
|
+
}
|
|
857
|
+
if (!initialAction)
|
|
858
|
+
return { nodeOutcomes: [], outputs: {} };
|
|
859
|
+
if (initialAction.kind !== "spawn_workflow") {
|
|
860
|
+
throw new Error(`workflow load returned unexpected kernel effect: ${initialAction.kind}`);
|
|
861
|
+
}
|
|
862
|
+
let nodes = initialAction.nodes;
|
|
863
|
+
let budget = initialAction.budget;
|
|
864
|
+
observations = await acceptSpawn(initialAction);
|
|
865
|
+
done = findDone(observations);
|
|
673
866
|
// G2: each completed node's output, keyed by agent id — a reduce node reads its dependencies'
|
|
674
867
|
// outputs from here. Deps always complete in an earlier round than the reduce node that needs
|
|
675
868
|
// them (the kernel keeps the reduce node un-ready until its deps finish), so this is populated.
|
|
@@ -678,7 +871,7 @@ export class RuntimeRunner {
|
|
|
678
871
|
const outputs = new Map(seedOutputs ?? []);
|
|
679
872
|
for (;;) {
|
|
680
873
|
if (nodes.length === 0)
|
|
681
|
-
return {
|
|
874
|
+
return { nodeOutcomes: [], outputs: Object.fromEntries(outputs) }; // nothing to run (e.g. all gated)
|
|
682
875
|
// Run the currently-runnable nodes in parallel — each is independent within a round.
|
|
683
876
|
const roundBudget = budget;
|
|
684
877
|
// #2-B-ii: per-node abort controllers + a concurrent preemption monitor. While the batch is in
|
|
@@ -692,7 +885,7 @@ export class RuntimeRunner {
|
|
|
692
885
|
batchState.settled = true;
|
|
693
886
|
const preempted = await monitor;
|
|
694
887
|
if (preempted)
|
|
695
|
-
return {
|
|
888
|
+
return { nodeOutcomes: preempted, outputs: Object.fromEntries(outputs) };
|
|
696
889
|
// Feed completions back one at a time. The kernel's run-queue executor may spawn a node's
|
|
697
890
|
// dependents the moment *that* node completes (per-node unblock), so each feed can emit its
|
|
698
891
|
// own `workflow_batch_spawned`; ACCUMULATE them across the round rather than keeping only the
|
|
@@ -719,26 +912,50 @@ export class RuntimeRunner {
|
|
|
719
912
|
// G1: stamp the submitting node's agent id so the kernel can coerce a quarantined
|
|
720
913
|
// submitter's nodes to quarantined (no topological privilege escalation).
|
|
721
914
|
const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
915
|
+
const observationStart = this.pendingObservations.length;
|
|
916
|
+
const submitAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, submitEvent);
|
|
917
|
+
const subObs = this.pendingObservations.slice(observationStart);
|
|
918
|
+
if (submitAction?.kind === "spawn_workflow") {
|
|
919
|
+
nextNodes.push(...submitAction.nodes);
|
|
920
|
+
budget = submitAction.budget ?? budget;
|
|
921
|
+
const accepted = await acceptSpawn(submitAction);
|
|
922
|
+
const submittedDone = findDone([...subObs, ...accepted]);
|
|
923
|
+
if (submittedDone)
|
|
924
|
+
done = submittedDone;
|
|
925
|
+
}
|
|
926
|
+
else if (submitAction) {
|
|
927
|
+
throw new Error(`workflow node submission returned unexpected effect: ${submitAction.kind}`);
|
|
928
|
+
}
|
|
725
929
|
// R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
|
|
726
930
|
// so resume can re-apply the batch at the exact original graph position. W-N3: also the
|
|
727
931
|
// submitter, so resume drops batches whose submitter re-runs (it will re-submit).
|
|
728
932
|
const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
933
|
+
if (submitted) {
|
|
934
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
935
|
+
turn: runtime.turn(),
|
|
936
|
+
nodes: submitEvent.nodes ?? [],
|
|
937
|
+
baseIndex: submitted.base,
|
|
938
|
+
submitterAgentId: result.agentId,
|
|
939
|
+
}));
|
|
940
|
+
}
|
|
735
941
|
}
|
|
736
|
-
const
|
|
942
|
+
const observationStart = this.pendingObservations.length;
|
|
943
|
+
const completionAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
737
944
|
kind: "sub_agent_completed",
|
|
738
945
|
result: subAgentResultToKernel(result),
|
|
739
946
|
});
|
|
740
|
-
|
|
741
|
-
|
|
947
|
+
let obs = this.pendingObservations.slice(observationStart);
|
|
948
|
+
if (completionAction?.kind === "spawn_workflow") {
|
|
949
|
+
nextNodes.push(...completionAction.nodes);
|
|
950
|
+
budget = completionAction.budget ?? budget;
|
|
951
|
+
obs = [...obs, ...await acceptSpawn(completionAction)];
|
|
952
|
+
}
|
|
953
|
+
else if (completionAction?.kind === "call_provider") {
|
|
954
|
+
this.workflowContinuation = completionAction;
|
|
955
|
+
}
|
|
956
|
+
else if (completionAction) {
|
|
957
|
+
throw new Error(`workflow completion returned unexpected effect: ${completionAction.kind}`);
|
|
958
|
+
}
|
|
742
959
|
const d = findDone(obs);
|
|
743
960
|
if (d)
|
|
744
961
|
done = d;
|
|
@@ -748,15 +965,19 @@ export class RuntimeRunner {
|
|
|
748
965
|
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
|
|
749
966
|
turn: runtime.turn(),
|
|
750
967
|
agentId: result.agentId,
|
|
968
|
+
status: workflowNodeStatusFromTermination(result.result.termination),
|
|
751
969
|
termination: result.result.termination,
|
|
752
970
|
classifyBranch: result.result.classifyBranch,
|
|
753
971
|
tournamentWinner: result.result.tournamentWinner,
|
|
754
972
|
loopContinue: result.result.loopContinue,
|
|
755
|
-
...(result.result.
|
|
973
|
+
...(result.result.finalMessage ? { output: result.result.finalMessage } : {}),
|
|
756
974
|
}));
|
|
757
975
|
}
|
|
758
976
|
if (done && nextNodes.length === 0) {
|
|
759
|
-
return {
|
|
977
|
+
return {
|
|
978
|
+
nodeOutcomes: (done.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel),
|
|
979
|
+
outputs: Object.fromEntries(outputs),
|
|
980
|
+
};
|
|
760
981
|
}
|
|
761
982
|
nodes = nextNodes;
|
|
762
983
|
}
|
|
@@ -775,39 +996,33 @@ export class RuntimeRunner {
|
|
|
775
996
|
throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
|
|
776
997
|
}
|
|
777
998
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
778
|
-
const
|
|
779
|
-
const completedIds = new Set(
|
|
999
|
+
const resumedOutcomes = recoverWorkflowNodeOutcomes(events);
|
|
1000
|
+
const completedIds = new Set(resumedOutcomes.map(r => r.agentId));
|
|
780
1001
|
const recovered = recoverSubmittedWorkflowNodes(events);
|
|
781
1002
|
// W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
|
|
782
1003
|
// re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
|
|
783
|
-
//
|
|
784
|
-
// order-only log keeps every batch, since dropping would shift all later indices.
|
|
1004
|
+
// Exact bases keep later graph indices stable while dropped slots remain inert placeholders.
|
|
785
1005
|
let { submissions, bases } = recovered;
|
|
786
|
-
if (
|
|
1006
|
+
if (submissions.length > 0) {
|
|
787
1007
|
const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
|
|
788
1008
|
submissions = submissions.filter((_, i) => keep[i]);
|
|
789
1009
|
bases = bases.filter((_, i) => keep[i]);
|
|
790
1010
|
}
|
|
791
|
-
const resumedOutputs = new Map(resumedResults.filter(r => r.output).map(r => [r.agentId, r.output]));
|
|
792
|
-
// Alias loop iterations onto their stable node id (last iteration wins) — dependents consume
|
|
793
|
-
// `wf-node{N}`, not `wf-node{N}-i{k}`.
|
|
794
|
-
for (const r of resumedResults) {
|
|
795
|
-
const stableId = r.agentId.replace(/-i\d+$/, "");
|
|
796
|
-
if (stableId !== r.agentId && r.output)
|
|
797
|
-
resumedOutputs.set(stableId, r.output);
|
|
798
|
-
}
|
|
799
1011
|
return this.runWorkflow(spec, {
|
|
800
|
-
|
|
1012
|
+
resumedOutcomes,
|
|
801
1013
|
resumedSubmissions: submissions,
|
|
802
1014
|
resumedSubmissionBases: bases,
|
|
803
|
-
resumedOutputs,
|
|
804
1015
|
sessionId,
|
|
805
1016
|
});
|
|
806
1017
|
}
|
|
807
|
-
interrupt(
|
|
1018
|
+
interrupt(reason = "user") {
|
|
1019
|
+
this.interrupted = true;
|
|
1020
|
+
this.cancellationReason = reason;
|
|
1021
|
+
this.abortController?.abort(reason);
|
|
1022
|
+
}
|
|
808
1023
|
/** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
|
|
809
|
-
* the next turn boundary, routes through the kernel attention policy, and
|
|
810
|
-
*
|
|
1024
|
+
* the next turn boundary, routes through the kernel attention policy, and renders once as a
|
|
1025
|
+
* `[SIGNAL] <text>` line in the volatile state turn. Use it to feed
|
|
811
1026
|
* host-detected events back to the model mid-run (e.g. "that write was a no-op — stop repeating it")
|
|
812
1027
|
* without wiring a full `SignalSource`. `urgency` maps to the kernel disposition ladder: `"normal"`
|
|
813
1028
|
* queues for the next boundary (default), `"high"` soft-interrupts, `"critical"` preempts. */
|
|
@@ -830,18 +1045,63 @@ export class RuntimeRunner {
|
|
|
830
1045
|
async nextInboundSignal() {
|
|
831
1046
|
const injected = this.injectedSignals.shift();
|
|
832
1047
|
if (injected)
|
|
833
|
-
return
|
|
1048
|
+
return {
|
|
1049
|
+
signalId: crypto.randomUUID(),
|
|
1050
|
+
deliveryId: `injected-${crypto.randomUUID()}`,
|
|
1051
|
+
deliveryAttempt: 1,
|
|
1052
|
+
signal: injected,
|
|
1053
|
+
ack: async () => true,
|
|
1054
|
+
nack: async () => true,
|
|
1055
|
+
};
|
|
834
1056
|
if (!this.opts.signalSource)
|
|
835
1057
|
return null;
|
|
836
|
-
|
|
1058
|
+
const source = this.opts.signalSource;
|
|
1059
|
+
const claim = await source.claimSignal(this.currentSessionId ?? undefined);
|
|
1060
|
+
if (!claim)
|
|
1061
|
+
return null;
|
|
1062
|
+
const receipt = {
|
|
1063
|
+
deliveryId: claim.deliveryId,
|
|
1064
|
+
leaseToken: claim.leaseToken,
|
|
1065
|
+
};
|
|
1066
|
+
return {
|
|
1067
|
+
signalId: claim.signalId,
|
|
1068
|
+
deliveryId: claim.deliveryId,
|
|
1069
|
+
deliveryAttempt: claim.deliveryAttempt,
|
|
1070
|
+
signal: claim.signal,
|
|
1071
|
+
ack: () => source.ackSignal(receipt),
|
|
1072
|
+
nack: () => source.nackSignal(receipt),
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
async consumeInboundSignal(delivery, consume) {
|
|
1076
|
+
try {
|
|
1077
|
+
const observationStart = this.pendingObservations.length;
|
|
1078
|
+
const result = await consume(delivery);
|
|
1079
|
+
const dispositions = this.pendingObservations.slice(observationStart).filter(observation => observation.kind === "signal_delivery_disposed"
|
|
1080
|
+
&& observation.delivery_id === delivery.deliveryId
|
|
1081
|
+
&& observation.attempt === delivery.deliveryAttempt);
|
|
1082
|
+
if (dispositions.length !== 1) {
|
|
1083
|
+
throw new Error("kernel did not return the matching signal delivery disposition");
|
|
1084
|
+
}
|
|
1085
|
+
if (!await delivery.ack())
|
|
1086
|
+
throw new Error("signal lease was lost before acknowledgement");
|
|
1087
|
+
return result;
|
|
1088
|
+
}
|
|
1089
|
+
catch (cause) {
|
|
1090
|
+
await delivery.nack();
|
|
1091
|
+
throw cause;
|
|
1092
|
+
}
|
|
837
1093
|
}
|
|
838
1094
|
async *run(req) {
|
|
839
1095
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
840
1096
|
const midRun = isMidRun(prior);
|
|
1097
|
+
const resumedStart = [...prior].reverse().find(entry => entry.event.kind === "run_started");
|
|
1098
|
+
const runId = midRun && resumedStart?.event.kind === "run_started"
|
|
1099
|
+
? resumedStart.event.run_id
|
|
1100
|
+
: crypto.randomUUID();
|
|
841
1101
|
if (!midRun) {
|
|
842
1102
|
await this.opts.sessionLog.append(req.sessionId, {
|
|
843
1103
|
kind: "run_started",
|
|
844
|
-
run_id:
|
|
1104
|
+
run_id: runId,
|
|
845
1105
|
goal: req.goal,
|
|
846
1106
|
criteria: req.criteria ?? [],
|
|
847
1107
|
agent_id: this.opts.agentId,
|
|
@@ -849,7 +1109,7 @@ export class RuntimeRunner {
|
|
|
849
1109
|
...(req.attachments?.length ? { attachments: req.attachments } : {}),
|
|
850
1110
|
});
|
|
851
1111
|
}
|
|
852
|
-
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, req.attachments);
|
|
1112
|
+
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, req.attachments, runId);
|
|
853
1113
|
}
|
|
854
1114
|
async *wake(sessionId, extensions) {
|
|
855
1115
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
@@ -859,101 +1119,28 @@ export class RuntimeRunner {
|
|
|
859
1119
|
if (!startEntry)
|
|
860
1120
|
throw new Error(`No run_started event for session: ${sessionId}`);
|
|
861
1121
|
const start = startEntry.event;
|
|
862
|
-
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true, start.attachments);
|
|
1122
|
+
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true, start.attachments, start.run_id);
|
|
863
1123
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
throw new Error("dreamStore not configured");
|
|
867
|
-
const kernel = getKernel();
|
|
868
|
-
const sessions = await this.opts.dreamStore.loadSessions(agentId);
|
|
869
|
-
const existingMemories = await this.opts.dreamStore.loadMemories(agentId);
|
|
870
|
-
if (!sessions.length) {
|
|
871
|
-
yield { type: "done", iterations: 0, totalTokens: 0, status: "completed", dreamResult: { sessionsProcessed: 0, insightsExtracted: 0, entriesAdded: 0, entriesRemoved: 0 } };
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
const pipeline = new kernel.IdlePipeline(agentId);
|
|
875
|
-
const action1 = pipeline.feedTrigger(sessions.map(s => ({
|
|
876
|
-
sessionId: s.sessionId, agentId: s.agentId,
|
|
877
|
-
messages: s.messages.map(m => ({
|
|
878
|
-
role: m.role, content: m.content, tokenCount: m.tokenCount,
|
|
879
|
-
toolCalls: (m.toolCalls ?? []).map(tc => ({ id: tc.id, name: tc.name, arguments: tc.arguments })),
|
|
880
|
-
})),
|
|
881
|
-
metadata: JSON.stringify(s.metadata ?? null),
|
|
882
|
-
createdAtMs: s.createdAtMs, updatedAtMs: s.updatedAtMs,
|
|
883
|
-
})), existingMemories.map(e => ({ text: e.text, score: e.score, metadata: JSON.stringify(e.metadata ?? null) })), nowMs);
|
|
884
|
-
if (action1.kind === "noop" || action1.kind === "aborted") {
|
|
885
|
-
yield { type: "done", iterations: 0, totalTokens: 0, status: "completed", dreamResult: { sessionsProcessed: 0, insightsExtracted: 0, entriesAdded: 0, entriesRemoved: 0 } };
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
if (action1.kind !== "synthesize_insights")
|
|
889
|
-
throw new Error(`unexpected: ${action1.kind}`);
|
|
890
|
-
let synthesisText = "";
|
|
891
|
-
const dreamProvider = this.opts.dreamProvider ?? this.opts.provider;
|
|
892
|
-
const providerState = dreamProvider.createRunState?.();
|
|
893
|
-
const synthMsgs = (action1.messages ?? []);
|
|
894
|
-
const kernelSystemText = synthMsgs.filter(m => m.role === "system").map(m => m.content).join("\n\n");
|
|
895
|
-
const synthContext = {
|
|
896
|
-
systemText: [kernelSystemText, this.opts.dreamSystemPrompt].filter(Boolean).join("\n\n"),
|
|
897
|
-
turns: synthMsgs.filter(m => m.role !== "system"),
|
|
898
|
-
};
|
|
899
|
-
let totalTokens = 0;
|
|
900
|
-
for await (const evt of dreamProvider.stream(synthContext, [], undefined, providerState)) {
|
|
901
|
-
if (evt.type === "text_delta") {
|
|
902
|
-
synthesisText += evt.delta;
|
|
903
|
-
yield evt;
|
|
904
|
-
}
|
|
905
|
-
else if (evt.type === "usage")
|
|
906
|
-
totalTokens = evt.totalTokens;
|
|
907
|
-
}
|
|
908
|
-
const action2 = pipeline.feedSynthesisResult(synthesisText);
|
|
909
|
-
if (action2.kind !== "commit_memories")
|
|
910
|
-
throw new Error(`unexpected: ${action2.kind}`);
|
|
911
|
-
const cr = action2.curationResult;
|
|
912
|
-
const rr = action2.runResult;
|
|
913
|
-
const dsResult = {
|
|
914
|
-
toAdd: (cr.toAdd ?? []).map((e) => ({
|
|
915
|
-
text: e.text, score: e.score, metadata: tryParseJson(e.metadata),
|
|
916
|
-
})),
|
|
917
|
-
toRemoveIndices: (cr.toRemoveIndices ?? []).map(Number),
|
|
918
|
-
stats: {
|
|
919
|
-
insightsProcessed: cr.stats?.insightsProcessed ?? 0,
|
|
920
|
-
duplicatesRemoved: cr.stats?.duplicatesRemoved ?? 0,
|
|
921
|
-
conflictsResolved: cr.stats?.conflictsResolved ?? 0,
|
|
922
|
-
entriesAdded: cr.stats?.entriesAdded ?? 0,
|
|
923
|
-
},
|
|
924
|
-
};
|
|
925
|
-
await this.opts.dreamStore.commit(agentId, dsResult, existingMemories);
|
|
926
|
-
yield {
|
|
927
|
-
type: "done", iterations: 1, totalTokens, status: "completed",
|
|
928
|
-
dreamResult: {
|
|
929
|
-
sessionsProcessed: rr.sessionsProcessed,
|
|
930
|
-
insightsExtracted: rr.insightsExtracted,
|
|
931
|
-
entriesAdded: cr.stats?.entriesAdded ?? 0,
|
|
932
|
-
entriesRemoved: (cr.toRemoveIndices ?? []).length,
|
|
933
|
-
},
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
/** Resolve in-kernel AskUser suspend; returns resume lists and stream events to yield. */
|
|
937
|
-
async resolveKernelSuspend(runtime, sessionId) {
|
|
938
|
-
const gated = this.pendingObservations.filter((o) => o.kind === "tool_gated" && typeof o.call_id === "string" && typeof o.tool === "string");
|
|
1124
|
+
/** Execute a kernel-owned approval effect and return the correlated decision lists. */
|
|
1125
|
+
async resolveApprovalRequests(requests, runtime, sessionId) {
|
|
939
1126
|
const approved = [];
|
|
940
1127
|
const denied = [];
|
|
941
1128
|
const events = [];
|
|
942
1129
|
const runCtx = { onPermissionRequest: this.opts.onPermissionRequest };
|
|
943
|
-
for (const
|
|
1130
|
+
for (const approval of requests) {
|
|
944
1131
|
const request = {
|
|
945
1132
|
type: "permission_request",
|
|
946
|
-
callId:
|
|
947
|
-
toolName:
|
|
948
|
-
arguments:
|
|
949
|
-
reason:
|
|
1133
|
+
callId: approval.callId,
|
|
1134
|
+
toolName: approval.tool,
|
|
1135
|
+
arguments: approval.arguments,
|
|
1136
|
+
reason: approval.reason,
|
|
950
1137
|
};
|
|
951
1138
|
events.push(request);
|
|
952
1139
|
const decision = await resolvePermissionRequest(request, runCtx);
|
|
953
1140
|
events.push({
|
|
954
1141
|
type: "permission_resolved",
|
|
955
|
-
callId:
|
|
956
|
-
toolName:
|
|
1142
|
+
callId: approval.callId,
|
|
1143
|
+
toolName: approval.tool,
|
|
957
1144
|
approved: decision.approved,
|
|
958
1145
|
responder: decision.responder ?? "host",
|
|
959
1146
|
...(decision.reason ? { reason: decision.reason } : {}),
|
|
@@ -961,8 +1148,8 @@ export class RuntimeRunner {
|
|
|
961
1148
|
await this.opts.sessionLog.append(sessionId, {
|
|
962
1149
|
kind: "permission_requested",
|
|
963
1150
|
turn: runtime.turn(),
|
|
964
|
-
tool:
|
|
965
|
-
arguments:
|
|
1151
|
+
tool: approval.tool,
|
|
1152
|
+
arguments: approval.arguments,
|
|
966
1153
|
reason: request.reason,
|
|
967
1154
|
});
|
|
968
1155
|
await this.opts.sessionLog.append(sessionId, {
|
|
@@ -972,21 +1159,21 @@ export class RuntimeRunner {
|
|
|
972
1159
|
responder: decision.responder ?? "host",
|
|
973
1160
|
});
|
|
974
1161
|
if (decision.approved) {
|
|
975
|
-
approved.push(
|
|
1162
|
+
approved.push(approval.callId);
|
|
976
1163
|
}
|
|
977
1164
|
else {
|
|
978
|
-
denied.push(
|
|
1165
|
+
denied.push(approval.callId);
|
|
979
1166
|
const denyReason = decision.reason ?? "permission denied";
|
|
980
1167
|
events.push({
|
|
981
1168
|
type: "tool_denied",
|
|
982
|
-
callId:
|
|
983
|
-
toolName:
|
|
1169
|
+
callId: approval.callId,
|
|
1170
|
+
toolName: approval.tool,
|
|
984
1171
|
reason: denyReason,
|
|
985
1172
|
});
|
|
986
1173
|
events.push({
|
|
987
1174
|
type: "tool_result",
|
|
988
|
-
callId:
|
|
989
|
-
name:
|
|
1175
|
+
callId: approval.callId,
|
|
1176
|
+
name: approval.tool,
|
|
990
1177
|
content: `permission denied: ${denyReason}`,
|
|
991
1178
|
isError: true,
|
|
992
1179
|
errorKind: "governance_denied",
|
|
@@ -994,15 +1181,15 @@ export class RuntimeRunner {
|
|
|
994
1181
|
await this.opts.sessionLog.append(sessionId, {
|
|
995
1182
|
kind: "tool_denied",
|
|
996
1183
|
turn: runtime.turn(),
|
|
997
|
-
call_id:
|
|
998
|
-
tool_name:
|
|
1184
|
+
call_id: approval.callId,
|
|
1185
|
+
tool_name: approval.tool,
|
|
999
1186
|
reason: denyReason,
|
|
1000
1187
|
});
|
|
1001
1188
|
await this.opts.sessionLog.append(sessionId, {
|
|
1002
1189
|
kind: "tool_completed",
|
|
1003
1190
|
turn: runtime.turn(),
|
|
1004
1191
|
results: [{
|
|
1005
|
-
call_id:
|
|
1192
|
+
call_id: approval.callId,
|
|
1006
1193
|
output: `permission denied: ${denyReason}`,
|
|
1007
1194
|
is_error: true,
|
|
1008
1195
|
error_kind: "governance_denied",
|
|
@@ -1014,10 +1201,9 @@ export class RuntimeRunner {
|
|
|
1014
1201
|
}
|
|
1015
1202
|
/**
|
|
1016
1203
|
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
1017
|
-
* output. Resolution order: (a)
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
* session-log scan for the original `tool_completed` event carrying that `call_id`. Slices the
|
|
1204
|
+
* output. Resolution order: (a) the on-disk result spool committed by the explicit
|
|
1205
|
+
* `spool_large_result` host effect, then (b) a session-log scan for the original
|
|
1206
|
+
* `tool_completed` event carrying that `call_id`. Slices the
|
|
1021
1207
|
* resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
1022
1208
|
*/
|
|
1023
1209
|
async resolveReadResult(sessionId, argsJson) {
|
|
@@ -1035,15 +1221,13 @@ export class RuntimeRunner {
|
|
|
1035
1221
|
catch {
|
|
1036
1222
|
// malformed arguments — callId stays empty, falls through to "not found" below
|
|
1037
1223
|
}
|
|
1038
|
-
let full
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
full = undefined;
|
|
1046
|
-
}
|
|
1224
|
+
let full;
|
|
1225
|
+
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
1226
|
+
try {
|
|
1227
|
+
full = await spool.findByCallId(callId);
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
full = undefined;
|
|
1047
1231
|
}
|
|
1048
1232
|
if (full === undefined) {
|
|
1049
1233
|
try {
|
|
@@ -1071,11 +1255,13 @@ export class RuntimeRunner {
|
|
|
1071
1255
|
isError: false,
|
|
1072
1256
|
};
|
|
1073
1257
|
}
|
|
1074
|
-
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments) {
|
|
1258
|
+
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments, runId = crypto.randomUUID()) {
|
|
1075
1259
|
this.interrupted = false;
|
|
1260
|
+
this.cancellationReason = undefined;
|
|
1076
1261
|
this.abortController = new AbortController();
|
|
1077
1262
|
this.pendingObservations = [];
|
|
1078
|
-
this.
|
|
1263
|
+
this.pendingPageOutArchives = [];
|
|
1264
|
+
this.activePageOutArchive = undefined;
|
|
1079
1265
|
this.currentSessionId = sessionId;
|
|
1080
1266
|
if (this.opts.enableDiagnosticsDashboard) {
|
|
1081
1267
|
this.dashboard = new KernelPrimitivesDashboard(sessionId);
|
|
@@ -1087,795 +1273,920 @@ export class RuntimeRunner {
|
|
|
1087
1273
|
const providerPolicy = this.opts.provider.runtimePolicy?.() ?? {};
|
|
1088
1274
|
const effectiveMaxTurns = this.opts.maxTurns ?? providerPolicy.maxTurns ?? 25;
|
|
1089
1275
|
const effectiveTimeoutMs = this.opts.timeoutMs ?? providerPolicy.timeoutMs;
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1106
|
-
kind: "set_plan_tool_enabled",
|
|
1107
|
-
enabled: this.opts.enablePlanTool,
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1111
|
-
kind: "set_tools",
|
|
1112
|
-
tools: this.opts.executionPlane.schemas().map(toolSchemaToKernel),
|
|
1113
|
-
});
|
|
1114
|
-
if (this.opts.systemPrompt) {
|
|
1115
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1116
|
-
kind: "add_system_message",
|
|
1117
|
-
content: this.opts.systemPrompt,
|
|
1118
|
-
tokens: Math.max(1, Math.ceil(this.opts.systemPrompt.length / 4)),
|
|
1276
|
+
const operation = {
|
|
1277
|
+
runId,
|
|
1278
|
+
sessionId,
|
|
1279
|
+
agentId: this.opts.agentId,
|
|
1280
|
+
signal: this.abortController.signal,
|
|
1281
|
+
...(effectiveTimeoutMs !== undefined ? { deadlineMs: Date.now() + effectiveTimeoutMs } : {}),
|
|
1282
|
+
};
|
|
1283
|
+
const taskScope = new ManagedTaskScope(operation, this.opts.onBackgroundTaskError);
|
|
1284
|
+
let groupBudgetScope;
|
|
1285
|
+
try {
|
|
1286
|
+
const runtime = new kernel.KernelRuntime({
|
|
1287
|
+
maxTokens: this.opts.maxTokens,
|
|
1288
|
+
maxTurns: effectiveMaxTurns,
|
|
1289
|
+
timeoutMs: effectiveTimeoutMs !== undefined ? BigInt(effectiveTimeoutMs) : undefined,
|
|
1290
|
+
maxTotalTokens: this.opts.maxTotalTokens !== undefined ? BigInt(this.opts.maxTotalTokens) : undefined,
|
|
1119
1291
|
});
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
kind: "
|
|
1125
|
-
|
|
1126
|
-
tokens: Math.max(1, Math.ceil(mem.length / 4)),
|
|
1292
|
+
this.activeKernel = runtime;
|
|
1293
|
+
this.nextArchiveStart = nextCompressedArchiveStart;
|
|
1294
|
+
if (this.opts.tokenizer) {
|
|
1295
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1296
|
+
kind: "set_tokenizer",
|
|
1297
|
+
name: this.opts.tokenizer,
|
|
1127
1298
|
});
|
|
1128
1299
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
kind: "
|
|
1137
|
-
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
// P1-B/D: configure the stable-core tool ids (always exposed under skill gating). Empty/absent
|
|
1141
|
-
// ⇒ skills narrow to exactly their declared tools + meta-tools.
|
|
1142
|
-
if (this.opts.stableCoreToolIds?.length) {
|
|
1143
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1144
|
-
kind: "set_stable_core_tools",
|
|
1145
|
-
tool_ids: this.opts.stableCoreToolIds,
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
if (this.opts.dreamStore && this.opts.agentId) {
|
|
1149
|
-
kernelApply(runtime, this.pendingObservations, { kind: "set_memory_enabled", enabled: true });
|
|
1150
|
-
}
|
|
1151
|
-
// Install optional memory policy. Maps the ergonomic camelCase option onto the kernel's
|
|
1152
|
-
// snake_case `set_memory_policy` event; omitted fields fall back to kernel defaults.
|
|
1153
|
-
if (this.opts.memoryPolicy) {
|
|
1154
|
-
const m = this.opts.memoryPolicy;
|
|
1155
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1156
|
-
kind: "set_memory_policy",
|
|
1157
|
-
...(m.memoryPath !== undefined ? { memory_path: m.memoryPath } : {}),
|
|
1158
|
-
...(m.staleWarningDays !== undefined ? { stale_warning_days: m.staleWarningDays } : {}),
|
|
1159
|
-
...(m.retrievalTopK !== undefined ? { retrieval_top_k: m.retrievalTopK } : {}),
|
|
1160
|
-
...(m.validationEnabled !== undefined ? { validation_enabled: m.validationEnabled } : {}),
|
|
1161
|
-
...(m.maxContentBytes !== undefined ? { max_content_bytes: m.maxContentBytes } : {}),
|
|
1162
|
-
...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
|
-
if (this.opts.knowledgeSource) {
|
|
1166
|
-
kernelApply(runtime, this.pendingObservations, { kind: "set_knowledge_enabled", enabled: true });
|
|
1167
|
-
}
|
|
1168
|
-
if (this.opts.milestoneContract) {
|
|
1169
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1170
|
-
kind: "load_milestone_contract",
|
|
1171
|
-
contract: {
|
|
1172
|
-
phases: this.opts.milestoneContract.phases.map(p => ({
|
|
1173
|
-
id: p.id,
|
|
1174
|
-
criteria: p.criteria ?? [],
|
|
1175
|
-
unlocks: p.unlocks ?? [],
|
|
1176
|
-
required_evidence: p.requiredEvidence ?? [],
|
|
1177
|
-
...(p.verifier ? { verifier: p.verifier } : {}),
|
|
1178
|
-
})),
|
|
1179
|
-
},
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
const maxBytes = runtime.recoveryContentBytes();
|
|
1183
|
-
if (priorEvents && priorEvents.length > 0) {
|
|
1184
|
-
const repaired = repairEventsForRecovery(priorEvents, maxBytes);
|
|
1185
|
-
seedProviderReplayFromEvents(this.opts.provider, repaired);
|
|
1186
|
-
const loadArchive = this.opts.compressionStore
|
|
1187
|
-
? (ref) => this.opts.compressionStore.read(ref)
|
|
1188
|
-
: undefined;
|
|
1189
|
-
const replayed = await replayMessagesAsync(repaired, maxBytes, loadArchive);
|
|
1190
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1191
|
-
kind: "preload_history",
|
|
1192
|
-
messages: replayed.map(messageToKernelMessage),
|
|
1300
|
+
if (this.opts.enablePlanTool !== undefined) {
|
|
1301
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1302
|
+
kind: "set_plan_tool_enabled",
|
|
1303
|
+
enabled: this.opts.enablePlanTool,
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1307
|
+
kind: "set_tools",
|
|
1308
|
+
tools: this.opts.executionPlane.schemas().map(toolSchemaToKernel),
|
|
1193
1309
|
});
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1310
|
+
if (this.opts.systemPrompt) {
|
|
1311
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1312
|
+
kind: "add_system_message",
|
|
1313
|
+
content: this.opts.systemPrompt,
|
|
1314
|
+
tokens: Math.max(1, Math.ceil(this.opts.systemPrompt.length / 4)),
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
if (this.opts.initialMemory) {
|
|
1318
|
+
for (const mem of this.opts.initialMemory) {
|
|
1319
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1320
|
+
kind: "add_knowledge_message",
|
|
1321
|
+
content: mem,
|
|
1322
|
+
tokens: Math.max(1, Math.ceil(mem.length / 4)),
|
|
1323
|
+
});
|
|
1204
1324
|
}
|
|
1205
1325
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1326
|
+
if (this.opts.skillDir) {
|
|
1327
|
+
const { scanSkillDir } = await import("../skills/loader.js");
|
|
1328
|
+
const metas = await scanSkillDir(this.opts.skillDir);
|
|
1329
|
+
// P1-B: pass the full SkillMetadata (incl. `allowedTools`) straight through — re-mapping it
|
|
1330
|
+
// field-by-field previously dropped `allowedTools`.
|
|
1331
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1332
|
+
kind: "set_available_skills",
|
|
1333
|
+
skills: metas.map(m => skillMetadataToKernel(m)),
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
// P1-B/D: configure the stable-core tool ids (always exposed under skill gating). Empty/absent
|
|
1337
|
+
// ⇒ skills narrow to exactly their declared tools + meta-tools.
|
|
1338
|
+
if (this.opts.stableCoreToolIds?.length) {
|
|
1339
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1340
|
+
kind: "set_stable_core_tools",
|
|
1341
|
+
tool_ids: this.opts.stableCoreToolIds,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
if (this.opts.dreamStore && this.opts.agentId) {
|
|
1345
|
+
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_memory_enabled", enabled: true });
|
|
1346
|
+
}
|
|
1347
|
+
// Install optional memory policy. Maps the ergonomic camelCase option onto the kernel's
|
|
1348
|
+
// snake_case `set_memory_policy` event; omitted fields fall back to kernel defaults.
|
|
1349
|
+
if (this.opts.memoryPolicy) {
|
|
1350
|
+
const m = this.opts.memoryPolicy;
|
|
1351
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1352
|
+
kind: "set_memory_policy",
|
|
1353
|
+
...(m.memoryPath !== undefined ? { memory_path: m.memoryPath } : {}),
|
|
1354
|
+
...(m.staleWarningDays !== undefined ? { stale_warning_days: m.staleWarningDays } : {}),
|
|
1355
|
+
...(m.retrievalTopK !== undefined ? { retrieval_top_k: m.retrievalTopK } : {}),
|
|
1356
|
+
...(m.validationEnabled !== undefined ? { validation_enabled: m.validationEnabled } : {}),
|
|
1357
|
+
...(m.maxContentBytes !== undefined ? { max_content_bytes: m.maxContentBytes } : {}),
|
|
1358
|
+
...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
if (this.opts.knowledgeSource) {
|
|
1362
|
+
await this.commitKernelApply(runtime, this.pendingObservations, { kind: "set_knowledge_enabled", enabled: true });
|
|
1363
|
+
}
|
|
1364
|
+
if (this.opts.milestoneContract) {
|
|
1365
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1366
|
+
kind: "load_milestone_contract",
|
|
1367
|
+
contract: {
|
|
1368
|
+
phases: this.opts.milestoneContract.phases.map(p => ({
|
|
1369
|
+
id: p.id,
|
|
1370
|
+
criteria: p.criteria ?? [],
|
|
1371
|
+
unlocks: p.unlocks ?? [],
|
|
1372
|
+
required_evidence: p.requiredEvidence ?? [],
|
|
1373
|
+
...(p.verifier ? { verifier: p.verifier } : {}),
|
|
1374
|
+
})),
|
|
1375
|
+
},
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
const maxBytes = runtime.recoveryContentBytes();
|
|
1379
|
+
if (priorEvents && priorEvents.length > 0) {
|
|
1380
|
+
const repaired = repairEventsForRecovery(priorEvents, maxBytes);
|
|
1381
|
+
seedProviderReplayFromEvents(this.opts.provider, repaired);
|
|
1382
|
+
const loadArchive = this.opts.compressionStore
|
|
1383
|
+
? (ref) => this.opts.compressionStore.read(ref)
|
|
1384
|
+
: undefined;
|
|
1385
|
+
const replayed = await replayMessagesAsync(repaired, maxBytes, loadArchive);
|
|
1386
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1387
|
+
kind: "preload_history",
|
|
1388
|
+
messages: replayed.map(messageToKernelMessage),
|
|
1389
|
+
});
|
|
1390
|
+
// P1-B B3: rebuild active-skill gating after a wake by re-emitting SkillActivated for each
|
|
1391
|
+
// `skill` tool call in the replayed history (active_skills is not snapshotted — graceful).
|
|
1392
|
+
// The catalog (set_available_skills) was already fed above, so allowed_tools resolves.
|
|
1393
|
+
// `knowledge` isn't snapshotted either (same graceful-reset philosophy) — best-effort re-push
|
|
1394
|
+
// the skill's content from its replayed tool_result so the durable copy survives a wake too.
|
|
1395
|
+
const toolResultByCallId = new Map();
|
|
1396
|
+
for (const m of replayed) {
|
|
1397
|
+
for (const part of m.contentParts ?? []) {
|
|
1398
|
+
if (part.type === "tool_result")
|
|
1399
|
+
toolResultByCallId.set(part.callId, part.output);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
for (const m of replayed) {
|
|
1403
|
+
for (const tc of m.toolCalls ?? []) {
|
|
1404
|
+
if (tc.name !== "skill")
|
|
1213
1405
|
continue;
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
name
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1406
|
+
try {
|
|
1407
|
+
const name = JSON.parse(tc.arguments || "{}").name;
|
|
1408
|
+
if (!name)
|
|
1409
|
+
continue;
|
|
1410
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1411
|
+
kind: "skill_activated",
|
|
1412
|
+
name,
|
|
1413
|
+
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
1414
|
+
});
|
|
1415
|
+
const output = toolResultByCallId.get(tc.id);
|
|
1416
|
+
if (output && !this.knowledgePushedSkills.has(name)) {
|
|
1417
|
+
this.knowledgePushedSkills.add(name);
|
|
1418
|
+
// K1: keyed — the kernel-side upsert is the authoritative dedup, so a wake re-push
|
|
1419
|
+
// of a skill already pinned live can never double-pin (the in-run Set resets with
|
|
1420
|
+
// each runner instance; the key does not).
|
|
1421
|
+
await this.pushKnowledge({ role: "system", content: output, toolCalls: [] }, undefined, { key: `skill:${name}` });
|
|
1422
|
+
}
|
|
1226
1423
|
}
|
|
1424
|
+
catch { /* malformed skill args — skip */ }
|
|
1227
1425
|
}
|
|
1228
|
-
catch { /* malformed skill args — skip */ }
|
|
1229
1426
|
}
|
|
1230
1427
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
task: { goal, criteria },
|
|
1236
|
-
};
|
|
1237
|
-
// P0-A: lower an explicit `runSpec` and/or the `allowedToolIds` profile to the kernel's
|
|
1238
|
-
// `capability_filter`. `allowedToolIds` augments an explicit spec's filter, else synthesizes
|
|
1239
|
-
// a minimal top-level spec carrying just the filter (reuses the existing run_spec wire — no
|
|
1240
|
-
// new ABI). Unset on both ⇒ no run_spec ⇒ no gating (铁律: no config = old behavior).
|
|
1241
|
-
const allowedToolIds = this.opts.allowedToolIds;
|
|
1242
|
-
const hasProfile = allowedToolIds !== undefined && allowedToolIds.length > 0;
|
|
1243
|
-
if (this.opts.runSpec || hasProfile) {
|
|
1244
|
-
const baseSpec = this.opts.runSpec ?? {
|
|
1245
|
-
identity: { agentId: this.opts.agentId ?? "root", sessionId, isSubAgent: false },
|
|
1246
|
-
role: "custom",
|
|
1247
|
-
goal,
|
|
1428
|
+
const sessionStart = Date.now();
|
|
1429
|
+
const startPayload = {
|
|
1430
|
+
kind: "start_run",
|
|
1431
|
+
task: { goal, criteria },
|
|
1248
1432
|
};
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
kind: "
|
|
1271
|
-
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
:
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
// report `reason: "error"` indistinguishable from a crash. Mark it here so the final
|
|
1320
|
-
// classification in the run_terminal emit picks `user_abort`.
|
|
1321
|
-
if (sig.urgency === "critical")
|
|
1322
|
-
this.interrupted = true;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
if (runtime.isTerminal())
|
|
1326
|
-
break;
|
|
1327
|
-
if (action.kind === "call_provider") {
|
|
1328
|
-
// M5 v2.1: top-level auto-pivot at the safe point. If the agent authored sub-workflow(s) via
|
|
1329
|
-
// `start_workflow`, drive each in THIS kernel now (the kernel is in Reason / `call_provider`,
|
|
1330
|
-
// NOT suspended — driving mid-suspend would clobber the single-slot suspend state), inject the
|
|
1331
|
-
// outcome into context, and re-render. Loop-top placement (vs only after `tool_results`) catches
|
|
1332
|
-
// EVERY path to `call_provider` — including resuming after an approval gate — so a queued spec
|
|
1333
|
-
// is never stranded. Drains the queue; fires once per authored batch.
|
|
1334
|
-
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
1335
|
-
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
1433
|
+
// P0-A: lower an explicit `runSpec` and/or the `allowedToolIds` profile to the kernel's
|
|
1434
|
+
// `capability_filter`. `allowedToolIds` augments an explicit spec's filter, else synthesizes
|
|
1435
|
+
// a minimal top-level spec carrying just the filter (reuses the existing run_spec wire — no
|
|
1436
|
+
// new ABI). Unset on both ⇒ no run_spec ⇒ no gating (铁律: no config = old behavior).
|
|
1437
|
+
const allowedToolIds = this.opts.allowedToolIds;
|
|
1438
|
+
const hasProfile = allowedToolIds !== undefined && allowedToolIds.length > 0;
|
|
1439
|
+
if (this.opts.runSpec || hasProfile) {
|
|
1440
|
+
const baseSpec = this.opts.runSpec ?? {
|
|
1441
|
+
identity: { agentId: this.opts.agentId ?? "root", sessionId, isSubAgent: false },
|
|
1442
|
+
role: "custom",
|
|
1443
|
+
goal,
|
|
1444
|
+
};
|
|
1445
|
+
const spec = hasProfile
|
|
1446
|
+
? { ...baseSpec, capabilityFilter: { ...baseSpec.capabilityFilter, allowedIds: allowedToolIds } }
|
|
1447
|
+
: baseSpec;
|
|
1448
|
+
startPayload.run_spec = agentRunSpecToKernel(spec);
|
|
1449
|
+
}
|
|
1450
|
+
// Reserve capacity before start_run. The kernel enforces only this vehicle's grant and reports
|
|
1451
|
+
// exact terminal usage against the same opaque reservation identity.
|
|
1452
|
+
if (this.opts.runGroup) {
|
|
1453
|
+
const g = this.opts.runGroup;
|
|
1454
|
+
groupBudgetScope = await GroupBudgetScope.open(g, { sessionId, role: this.opts.agentId, kind: "vehicle" }, this.groupBudgetRequest());
|
|
1455
|
+
this.activeGroupBudgetScope = groupBudgetScope;
|
|
1456
|
+
}
|
|
1457
|
+
await this.applyKernelPolicies(runtime, groupBudgetScope);
|
|
1458
|
+
// Multimodal upload: seed the user's attachments (images/audio) as a history
|
|
1459
|
+
// message before start_run pushes the "[TASK STATE]" anchor. init_task does not
|
|
1460
|
+
// clear history, so order becomes [attachment user msg, "Proceed…"] — both land
|
|
1461
|
+
// in the first render. On resume the message is already in the replayed history.
|
|
1462
|
+
if (!resumeMidRun && attachments?.length) {
|
|
1463
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1464
|
+
kind: "add_history_message",
|
|
1465
|
+
message: attachmentsToKernelMessage(attachments),
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
// I4: pre-fetch memory before the first LLM turn so the model sees it on turn 1 instead of
|
|
1469
|
+
// discovering it via the `memory` tool on turn 3+. Skipped on resumes (already in prior
|
|
1470
|
+
// context) and when dreamStore/agentId is absent.
|
|
1471
|
+
//
|
|
1472
|
+
// Strict dynamic context control: this is single-use retrieval content (facts relevant to
|
|
1473
|
+
// THIS run's goal right now), not a stable method/skill — so it lands in `history` as an
|
|
1474
|
+
// ordinary turn, exactly like a real `memory` tool result would, and decays with the
|
|
1475
|
+
// compression pyramid over subsequent turns instead of pinning itself in `knowledge` forever.
|
|
1476
|
+
this.currentGoal = goal;
|
|
1477
|
+
if (!resumeMidRun) {
|
|
1478
|
+
await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
1479
|
+
}
|
|
1480
|
+
let action = resumeMidRun
|
|
1481
|
+
? await this.commitKernelAction(runtime, this.pendingObservations, { kind: "resume" })
|
|
1482
|
+
: await this.commitKernelAction(runtime, this.pendingObservations, startPayload);
|
|
1483
|
+
// P0-C: the skill loaded and in effect going into the current turn (updated when the model's
|
|
1484
|
+
// `skill` tool call resolves). Drives the per-turn `activeSkill` metric → dwell measurement.
|
|
1485
|
+
let activeSkill;
|
|
1486
|
+
// I0b: wrap the main loop so any uncaught kernel exception (typically a NAPI
|
|
1487
|
+
// Status::InvalidArg from a malformed input — e.g. RuntimeSignal.source with a wrong shape,
|
|
1488
|
+
// or an unrecognized event kind) is observable rather than silently propagating out of the
|
|
1489
|
+
// async generator. Without this wrap the runner emits no `run_terminal` event, so downstream
|
|
1490
|
+
// observability (session log, bench mechanism hooks) can't distinguish "the kernel rejected
|
|
1491
|
+
// an input" from "the run is still in progress."
|
|
1492
|
+
try {
|
|
1493
|
+
while (!runtime.isTerminal()) {
|
|
1494
|
+
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart, taskScope);
|
|
1495
|
+
this.nextArchiveStart = nextCompressedArchiveStart;
|
|
1496
|
+
if (this.interrupted) {
|
|
1497
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1498
|
+
kind: "cancel_operation",
|
|
1499
|
+
reason: this.cancellationReason ?? "user",
|
|
1500
|
+
pending_call_ids: pendingCallIds(action),
|
|
1501
|
+
});
|
|
1502
|
+
break;
|
|
1336
1503
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
const { allowed, denied } = governanceFilterSchema(tools, this.opts.governancePolicy);
|
|
1348
|
-
if (denied.length > 0) {
|
|
1349
|
-
tools = allowed;
|
|
1350
|
-
const note = `[governance] the following tools are denied for this run and will fail if called: ${denied.join(", ")}.`;
|
|
1351
|
-
context = {
|
|
1352
|
-
...context,
|
|
1353
|
-
systemKnowledge: context.systemKnowledge
|
|
1354
|
-
? `${context.systemKnowledge}\n\n${note}`
|
|
1355
|
-
: note,
|
|
1356
|
-
};
|
|
1504
|
+
if (this.opts.signalSource || this.injectedSignals.length > 0) {
|
|
1505
|
+
const delivery = await this.nextInboundSignal();
|
|
1506
|
+
if (delivery) {
|
|
1507
|
+
// Kernel-routed: the kernel decides disposition (dedup/queue/interrupt) and emits
|
|
1508
|
+
// `signal_delivery_disposed`. An actionable disposition yields a new action to adopt; queued/observed/
|
|
1509
|
+
// ignored yields none (kernel buffers).
|
|
1510
|
+
const sigAction = await this.consumeInboundSignal(delivery, sig => this.commitKernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig)));
|
|
1511
|
+
if (sigAction)
|
|
1512
|
+
action = sigAction;
|
|
1513
|
+
// A critical signal is a kernel attention/preemption decision, not operation cancellation.
|
|
1357
1514
|
}
|
|
1358
1515
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1516
|
+
if (runtime.isTerminal())
|
|
1517
|
+
break;
|
|
1518
|
+
if (action.kind === "call_provider") {
|
|
1519
|
+
// M5 v2.1: top-level auto-pivot at the safe point. If the agent authored sub-workflow(s) via
|
|
1520
|
+
// `start_workflow`, drive each in THIS kernel now (the kernel is in Reason / `call_provider`,
|
|
1521
|
+
// NOT suspended — driving mid-suspend would clobber the single-slot suspend state), inject the
|
|
1522
|
+
// outcome into context, and re-render. Loop-top placement (vs only after `tool_results`) catches
|
|
1523
|
+
// EVERY path to `call_provider` — including resuming after an approval gate — so a queued spec
|
|
1524
|
+
// is never stranded. Drains the queue; fires once per authored batch.
|
|
1525
|
+
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
1526
|
+
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
1527
|
+
}
|
|
1528
|
+
const providerEffectId = action.effectId;
|
|
1529
|
+
const finalToolCalls = [];
|
|
1530
|
+
let finalText = "";
|
|
1531
|
+
// I5: governance schema-level pre-filter. When a declarative GovernancePolicy is loaded
|
|
1532
|
+
// and `surfaceDeniedInSystem !== false`, drop denied tools from the schema BEFORE the
|
|
1533
|
+
// model sees them — the model can't plan a call it doesn't know about, so the rollback
|
|
1534
|
+
// overhead disappears. The list of denied names is appended to systemKnowledge so the
|
|
1535
|
+
// model knows not to plan around them.
|
|
1536
|
+
let context = action.context;
|
|
1537
|
+
let tools = action.tools;
|
|
1538
|
+
if (this.opts.governancePolicy && this.opts.governancePolicy.surfaceDeniedInSystem !== false) {
|
|
1539
|
+
const { allowed, denied } = governanceFilterSchema(tools, this.opts.governancePolicy);
|
|
1540
|
+
if (denied.length > 0) {
|
|
1541
|
+
tools = allowed;
|
|
1542
|
+
const note = `[governance] the following tools are denied for this run and will fail if called: ${denied.join(", ")}.`;
|
|
1543
|
+
context = {
|
|
1544
|
+
...context,
|
|
1545
|
+
systemKnowledge: context.systemKnowledge
|
|
1546
|
+
? `${context.systemKnowledge}\n\n${note}`
|
|
1547
|
+
: note,
|
|
1548
|
+
};
|
|
1390
1549
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1550
|
+
}
|
|
1551
|
+
let turnTokens = 0;
|
|
1552
|
+
let turnInputTokens = 0;
|
|
1553
|
+
let turnOutputTokens = 0;
|
|
1554
|
+
let turnCacheReadTokens = 0;
|
|
1555
|
+
let turnCacheCreationTokens = 0;
|
|
1556
|
+
let turnCacheReadBySlot;
|
|
1557
|
+
let turnStopReason;
|
|
1558
|
+
const abortSignal = this.abortController?.signal;
|
|
1559
|
+
try {
|
|
1560
|
+
for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState, abortSignal)) {
|
|
1561
|
+
// #2-B-ii: a preempting `interrupt()` fires `abortController` — stop consuming the live
|
|
1562
|
+
// stream immediately (providers that forward `signal` also abort the socket; the rest at
|
|
1563
|
+
// least stop here at the next event). The loop-top `interrupted` check then ends the run.
|
|
1564
|
+
if (abortSignal?.aborted)
|
|
1565
|
+
break;
|
|
1566
|
+
if (evt.type === "usage") {
|
|
1567
|
+
const usageEvt = evt;
|
|
1568
|
+
turnTokens = usageEvt.totalTokens;
|
|
1569
|
+
turnInputTokens = usageEvt.inputTokens ?? 0;
|
|
1570
|
+
turnOutputTokens = usageEvt.outputTokens ?? 0;
|
|
1571
|
+
// P0-C: capture the prompt-cache split for the tool-gating hit-rate baseline.
|
|
1572
|
+
turnCacheReadTokens = usageEvt.cacheReadInputTokens ?? 0;
|
|
1573
|
+
turnCacheCreationTokens = usageEvt.cacheCreationInputTokens ?? 0;
|
|
1574
|
+
// I1: per-slot attribution forwarded into TurnMetrics. Undefined when the provider
|
|
1575
|
+
// doesn't honor cache_control (OpenAI-family auto-cache).
|
|
1576
|
+
turnCacheReadBySlot = usageEvt.cacheReadInputTokensBySlot;
|
|
1577
|
+
// Phase 4: stop_reason drives the kernel's max-output-tokens recovery. The closing
|
|
1578
|
+
// usage frame carries it; keep the last non-empty value seen this turn.
|
|
1579
|
+
if (usageEvt.stopReason)
|
|
1580
|
+
turnStopReason = usageEvt.stopReason;
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
yield evt;
|
|
1584
|
+
if (evt.type === "text_delta")
|
|
1585
|
+
finalText += evt.delta;
|
|
1586
|
+
else if (evt.type === "tool_call") {
|
|
1587
|
+
const tc = evt;
|
|
1588
|
+
finalToolCalls.push({ id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) });
|
|
1589
|
+
}
|
|
1397
1590
|
}
|
|
1398
1591
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1592
|
+
catch (err) {
|
|
1593
|
+
if (abortSignal?.aborted) {
|
|
1594
|
+
// External I/O is already stopped; the post-stream branch commits cancellation.
|
|
1595
|
+
this.interrupted = true;
|
|
1596
|
+
this.cancellationReason ??= "user";
|
|
1597
|
+
}
|
|
1598
|
+
else {
|
|
1599
|
+
// Reactive recovery is now a kernel decision. Forward the raw provider error and
|
|
1600
|
+
// dispatch whatever the kernel returns: `call_provider` to retry with a freshly
|
|
1601
|
+
// compacted context, or `done` to terminate with an honest `ContextOverflow`. The
|
|
1602
|
+
// classify + compact + retry + give-up policy lives in the kernel (one place), not
|
|
1603
|
+
// duplicated across the four SDK runners. `continue` re-enters the loop: a recovered
|
|
1604
|
+
// turn persists its compaction archive via the loop-top appendObservations, and a
|
|
1605
|
+
// terminal `done` exits through `isTerminal()` into the run_terminal emit below.
|
|
1606
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1607
|
+
kind: "provider_error",
|
|
1608
|
+
effect_id: providerEffectId,
|
|
1609
|
+
message: formatToolError(err),
|
|
1610
|
+
});
|
|
1611
|
+
// Withholding (query.ts parity): surface the raw provider error only when the kernel
|
|
1612
|
+
// could NOT recover (it returned a terminal). On a recovered retry (`call_provider`)
|
|
1613
|
+
// the error stays hidden, so embedders that terminate on `error` events don't see a
|
|
1614
|
+
// phantom failure mid-recovery.
|
|
1615
|
+
if (action.kind === "done") {
|
|
1616
|
+
yield { type: "error", message: formatToolError(err) };
|
|
1617
|
+
}
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1406
1620
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
// turn persists its compaction archive via the loop-top appendObservations, and a
|
|
1414
|
-
// terminal `done` exits through `isTerminal()` into the run_terminal emit below.
|
|
1415
|
-
action = kernelAction(runtime, this.pendingObservations, {
|
|
1416
|
-
kind: "provider_error",
|
|
1417
|
-
message: formatToolError(err),
|
|
1621
|
+
// Do not commit partial provider output after host cancellation.
|
|
1622
|
+
if (abortSignal?.aborted) {
|
|
1623
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1624
|
+
kind: "cancel_operation",
|
|
1625
|
+
reason: this.cancellationReason ?? "user",
|
|
1626
|
+
pending_call_ids: [providerEffectId],
|
|
1418
1627
|
});
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1628
|
+
break;
|
|
1629
|
+
}
|
|
1630
|
+
const assistantMessage = {
|
|
1631
|
+
role: "assistant",
|
|
1632
|
+
content: finalText,
|
|
1633
|
+
toolCalls: finalToolCalls,
|
|
1634
|
+
tokenCount: turnOutputTokens || turnTokens || undefined,
|
|
1635
|
+
};
|
|
1636
|
+
const providerEvent = {
|
|
1637
|
+
kind: "provider_result",
|
|
1638
|
+
effect_id: providerEffectId,
|
|
1639
|
+
message: messageToKernelMessage(assistantMessage),
|
|
1640
|
+
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
1641
|
+
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
1642
|
+
now_ms: Date.now(),
|
|
1643
|
+
...(turnStopReason ? { stop_reason: turnStopReason } : {}),
|
|
1644
|
+
};
|
|
1645
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
|
|
1646
|
+
const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
|
|
1647
|
+
await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
|
|
1648
|
+
turn: runtime.turn(),
|
|
1649
|
+
content: finalText,
|
|
1650
|
+
tokenCount: turnOutputTokens || turnTokens || undefined,
|
|
1651
|
+
toolCalls: finalToolCalls,
|
|
1652
|
+
providerReplay,
|
|
1653
|
+
}));
|
|
1654
|
+
// P0-C: emit per-turn tool-gating telemetry. `activeSkill` reflects the skill in effect
|
|
1655
|
+
// GOING INTO this turn; a `skill` call here only takes effect next turn, so emit first, then
|
|
1656
|
+
// advance. Wrapped so a faulty sink can never break the run (pure observation).
|
|
1657
|
+
if (this.opts.onTurnMetrics) {
|
|
1658
|
+
try {
|
|
1659
|
+
this.opts.onTurnMetrics({
|
|
1660
|
+
turn: runtime.turn(),
|
|
1661
|
+
toolsExposed: tools.length,
|
|
1662
|
+
toolsCalled: finalToolCalls.length,
|
|
1663
|
+
activeSkill,
|
|
1664
|
+
inputTokens: turnInputTokens,
|
|
1665
|
+
cacheReadTokens: turnCacheReadTokens,
|
|
1666
|
+
cacheCreationTokens: turnCacheCreationTokens,
|
|
1667
|
+
...(turnCacheReadBySlot ? { cacheReadTokensBySlot: turnCacheReadBySlot } : {}),
|
|
1668
|
+
});
|
|
1425
1669
|
}
|
|
1426
|
-
|
|
1670
|
+
catch { /* metrics must never break the run */ }
|
|
1671
|
+
}
|
|
1672
|
+
const skillCall = finalToolCalls.find(c => c.name === "skill");
|
|
1673
|
+
if (skillCall) {
|
|
1674
|
+
try {
|
|
1675
|
+
const name = JSON.parse(skillCall.arguments || "{}").name;
|
|
1676
|
+
if (name)
|
|
1677
|
+
activeSkill = name;
|
|
1678
|
+
}
|
|
1679
|
+
catch { /* malformed skill args — leave activeSkill unchanged */ }
|
|
1427
1680
|
}
|
|
1428
1681
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
action =
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
const assistantMessage = {
|
|
1437
|
-
role: "assistant",
|
|
1438
|
-
content: finalText,
|
|
1439
|
-
toolCalls: finalToolCalls,
|
|
1440
|
-
tokenCount: turnOutputTokens || turnTokens || undefined,
|
|
1441
|
-
};
|
|
1442
|
-
const providerEvent = {
|
|
1443
|
-
kind: "provider_result",
|
|
1444
|
-
message: messageToKernelMessage(assistantMessage),
|
|
1445
|
-
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
1446
|
-
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
1447
|
-
now_ms: Date.now(),
|
|
1448
|
-
...(turnStopReason ? { stop_reason: turnStopReason } : {}),
|
|
1449
|
-
};
|
|
1450
|
-
let nextAction = kernelMaybeAction(runtime, this.pendingObservations, providerEvent);
|
|
1451
|
-
if (!nextAction && this.pendingObservations.some(o => o.kind === "suspended")) {
|
|
1452
|
-
const resolved = await this.resolveKernelSuspend(runtime, sessionId);
|
|
1453
|
-
for (const evt of resolved.events)
|
|
1454
|
-
yield evt;
|
|
1455
|
-
nextAction = kernelAction(runtime, this.pendingObservations, {
|
|
1456
|
-
kind: "resume",
|
|
1682
|
+
else if (action.kind === "request_approval") {
|
|
1683
|
+
const resolved = await this.resolveApprovalRequests(action.requests, runtime, sessionId);
|
|
1684
|
+
for (const event of resolved.events)
|
|
1685
|
+
yield event;
|
|
1686
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1687
|
+
kind: "approval_result",
|
|
1688
|
+
effect_id: action.effectId,
|
|
1457
1689
|
approved_calls: resolved.approved,
|
|
1458
1690
|
denied_calls: resolved.denied,
|
|
1459
1691
|
});
|
|
1460
1692
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
turn: runtime.turn(),
|
|
1465
|
-
content: finalText,
|
|
1466
|
-
tokenCount: turnOutputTokens || turnTokens || undefined,
|
|
1467
|
-
toolCalls: finalToolCalls,
|
|
1468
|
-
providerReplay,
|
|
1469
|
-
}));
|
|
1470
|
-
// P0-C: emit per-turn tool-gating telemetry. `activeSkill` reflects the skill in effect
|
|
1471
|
-
// GOING INTO this turn; a `skill` call here only takes effect next turn, so emit first, then
|
|
1472
|
-
// advance. Wrapped so a faulty sink can never break the run (pure observation).
|
|
1473
|
-
if (this.opts.onTurnMetrics) {
|
|
1693
|
+
else if (action.kind === "persist_memory") {
|
|
1694
|
+
let error;
|
|
1695
|
+
const agentId = this.opts.agentId;
|
|
1474
1696
|
try {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
cacheReadTokens: turnCacheReadTokens,
|
|
1482
|
-
cacheCreationTokens: turnCacheCreationTokens,
|
|
1483
|
-
...(turnCacheReadBySlot ? { cacheReadTokensBySlot: turnCacheReadBySlot } : {}),
|
|
1484
|
-
});
|
|
1697
|
+
if (!agentId)
|
|
1698
|
+
throw new Error("memory persistence requires RuntimeOptions.agentId");
|
|
1699
|
+
await this.persistMemoryToStore(action.memory, agentId);
|
|
1700
|
+
}
|
|
1701
|
+
catch (cause) {
|
|
1702
|
+
error = formatToolError(cause);
|
|
1485
1703
|
}
|
|
1486
|
-
|
|
1704
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1705
|
+
kind: "memory_persist_result",
|
|
1706
|
+
effect_id: action.effectId,
|
|
1707
|
+
...(error ? { error } : {}),
|
|
1708
|
+
});
|
|
1487
1709
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1710
|
+
else if (action.kind === "query_memory") {
|
|
1711
|
+
const query = action.query;
|
|
1712
|
+
let hits = [];
|
|
1713
|
+
let error;
|
|
1714
|
+
const agentId = this.opts.agentId;
|
|
1490
1715
|
try {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1716
|
+
if (!agentId)
|
|
1717
|
+
throw new Error("memory queries require RuntimeOptions.agentId");
|
|
1718
|
+
hits = await this.retrieveMemoryFromStore(query, action.requestedK, agentId);
|
|
1494
1719
|
}
|
|
1495
|
-
catch {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
skillDir: this.opts.skillDir,
|
|
1504
|
-
dreamStore: this.opts.dreamStore,
|
|
1505
|
-
knowledgeSource: this.opts.knowledgeSource,
|
|
1506
|
-
onToolSuspend: this.opts.onToolSuspend,
|
|
1507
|
-
onPermissionRequest: this.opts.onPermissionRequest,
|
|
1508
|
-
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
1509
|
-
};
|
|
1510
|
-
const toolResults = [];
|
|
1511
|
-
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow"
|
|
1512
|
-
&& c.name !== "read_result");
|
|
1513
|
-
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
1514
|
-
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path as
|
|
1515
|
-
// `submit_workflow_nodes` — a `WorkflowSpec` is a node batch. (v2 adds top-level bootstrap.)
|
|
1516
|
-
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
1517
|
-
// O7: `read_result` re-fetches a tool output the kernel evicted from context. Content is
|
|
1518
|
-
// host-resolved: (a) this turn's in-memory pending spool map, (b) the on-disk result spool
|
|
1519
|
-
// (persisted once the kernel observes `large_result_spooled`), (c) a session-log scan for
|
|
1520
|
-
// the original `tool_completed` event. The kernel only advertises the capability.
|
|
1521
|
-
const readResultCalls = allCalls.filter(c => c.name === "read_result");
|
|
1522
|
-
for (const call of planCalls) {
|
|
1523
|
-
const update = parseUpdatePlanArgs(call.arguments);
|
|
1524
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1525
|
-
kind: "update_task",
|
|
1526
|
-
update: taskUpdateToKernel(update),
|
|
1720
|
+
catch (cause) {
|
|
1721
|
+
error = formatToolError(cause);
|
|
1722
|
+
}
|
|
1723
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1724
|
+
kind: "memory_query_result",
|
|
1725
|
+
effect_id: action.effectId,
|
|
1726
|
+
hits,
|
|
1727
|
+
...(error ? { error } : {}),
|
|
1527
1728
|
});
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
1729
|
+
if (!error)
|
|
1730
|
+
await this.logMemoryRetrievalResult(sessionId, hits);
|
|
1531
1731
|
}
|
|
1532
|
-
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
// simply unconsumed — a no-op.)
|
|
1542
|
-
for (const call of submitCalls) {
|
|
1543
|
-
// M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
|
|
1544
|
-
// full spec and AUTO-PIVOT once this tool turn resolves (the loop drives it in this kernel and
|
|
1545
|
-
// injects the outcome). A workflow-NODE's `start_workflow` (and every `submit_workflow_nodes`)
|
|
1546
|
-
// instead FLATTENS: the batch is surfaced for the parent `runWorkflow` to append.
|
|
1547
|
-
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
1548
|
-
const spec = parseStartWorkflowSpec(call.arguments);
|
|
1549
|
-
if (spec) {
|
|
1550
|
-
this.pendingAuthoredWorkflows.push(spec);
|
|
1551
|
-
const out = "workflow authored; executing now";
|
|
1552
|
-
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
1553
|
-
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
1554
|
-
continue;
|
|
1555
|
-
}
|
|
1732
|
+
else if (action.kind === "spool_large_result") {
|
|
1733
|
+
const spool = this.opts.resultSpool ?? new LargeResultSpool();
|
|
1734
|
+
let spoolRef;
|
|
1735
|
+
let error;
|
|
1736
|
+
try {
|
|
1737
|
+
spoolRef = await spool.persistOutput(action.callId, action.output);
|
|
1738
|
+
}
|
|
1739
|
+
catch (cause) {
|
|
1740
|
+
error = formatToolError(cause);
|
|
1556
1741
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
:
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
toolResults.push(result);
|
|
1564
|
-
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
1742
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1743
|
+
kind: "large_result_spool_result",
|
|
1744
|
+
effect_id: action.effectId,
|
|
1745
|
+
...(spoolRef ? { spool_ref: spoolRef } : {}),
|
|
1746
|
+
...(error ? { error } : {}),
|
|
1747
|
+
});
|
|
1565
1748
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1749
|
+
else if (action.kind === "archive_page_out") {
|
|
1750
|
+
const archiveMeta = this.activePageOutArchive
|
|
1751
|
+
?? this.pendingPageOutArchives.shift()
|
|
1752
|
+
?? {
|
|
1753
|
+
archiveStart: this.nextArchiveStart,
|
|
1754
|
+
compressedSeq: await this.opts.sessionLog.latestSeq(sessionId),
|
|
1755
|
+
};
|
|
1756
|
+
this.activePageOutArchive = archiveMeta;
|
|
1757
|
+
let archiveRef;
|
|
1758
|
+
let error;
|
|
1759
|
+
try {
|
|
1760
|
+
if (this.opts.compressionStore) {
|
|
1761
|
+
const ref = await this.opts.compressionStore.write(sessionId, archiveMeta.archiveStart, action.archived);
|
|
1762
|
+
if (ref)
|
|
1763
|
+
archiveRef = ref;
|
|
1576
1764
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1765
|
+
}
|
|
1766
|
+
catch (cause) {
|
|
1767
|
+
error = formatToolError(cause);
|
|
1768
|
+
}
|
|
1769
|
+
const archived = action.archived;
|
|
1770
|
+
const archiveAction = compressionAction(action.action) ?? "auto_compact";
|
|
1771
|
+
const archiveTier = action.tier;
|
|
1772
|
+
const compressedSeq = archiveMeta.compressedSeq;
|
|
1773
|
+
if (!error)
|
|
1774
|
+
this.activePageOutArchive = undefined;
|
|
1775
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1776
|
+
kind: "page_out_archive_result",
|
|
1777
|
+
effect_id: action.effectId,
|
|
1778
|
+
...(archiveRef ? { archive_ref: archiveRef } : {}),
|
|
1779
|
+
...(error ? { error } : {}),
|
|
1780
|
+
});
|
|
1781
|
+
if (!error) {
|
|
1782
|
+
if (this.opts.asyncSummarizer && archived.length > 0) {
|
|
1783
|
+
const upgrade = () => this.upgradeCompressedSummary(sessionId, compressedSeq, archived, archiveAction);
|
|
1784
|
+
taskScope.spawn("compressed-summary-upgrade", upgrade);
|
|
1579
1785
|
}
|
|
1580
|
-
if (
|
|
1581
|
-
|
|
1582
|
-
yield { type: "tool_denied", callId: call.id, toolName: call.name, reason };
|
|
1583
|
-
await this.opts.sessionLog.append(sessionId, {
|
|
1584
|
-
kind: "tool_denied", turn: runtime.turn(), call_id: call.id, tool_name: call.name, reason,
|
|
1585
|
-
});
|
|
1586
|
-
const out = `blocked by host hook: ${reason}`;
|
|
1587
|
-
toolResults.push({ callId: call.id, output: out, isError: true, errorKind: "governance_denied" });
|
|
1588
|
-
yield { type: "tool_result", callId: call.id, name: call.name, content: out, isError: true };
|
|
1589
|
-
continue;
|
|
1786
|
+
if (archiveTier === "semantic" && archived.length > 0) {
|
|
1787
|
+
taskScope.spawn("semantic-page-out", () => this.archiveSemanticPageOut(archived, archiveAction, sessionId));
|
|
1590
1788
|
}
|
|
1591
|
-
allowed.push(call);
|
|
1592
1789
|
}
|
|
1593
|
-
executableCalls = allowed;
|
|
1594
1790
|
}
|
|
1595
|
-
if (
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1791
|
+
else if (action.kind === "execute_tool") {
|
|
1792
|
+
const toolEffectId = action.effectId;
|
|
1793
|
+
const allCalls = action.calls;
|
|
1794
|
+
await this.opts.sessionLog.append(sessionId, { kind: "tool_requested", turn: runtime.turn(), calls: allCalls });
|
|
1795
|
+
const runCtx = {
|
|
1796
|
+
operation,
|
|
1797
|
+
agentId: this.opts.agentId,
|
|
1798
|
+
memoryScope: this.opts.memoryScope,
|
|
1799
|
+
skillDir: this.opts.skillDir,
|
|
1800
|
+
dreamStore: this.opts.dreamStore,
|
|
1801
|
+
knowledgeSource: this.opts.knowledgeSource,
|
|
1802
|
+
onToolSuspend: this.opts.onToolSuspend,
|
|
1803
|
+
onPermissionRequest: this.opts.onPermissionRequest,
|
|
1804
|
+
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
1805
|
+
};
|
|
1806
|
+
const toolResults = [];
|
|
1807
|
+
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow"
|
|
1808
|
+
&& c.name !== "read_result");
|
|
1809
|
+
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
1810
|
+
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path as
|
|
1811
|
+
// `submit_workflow_nodes` — a `WorkflowSpec` is a node batch. (v2 adds top-level bootstrap.)
|
|
1812
|
+
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
1813
|
+
// O7: `read_result` re-fetches a tool output the kernel evicted from context. Content is
|
|
1814
|
+
// host-resolved from the effect-committed spool, then from the durable session log.
|
|
1815
|
+
const readResultCalls = allCalls.filter(c => c.name === "read_result");
|
|
1816
|
+
for (const call of planCalls) {
|
|
1817
|
+
const update = parseUpdatePlanArgs(call.arguments);
|
|
1818
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1819
|
+
kind: "update_task",
|
|
1820
|
+
update: taskUpdateToKernel(update),
|
|
1821
|
+
});
|
|
1822
|
+
const result = { callId: call.id, output: "success", isError: false };
|
|
1823
|
+
toolResults.push(result);
|
|
1824
|
+
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
1825
|
+
}
|
|
1826
|
+
for (const call of readResultCalls) {
|
|
1827
|
+
const out = await this.resolveReadResult(sessionId, call.arguments);
|
|
1828
|
+
toolResults.push({ callId: call.id, output: out.text, isError: out.isError });
|
|
1829
|
+
yield { type: "tool_result", callId: call.id, content: out.text, isError: out.isError };
|
|
1830
|
+
}
|
|
1831
|
+
// R3-1: `submit_workflow_nodes` cannot be applied to this runner's kernel — when this runner
|
|
1832
|
+
// is a workflow node, the workflow lives in the *parent* kernel. Surface the requested nodes
|
|
1833
|
+
// as a stream event; the orchestrator collects them onto the node's result and `runWorkflow`
|
|
1834
|
+
// sends `submit_workflow_nodes` to the parent kernel. (When not a workflow node, the event is
|
|
1835
|
+
// simply unconsumed — a no-op.)
|
|
1836
|
+
for (const call of submitCalls) {
|
|
1837
|
+
// M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
|
|
1838
|
+
// full spec and AUTO-PIVOT once this tool turn resolves (the loop drives it in this kernel and
|
|
1839
|
+
// injects the outcome). A workflow-NODE's `start_workflow` (and every `submit_workflow_nodes`)
|
|
1840
|
+
// instead FLATTENS: the batch is surfaced for the parent `runWorkflow` to append.
|
|
1841
|
+
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
1842
|
+
const spec = parseStartWorkflowSpec(call.arguments);
|
|
1843
|
+
if (spec) {
|
|
1844
|
+
this.pendingAuthoredWorkflows.push(spec);
|
|
1845
|
+
const out = "workflow authored; executing now";
|
|
1846
|
+
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
1847
|
+
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
1848
|
+
continue;
|
|
1849
|
+
}
|
|
1617
1850
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1851
|
+
// `start_workflow` wraps the batch as `{ spec: { nodes } }`; `submit_workflow_nodes` is `{ nodes }`.
|
|
1852
|
+
const nodes = call.name === "start_workflow"
|
|
1853
|
+
? parseStartWorkflowArgs(call.arguments)
|
|
1854
|
+
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
1855
|
+
yield { type: "workflow_nodes_submitted", nodes };
|
|
1856
|
+
const result = { callId: call.id, output: "submitted", isError: false };
|
|
1857
|
+
toolResults.push(result);
|
|
1858
|
+
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
1859
|
+
}
|
|
1860
|
+
// O5 (PreToolUse-hook analog): give the host a STATEFUL veto over each kernel-approved
|
|
1861
|
+
// call. A blocked call never executes; its reason reaches the model as a governance-denied
|
|
1862
|
+
// tool result (the kernel rolls the turn back with the note). Decision failures are closed
|
|
1863
|
+
// unless the host explicitly marks this hook advisory with `onToolCallFailure: "open"`.
|
|
1864
|
+
let executableCalls = normalCalls;
|
|
1865
|
+
if (this.opts.onToolCall) {
|
|
1866
|
+
const allowed = [];
|
|
1867
|
+
for (const call of normalCalls) {
|
|
1868
|
+
let decision;
|
|
1869
|
+
try {
|
|
1870
|
+
decision = await this.opts.onToolCall({ callId: call.id, name: call.name, arguments: call.arguments });
|
|
1871
|
+
}
|
|
1872
|
+
catch (cause) {
|
|
1873
|
+
decision = this.opts.onToolCallFailure === "open"
|
|
1874
|
+
? undefined
|
|
1875
|
+
: { block: true, reason: `onToolCall hook failed: ${formatToolError(cause)}` };
|
|
1876
|
+
}
|
|
1877
|
+
if (decision?.block) {
|
|
1878
|
+
const reason = decision.reason ?? "blocked by host onToolCall hook";
|
|
1879
|
+
yield { type: "tool_denied", callId: call.id, toolName: call.name, reason };
|
|
1880
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1881
|
+
kind: "tool_denied", turn: runtime.turn(), call_id: call.id, tool_name: call.name, reason,
|
|
1882
|
+
});
|
|
1883
|
+
const out = `blocked by host hook: ${reason}`;
|
|
1884
|
+
toolResults.push({ callId: call.id, output: out, isError: true, errorKind: "governance_denied" });
|
|
1885
|
+
yield { type: "tool_result", callId: call.id, name: call.name, content: out, isError: true };
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
allowed.push(call);
|
|
1627
1889
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1890
|
+
executableCalls = allowed;
|
|
1891
|
+
}
|
|
1892
|
+
if (executableCalls.length > 0) {
|
|
1893
|
+
for await (const evt of this.opts.executionPlane.executeAll(executableCalls, runCtx)) {
|
|
1894
|
+
yield evt;
|
|
1895
|
+
if (evt.type === "tool_result") {
|
|
1896
|
+
const tre = evt;
|
|
1897
|
+
toolResults.push({
|
|
1898
|
+
callId: tre.callId,
|
|
1899
|
+
output: tre.content,
|
|
1900
|
+
isError: tre.isError,
|
|
1901
|
+
isFatal: tre.isFatal,
|
|
1902
|
+
errorKind: tre.errorKind,
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
else if (evt.type === "tool_argument_repaired") {
|
|
1906
|
+
const tare = evt;
|
|
1907
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1908
|
+
kind: "tool_argument_repaired",
|
|
1909
|
+
turn: runtime.turn(),
|
|
1910
|
+
tool: tare.name,
|
|
1911
|
+
original_arguments: tare.originalArguments,
|
|
1912
|
+
repaired_arguments: tare.repairedArguments,
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
else if (evt.type === "tool_denied") {
|
|
1916
|
+
const tde = evt;
|
|
1917
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1918
|
+
kind: "tool_denied",
|
|
1919
|
+
turn: runtime.turn(),
|
|
1920
|
+
call_id: tde.callId,
|
|
1921
|
+
tool_name: tde.toolName,
|
|
1922
|
+
reason: tde.reason,
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
else if (evt.type === "permission_request") {
|
|
1926
|
+
const pre = evt;
|
|
1927
|
+
const turn = runtime.turn();
|
|
1928
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1929
|
+
kind: "permission_requested",
|
|
1930
|
+
turn,
|
|
1931
|
+
tool: pre.toolName,
|
|
1932
|
+
arguments: pre.arguments,
|
|
1933
|
+
reason: pre.reason,
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
else if (evt.type === "permission_resolved") {
|
|
1937
|
+
const resolved = evt;
|
|
1938
|
+
const turn = runtime.turn();
|
|
1939
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1940
|
+
kind: "permission_resolved",
|
|
1941
|
+
turn,
|
|
1942
|
+
approved: resolved.approved,
|
|
1943
|
+
responder: resolved.responder,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1638
1946
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1947
|
+
const names = executableCalls.map(c => c.name).join(", ");
|
|
1948
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1949
|
+
kind: "update_task",
|
|
1950
|
+
update: taskUpdateToKernel({ progress: `Executed tools: ${names}` }),
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
// O5 (PostToolUse-hook analog): let the host inspect each executed result BEFORE it
|
|
1954
|
+
// reaches the kernel/session-log — replace the output (redact/annotate) and/or push a
|
|
1955
|
+
// contextual note into the signal stream. Errs-open on hook throw.
|
|
1956
|
+
if (this.opts.onToolResult) {
|
|
1957
|
+
for (const r of toolResults) {
|
|
1958
|
+
const call = executableCalls.find(c => c.id === r.callId);
|
|
1959
|
+
if (!call)
|
|
1960
|
+
continue; // plan/submit synthetics and hook-blocked calls are not host results
|
|
1961
|
+
let decision;
|
|
1962
|
+
try {
|
|
1963
|
+
decision = await this.opts.onToolResult({
|
|
1964
|
+
callId: r.callId, name: call.name, arguments: call.arguments,
|
|
1965
|
+
output: r.output, isError: r.isError,
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
catch {
|
|
1969
|
+
decision = undefined;
|
|
1970
|
+
}
|
|
1971
|
+
if (!decision)
|
|
1972
|
+
continue;
|
|
1973
|
+
if (typeof decision.replaceOutput === "string")
|
|
1974
|
+
r.output = decision.replaceOutput;
|
|
1975
|
+
if (decision.note)
|
|
1976
|
+
this.injectNote(decision.note);
|
|
1648
1977
|
}
|
|
1649
1978
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1979
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1980
|
+
kind: "tool_completed",
|
|
1981
|
+
turn: runtime.turn(),
|
|
1982
|
+
results: toolResults.map(r => ({
|
|
1983
|
+
call_id: r.callId,
|
|
1984
|
+
output: r.output,
|
|
1985
|
+
is_error: r.isError,
|
|
1986
|
+
token_count: r.tokenCount,
|
|
1987
|
+
})),
|
|
1654
1988
|
});
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
for
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1989
|
+
// P1-B B3: a `skill` call that resolved successfully activates that skill in the kernel, so
|
|
1990
|
+
// the next `call_provider` narrows the toolset to its declared tools. Fed before `tool_results`
|
|
1991
|
+
// (which computes the next action). Errs-open: a failed/missing skill load doesn't activate.
|
|
1992
|
+
//
|
|
1993
|
+
// Strict dynamic context control: a skill is METHOD content — how to do something — reused
|
|
1994
|
+
// for the rest of the run, unlike a one-off memory/knowledge lookup (fact content, relevant
|
|
1995
|
+
// for the moment it's used). So its text ALSO goes into the durable `knowledge` slot here
|
|
1996
|
+
// (in addition to the ordinary tool_result already headed for `history`, where it will decay
|
|
1997
|
+
// with the compression pyramid like any other tool output — that's fine, the permanent copy
|
|
1998
|
+
// now lives in `knowledge`). First activation only (see `knowledgePushedSkills`).
|
|
1999
|
+
for (const call of allCalls) {
|
|
2000
|
+
if (call.name !== "skill")
|
|
2001
|
+
continue;
|
|
2002
|
+
const res = toolResults.find(r => r.callId === call.id);
|
|
2003
|
+
if (!res || res.isError)
|
|
2004
|
+
continue;
|
|
1665
2005
|
try {
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
2006
|
+
const name = JSON.parse(call.arguments || "{}").name;
|
|
2007
|
+
if (!name)
|
|
2008
|
+
continue;
|
|
2009
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
2010
|
+
kind: "skill_activated",
|
|
2011
|
+
name,
|
|
2012
|
+
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
1669
2013
|
});
|
|
2014
|
+
// K1: keyed `skill:<name>` — the kernel-side upsert dedupes across runner instances
|
|
2015
|
+
// (wake re-push of an already-pinned skill upserts instead of duplicating). With a
|
|
2016
|
+
// lease configured, the Set optimization is skipped: an expired-then-reloaded skill
|
|
2017
|
+
// must re-pin, and only the kernel knows the lease state — its upsert dedupes anyway.
|
|
2018
|
+
if (this.opts.skillLeaseTurns !== undefined || !this.knowledgePushedSkills.has(name)) {
|
|
2019
|
+
this.knowledgePushedSkills.add(name);
|
|
2020
|
+
await this.pushKnowledge({ role: "system", content: res.output, toolCalls: [] }, undefined, { key: `skill:${name}` });
|
|
2021
|
+
}
|
|
1670
2022
|
}
|
|
1671
|
-
catch {
|
|
1672
|
-
decision = undefined;
|
|
1673
|
-
}
|
|
1674
|
-
if (!decision)
|
|
1675
|
-
continue;
|
|
1676
|
-
if (typeof decision.replaceOutput === "string")
|
|
1677
|
-
r.output = decision.replaceOutput;
|
|
1678
|
-
if (decision.note)
|
|
1679
|
-
this.injectNote(decision.note);
|
|
2023
|
+
catch { /* malformed skill args — skip activation */ }
|
|
1680
2024
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2025
|
+
const entropyObsStart = this.pendingObservations.length;
|
|
2026
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
2027
|
+
kind: "tool_results",
|
|
2028
|
+
effect_id: toolEffectId,
|
|
2029
|
+
results: toolResults.map(toolResultToKernel),
|
|
2030
|
+
});
|
|
2031
|
+
// Surface the boundary's entropy measurement live (the heartbeat watch source) —
|
|
2032
|
+
// the session-log record lands via the normal appendObservations path.
|
|
2033
|
+
for (const obs of this.pendingObservations.slice(entropyObsStart)) {
|
|
2034
|
+
if (obs.kind === "entropy_sample") {
|
|
2035
|
+
this.lastEntropySample = entropySampleFromObservation(obs);
|
|
2036
|
+
yield { type: "entropy_sample", sample: this.lastEntropySample };
|
|
2037
|
+
}
|
|
2038
|
+
else if (obs.kind === "entropy_alert") {
|
|
2039
|
+
yield {
|
|
2040
|
+
type: "entropy_alert",
|
|
2041
|
+
turn: obs.turn ?? 0,
|
|
2042
|
+
score: obs.score ?? 0,
|
|
2043
|
+
threshold: obs.threshold ?? 0,
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
1696
2046
|
}
|
|
1697
2047
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
// with the compression pyramid like any other tool output — that's fine, the permanent copy
|
|
1707
|
-
// now lives in `knowledge`). First activation only (see `knowledgePushedSkills`).
|
|
1708
|
-
for (const call of allCalls) {
|
|
1709
|
-
if (call.name !== "skill")
|
|
1710
|
-
continue;
|
|
1711
|
-
const res = toolResults.find(r => r.callId === call.id);
|
|
1712
|
-
if (!res || res.isError)
|
|
1713
|
-
continue;
|
|
1714
|
-
try {
|
|
1715
|
-
const name = JSON.parse(call.arguments || "{}").name;
|
|
1716
|
-
if (!name)
|
|
1717
|
-
continue;
|
|
1718
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1719
|
-
kind: "skill_activated",
|
|
1720
|
-
name,
|
|
1721
|
-
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
2048
|
+
else if (action.kind === "evaluate_milestone") {
|
|
2049
|
+
const milestoneEffectId = action.effectId;
|
|
2050
|
+
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
2051
|
+
if (milestonePolicy === "auto_pass") {
|
|
2052
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
2053
|
+
kind: "milestone_result",
|
|
2054
|
+
effect_id: milestoneEffectId,
|
|
2055
|
+
result: milestoneCheckResultToKernel(milestoneCheckPass(action.phaseId)),
|
|
1722
2056
|
});
|
|
1723
|
-
|
|
1724
|
-
// (wake re-push of an already-pinned skill upserts instead of duplicating). With a
|
|
1725
|
-
// lease configured, the Set optimization is skipped: an expired-then-reloaded skill
|
|
1726
|
-
// must re-pin, and only the kernel knows the lease state — its upsert dedupes anyway.
|
|
1727
|
-
if (this.opts.skillLeaseTurns !== undefined || !this.knowledgePushedSkills.has(name)) {
|
|
1728
|
-
this.knowledgePushedSkills.add(name);
|
|
1729
|
-
this.pushKnowledge({ role: "system", content: res.output, toolCalls: [] }, undefined, { key: `skill:${name}` });
|
|
1730
|
-
}
|
|
2057
|
+
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart, taskScope);
|
|
1731
2058
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
this.
|
|
1744
|
-
yield { type: "entropy_sample", sample: this.lastEntropySample };
|
|
2059
|
+
else if (this.opts.onMilestoneEvaluate) {
|
|
2060
|
+
const check = await this.opts.onMilestoneEvaluate({
|
|
2061
|
+
phaseId: action.phaseId,
|
|
2062
|
+
criteria: action.criteria,
|
|
2063
|
+
requiredEvidence: action.requiredEvidence,
|
|
2064
|
+
});
|
|
2065
|
+
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
2066
|
+
kind: "milestone_result",
|
|
2067
|
+
effect_id: milestoneEffectId,
|
|
2068
|
+
result: milestoneCheckResultToKernel(check),
|
|
2069
|
+
});
|
|
2070
|
+
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart, taskScope);
|
|
1745
2071
|
}
|
|
1746
|
-
else
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
2072
|
+
else {
|
|
2073
|
+
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart, taskScope);
|
|
2074
|
+
const turnsUsed = Math.max(1, runtime.turn());
|
|
2075
|
+
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
2076
|
+
reason: "milestone_pending",
|
|
2077
|
+
turnsUsed,
|
|
2078
|
+
totalTokens: 0,
|
|
2079
|
+
}));
|
|
2080
|
+
await groupBudgetScope?.release();
|
|
2081
|
+
await taskScope.drain();
|
|
2082
|
+
yield { type: "done", iterations: turnsUsed, totalTokens: 0, status: "milestone_pending" };
|
|
2083
|
+
this.activeKernel = null;
|
|
2084
|
+
this.currentSessionId = null;
|
|
2085
|
+
return;
|
|
1753
2086
|
}
|
|
1754
2087
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
1758
|
-
if (milestonePolicy === "auto_pass") {
|
|
1759
|
-
action = kernelAction(runtime, this.pendingObservations, {
|
|
1760
|
-
kind: "milestone_result",
|
|
1761
|
-
result: milestoneCheckResultToKernel(milestoneCheckPass(action.phaseId)),
|
|
1762
|
-
});
|
|
1763
|
-
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart);
|
|
1764
|
-
}
|
|
1765
|
-
else if (this.opts.onMilestoneEvaluate) {
|
|
1766
|
-
const check = await this.opts.onMilestoneEvaluate({
|
|
1767
|
-
phaseId: action.phaseId,
|
|
1768
|
-
criteria: action.criteria,
|
|
1769
|
-
requiredEvidence: action.requiredEvidence,
|
|
1770
|
-
});
|
|
1771
|
-
action = kernelAction(runtime, this.pendingObservations, {
|
|
1772
|
-
kind: "milestone_result",
|
|
1773
|
-
result: milestoneCheckResultToKernel(check),
|
|
1774
|
-
});
|
|
1775
|
-
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart);
|
|
1776
|
-
}
|
|
1777
|
-
else {
|
|
1778
|
-
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart);
|
|
1779
|
-
const turnsUsed = Math.max(1, runtime.turn());
|
|
1780
|
-
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
1781
|
-
reason: "milestone_pending",
|
|
1782
|
-
turnsUsed,
|
|
1783
|
-
totalTokens: 0,
|
|
1784
|
-
}));
|
|
1785
|
-
yield { type: "done", iterations: turnsUsed, totalTokens: 0, status: "milestone_pending" };
|
|
1786
|
-
this.activeKernel = null;
|
|
1787
|
-
this.currentSessionId = null;
|
|
1788
|
-
return;
|
|
2088
|
+
else if (action.kind === "done") {
|
|
2089
|
+
break;
|
|
1789
2090
|
}
|
|
1790
2091
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
2092
|
+
}
|
|
2093
|
+
catch (err) {
|
|
2094
|
+
// I0b: kernel rejection (or any other thrown error inside the loop) reaches us here.
|
|
2095
|
+
// Classify by NAPI status code or message pattern — `invalid_arg` for surface-shape rejects,
|
|
2096
|
+
// `error` for everything else — then emit run_terminal so observability sees a clean end.
|
|
2097
|
+
// The yield-error path mirrors what the in-flight provider-stream catch does.
|
|
2098
|
+
const errMsg = formatToolError(err);
|
|
2099
|
+
const code = err.code;
|
|
2100
|
+
const isInvalidArg = code === "InvalidArg" ||
|
|
2101
|
+
errMsg.toLowerCase().includes("invalidarg") ||
|
|
2102
|
+
errMsg.toLowerCase().includes("invalid argument");
|
|
2103
|
+
const reason = isInvalidArg ? "invalid_arg" : "error";
|
|
2104
|
+
yield { type: "error", message: errMsg };
|
|
2105
|
+
try {
|
|
2106
|
+
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
2107
|
+
reason,
|
|
2108
|
+
turnsUsed: runtime.turn() || 0,
|
|
2109
|
+
totalTokens: 0,
|
|
2110
|
+
}));
|
|
1793
2111
|
}
|
|
2112
|
+
catch { /* session log failure must not mask the original error */ }
|
|
2113
|
+
await groupBudgetScope?.release();
|
|
2114
|
+
await taskScope.drain();
|
|
2115
|
+
yield { type: "done", iterations: runtime.turn() || 0, totalTokens: 0, status: reason };
|
|
2116
|
+
this.activeKernel = null;
|
|
2117
|
+
this.currentSessionId = null;
|
|
2118
|
+
this.dashboard = null;
|
|
2119
|
+
return;
|
|
1794
2120
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
//
|
|
1798
|
-
//
|
|
1799
|
-
//
|
|
1800
|
-
//
|
|
1801
|
-
|
|
1802
|
-
const
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2121
|
+
const result = action.kind === "done" ? action.result : undefined;
|
|
2122
|
+
// I0a: when the loop exits without a clean kernel-done — typically because a hard interrupt
|
|
2123
|
+
// aborted the in-flight LLM stream and the catch path sent `timeout` (which the kernel handles
|
|
2124
|
+
// by injecting a rollback note and continuing, not by terminating) — preserve the preempt
|
|
2125
|
+
// intent in the run_terminal reason. Without this, every interrupt-curtailed run reports
|
|
2126
|
+
// `reason: "error"` and the bench / observability layer can't distinguish preemption from a
|
|
2127
|
+
// genuine crash. Mirrors WASM/Python/Rust.
|
|
2128
|
+
const status = result?.termination ?? "error";
|
|
2129
|
+
const turnsUsed = result ? Math.max(1, result.turnsUsed) : runtime.turn() || 0;
|
|
2130
|
+
const totalTokens = result?.totalTokensUsed ?? 0;
|
|
2131
|
+
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart, taskScope);
|
|
2132
|
+
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
2133
|
+
reason: status,
|
|
2134
|
+
turnsUsed,
|
|
2135
|
+
totalTokens,
|
|
2136
|
+
}));
|
|
2137
|
+
if (groupBudgetScope && !groupBudgetScope.isClosed) {
|
|
2138
|
+
throw new Error("kernel terminated without a correlated budget_usage_reported observation");
|
|
2139
|
+
}
|
|
2140
|
+
if (this.opts.dreamStore && this.opts.agentId) {
|
|
2141
|
+
const newMsgs = runtime.drainNewMessages().map(m => ({
|
|
2142
|
+
role: m.role,
|
|
2143
|
+
content: m.content,
|
|
2144
|
+
contentParts: m.contentParts,
|
|
2145
|
+
tokenCount: m.tokenCount,
|
|
2146
|
+
toolCalls: m.toolCalls?.length ? m.toolCalls : undefined,
|
|
1813
2147
|
}));
|
|
2148
|
+
if (newMsgs.length > 0) {
|
|
2149
|
+
try {
|
|
2150
|
+
const completedSession = {
|
|
2151
|
+
sessionId,
|
|
2152
|
+
agentId: this.opts.agentId,
|
|
2153
|
+
messages: newMsgs,
|
|
2154
|
+
metadata: null,
|
|
2155
|
+
createdAtMs: sessionStart,
|
|
2156
|
+
updatedAtMs: Date.now(),
|
|
2157
|
+
};
|
|
2158
|
+
await this.opts.dreamStore.saveSession(completedSession);
|
|
2159
|
+
if (this.opts.memoryScope) {
|
|
2160
|
+
const extracted = await extractSessionMemories(this.opts.dreamProvider ?? this.opts.provider, completedSession, this.opts.memoryScope, this.opts.dreamSystemPrompt);
|
|
2161
|
+
for (const memory of extracted) {
|
|
2162
|
+
await this.writeMemory(memory, { sessionId, agentId: this.opts.agentId });
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
catch { /* non-fatal */ }
|
|
2167
|
+
}
|
|
1814
2168
|
}
|
|
1815
|
-
|
|
1816
|
-
yield {
|
|
2169
|
+
await taskScope.drain();
|
|
2170
|
+
yield {
|
|
2171
|
+
type: "done",
|
|
2172
|
+
iterations: turnsUsed,
|
|
2173
|
+
totalTokens,
|
|
2174
|
+
status,
|
|
2175
|
+
// ③ loop-agent: surface the kernel-adjudicated after-round decision to the driver.
|
|
2176
|
+
...(result?.paceDecision ? { paceDecision: result.paceDecision } : {}),
|
|
2177
|
+
};
|
|
1817
2178
|
this.activeKernel = null;
|
|
1818
2179
|
this.currentSessionId = null;
|
|
1819
2180
|
this.dashboard = null;
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
const result = action.kind === "done" ? action.result : undefined;
|
|
1823
|
-
// I0a: when the loop exits without a clean kernel-done — typically because a hard interrupt
|
|
1824
|
-
// aborted the in-flight LLM stream and the catch path sent `timeout` (which the kernel handles
|
|
1825
|
-
// by injecting a rollback note and continuing, not by terminating) — preserve the preempt
|
|
1826
|
-
// intent in the run_terminal reason. Without this, every interrupt-curtailed run reports
|
|
1827
|
-
// `reason: "error"` and the bench / observability layer can't distinguish preemption from a
|
|
1828
|
-
// genuine crash. Mirrors WASM/Python/Rust.
|
|
1829
|
-
const status = result?.termination ?? (this.interrupted ? "user_abort" : "error");
|
|
1830
|
-
const turnsUsed = result ? Math.max(1, result.turnsUsed) : runtime.turn() || 0;
|
|
1831
|
-
const totalTokens = result?.totalTokensUsed ?? 0;
|
|
1832
|
-
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
1833
|
-
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
1834
|
-
reason: status,
|
|
1835
|
-
turnsUsed,
|
|
1836
|
-
totalTokens,
|
|
1837
|
-
}));
|
|
1838
|
-
// L1: charge this vehicle's local spend (tokens + sub-agent spawns) back to the governance domain
|
|
1839
|
-
// so the next member is seeded with the updated cumulative totals.
|
|
1840
|
-
if (this.opts.runGroup) {
|
|
1841
|
-
const subagents = runtime.localSubagentsSpawned?.() ?? 0;
|
|
1842
|
-
if (totalTokens > 0 || subagents > 0) {
|
|
1843
|
-
await this.opts.runGroup.budgetStore.charge(this.opts.runGroup.id, { tokens: totalTokens, subagents });
|
|
1844
|
-
}
|
|
1845
2181
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
}));
|
|
1854
|
-
if (newMsgs.length > 0) {
|
|
1855
|
-
try {
|
|
1856
|
-
await this.opts.dreamStore.saveSession({
|
|
1857
|
-
sessionId: crypto.randomUUID(),
|
|
1858
|
-
agentId: this.opts.agentId,
|
|
1859
|
-
messages: newMsgs,
|
|
1860
|
-
metadata: null,
|
|
1861
|
-
createdAtMs: sessionStart,
|
|
1862
|
-
updatedAtMs: Date.now(),
|
|
1863
|
-
});
|
|
1864
|
-
}
|
|
1865
|
-
catch { /* non-fatal */ }
|
|
1866
|
-
}
|
|
2182
|
+
finally {
|
|
2183
|
+
await groupBudgetScope?.release();
|
|
2184
|
+
if (taskScope.pending > 0)
|
|
2185
|
+
await taskScope.cancel("run scope closed");
|
|
2186
|
+
this.activeKernel = null;
|
|
2187
|
+
this.currentSessionId = null;
|
|
2188
|
+
this.dashboard = null;
|
|
1867
2189
|
}
|
|
1868
|
-
yield {
|
|
1869
|
-
type: "done",
|
|
1870
|
-
iterations: turnsUsed,
|
|
1871
|
-
totalTokens,
|
|
1872
|
-
status,
|
|
1873
|
-
// ③ loop-agent: surface the kernel-adjudicated after-round decision to the driver.
|
|
1874
|
-
...(result?.paceDecision ? { paceDecision: result.paceDecision } : {}),
|
|
1875
|
-
};
|
|
1876
|
-
this.activeKernel = null;
|
|
1877
|
-
this.currentSessionId = null;
|
|
1878
|
-
this.dashboard = null;
|
|
1879
2190
|
}
|
|
1880
2191
|
/** I4 + K4: fetch long-term memory hits for the current goal and land them in `history` as an
|
|
1881
2192
|
* ordinary user turn — single-use retrieval content that decays with the compression pyramid,
|
|
@@ -1883,12 +2194,17 @@ export class RuntimeRunner {
|
|
|
1883
2194
|
* after each sprint renewal (`phase: "renewal"`): renewal drops the old history INCLUDING the
|
|
1884
2195
|
* earlier memory hits, so the new sprint gets a fresh recall pass. Errs-open throughout. */
|
|
1885
2196
|
async prefetchMemoryIntoHistory(runtime, phase) {
|
|
1886
|
-
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
2197
|
+
if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
|
|
1887
2198
|
return;
|
|
1888
2199
|
// P10: recall is default-on (CC session-start recall) — with no hook configured,
|
|
1889
2200
|
// the goal itself is the query. preQueryMemory stays as the targeting override.
|
|
1890
2201
|
const preQuery = this.opts.preQueryMemory
|
|
1891
|
-
?? ((ctx) => [
|
|
2202
|
+
?? ((ctx) => [{
|
|
2203
|
+
scope: this.opts.memoryScope,
|
|
2204
|
+
query: ctx.goal,
|
|
2205
|
+
top_k: 5,
|
|
2206
|
+
kinds: [],
|
|
2207
|
+
}]);
|
|
1892
2208
|
try {
|
|
1893
2209
|
const queries = await preQuery({
|
|
1894
2210
|
goal: this.currentGoal,
|
|
@@ -1897,15 +2213,15 @@ export class RuntimeRunner {
|
|
|
1897
2213
|
});
|
|
1898
2214
|
const lines = [];
|
|
1899
2215
|
for (const q of queries ?? []) {
|
|
1900
|
-
if (
|
|
2216
|
+
if (!q.query.trim())
|
|
1901
2217
|
continue;
|
|
1902
|
-
const hits = await this.opts.dreamStore.search(this.opts.agentId, q
|
|
2218
|
+
const hits = await this.opts.dreamStore.search(this.opts.agentId, q);
|
|
1903
2219
|
for (const hit of hits) {
|
|
1904
|
-
lines.push(`[memory score=${hit.score.toFixed(3)}] ${hit.
|
|
2220
|
+
lines.push(`[memory record_id=${hit.record.record_id} trust=${hit.record.provenance.trust} score=${hit.score.toFixed(3)}] ${hit.record.content}`);
|
|
1905
2221
|
}
|
|
1906
2222
|
}
|
|
1907
2223
|
if (lines.length > 0) {
|
|
1908
|
-
|
|
2224
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1909
2225
|
kind: "add_history_message",
|
|
1910
2226
|
message: { role: "user", content: lines.join("\n") },
|
|
1911
2227
|
});
|
|
@@ -1913,50 +2229,45 @@ export class RuntimeRunner {
|
|
|
1913
2229
|
}
|
|
1914
2230
|
catch { /* errs-open — a faulty pre-fetch never breaks the run */ }
|
|
1915
2231
|
}
|
|
1916
|
-
async appendObservations(sessionId, runtime, nextArchiveStart) {
|
|
2232
|
+
async appendObservations(sessionId, runtime, nextArchiveStart, _taskScope) {
|
|
1917
2233
|
const turn = runtime.turn();
|
|
1918
2234
|
const preservedRefs = runtime.preservedRefs();
|
|
1919
2235
|
const observations = this.pendingObservations.splice(0);
|
|
1920
|
-
for (
|
|
2236
|
+
for (const obs of observations) {
|
|
1921
2237
|
if (obs.kind === "page_in_requested")
|
|
1922
2238
|
continue;
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
if (this.opts.compressionStore && archived && archived.length > 0) {
|
|
1928
|
-
try {
|
|
1929
|
-
const pathRef = await this.opts.compressionStore.write(sessionId, nextArchiveStart, archived);
|
|
1930
|
-
if (pathRef)
|
|
1931
|
-
archiveRef = pathRef;
|
|
1932
|
-
}
|
|
1933
|
-
catch {
|
|
1934
|
-
// non-fatal
|
|
1935
|
-
}
|
|
2239
|
+
if (obs.kind === "budget_usage_reported") {
|
|
2240
|
+
const scope = this.activeGroupBudgetScope;
|
|
2241
|
+
if (!scope || obs.reservation_id !== scope.reservationId) {
|
|
2242
|
+
throw new Error("budget usage report does not match the active reservation");
|
|
1936
2243
|
}
|
|
2244
|
+
await this.settleGroupBudget(scope, {
|
|
2245
|
+
tokens: obs.tokens ?? 0,
|
|
2246
|
+
subagents: obs.subagents ?? 0,
|
|
2247
|
+
rounds: obs.rounds ?? 0,
|
|
2248
|
+
});
|
|
2249
|
+
this.activeGroupBudgetScope = undefined;
|
|
1937
2250
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
}
|
|
1945
|
-
catch {
|
|
1946
|
-
// non-fatal: preview remains in kernel context; full output still in tool_completed log
|
|
1947
|
-
}
|
|
1948
|
-
if (!obs.tool && pending.tool) {
|
|
1949
|
-
obs = { ...obs, tool: pending.tool };
|
|
1950
|
-
}
|
|
1951
|
-
this.pendingSpoolOutputs.delete(obs.call_id ?? "");
|
|
2251
|
+
// M3: mirror the kernel's journaled recall lifecycle into the durable store so recall
|
|
2252
|
+
// history (count + last-recalled turn) survives across sessions.
|
|
2253
|
+
if (obs.kind === "memory_recalled" && obs.recalls?.length) {
|
|
2254
|
+
const agentId = this.opts.agentId;
|
|
2255
|
+
if (agentId && this.opts.dreamStore?.recordRecall) {
|
|
2256
|
+
await this.opts.dreamStore.recordRecall(agentId, obs.recalls);
|
|
1952
2257
|
}
|
|
1953
2258
|
}
|
|
2259
|
+
// M4: a recall crossed the promotion threshold. Advisory — surface it for the host/model to
|
|
2260
|
+
// act on (pin or promote to knowledge); the runner does not auto-pin.
|
|
2261
|
+
if (obs.kind === "promotion_suggested" && obs.record_id) {
|
|
2262
|
+
this.opts.onPromotionSuggested?.({
|
|
2263
|
+
recordId: obs.record_id,
|
|
2264
|
+
recallCount: obs.recall_count ?? 0,
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
1954
2267
|
const latest = obs.kind === "compressed" ? await this.opts.sessionLog.latestSeq(sessionId) : undefined;
|
|
1955
2268
|
const event = kernelObservationToSessionEvent(obs, turn, {
|
|
1956
2269
|
nextArchiveStart,
|
|
1957
2270
|
latestSeq: latest,
|
|
1958
|
-
archiveRef,
|
|
1959
|
-
spoolRef,
|
|
1960
2271
|
preservedRefs,
|
|
1961
2272
|
compressionAction,
|
|
1962
2273
|
});
|
|
@@ -1964,27 +2275,10 @@ export class RuntimeRunner {
|
|
|
1964
2275
|
continue;
|
|
1965
2276
|
const compressedSeq = await this.opts.sessionLog.append(sessionId, event);
|
|
1966
2277
|
if (event.kind === "compressed") {
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
if (this.opts.asyncSummarizer && archived && archived.length > 0) {
|
|
1970
|
-
void this.upgradeCompressedSummary(sessionId, compressedSeq, archived, compressionAction(obs.action) ?? "auto_compact", runtime);
|
|
1971
|
-
}
|
|
1972
|
-
// One compaction = one kernel observation: the page_out session record (and the
|
|
1973
|
-
// semantic-archive branch) is DERIVED here from Compressed.tier_hint, preserving the
|
|
1974
|
-
// session-log format and OsSnapshot page_out_count.
|
|
1975
|
-
if (obs.tier_hint && Array.isArray(archived) && archived.length > 0) {
|
|
1976
|
-
await this.opts.sessionLog.append(sessionId, {
|
|
1977
|
-
kind: "page_out",
|
|
1978
|
-
turn: obs.turn ?? turn,
|
|
1979
|
-
action: compressionAction(obs.action),
|
|
1980
|
-
summary: obs.summary,
|
|
1981
|
-
tier_hint: obs.tier_hint ?? "durable",
|
|
1982
|
-
message_count: archived.length,
|
|
1983
|
-
});
|
|
1984
|
-
if (obs.tier_hint === "semantic") {
|
|
1985
|
-
void this.archiveSemanticPageOut(archived, compressionAction(obs.action));
|
|
1986
|
-
}
|
|
2278
|
+
if ((obs.archived_count ?? 0) > 0) {
|
|
2279
|
+
this.pendingPageOutArchives.push({ archiveStart: nextArchiveStart, compressedSeq });
|
|
1987
2280
|
}
|
|
2281
|
+
nextArchiveStart = compressedSeq + 1;
|
|
1988
2282
|
}
|
|
1989
2283
|
// K4: a sprint renewal dropped the old history — including any earlier memory hits — so
|
|
1990
2284
|
// re-run the preQueryMemory prefetch for the new sprint (live observations only: this
|
|
@@ -1995,56 +2289,58 @@ export class RuntimeRunner {
|
|
|
1995
2289
|
}
|
|
1996
2290
|
return nextArchiveStart;
|
|
1997
2291
|
}
|
|
1998
|
-
async archiveSemanticPageOut(archived, action) {
|
|
1999
|
-
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
2292
|
+
async archiveSemanticPageOut(archived, action, sessionId) {
|
|
2293
|
+
if (!this.opts.dreamStore || !this.opts.agentId || !this.opts.memoryScope)
|
|
2000
2294
|
return;
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2295
|
+
const summary = this.opts.dreamSummarizer
|
|
2296
|
+
? await this.opts.dreamSummarizer.summarize(archived, { action })
|
|
2297
|
+
: await summarizeForLongTermMemory(this.opts.dreamProvider ?? this.opts.provider, archived, this.opts.dreamSystemPrompt);
|
|
2298
|
+
// P2 write-funnel: route through the ONE gated WriteMemory syscall so validation,
|
|
2299
|
+
// the rolling write quota, dedup, and the memory_written audit all apply. Score is
|
|
2300
|
+
// advisory (0.6) — an automatic summary must never outrank curated content.
|
|
2301
|
+
const now = Date.now();
|
|
2302
|
+
const name = `page-out-${now}`;
|
|
2303
|
+
await this.writeMemory({
|
|
2304
|
+
record_id: `${this.opts.memoryScope.tenant_id}:${this.opts.memoryScope.namespace}:project:${name}`,
|
|
2305
|
+
scope: this.opts.memoryScope,
|
|
2306
|
+
name,
|
|
2307
|
+
kind: "project",
|
|
2308
|
+
content: summary,
|
|
2309
|
+
description: `auto summary of ${action ?? "compaction"} archive`,
|
|
2310
|
+
provenance: {
|
|
2311
|
+
session_id: sessionId,
|
|
2312
|
+
author: "extraction",
|
|
2313
|
+
trust: "untrusted",
|
|
2314
|
+
evidence_refs: [],
|
|
2315
|
+
},
|
|
2316
|
+
created_at: now,
|
|
2317
|
+
updated_at: now,
|
|
2318
|
+
recall_count: 0,
|
|
2319
|
+
confidence: 0.6,
|
|
2320
|
+
links: [],
|
|
2321
|
+
pinned: false,
|
|
2322
|
+
}, { sessionId, agentId: this.opts.agentId });
|
|
2022
2323
|
}
|
|
2023
2324
|
async upgradeCompressedSummary(sessionId, compressedSeq, archived, action, runtime) {
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2325
|
+
const summary = await this.opts.asyncSummarizer.summarize(archived, action);
|
|
2326
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
2327
|
+
kind: "summary_upgraded",
|
|
2328
|
+
compressed_seq: compressedSeq,
|
|
2329
|
+
summary,
|
|
2330
|
+
});
|
|
2331
|
+
// P4: the LLM summary also re-enters the LIVE session as a keyed page-in entry —
|
|
2332
|
+
// K1 boundary-deferred upsert lands it with zero mid-generation cache churn and the
|
|
2333
|
+
// K2 budget governs its size. The RuleSummarizer text remains the synchronous label.
|
|
2334
|
+
if (runtime) {
|
|
2335
|
+
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
2336
|
+
kind: "page_in",
|
|
2337
|
+
entries: [{
|
|
2338
|
+
content: `[ARCHIVE SUMMARY] ${summary}`,
|
|
2339
|
+
key: `summary:seq-${compressedSeq}`,
|
|
2340
|
+
pinned: false,
|
|
2341
|
+
source: "async_summarizer",
|
|
2342
|
+
}],
|
|
2030
2343
|
});
|
|
2031
|
-
// P4: the LLM summary also re-enters the LIVE session as a keyed page-in entry —
|
|
2032
|
-
// K1 boundary-deferred upsert lands it with zero mid-generation cache churn and the
|
|
2033
|
-
// K2 budget governs its size. The RuleSummarizer text remains the synchronous label.
|
|
2034
|
-
if (runtime) {
|
|
2035
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
2036
|
-
kind: "page_in",
|
|
2037
|
-
entries: [{
|
|
2038
|
-
content: `[ARCHIVE SUMMARY] ${summary}`,
|
|
2039
|
-
key: `summary:seq-${compressedSeq}`,
|
|
2040
|
-
pinned: false,
|
|
2041
|
-
source: "async_summarizer",
|
|
2042
|
-
}],
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
catch {
|
|
2047
|
-
// non-fatal: rule-based summary stays in place
|
|
2048
2344
|
}
|
|
2049
2345
|
}
|
|
2050
2346
|
}
|
|
@@ -2169,14 +2465,22 @@ export function replayMessages(events, maxBytes) {
|
|
|
2169
2465
|
upgradedSummaries.set(e.compressed_seq, e.summary);
|
|
2170
2466
|
}
|
|
2171
2467
|
const messages = [];
|
|
2172
|
-
for (
|
|
2468
|
+
for (let eventIndex = 0; eventIndex < events.length; eventIndex++) {
|
|
2469
|
+
const { seq, event: e } = events[eventIndex];
|
|
2173
2470
|
if (e.kind === "run_started") {
|
|
2174
2471
|
const userText = e.criteria.length
|
|
2175
2472
|
? `${e.goal}\n\nCriteria:\n${e.criteria.map((c, i) => `${i + 1}. ${c}`).join("\n")}`
|
|
2176
2473
|
: e.goal;
|
|
2474
|
+
// Multimodal parity: the live seed of `attachments` is gated behind `!resumeMidRun`, so on
|
|
2475
|
+
// resume the image/audio must be recovered from the persisted run_started event or it is lost.
|
|
2476
|
+
const attachments = (e.attachments ?? []);
|
|
2477
|
+
const contentParts = attachments.length
|
|
2478
|
+
? [...(userText ? [{ type: "text", text: userText }] : []), ...attachments]
|
|
2479
|
+
: undefined;
|
|
2177
2480
|
messages.push({
|
|
2178
2481
|
role: "user",
|
|
2179
2482
|
content: userText,
|
|
2483
|
+
...(contentParts ? { contentParts } : {}),
|
|
2180
2484
|
toolCalls: [],
|
|
2181
2485
|
tokenCount: Math.max(1, Math.ceil(userText.length / 4)),
|
|
2182
2486
|
});
|
|
@@ -2229,38 +2533,32 @@ export async function replayMessagesAsync(events, maxBytes, loadArchive) {
|
|
|
2229
2533
|
upgradedSummaries.set(e.compressed_seq, e.summary);
|
|
2230
2534
|
}
|
|
2231
2535
|
const messages = [];
|
|
2232
|
-
for (
|
|
2536
|
+
for (let eventIndex = 0; eventIndex < events.length; eventIndex++) {
|
|
2537
|
+
const { seq, event: e } = events[eventIndex];
|
|
2233
2538
|
if (e.kind === "run_started") {
|
|
2234
2539
|
const userText = e.criteria.length
|
|
2235
2540
|
? `${e.goal}\n\nCriteria:\n${e.criteria.map((c, i) => `${i + 1}. ${c}`).join("\n")}`
|
|
2236
2541
|
: e.goal;
|
|
2542
|
+
// Multimodal parity: the live seed of `attachments` is gated behind `!resumeMidRun`, so on
|
|
2543
|
+
// resume the image/audio must be recovered from the persisted run_started event or it is lost.
|
|
2544
|
+
const attachments = (e.attachments ?? []);
|
|
2545
|
+
const contentParts = attachments.length
|
|
2546
|
+
? [...(userText ? [{ type: "text", text: userText }] : []), ...attachments]
|
|
2547
|
+
: undefined;
|
|
2237
2548
|
messages.push({
|
|
2238
2549
|
role: "user",
|
|
2239
2550
|
content: userText,
|
|
2551
|
+
...(contentParts ? { contentParts } : {}),
|
|
2240
2552
|
toolCalls: [],
|
|
2241
2553
|
tokenCount: Math.max(1, Math.ceil(userText.length / 4)),
|
|
2242
2554
|
});
|
|
2243
2555
|
}
|
|
2244
2556
|
else if (e.kind === "compressed") {
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
messages.push({
|
|
2251
|
-
role: msg.role,
|
|
2252
|
-
content: sanitizeReplayText(msg.content, maxBytes),
|
|
2253
|
-
toolCalls: msg.toolCalls ?? [],
|
|
2254
|
-
tokenCount: msg.tokenCount,
|
|
2255
|
-
});
|
|
2256
|
-
}
|
|
2257
|
-
loadedSuccessfully = true;
|
|
2258
|
-
}
|
|
2259
|
-
catch (err) {
|
|
2260
|
-
// Loader failed (e.g. MissingArchive). We degrade and fallback.
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
if (!loadedSuccessfully) {
|
|
2557
|
+
const pageOutWillSupplyArchive = events.slice(eventIndex + 1).some(({ event }) => event.kind === "page_out"
|
|
2558
|
+
&& event.turn === e.turn
|
|
2559
|
+
&& typeof event.archive_ref === "string"
|
|
2560
|
+
&& event.archive_ref.length > 0);
|
|
2561
|
+
if (!pageOutWillSupplyArchive) {
|
|
2264
2562
|
const summary = upgradedSummaries.get(seq) ?? e.summary;
|
|
2265
2563
|
if (summary) {
|
|
2266
2564
|
const systemText = `[Compressed context: turn ${e.turn}]\n${summary}`;
|
|
@@ -2273,6 +2571,30 @@ export async function replayMessagesAsync(events, maxBytes, loadArchive) {
|
|
|
2273
2571
|
}
|
|
2274
2572
|
}
|
|
2275
2573
|
}
|
|
2574
|
+
else if (e.kind === "page_out" && e.archive_ref && loadArchive) {
|
|
2575
|
+
try {
|
|
2576
|
+
const archivedMsgs = await loadArchive(e.archive_ref);
|
|
2577
|
+
for (const msg of archivedMsgs) {
|
|
2578
|
+
messages.push({
|
|
2579
|
+
role: msg.role,
|
|
2580
|
+
content: sanitizeReplayText(msg.content, maxBytes),
|
|
2581
|
+
toolCalls: msg.toolCalls ?? [],
|
|
2582
|
+
tokenCount: msg.tokenCount,
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
catch {
|
|
2587
|
+
if (e.summary) {
|
|
2588
|
+
const systemText = `[Compressed context: turn ${e.turn}]\n${e.summary}`;
|
|
2589
|
+
messages.push({
|
|
2590
|
+
role: "system",
|
|
2591
|
+
content: systemText,
|
|
2592
|
+
toolCalls: [],
|
|
2593
|
+
tokenCount: Math.max(1, Math.ceil(systemText.length / 4)),
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2276
2598
|
else if (e.kind === "llm_completed") {
|
|
2277
2599
|
messages.push({
|
|
2278
2600
|
role: "assistant",
|
|
@@ -2309,14 +2631,6 @@ function nextArchivedSeqStart(events) {
|
|
|
2309
2631
|
}
|
|
2310
2632
|
return next;
|
|
2311
2633
|
}
|
|
2312
|
-
function tryParseJson(s) {
|
|
2313
|
-
try {
|
|
2314
|
-
return JSON.parse(s);
|
|
2315
|
-
}
|
|
2316
|
-
catch {
|
|
2317
|
-
return null;
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
2634
|
/** Collect all text_delta events from a run into a single string. */
|
|
2321
2635
|
export async function collectText(stream) {
|
|
2322
2636
|
let text = "";
|
|
@@ -2388,46 +2702,51 @@ function parseStartWorkflowSpec(argsStr) {
|
|
|
2388
2702
|
}
|
|
2389
2703
|
/** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
|
|
2390
2704
|
* agent's context, so the agent's next turn continues with the sub-workflow's results in view. */
|
|
2705
|
+
function recoveredOutputs(outcomes) {
|
|
2706
|
+
const outputs = new Map();
|
|
2707
|
+
for (const outcome of outcomes ?? []) {
|
|
2708
|
+
if (!outcome.output)
|
|
2709
|
+
continue;
|
|
2710
|
+
outputs.set(outcome.agentId, outcome.output.content);
|
|
2711
|
+
outputs.set(outcome.agentId.replace(/-i\d+$/, ""), outcome.output.content);
|
|
2712
|
+
}
|
|
2713
|
+
return outputs;
|
|
2714
|
+
}
|
|
2391
2715
|
function authoredWorkflowOutcomeNote(outcome) {
|
|
2716
|
+
const counts = new Map();
|
|
2717
|
+
for (const node of outcome.nodeOutcomes)
|
|
2718
|
+
counts.set(node.status, (counts.get(node.status) ?? 0) + 1);
|
|
2392
2719
|
const lines = [
|
|
2393
|
-
`[authored workflow result] ${outcome.
|
|
2394
|
-
(
|
|
2720
|
+
`[authored workflow result] ${outcome.nodeOutcomes.length} terminal node(s): ` +
|
|
2721
|
+
[...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ") + ".",
|
|
2395
2722
|
];
|
|
2396
|
-
for (const
|
|
2397
|
-
const out = outcome.outputs[
|
|
2723
|
+
for (const node of outcome.nodeOutcomes) {
|
|
2724
|
+
const out = outcome.outputs[node.nodeId] ?? node.output?.content;
|
|
2398
2725
|
if (out)
|
|
2399
|
-
lines.push(`- ${
|
|
2726
|
+
lines.push(`- ${node.nodeId} (${node.status}): ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
|
|
2400
2727
|
}
|
|
2401
2728
|
return lines.join("\n");
|
|
2402
2729
|
}
|
|
2403
2730
|
/** Lower a host `RuntimeSignal` to the kernel's snake_case `signal` input event. Shared by the main
|
|
2404
2731
|
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
const sa = new Set(a.split(/\s+/).filter(Boolean));
|
|
2408
|
-
const sb = new Set(b.split(/\s+/).filter(Boolean));
|
|
2409
|
-
if (sa.size === 0 && sb.size === 0)
|
|
2410
|
-
return 1;
|
|
2411
|
-
let inter = 0;
|
|
2412
|
-
for (const w of sa)
|
|
2413
|
-
if (sb.has(w))
|
|
2414
|
-
inter++;
|
|
2415
|
-
const union = sa.size + sb.size - inter;
|
|
2416
|
-
return union === 0 ? 0 : inter / union;
|
|
2417
|
-
}
|
|
2418
|
-
function signalToKernelEvent(sig) {
|
|
2732
|
+
function signalToKernelEvent(delivery) {
|
|
2733
|
+
const sig = delivery.signal;
|
|
2419
2734
|
return {
|
|
2420
|
-
kind: "
|
|
2735
|
+
kind: "deliver_signal",
|
|
2736
|
+
delivery_id: delivery.deliveryId,
|
|
2737
|
+
attempt: delivery.deliveryAttempt,
|
|
2421
2738
|
signal: {
|
|
2422
|
-
id:
|
|
2739
|
+
id: delivery.signalId,
|
|
2423
2740
|
source: sig.source ?? "custom",
|
|
2424
2741
|
signal_type: sig.signalType ?? "event",
|
|
2425
2742
|
urgency: sig.urgency ?? "normal",
|
|
2426
|
-
summary: String(sig.payload?.goal ??
|
|
2743
|
+
summary: String(sig.payload?.goal ?? "signal"),
|
|
2427
2744
|
payload: sig.payload ?? {},
|
|
2428
2745
|
...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
|
|
2429
2746
|
...(sig.recipient ? { recipient: sig.recipient } : {}),
|
|
2430
|
-
...(sig.
|
|
2747
|
+
...(sig.deadlineMs !== undefined ? { deadline_ms: sig.deadlineMs } : {}),
|
|
2748
|
+
...(sig.coalesceKey ? { coalesce_key: sig.coalesceKey } : {}),
|
|
2749
|
+
coalesced_count: Math.max(1, sig.coalescedCount ?? 1),
|
|
2431
2750
|
timestamp_ms: Date.now(),
|
|
2432
2751
|
},
|
|
2433
2752
|
};
|