@deepstrike/wasm 0.2.35 → 0.2.37
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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/facade.js +11 -7
- package/dist/runtime/kernel-event-log.d.ts +0 -6
- package/dist/runtime/kernel-event-log.js +38 -62
- package/dist/runtime/kernel-step.d.ts +9 -0
- package/dist/runtime/kernel-step.js +12 -0
- package/dist/runtime/large-result-spool.d.ts +7 -0
- package/dist/runtime/large-result-spool.js +25 -0
- package/dist/runtime/os-snapshot.d.ts +0 -1
- package/dist/runtime/os-snapshot.js +0 -19
- package/dist/runtime/runner.d.ts +103 -8
- package/dist/runtime/runner.js +415 -100
- package/dist/runtime/session-log.d.ts +15 -54
- package/dist/runtime/session-repair.d.ts +29 -6
- package/dist/runtime/session-repair.js +37 -8
- package/dist/runtime/sub-agent-orchestrator.d.ts +7 -0
- package/dist/runtime/sub-agent-orchestrator.js +39 -4
- package/dist/runtime/types/agent.d.ts +46 -2
- package/dist/runtime/types/agent.js +54 -1
- package/dist/runtime/workflow-control-flow.d.ts +10 -2
- package/dist/runtime/workflow-control-flow.js +27 -6
- package/dist/types.d.ts +2 -1
- package/package.json +2 -2
package/dist/runtime/runner.js
CHANGED
|
@@ -10,8 +10,8 @@ import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass,
|
|
|
10
10
|
import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
11
11
|
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
12
12
|
import { resolveReducer } from "./reducers.js";
|
|
13
|
-
import { loopInstruction, classifyInstruction, judgeGoal, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
|
|
14
|
-
import { kernelObservationToSessionEvent
|
|
13
|
+
import { loopInstruction, classifyInstruction, judgeGoal, dependencyOutputsNote, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
|
|
14
|
+
import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
|
|
15
15
|
import { assertNativeProfile } from "./os-profile.js";
|
|
16
16
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
17
17
|
export class RuntimeRunner {
|
|
@@ -22,8 +22,15 @@ export class RuntimeRunner {
|
|
|
22
22
|
pendingObservations = [];
|
|
23
23
|
activeKernel = null;
|
|
24
24
|
currentSessionId = null;
|
|
25
|
+
/** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
|
|
26
|
+
injectedSignals = [];
|
|
27
|
+
/** Skill names whose content has already been pushed into the durable `knowledge` slot this
|
|
28
|
+
* run — guards against re-pushing a duplicate entry if the model calls `skill(name)` again for
|
|
29
|
+
* an already-active skill (loading is idempotent; the knowledge push should be too). */
|
|
30
|
+
knowledgePushedSkills = new Set();
|
|
31
|
+
/** K4: the active run's goal, kept for the renewal-boundary memory re-query. */
|
|
32
|
+
currentGoal = "";
|
|
25
33
|
nextArchiveStart = 0;
|
|
26
|
-
localPageOutCache = [];
|
|
27
34
|
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
28
35
|
* at the next safe point (after the tool turn resolves, kernel back in Reason). */
|
|
29
36
|
pendingAuthoredWorkflows = [];
|
|
@@ -33,6 +40,24 @@ export class RuntimeRunner {
|
|
|
33
40
|
}
|
|
34
41
|
get hostOptions() { return this.opts; }
|
|
35
42
|
interrupt() { this.interrupted = true; this.abortController?.abort(); }
|
|
43
|
+
/** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
|
|
44
|
+
* the next turn boundary, routes through the kernel attention policy, and — once acted on — renders
|
|
45
|
+
* as a `[SIGNAL] <text>` line in the volatile state turn plus a durable directive. `urgency` maps to
|
|
46
|
+
* the kernel disposition ladder: `"normal"` queues (default), `"high"` soft-interrupts, `"critical"`
|
|
47
|
+
* preempts. */
|
|
48
|
+
injectNote(text, urgency = "normal") {
|
|
49
|
+
this.injectedSignals.push({ source: "custom", signalType: "event", urgency, payload: { goal: text } });
|
|
50
|
+
}
|
|
51
|
+
/** Injected-note drain shared with the main loop's per-turn poll: injected notes first (FIFO), then
|
|
52
|
+
* the configured `signalSource` — one code path so the two inbound channels never drift. */
|
|
53
|
+
async nextInboundSignal() {
|
|
54
|
+
const injected = this.injectedSignals.shift();
|
|
55
|
+
if (injected)
|
|
56
|
+
return injected;
|
|
57
|
+
if (!this.opts.signalSource)
|
|
58
|
+
return null;
|
|
59
|
+
return this.opts.signalSource.nextSignal();
|
|
60
|
+
}
|
|
36
61
|
async *run(req) {
|
|
37
62
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
38
63
|
const midRun = isMidRun(prior);
|
|
@@ -58,59 +83,35 @@ export class RuntimeRunner {
|
|
|
58
83
|
const start = startEntry.event;
|
|
59
84
|
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true);
|
|
60
85
|
}
|
|
61
|
-
/** Push content into Slot 2 (system_knowledge) via add_knowledge_message.
|
|
62
|
-
|
|
86
|
+
/** Push content into Slot 2 (system_knowledge) via add_knowledge_message.
|
|
87
|
+
* K1: `opts.key` gives the entry identity — a same-key push upserts (applied at the next
|
|
88
|
+
* compaction/renewal boundary) instead of appending a duplicate. `opts.pinned` exempts the
|
|
89
|
+
* entry from the knowledge-budget sweep. */
|
|
90
|
+
pushKnowledge(message, tokens, opts) {
|
|
63
91
|
if (!this.activeKernel)
|
|
64
92
|
return;
|
|
65
93
|
kernelApply(this.activeKernel, this.pendingObservations, {
|
|
66
94
|
kind: "add_knowledge_message",
|
|
67
95
|
content: message.content ?? "",
|
|
68
96
|
tokens: tokens ?? Math.max(1, Math.ceil((message.content?.length ?? 0) / 4)),
|
|
97
|
+
...(opts?.key !== undefined ? { key: opts.key } : {}),
|
|
98
|
+
...(opts?.pinned ? { pinned: true } : {}),
|
|
69
99
|
});
|
|
70
100
|
}
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
101
|
+
/** K1: mark a keyed knowledge entry for removal at the next compaction/renewal boundary.
|
|
102
|
+
* Errs-open: an unknown key is a kernel-side no-op. */
|
|
103
|
+
removeKnowledge(key) {
|
|
104
|
+
if (!this.activeKernel)
|
|
75
105
|
return;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
for (const hit of localHits) {
|
|
83
|
-
entries.push({
|
|
84
|
-
content: `[local semantic cache] ${hit.role}: ${hit.content}`,
|
|
85
|
-
source: "semantic_cache",
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
const remainingK = topK - entries.length;
|
|
89
|
-
if (remainingK > 0 && this.opts.dreamStore && this.opts.agentId) {
|
|
90
|
-
const hits = await this.opts.dreamStore.search(this.opts.agentId, query, remainingK);
|
|
91
|
-
for (const hit of hits) {
|
|
92
|
-
entries.push({
|
|
93
|
-
content: `[memory score=${hit.score.toFixed(3)}] ${hit.text}`,
|
|
94
|
-
source: "memory",
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
else if (req.tool === "knowledge" && this.opts.knowledgeSource) {
|
|
100
|
-
const snippets = await this.opts.knowledgeSource.retrieve(query, topK);
|
|
101
|
-
for (const snippet of snippets) {
|
|
102
|
-
entries.push({ content: snippet, source: "knowledge" });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (entries.length === 0)
|
|
106
|
+
kernelApply(this.activeKernel, this.pendingObservations, { kind: "remove_knowledge", key });
|
|
107
|
+
}
|
|
108
|
+
/** K3: host-driven skill deactivation — toolset re-widens at the next provider call, the
|
|
109
|
+
* skill's knowledge pin drops at the next boundary. Errs-open: not-active is a no-op. */
|
|
110
|
+
deactivateSkill(name) {
|
|
111
|
+
if (!this.activeKernel)
|
|
107
112
|
return;
|
|
108
|
-
kernelApply(
|
|
109
|
-
|
|
110
|
-
kind: "page_in",
|
|
111
|
-
turn: runtime.turn(),
|
|
112
|
-
entry_count: entries.length,
|
|
113
|
-
}));
|
|
113
|
+
kernelApply(this.activeKernel, this.pendingObservations, { kind: "skill_deactivated", name });
|
|
114
|
+
this.knowledgePushedSkills.delete(name);
|
|
114
115
|
}
|
|
115
116
|
async resolveKernelSuspend(runtime, sessionId) {
|
|
116
117
|
const gated = this.pendingObservations.filter((o) => o.kind === "tool_gated" && typeof o.call_id === "string" && typeof o.tool === "string");
|
|
@@ -190,6 +191,63 @@ export class RuntimeRunner {
|
|
|
190
191
|
}
|
|
191
192
|
return { approved, denied, events };
|
|
192
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* O7: resolve a `read_result` meta-tool call to the full text of a previously-evicted tool
|
|
196
|
+
* output. Resolution order: (a) this turn's in-memory `pendingSpoolOutputs` map, (b) the result
|
|
197
|
+
* spool (persisted once the kernel observation `large_result_spooled` was processed), (c) a
|
|
198
|
+
* session-log scan for the original `tool_completed` event carrying that `call_id`. Slices the
|
|
199
|
+
* resolved text by `[offset, offset + maxBytes)` (plain string slice — "bytes-ish").
|
|
200
|
+
*/
|
|
201
|
+
async resolveReadResult(sessionId, argsJson) {
|
|
202
|
+
let callId = "";
|
|
203
|
+
let offset = 0;
|
|
204
|
+
let maxBytes = 4000;
|
|
205
|
+
try {
|
|
206
|
+
const args = JSON.parse(argsJson || "{}");
|
|
207
|
+
callId = typeof args.call_id === "string" ? args.call_id : "";
|
|
208
|
+
if (typeof args.offset === "number" && Number.isFinite(args.offset))
|
|
209
|
+
offset = args.offset;
|
|
210
|
+
if (typeof args.max_bytes === "number" && Number.isFinite(args.max_bytes))
|
|
211
|
+
maxBytes = args.max_bytes;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// malformed arguments — callId stays empty, falls through to "not found" below
|
|
215
|
+
}
|
|
216
|
+
let full = this.pendingSpoolOutputs.get(callId)?.output;
|
|
217
|
+
if (full === undefined && this.opts.resultSpool) {
|
|
218
|
+
try {
|
|
219
|
+
full = await this.opts.resultSpool.findByCallId(callId);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
full = undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (full === undefined) {
|
|
226
|
+
try {
|
|
227
|
+
const events = await this.opts.sessionLog.read(sessionId);
|
|
228
|
+
for (const { event } of events) {
|
|
229
|
+
if (event.kind !== "tool_completed")
|
|
230
|
+
continue;
|
|
231
|
+
const match = event.results.find(r => r.call_id === callId);
|
|
232
|
+
if (match)
|
|
233
|
+
full = match.output;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
full = undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (full === undefined) {
|
|
241
|
+
return { text: `no stored output for call_id "${callId}"`, isError: true };
|
|
242
|
+
}
|
|
243
|
+
const start = Math.max(0, offset);
|
|
244
|
+
const end = Math.min(full.length, start + Math.max(0, maxBytes));
|
|
245
|
+
const slice = full.slice(start, end);
|
|
246
|
+
return {
|
|
247
|
+
text: `[read_result ${callId}: chars ${start}–${end} of ${full.length}]\n${slice}`,
|
|
248
|
+
isError: false,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
193
251
|
async dream(agentId, nowMs = Date.now()) {
|
|
194
252
|
if (!this.opts.dreamStore)
|
|
195
253
|
throw new Error("dreamStore not configured");
|
|
@@ -351,14 +409,35 @@ export class RuntimeRunner {
|
|
|
351
409
|
messages: replayed.map(messageToKernelMessage),
|
|
352
410
|
});
|
|
353
411
|
// P1-B B3: rebuild active-skill gating after a wake (active_skills is not snapshotted).
|
|
412
|
+
// `knowledge` isn't snapshotted either (same graceful-reset philosophy) — best-effort re-push
|
|
413
|
+
// the skill's content from its replayed tool_result so the durable copy survives a wake too.
|
|
414
|
+
const toolResultByCallId = new Map();
|
|
415
|
+
for (const m of replayed) {
|
|
416
|
+
for (const part of m.contentParts ?? []) {
|
|
417
|
+
if (part.type === "tool_result" && part.callId && part.output !== undefined) {
|
|
418
|
+
toolResultByCallId.set(part.callId, part.output);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
354
422
|
for (const m of replayed) {
|
|
355
423
|
for (const tc of m.toolCalls ?? []) {
|
|
356
424
|
if (tc.name !== "skill")
|
|
357
425
|
continue;
|
|
358
426
|
try {
|
|
359
427
|
const name = JSON.parse(tc.arguments || "{}").name;
|
|
360
|
-
if (name)
|
|
361
|
-
|
|
428
|
+
if (!name)
|
|
429
|
+
continue;
|
|
430
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
431
|
+
kind: "skill_activated",
|
|
432
|
+
name,
|
|
433
|
+
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
434
|
+
});
|
|
435
|
+
const output = toolResultByCallId.get(tc.id);
|
|
436
|
+
if (output && !this.knowledgePushedSkills.has(name)) {
|
|
437
|
+
this.knowledgePushedSkills.add(name);
|
|
438
|
+
// K1: keyed — the kernel-side upsert is the authoritative dedup across wake replays.
|
|
439
|
+
this.pushKnowledge({ role: "system", content: output }, undefined, { key: `skill:${name}` });
|
|
440
|
+
}
|
|
362
441
|
}
|
|
363
442
|
catch { /* skip */ }
|
|
364
443
|
}
|
|
@@ -386,24 +465,13 @@ export class RuntimeRunner {
|
|
|
386
465
|
startPayload.run_spec = agentRunSpecToKernel(spec);
|
|
387
466
|
}
|
|
388
467
|
this.applyKernelPolicies(runtime);
|
|
389
|
-
// I4: pre-fetch memory
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
continue;
|
|
397
|
-
const hits = await this.opts.dreamStore.search(this.opts.agentId, q, 5);
|
|
398
|
-
for (const hit of hits) {
|
|
399
|
-
entries.push({ content: `[memory score=${hit.score.toFixed(3)}] ${hit.text}`, source: "memory" });
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if (entries.length > 0) {
|
|
403
|
-
kernelApply(runtime, this.pendingObservations, { kind: "page_in", entries });
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
catch { /* errs-open */ }
|
|
468
|
+
// I4: pre-fetch memory before the first LLM turn (mirrors Node). Strict dynamic context
|
|
469
|
+
// control: single-use retrieval content, not a stable skill — lands in `history` like an
|
|
470
|
+
// ordinary `memory` tool result, so it decays with the compression pyramid instead of
|
|
471
|
+
// pinning itself in `knowledge` forever.
|
|
472
|
+
this.currentGoal = goal;
|
|
473
|
+
if (!resumeMidRun) {
|
|
474
|
+
await this.prefetchMemoryIntoHistory(runtime, "initial");
|
|
407
475
|
}
|
|
408
476
|
let action = resumeMidRun
|
|
409
477
|
? kernelAction(runtime, this.pendingObservations, { kind: "resume" })
|
|
@@ -413,16 +481,13 @@ export class RuntimeRunner {
|
|
|
413
481
|
// I0b: kernel-throw safety net — see Node runner for full rationale.
|
|
414
482
|
try {
|
|
415
483
|
while (!runtime.isTerminal()) {
|
|
416
|
-
if (action.kind === "execute_tool") {
|
|
417
|
-
await this.applyKernelPageIn(runtime, sessionId);
|
|
418
|
-
}
|
|
419
484
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
420
485
|
if (this.interrupted) {
|
|
421
486
|
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
422
487
|
break;
|
|
423
488
|
}
|
|
424
|
-
if (this.opts.signalSource) {
|
|
425
|
-
const sig = await this.
|
|
489
|
+
if (this.opts.signalSource || this.injectedSignals.length > 0) {
|
|
490
|
+
const sig = await this.nextInboundSignal();
|
|
426
491
|
if (sig) {
|
|
427
492
|
const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
|
|
428
493
|
if (sigAction)
|
|
@@ -608,7 +673,16 @@ export class RuntimeRunner {
|
|
|
608
673
|
// the orchestrator collects them and `runWorkflow` sends them to the parent kernel.
|
|
609
674
|
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path.
|
|
610
675
|
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
611
|
-
|
|
676
|
+
// O7: `read_result` re-fetches a tool output the kernel evicted from context. Content is
|
|
677
|
+
// host-resolved: (a) this turn's in-memory pending spool map, (b) the on-disk result spool,
|
|
678
|
+
// (c) a session-log scan for the original `tool_completed` event.
|
|
679
|
+
const readResultCalls = allCalls.filter(c => c.name === "read_result");
|
|
680
|
+
const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes" && c.name !== "start_workflow" && c.name !== "read_result");
|
|
681
|
+
for (const call of readResultCalls) {
|
|
682
|
+
const out = await this.resolveReadResult(sessionId, call.arguments);
|
|
683
|
+
toolResults.push({ callId: call.id, output: out.text, isError: out.isError });
|
|
684
|
+
yield { type: "tool_result", callId: call.id, content: out.text, isError: out.isError };
|
|
685
|
+
}
|
|
612
686
|
for (const call of submitCalls) {
|
|
613
687
|
// M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
|
|
614
688
|
// spec and AUTO-PIVOT once this tool turn resolves. A workflow-NODE's `start_workflow` (and
|
|
@@ -630,7 +704,35 @@ export class RuntimeRunner {
|
|
|
630
704
|
toolResults.push({ callId: call.id, output: "submitted", isError: false });
|
|
631
705
|
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
632
706
|
}
|
|
633
|
-
|
|
707
|
+
// O5 (PreToolUse-hook analog): stateful host veto over each kernel-approved call.
|
|
708
|
+
// A blocked call never executes; its reason reaches the model as a denied result.
|
|
709
|
+
let executableCalls = normalCalls;
|
|
710
|
+
if (this.opts.onToolCall) {
|
|
711
|
+
const allowed = [];
|
|
712
|
+
for (const call of normalCalls) {
|
|
713
|
+
let decision;
|
|
714
|
+
try {
|
|
715
|
+
decision = await this.opts.onToolCall({ callId: call.id, name: call.name, arguments: call.arguments });
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
decision = undefined;
|
|
719
|
+
}
|
|
720
|
+
if (decision?.block) {
|
|
721
|
+
const reason = decision.reason ?? "blocked by host onToolCall hook";
|
|
722
|
+
yield { type: "tool_denied", callId: call.id, toolName: call.name, reason };
|
|
723
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
724
|
+
kind: "tool_denied", turn: runtime.turn(), call_id: call.id, tool_name: call.name, reason,
|
|
725
|
+
});
|
|
726
|
+
const out = `blocked by host hook: ${reason}`;
|
|
727
|
+
toolResults.push({ callId: call.id, output: out, isError: true, errorKind: "governance_denied" });
|
|
728
|
+
yield { type: "tool_result", callId: call.id, name: call.name, content: out, isError: true };
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
allowed.push(call);
|
|
732
|
+
}
|
|
733
|
+
executableCalls = allowed;
|
|
734
|
+
}
|
|
735
|
+
for await (const evt of this.opts.executionPlane.executeAll(executableCalls, runCtx)) {
|
|
634
736
|
yield evt;
|
|
635
737
|
if (evt.type === "tool_result") {
|
|
636
738
|
const tre = evt;
|
|
@@ -684,6 +786,31 @@ export class RuntimeRunner {
|
|
|
684
786
|
});
|
|
685
787
|
}
|
|
686
788
|
}
|
|
789
|
+
// O5 (PostToolUse-hook analog): host inspection of each executed result before it reaches
|
|
790
|
+
// the kernel/session-log — replace the output and/or inject a signal note. Errs-open.
|
|
791
|
+
if (this.opts.onToolResult) {
|
|
792
|
+
for (const r of toolResults) {
|
|
793
|
+
const call = executableCalls.find(c => c.id === r.callId);
|
|
794
|
+
if (!call)
|
|
795
|
+
continue;
|
|
796
|
+
let decision;
|
|
797
|
+
try {
|
|
798
|
+
decision = await this.opts.onToolResult({
|
|
799
|
+
callId: r.callId, name: call.name, arguments: call.arguments,
|
|
800
|
+
output: r.output, isError: r.isError,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
decision = undefined;
|
|
805
|
+
}
|
|
806
|
+
if (!decision)
|
|
807
|
+
continue;
|
|
808
|
+
if (typeof decision.replaceOutput === "string")
|
|
809
|
+
r.output = decision.replaceOutput;
|
|
810
|
+
if (decision.note)
|
|
811
|
+
this.injectNote(decision.note);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
687
814
|
await this.opts.sessionLog.append(sessionId, {
|
|
688
815
|
kind: "tool_completed",
|
|
689
816
|
turn: runtime.turn(),
|
|
@@ -701,6 +828,12 @@ export class RuntimeRunner {
|
|
|
701
828
|
}
|
|
702
829
|
}
|
|
703
830
|
// P1-B B3: a successfully-resolved `skill` call activates that skill for the next turn.
|
|
831
|
+
//
|
|
832
|
+
// Strict dynamic context control: a skill is METHOD content — how to do something — reused
|
|
833
|
+
// for the rest of the run, unlike a one-off memory/knowledge lookup (fact content, relevant
|
|
834
|
+
// for the moment it's used). So its text ALSO goes into the durable `knowledge` slot here
|
|
835
|
+
// (in addition to the ordinary tool_result already headed for `history`, where it will decay
|
|
836
|
+
// with the compression pyramid like any other tool output). First activation only.
|
|
704
837
|
for (const call of allCalls) {
|
|
705
838
|
if (call.name !== "skill")
|
|
706
839
|
continue;
|
|
@@ -709,8 +842,20 @@ export class RuntimeRunner {
|
|
|
709
842
|
continue;
|
|
710
843
|
try {
|
|
711
844
|
const name = JSON.parse(call.arguments || "{}").name;
|
|
712
|
-
if (name)
|
|
713
|
-
|
|
845
|
+
if (!name)
|
|
846
|
+
continue;
|
|
847
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
848
|
+
kind: "skill_activated",
|
|
849
|
+
name,
|
|
850
|
+
...(this.opts.skillLeaseTurns !== undefined ? { lease_turns: this.opts.skillLeaseTurns } : {}),
|
|
851
|
+
});
|
|
852
|
+
// With a lease configured, skip the Set optimization: an expired-then-reloaded skill
|
|
853
|
+
// must re-pin — only the kernel knows the lease state; its upsert dedupes anyway.
|
|
854
|
+
if (this.opts.skillLeaseTurns !== undefined || !this.knowledgePushedSkills.has(name)) {
|
|
855
|
+
this.knowledgePushedSkills.add(name);
|
|
856
|
+
// K1: keyed `skill:<name>` — the kernel-side upsert dedupes across runner instances.
|
|
857
|
+
this.pushKnowledge({ role: "system", content: res.output }, undefined, { key: `skill:${name}` });
|
|
858
|
+
}
|
|
714
859
|
}
|
|
715
860
|
catch { /* skip */ }
|
|
716
861
|
}
|
|
@@ -814,7 +959,14 @@ export class RuntimeRunner {
|
|
|
814
959
|
catch { /* non-fatal */ }
|
|
815
960
|
}
|
|
816
961
|
}
|
|
817
|
-
yield {
|
|
962
|
+
yield {
|
|
963
|
+
type: "done",
|
|
964
|
+
iterations: turnsUsed,
|
|
965
|
+
totalTokens,
|
|
966
|
+
status,
|
|
967
|
+
// ③ loop-agent: surface the kernel-adjudicated after-round decision to the driver.
|
|
968
|
+
...(result?.paceDecision ? { paceDecision: result.paceDecision } : {}),
|
|
969
|
+
};
|
|
818
970
|
this.activeKernel = null;
|
|
819
971
|
this.currentSessionId = null;
|
|
820
972
|
}
|
|
@@ -864,7 +1016,10 @@ export class RuntimeRunner {
|
|
|
864
1016
|
const manifest = workflowNodeToManifest(node, parentSessionId);
|
|
865
1017
|
// G4: surface remaining workflow budget so a coordinator node can size its submission.
|
|
866
1018
|
const budgetNote = workflowBudgetNote(budget);
|
|
867
|
-
|
|
1019
|
+
// W-N2: a DAG edge carries data — every dependent node sees its dependencies' outputs (the
|
|
1020
|
+
// kernel sends `input_agent_ids` for all dependents; judges/reduce keep their special paths).
|
|
1021
|
+
const depsNote = dependencyOutputsNote(node.input_agent_ids, outputs);
|
|
1022
|
+
const withBudget = (goal) => [goal, depsNote, budgetNote].filter(Boolean).join("\n\n");
|
|
868
1023
|
const mkCtx = (goal) => ({
|
|
869
1024
|
parentOpts: this.opts,
|
|
870
1025
|
parentSessionId,
|
|
@@ -873,6 +1028,10 @@ export class RuntimeRunner {
|
|
|
873
1028
|
sessionLog: this.opts.sessionLog,
|
|
874
1029
|
// M5 v2.1: this child IS a workflow node — its `start_workflow` flattens to this kernel.
|
|
875
1030
|
isWorkflowNode: true,
|
|
1031
|
+
// W-N1: trusted workflow nodes run on the parent's execution plane (they carry no grant list
|
|
1032
|
+
// by design — filtering on the missing list ran every DAG node TOOL-LESS); quarantined nodes
|
|
1033
|
+
// stay deny-all filtered (they read untrusted content).
|
|
1034
|
+
toolAccess: (node.trust === "quarantined" ? "filtered" : "inherit"),
|
|
876
1035
|
// #2-B-ii: the per-node abort signal the driver fires when the kernel preempts this node.
|
|
877
1036
|
...(abortSignal ? { abortSignal } : {}),
|
|
878
1037
|
});
|
|
@@ -892,9 +1051,18 @@ export class RuntimeRunner {
|
|
|
892
1051
|
const winnerId = winner === "right" ? node.judge_match.right : node.judge_match.left;
|
|
893
1052
|
return withSignal(result, { tournamentWinner: winnerId });
|
|
894
1053
|
}
|
|
895
|
-
// A#2 v2 loop iteration: run the increment
|
|
1054
|
+
// A#2 v2 loop iteration: run the increment under the armed pacing trap (workflowNodeToSpec set
|
|
1055
|
+
// `loopRound`, and the iteration resumes the loop's stable session — transcript-as-carry).
|
|
1056
|
+
// DW-3 one vocabulary: the kernel-adjudicated `pace` verb IS the continuation signal
|
|
1057
|
+
// (stop → loopContinue=false); the legacy text-sniffed JSON blob survives only as the fallback
|
|
1058
|
+
// when no pace decision arrives (stub orchestrators, harness children), where no signal still
|
|
1059
|
+
// means "run to max_iters" (v1).
|
|
896
1060
|
if (node.loop_max_iters != null) {
|
|
897
|
-
const
|
|
1061
|
+
const iteration = Number(/-i(\d+)$/.exec(node.agent_id)?.[1] ?? "0");
|
|
1062
|
+
const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters, iteration)}`));
|
|
1063
|
+
const pace = result.result.paceDecision;
|
|
1064
|
+
if (pace)
|
|
1065
|
+
return withSignal(result, { loopContinue: pace.action !== "stop" });
|
|
898
1066
|
const cont = extractLoopContinue(textOf(result));
|
|
899
1067
|
return cont === undefined ? result : withSignal(result, { loopContinue: cont });
|
|
900
1068
|
}
|
|
@@ -991,6 +1159,21 @@ export class RuntimeRunner {
|
|
|
991
1159
|
: {}),
|
|
992
1160
|
};
|
|
993
1161
|
}
|
|
1162
|
+
// O6: tune/disable the in-kernel repeat fuse (absent ⇒ kernel defaults: enabled, 5/8).
|
|
1163
|
+
if (this.opts.repeatFuse !== undefined) {
|
|
1164
|
+
const rf = this.opts.repeatFuse;
|
|
1165
|
+
config.repeat_fuse = rf === false
|
|
1166
|
+
? { enabled: false, deny_after: 0, terminate_after: 0 }
|
|
1167
|
+
: { enabled: true, deny_after: rf.denyAfter ?? 5, terminate_after: rf.terminateAfter ?? 8 };
|
|
1168
|
+
}
|
|
1169
|
+
// O4: turn-end criteria gate toggle (absent ⇒ kernel default: enabled).
|
|
1170
|
+
if (this.opts.criteriaGate !== undefined) {
|
|
1171
|
+
config.criteria_gate = this.opts.criteriaGate;
|
|
1172
|
+
}
|
|
1173
|
+
// K2: knowledge budget ratio (absent ⇒ kernel default 0.25; 0 disables).
|
|
1174
|
+
if (this.opts.knowledgeBudgetRatio !== undefined) {
|
|
1175
|
+
config.knowledge_budget_ratio = this.opts.knowledgeBudgetRatio;
|
|
1176
|
+
}
|
|
994
1177
|
kernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
995
1178
|
if (this.opts.memoryPolicy) {
|
|
996
1179
|
const m = this.opts.memoryPolicy;
|
|
@@ -1055,10 +1238,22 @@ export class RuntimeRunner {
|
|
|
1055
1238
|
parent_session_id: parentSessionId,
|
|
1056
1239
|
// W0-ABI resume: skip nodes already completed before an interruption.
|
|
1057
1240
|
...(opts?.resumedCompleted && opts.resumedCompleted.length ? { resumed_completed: opts.resumedCompleted } : {}),
|
|
1241
|
+
// W-1: signal-carrying completion records (classify branch / loop stop replay).
|
|
1242
|
+
...(opts?.resumedResults?.length
|
|
1243
|
+
? {
|
|
1244
|
+
resumed_results: opts.resumedResults.map(r => ({
|
|
1245
|
+
agent_id: r.agentId,
|
|
1246
|
+
...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
|
|
1247
|
+
...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
|
|
1248
|
+
...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
|
|
1249
|
+
})),
|
|
1250
|
+
}
|
|
1251
|
+
: {}),
|
|
1058
1252
|
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
1059
1253
|
...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
1254
|
+
...(opts?.resumedSubmissionBases && opts.resumedSubmissionBases.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
|
|
1060
1255
|
});
|
|
1061
|
-
return await this.driveWorkflow(observations, parentSessionId, runtime);
|
|
1256
|
+
return await this.driveWorkflow(observations, parentSessionId, runtime, opts?.resumedOutputs);
|
|
1062
1257
|
}
|
|
1063
1258
|
finally {
|
|
1064
1259
|
if (bootstrapped) {
|
|
@@ -1081,6 +1276,18 @@ export class RuntimeRunner {
|
|
|
1081
1276
|
const parentSessionId = this.currentSessionId;
|
|
1082
1277
|
const runtime = this.activeKernel;
|
|
1083
1278
|
const observations = kernelApply(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
1279
|
+
// W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
|
|
1280
|
+
// announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
|
|
1281
|
+
// had this spec, unlike the `runWorkflow` path.
|
|
1282
|
+
const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
|
|
1283
|
+
if (submitted) {
|
|
1284
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1285
|
+
turn: runtime.turn(),
|
|
1286
|
+
nodes: workflowSpecToKernel(spec).nodes ?? [],
|
|
1287
|
+
baseIndex: submitted.base,
|
|
1288
|
+
submitterAgentId: opts?.submitterAgentId,
|
|
1289
|
+
}));
|
|
1290
|
+
}
|
|
1084
1291
|
return this.driveWorkflow(observations, parentSessionId, runtime);
|
|
1085
1292
|
}
|
|
1086
1293
|
/**
|
|
@@ -1113,7 +1320,8 @@ export class RuntimeRunner {
|
|
|
1113
1320
|
if (!source)
|
|
1114
1321
|
return null;
|
|
1115
1322
|
while (!batchState.settled) {
|
|
1116
|
-
|
|
1323
|
+
// O2: injected notes participate in the monitor too (drain order matches nextInboundSignal).
|
|
1324
|
+
const sig = this.injectedSignals.shift() ?? await source.nextSignal();
|
|
1117
1325
|
if (batchState.settled)
|
|
1118
1326
|
break;
|
|
1119
1327
|
if (!sig) {
|
|
@@ -1136,7 +1344,7 @@ export class RuntimeRunner {
|
|
|
1136
1344
|
* `submit_workflow`): run each kernel-emitted batch in parallel, feed completions back (appending any
|
|
1137
1345
|
* agent-submitted nodes first), and loop until the kernel reports the workflow complete.
|
|
1138
1346
|
*/
|
|
1139
|
-
async driveWorkflow(initial, parentSessionId, runtime) {
|
|
1347
|
+
async driveWorkflow(initial, parentSessionId, runtime, seedOutputs) {
|
|
1140
1348
|
const observations = initial;
|
|
1141
1349
|
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
1142
1350
|
const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.nodes ?? [];
|
|
@@ -1149,7 +1357,9 @@ export class RuntimeRunner {
|
|
|
1149
1357
|
let nodes = collectNodes(observations);
|
|
1150
1358
|
let budget = collectBudget(observations);
|
|
1151
1359
|
// G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
|
|
1152
|
-
|
|
1360
|
+
// W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
|
|
1361
|
+
// still see their (pre-crash) dependencies' outputs.
|
|
1362
|
+
const outputs = new Map(seedOutputs ?? []);
|
|
1153
1363
|
for (;;) {
|
|
1154
1364
|
if (nodes.length === 0)
|
|
1155
1365
|
return { completed: [], failed: [], outputs: Object.fromEntries(outputs) };
|
|
@@ -1169,7 +1379,13 @@ export class RuntimeRunner {
|
|
|
1169
1379
|
for (const result of results) {
|
|
1170
1380
|
// G2: record this node's output so a downstream reduce node can consume it.
|
|
1171
1381
|
const outContent = result.result.finalMessage?.content;
|
|
1172
|
-
|
|
1382
|
+
const outText = typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "";
|
|
1383
|
+
outputs.set(result.agentId, outText);
|
|
1384
|
+
// A loop iteration completes under `wf-node{N}-i{k}` but its dependents consume the STABLE
|
|
1385
|
+
// node id `wf-node{N}` — alias it so the LAST iteration's output is what dependents see.
|
|
1386
|
+
const stableId = result.agentId.replace(/-i\d+$/, "");
|
|
1387
|
+
if (stableId !== result.agentId)
|
|
1388
|
+
outputs.set(stableId, outText);
|
|
1173
1389
|
// R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
|
|
1174
1390
|
// reporting the node's completion — the workflow is still active, so even a last-node
|
|
1175
1391
|
// submission keeps the DAG alive.
|
|
@@ -1180,10 +1396,15 @@ export class RuntimeRunner {
|
|
|
1180
1396
|
const subObs = kernelApply(runtime, this.pendingObservations, submitEvent);
|
|
1181
1397
|
nextNodes.push(...collectNodes(subObs));
|
|
1182
1398
|
budget = collectBudget(subObs) ?? budget;
|
|
1183
|
-
// R3-1: persist the submission (kernel-shape nodes)
|
|
1399
|
+
// R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
|
|
1400
|
+
// so resume can re-apply the batch at the exact original graph position. W-N3: also the
|
|
1401
|
+
// submitter, so resume drops batches whose submitter re-runs (it will re-submit).
|
|
1402
|
+
const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
|
|
1184
1403
|
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
1185
1404
|
turn: runtime.turn(),
|
|
1186
1405
|
nodes: submitEvent.nodes ?? [],
|
|
1406
|
+
baseIndex: submitted?.base,
|
|
1407
|
+
submitterAgentId: result.agentId,
|
|
1187
1408
|
}));
|
|
1188
1409
|
}
|
|
1189
1410
|
const obs = kernelApply(runtime, this.pendingObservations, {
|
|
@@ -1195,11 +1416,17 @@ export class RuntimeRunner {
|
|
|
1195
1416
|
const d = findDone(obs);
|
|
1196
1417
|
if (d)
|
|
1197
1418
|
done = d;
|
|
1198
|
-
// Persist node completion for resume recovery.
|
|
1419
|
+
// Persist node completion for resume recovery. W-1: the result-borne control signals ride
|
|
1420
|
+
// along (a resumed classifier re-prunes; a recorded loop stop is honored) plus the output
|
|
1421
|
+
// text (post-resume dependents/reduce still see this node's output).
|
|
1199
1422
|
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
|
|
1200
1423
|
turn: runtime.turn(),
|
|
1201
1424
|
agentId: result.agentId,
|
|
1202
1425
|
termination: result.result.termination,
|
|
1426
|
+
classifyBranch: result.result.classifyBranch,
|
|
1427
|
+
tournamentWinner: result.result.tournamentWinner,
|
|
1428
|
+
loopContinue: result.result.loopContinue,
|
|
1429
|
+
...(result.result.termination === "completed" && outText ? { output: outText } : {}),
|
|
1203
1430
|
}));
|
|
1204
1431
|
}
|
|
1205
1432
|
if (done && nextNodes.length === 0) {
|
|
@@ -1210,8 +1437,9 @@ export class RuntimeRunner {
|
|
|
1210
1437
|
}
|
|
1211
1438
|
/**
|
|
1212
1439
|
* Resume a workflow from the parent session's completed nodes.
|
|
1213
|
-
* Reads the session log, extracts completed workflow node
|
|
1214
|
-
* calls runWorkflow
|
|
1440
|
+
* Reads the session log, extracts completed workflow node records (with their W-1 control
|
|
1441
|
+
* signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
|
|
1442
|
+
* flow (classify prune / loop stop), and the driver re-seeds its outputs map.
|
|
1215
1443
|
*/
|
|
1216
1444
|
async resumeWorkflow(spec, opts) {
|
|
1217
1445
|
// Standalone resume: a stateless handler passes the prior `sessionId`; mid-run callers omit it.
|
|
@@ -1220,9 +1448,34 @@ export class RuntimeRunner {
|
|
|
1220
1448
|
throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
|
|
1221
1449
|
}
|
|
1222
1450
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
1223
|
-
const
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1451
|
+
const resumedResults = recoverCompletedWorkflowNodes(events);
|
|
1452
|
+
const completedIds = new Set(resumedResults.map(r => r.agentId));
|
|
1453
|
+
const recovered = recoverSubmittedWorkflowNodes(events);
|
|
1454
|
+
// W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
|
|
1455
|
+
// re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
|
|
1456
|
+
// Only safe with exact bases (the dropped batch's slots become inert placeholders); a legacy
|
|
1457
|
+
// order-only log keeps every batch, since dropping would shift all later indices.
|
|
1458
|
+
let { submissions, bases } = recovered;
|
|
1459
|
+
if (bases.length === submissions.length && submissions.length > 0) {
|
|
1460
|
+
const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
|
|
1461
|
+
submissions = submissions.filter((_, i) => keep[i]);
|
|
1462
|
+
bases = bases.filter((_, i) => keep[i]);
|
|
1463
|
+
}
|
|
1464
|
+
const resumedOutputs = new Map(resumedResults.filter(r => r.output).map(r => [r.agentId, r.output]));
|
|
1465
|
+
// Alias loop iterations onto their stable node id (last iteration wins) — dependents consume
|
|
1466
|
+
// `wf-node{N}`, not `wf-node{N}-i{k}`.
|
|
1467
|
+
for (const r of resumedResults) {
|
|
1468
|
+
const stableId = r.agentId.replace(/-i\d+$/, "");
|
|
1469
|
+
if (stableId !== r.agentId && r.output)
|
|
1470
|
+
resumedOutputs.set(stableId, r.output);
|
|
1471
|
+
}
|
|
1472
|
+
return this.runWorkflow(spec, {
|
|
1473
|
+
resumedResults,
|
|
1474
|
+
resumedSubmissions: submissions,
|
|
1475
|
+
resumedSubmissionBases: bases,
|
|
1476
|
+
resumedOutputs,
|
|
1477
|
+
sessionId,
|
|
1478
|
+
});
|
|
1226
1479
|
}
|
|
1227
1480
|
async appendObservations(sessionId, runtime, nextArchiveStart) {
|
|
1228
1481
|
const turn = runtime.turn();
|
|
@@ -1258,22 +1511,67 @@ export class RuntimeRunner {
|
|
|
1258
1511
|
});
|
|
1259
1512
|
if (!event)
|
|
1260
1513
|
continue;
|
|
1261
|
-
if (obs.kind === "page_out" && obs.archived) {
|
|
1262
|
-
this.localPageOutCache.push(...obs.archived);
|
|
1263
|
-
}
|
|
1264
1514
|
const compressedSeq = await this.opts.sessionLog.append(sessionId, event);
|
|
1265
1515
|
if (event.kind === "compressed") {
|
|
1266
1516
|
nextArchiveStart = compressedSeq + 1;
|
|
1517
|
+
// One compaction = one kernel observation: the page_out session record (and the
|
|
1518
|
+
// semantic-archive branch) is DERIVED from Compressed.tier_hint, preserving the
|
|
1519
|
+
// session-log format and OsSnapshot page_out_count.
|
|
1520
|
+
const archived = obs.archived;
|
|
1521
|
+
if (obs.tier_hint && Array.isArray(archived) && archived.length > 0) {
|
|
1522
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1523
|
+
kind: "page_out",
|
|
1524
|
+
turn: obs.turn ?? turn,
|
|
1525
|
+
action: compressionAction(obs.action),
|
|
1526
|
+
summary: obs.summary,
|
|
1527
|
+
tier_hint: obs.tier_hint ?? "durable",
|
|
1528
|
+
message_count: archived.length,
|
|
1529
|
+
});
|
|
1530
|
+
if (obs.tier_hint === "semantic") {
|
|
1531
|
+
void this.archiveSemanticPageOut(archived, compressionAction(obs.action));
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1267
1534
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
void this.archiveSemanticPageOut(obs.archived, compressionAction(obs.action));
|
|
1535
|
+
// K4: a sprint renewal dropped the old history — including any earlier memory hits — so
|
|
1536
|
+
// re-run the preQueryMemory prefetch for the new sprint (live observations only).
|
|
1537
|
+
if (obs.kind === "renewed") {
|
|
1538
|
+
await this.prefetchMemoryIntoHistory(runtime, "renewal");
|
|
1273
1539
|
}
|
|
1274
1540
|
}
|
|
1275
1541
|
return nextArchiveStart;
|
|
1276
1542
|
}
|
|
1543
|
+
/** I4 + K4: fetch long-term memory hits for the current goal and land them in `history` as an
|
|
1544
|
+
* ordinary user turn — single-use retrieval content that decays with the compression pyramid,
|
|
1545
|
+
* never pinned into `knowledge`. `phase: "initial"` = once before turn 1; `phase: "renewal"` =
|
|
1546
|
+
* re-fired after each sprint renewal (renewal drops the old history INCLUDING earlier memory
|
|
1547
|
+
* hits). Errs-open throughout. */
|
|
1548
|
+
async prefetchMemoryIntoHistory(runtime, phase) {
|
|
1549
|
+
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
1550
|
+
return;
|
|
1551
|
+
// P10: recall is default-on (CC session-start recall) — with no hook configured,
|
|
1552
|
+
// the goal itself is the query. preQueryMemory stays as the targeting override.
|
|
1553
|
+
const preQuery = this.opts.preQueryMemory
|
|
1554
|
+
?? ((ctx) => [ctx.goal]);
|
|
1555
|
+
try {
|
|
1556
|
+
const queries = await preQuery({ goal: this.currentGoal, phase });
|
|
1557
|
+
const lines = [];
|
|
1558
|
+
for (const q of queries ?? []) {
|
|
1559
|
+
if (typeof q !== "string" || !q.trim())
|
|
1560
|
+
continue;
|
|
1561
|
+
const hits = await this.opts.dreamStore.search(this.opts.agentId, q, 5);
|
|
1562
|
+
for (const hit of hits) {
|
|
1563
|
+
lines.push(`[memory score=${hit.score.toFixed(3)}] ${hit.text}`);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (lines.length > 0) {
|
|
1567
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
1568
|
+
kind: "add_history_message",
|
|
1569
|
+
message: { role: "user", content: lines.join("\n") },
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
catch { /* errs-open */ }
|
|
1574
|
+
}
|
|
1277
1575
|
async archiveSemanticPageOut(archived, action) {
|
|
1278
1576
|
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
1279
1577
|
return;
|
|
@@ -1282,8 +1580,12 @@ export class RuntimeRunner {
|
|
|
1282
1580
|
? await this.opts.dreamSummarizer.summarize(archived, { action })
|
|
1283
1581
|
: await summarizeForLongTermMemory(this.opts.dreamProvider ?? this.opts.provider, archived, this.opts.dreamSystemPrompt);
|
|
1284
1582
|
const existing = await this.opts.dreamStore.loadMemories(this.opts.agentId);
|
|
1583
|
+
// P2 write-funnel (wasm surface has no kernel write gate yet): jaccard dedup +
|
|
1584
|
+
// advisory 0.6 score — an automatic summary must never outrank curated content.
|
|
1585
|
+
if (existing.some(e => jaccardSimilarity(e.text, summary) >= 0.9))
|
|
1586
|
+
return;
|
|
1285
1587
|
await this.opts.dreamStore.commit(this.opts.agentId, {
|
|
1286
|
-
toAdd: [{ text: summary, score:
|
|
1588
|
+
toAdd: [{ text: summary, score: 0.6, metadata: { source: "semantic_page_out", action } }],
|
|
1287
1589
|
toRemoveIndices: [],
|
|
1288
1590
|
stats: {
|
|
1289
1591
|
insightsProcessed: 1,
|
|
@@ -1298,6 +1600,19 @@ export class RuntimeRunner {
|
|
|
1298
1600
|
}
|
|
1299
1601
|
}
|
|
1300
1602
|
}
|
|
1603
|
+
/** Word-set jaccard similarity — the curator's dedup rule at the write funnel. */
|
|
1604
|
+
function jaccardSimilarity(a, b) {
|
|
1605
|
+
const sa = new Set(a.split(/\s+/).filter(Boolean));
|
|
1606
|
+
const sb = new Set(b.split(/\s+/).filter(Boolean));
|
|
1607
|
+
if (sa.size === 0 && sb.size === 0)
|
|
1608
|
+
return 1;
|
|
1609
|
+
let inter = 0;
|
|
1610
|
+
for (const w of sa)
|
|
1611
|
+
if (sb.has(w))
|
|
1612
|
+
inter++;
|
|
1613
|
+
const union = sa.size + sb.size - inter;
|
|
1614
|
+
return union === 0 ? 0 : inter / union;
|
|
1615
|
+
}
|
|
1301
1616
|
async function summarizeForLongTermMemory(provider, archived, systemPrompt) {
|
|
1302
1617
|
const transcript = archived
|
|
1303
1618
|
.map(m => `${m.role}: ${m.content}`)
|