@deepstrike/sdk 0.2.36 → 0.2.38
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 +3 -1
- package/dist/index.js +2 -0
- package/dist/os/public.d.ts +1 -1
- package/dist/os/public.js +1 -1
- package/dist/runtime/facade.js +11 -11
- package/dist/runtime/kernel-event-log.d.ts +0 -6
- package/dist/runtime/kernel-event-log.js +57 -62
- package/dist/runtime/kernel-step.d.ts +20 -1
- package/dist/runtime/kernel-step.js +25 -0
- package/dist/runtime/loop-driver.d.ts +108 -0
- package/dist/runtime/loop-driver.js +198 -0
- package/dist/runtime/os-snapshot.d.ts +0 -1
- package/dist/runtime/os-snapshot.js +0 -19
- package/dist/runtime/reactive-session.d.ts +5 -2
- package/dist/runtime/reactive-session.js +17 -4
- package/dist/runtime/run-group.d.ts +9 -0
- package/dist/runtime/run-group.js +18 -4
- package/dist/runtime/runner.d.ts +43 -6
- package/dist/runtime/runner.js +305 -71
- package/dist/runtime/session-log.d.ts +46 -54
- package/dist/runtime/session-repair.d.ts +29 -7
- package/dist/runtime/session-repair.js +37 -9
- package/dist/runtime/sub-agent-orchestrator.d.ts +12 -0
- package/dist/runtime/sub-agent-orchestrator.js +48 -32
- package/dist/runtime/workflow-control-flow.d.ts +10 -2
- package/dist/runtime/workflow-control-flow.js +27 -6
- package/dist/signals/gateway.d.ts +4 -2
- package/dist/signals/gateway.js +8 -1
- package/dist/types/agent.d.ts +28 -1
- package/dist/types/agent.js +25 -1
- package/dist/types.d.ts +50 -0
- package/package.json +2 -2
package/dist/runtime/runner.js
CHANGED
|
@@ -5,12 +5,12 @@ import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-rep
|
|
|
5
5
|
import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
6
6
|
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverCompletedWorkflowNodes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
|
|
7
7
|
import { KernelPrimitivesDashboard } from "./kernel-primitives-dashboard.js";
|
|
8
|
-
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, kernelAction, kernelApply, kernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, taskUpdateToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
8
|
+
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, entropySampleFromObservation, kernelAction, kernelApply, kernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, taskUpdateToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
9
9
|
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "../types/agent.js";
|
|
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";
|
|
13
|
+
import { loopInstruction, classifyInstruction, judgeGoal, dependencyOutputsNote, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
|
|
14
14
|
import { governancePolicyToKernelEvent, governanceFilterSchema } from "../governance.js";
|
|
15
15
|
import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
|
|
16
16
|
import { assertNativeProfile } from "./os-profile.js";
|
|
@@ -40,6 +40,8 @@ export class RuntimeRunner {
|
|
|
40
40
|
* at the next safe point (after the tool turn resolves, kernel back in Reason — not suspended). */
|
|
41
41
|
pendingAuthoredWorkflows = [];
|
|
42
42
|
dashboard = null;
|
|
43
|
+
/** Most recent kernel entropy sample of the active/last run (see `latestEntropy`). */
|
|
44
|
+
lastEntropySample = null;
|
|
43
45
|
constructor(opts) {
|
|
44
46
|
this.opts = opts;
|
|
45
47
|
if (opts.enableDiagnosticsDashboard) {
|
|
@@ -72,23 +74,30 @@ export class RuntimeRunner {
|
|
|
72
74
|
return;
|
|
73
75
|
}
|
|
74
76
|
const existing = await this.opts.dreamStore.loadMemories(agentId);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
// Curator-style jaccard dedup at the single write path: a near-duplicate of an
|
|
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);
|
|
100
|
+
}
|
|
92
101
|
await this.appendMemorySyscallObservations(sessionId, observations);
|
|
93
102
|
}
|
|
94
103
|
async queryMemory(query, opts = {}) {
|
|
@@ -116,24 +125,19 @@ export class RuntimeRunner {
|
|
|
116
125
|
}
|
|
117
126
|
}
|
|
118
127
|
await this.appendMemorySyscallObservations(sessionId, observations);
|
|
119
|
-
await this.logMemoryRetrievalResult(sessionId,
|
|
128
|
+
await this.logMemoryRetrievalResult(sessionId, retrieval);
|
|
120
129
|
return hits;
|
|
121
130
|
}
|
|
122
|
-
async logMemoryRetrievalResult(sessionId,
|
|
131
|
+
async logMemoryRetrievalResult(sessionId, retrieval) {
|
|
123
132
|
if (!sessionId)
|
|
124
133
|
return;
|
|
134
|
+
// The session-log record is the durable audit artifact; the kernel needs no
|
|
135
|
+
// acknowledgment (the former kernel event was a no-op and was removed).
|
|
125
136
|
await this.opts.sessionLog.append(sessionId, {
|
|
126
137
|
kind: "memory_retrieval_result",
|
|
127
138
|
selected_memory_ids: retrieval.selected_memory_ids,
|
|
128
139
|
selection_rationale: retrieval.selection_rationale,
|
|
129
140
|
});
|
|
130
|
-
kernelApply(runtime, [], {
|
|
131
|
-
kind: "memory_retrieval_result",
|
|
132
|
-
retrieval: {
|
|
133
|
-
selected_memory_ids: retrieval.selected_memory_ids,
|
|
134
|
-
selection_rationale: retrieval.selection_rationale,
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
141
|
}
|
|
138
142
|
createSyscallRuntime() {
|
|
139
143
|
const { KernelRuntime } = getKernel();
|
|
@@ -152,7 +156,7 @@ export class RuntimeRunner {
|
|
|
152
156
|
* exactly as a mid-run spawn would be. Must run BEFORE `start_run` so the in-kernel gate enforces
|
|
153
157
|
* every policy from the first spawn. No config ⇒ the native-profile defaults (铁律: defaults only).
|
|
154
158
|
*/
|
|
155
|
-
applyKernelPolicies(runtime, groupTokensBase, groupSpawnsBase) {
|
|
159
|
+
applyKernelPolicies(runtime, groupTokensBase, groupSpawnsBase, groupRoundsBase) {
|
|
156
160
|
// K2: lower governance / attention / scheduler / quota in ONE `configure_run` event instead of
|
|
157
161
|
// the previous 2–4 separate `set_*` / `load_governance_policy` events. The kernel applies each
|
|
158
162
|
// present field via the same path its granular event uses; absent fields are left untouched.
|
|
@@ -187,6 +191,10 @@ export class RuntimeRunner {
|
|
|
187
191
|
if (groupSpawnsBase !== undefined && groupSpawnsBase > 0) {
|
|
188
192
|
config.group_spawns_base = groupSpawnsBase;
|
|
189
193
|
}
|
|
194
|
+
if (groupRoundsBase !== undefined && groupRoundsBase > 0) {
|
|
195
|
+
// ③ loop-agent: completed-round count seeds the pacing trap's max_rounds coercion.
|
|
196
|
+
config.group_rounds_base = groupRoundsBase;
|
|
197
|
+
}
|
|
190
198
|
// O6: tune/disable the in-kernel repeat fuse. `false` disables; an object overrides thresholds.
|
|
191
199
|
// Absent ⇒ kernel defaults (enabled, deny_after=5, terminate_after=8).
|
|
192
200
|
if (this.opts.repeatFuse !== undefined) {
|
|
@@ -203,6 +211,18 @@ export class RuntimeRunner {
|
|
|
203
211
|
if (this.opts.knowledgeBudgetRatio !== undefined) {
|
|
204
212
|
config.knowledge_budget_ratio = this.opts.knowledgeBudgetRatio;
|
|
205
213
|
}
|
|
214
|
+
// Entropy watch (opt-in): threshold alerting over the per-turn session-entropy score.
|
|
215
|
+
// Absent fields keep kernel defaults (threshold 0.65 / hysteresis 0.1 / cooldown 4).
|
|
216
|
+
if (this.opts.entropyWatch !== undefined) {
|
|
217
|
+
const ew = this.opts.entropyWatch;
|
|
218
|
+
config.entropy_watch = {
|
|
219
|
+
enabled: ew.enabled ?? true,
|
|
220
|
+
...(ew.threshold !== undefined ? { threshold: ew.threshold } : {}),
|
|
221
|
+
...(ew.hysteresis !== undefined ? { hysteresis: ew.hysteresis } : {}),
|
|
222
|
+
...(ew.cooldownTurns !== undefined ? { cooldown_turns: ew.cooldownTurns } : {}),
|
|
223
|
+
...(ew.notifyModel !== undefined ? { notify_model: ew.notifyModel } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
206
226
|
kernelApply(runtime, this.pendingObservations, { kind: "configure_run", config });
|
|
207
227
|
}
|
|
208
228
|
async appendMemorySyscallObservations(sessionId, observations) {
|
|
@@ -330,7 +350,10 @@ export class RuntimeRunner {
|
|
|
330
350
|
// G4: surface the workflow's remaining budget to the node's agent so a coordinator can size its
|
|
331
351
|
// `submit_workflow_nodes` batch to what is available (empty string ⇒ unbounded, no note).
|
|
332
352
|
const budgetNote = workflowBudgetNote(budget);
|
|
333
|
-
|
|
353
|
+
// W-N2: a DAG edge carries data — every dependent node sees its dependencies' outputs (the
|
|
354
|
+
// kernel sends `input_agent_ids` for all dependents; judges/reduce keep their special paths).
|
|
355
|
+
const depsNote = dependencyOutputsNote(node.input_agent_ids, outputs);
|
|
356
|
+
const withBudget = (goal) => [goal, depsNote, budgetNote].filter(Boolean).join("\n\n");
|
|
334
357
|
const mkCtx = (goal) => ({
|
|
335
358
|
parentOpts: this.opts,
|
|
336
359
|
parentSessionId,
|
|
@@ -340,6 +363,10 @@ export class RuntimeRunner {
|
|
|
340
363
|
// M5 v2.1: this child IS a workflow node — its `start_workflow` flattens to this kernel (the
|
|
341
364
|
// workflow it would author joins the running DAG) rather than bootstrapping a nested pivot.
|
|
342
365
|
isWorkflowNode: true,
|
|
366
|
+
// W-N1: trusted workflow nodes run on the parent's execution plane (they carry no grant list
|
|
367
|
+
// by design — filtering on the missing list ran every DAG node TOOL-LESS); quarantined nodes
|
|
368
|
+
// stay deny-all filtered (they read untrusted content).
|
|
369
|
+
toolAccess: (node.trust === "quarantined" ? "filtered" : "inherit"),
|
|
343
370
|
// #2-B-ii: the per-node abort signal the driver fires when the kernel preempts this node.
|
|
344
371
|
...(abortSignal ? { abortSignal } : {}),
|
|
345
372
|
...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
|
|
@@ -361,10 +388,18 @@ export class RuntimeRunner {
|
|
|
361
388
|
const winnerId = winner === "right" ? node.judge_match.right : node.judge_match.left;
|
|
362
389
|
return withSignal(result, { tournamentWinner: winnerId });
|
|
363
390
|
}
|
|
364
|
-
// A#2 v2 loop iteration: run the increment
|
|
365
|
-
//
|
|
391
|
+
// A#2 v2 loop iteration: run the increment under the armed pacing trap (workflowNodeToSpec set
|
|
392
|
+
// `loopRound`, and the iteration resumes the loop's stable session — transcript-as-carry).
|
|
393
|
+
// DW-3 one vocabulary: the kernel-adjudicated `pace` verb IS the continuation signal
|
|
394
|
+
// (stop → loopContinue=false); the legacy text-sniffed JSON blob survives only as the fallback
|
|
395
|
+
// when no pace decision arrives (stub orchestrators, harness children), where no signal still
|
|
396
|
+
// means "run to max_iters" (v1).
|
|
366
397
|
if (node.loop_max_iters != null) {
|
|
367
|
-
const
|
|
398
|
+
const iteration = Number(/-i(\d+)$/.exec(node.agent_id)?.[1] ?? "0");
|
|
399
|
+
const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters, iteration)}`));
|
|
400
|
+
const pace = result.result.paceDecision;
|
|
401
|
+
if (pace)
|
|
402
|
+
return withSignal(result, { loopContinue: pace.action !== "stop" });
|
|
368
403
|
const cont = extractLoopContinue(textOf(result));
|
|
369
404
|
return cont === undefined ? result : withSignal(result, { loopContinue: cont });
|
|
370
405
|
}
|
|
@@ -450,7 +485,7 @@ export class RuntimeRunner {
|
|
|
450
485
|
if (this.opts.runGroup) {
|
|
451
486
|
const g = this.opts.runGroup;
|
|
452
487
|
groupLedger = await g.budgetStore.read(g.id);
|
|
453
|
-
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId });
|
|
488
|
+
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId, kind: "vehicle" });
|
|
454
489
|
}
|
|
455
490
|
this.bootstrapWorkflowKernel(sessionId, spec, groupLedger?.tokensSpent, groupLedger?.subagentsSpawned);
|
|
456
491
|
}
|
|
@@ -463,10 +498,22 @@ export class RuntimeRunner {
|
|
|
463
498
|
parent_session_id: parentSessionId,
|
|
464
499
|
// W0-ABI resume: skip nodes already completed before an interruption.
|
|
465
500
|
...(opts?.resumedCompleted?.length ? { resumed_completed: opts.resumedCompleted } : {}),
|
|
501
|
+
// W-1: signal-carrying completion records (classify branch / loop stop replay).
|
|
502
|
+
...(opts?.resumedResults?.length
|
|
503
|
+
? {
|
|
504
|
+
resumed_results: opts.resumedResults.map(r => ({
|
|
505
|
+
agent_id: r.agentId,
|
|
506
|
+
...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
|
|
507
|
+
...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
|
|
508
|
+
...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
|
|
509
|
+
})),
|
|
510
|
+
}
|
|
511
|
+
: {}),
|
|
466
512
|
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
467
513
|
...(opts?.resumedSubmissions?.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
514
|
+
...(opts?.resumedSubmissionBases?.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
|
|
468
515
|
});
|
|
469
|
-
return await this.driveWorkflow(observations, parentSessionId, runtime);
|
|
516
|
+
return await this.driveWorkflow(observations, parentSessionId, runtime, opts?.resumedOutputs);
|
|
470
517
|
}
|
|
471
518
|
finally {
|
|
472
519
|
if (bootstrapped) {
|
|
@@ -534,6 +581,18 @@ export class RuntimeRunner {
|
|
|
534
581
|
const parentSessionId = this.currentSessionId;
|
|
535
582
|
const runtime = this.activeKernel;
|
|
536
583
|
const observations = kernelApply(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
584
|
+
// W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
|
|
585
|
+
// announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
|
|
586
|
+
// had this spec, unlike the `runWorkflow` path.
|
|
587
|
+
const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
|
|
588
|
+
if (submitted) {
|
|
589
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
590
|
+
turn: runtime.turn(),
|
|
591
|
+
nodes: workflowSpecToKernel(spec).nodes ?? [],
|
|
592
|
+
baseIndex: submitted.base,
|
|
593
|
+
submitterAgentId: opts?.submitterAgentId,
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
537
596
|
return this.driveWorkflow(observations, parentSessionId, runtime);
|
|
538
597
|
}
|
|
539
598
|
/**
|
|
@@ -597,7 +656,7 @@ export class RuntimeRunner {
|
|
|
597
656
|
* batch in parallel, feed completions back (appending any agent-submitted nodes first), and loop
|
|
598
657
|
* until the kernel reports the workflow complete. Returns the completed / failed node agent-ids.
|
|
599
658
|
*/
|
|
600
|
-
async driveWorkflow(initial, parentSessionId, runtime) {
|
|
659
|
+
async driveWorkflow(initial, parentSessionId, runtime, seedOutputs) {
|
|
601
660
|
let observations = initial;
|
|
602
661
|
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
603
662
|
const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")
|
|
@@ -614,7 +673,9 @@ export class RuntimeRunner {
|
|
|
614
673
|
// G2: each completed node's output, keyed by agent id — a reduce node reads its dependencies'
|
|
615
674
|
// outputs from here. Deps always complete in an earlier round than the reduce node that needs
|
|
616
675
|
// them (the kernel keeps the reduce node un-ready until its deps finish), so this is populated.
|
|
617
|
-
|
|
676
|
+
// W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
|
|
677
|
+
// still see their (pre-crash) dependencies' outputs.
|
|
678
|
+
const outputs = new Map(seedOutputs ?? []);
|
|
618
679
|
for (;;) {
|
|
619
680
|
if (nodes.length === 0)
|
|
620
681
|
return { completed: [], failed: [], outputs: Object.fromEntries(outputs) }; // nothing to run (e.g. all gated)
|
|
@@ -643,7 +704,13 @@ export class RuntimeRunner {
|
|
|
643
704
|
for (const result of results) {
|
|
644
705
|
// G2: record this node's output so a downstream reduce node can consume it.
|
|
645
706
|
const outContent = result.result.finalMessage?.content;
|
|
646
|
-
|
|
707
|
+
const outText = typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "";
|
|
708
|
+
outputs.set(result.agentId, outText);
|
|
709
|
+
// A loop iteration completes under `wf-node{N}-i{k}` but its dependents consume the STABLE
|
|
710
|
+
// node id `wf-node{N}` — alias it so the LAST iteration's output is what dependents see.
|
|
711
|
+
const stableId = result.agentId.replace(/-i\d+$/, "");
|
|
712
|
+
if (stableId !== result.agentId)
|
|
713
|
+
outputs.set(stableId, outText);
|
|
647
714
|
// R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
|
|
648
715
|
// reporting the node's completion — the workflow is still active (the kernel hasn't seen this
|
|
649
716
|
// node finish), so even a submission from the last running node keeps the DAG alive. The
|
|
@@ -655,10 +722,15 @@ export class RuntimeRunner {
|
|
|
655
722
|
const subObs = kernelApply(runtime, this.pendingObservations, submitEvent);
|
|
656
723
|
nextNodes.push(...collectNodes(subObs));
|
|
657
724
|
budget = collectBudget(subObs) ?? budget;
|
|
658
|
-
// R3-1: persist the submission (kernel-shape nodes)
|
|
725
|
+
// R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
|
|
726
|
+
// so resume can re-apply the batch at the exact original graph position. W-N3: also the
|
|
727
|
+
// submitter, so resume drops batches whose submitter re-runs (it will re-submit).
|
|
728
|
+
const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
|
|
659
729
|
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
660
730
|
turn: runtime.turn(),
|
|
661
731
|
nodes: submitEvent.nodes ?? [],
|
|
732
|
+
baseIndex: submitted?.base,
|
|
733
|
+
submitterAgentId: result.agentId,
|
|
662
734
|
}));
|
|
663
735
|
}
|
|
664
736
|
const obs = kernelApply(runtime, this.pendingObservations, {
|
|
@@ -670,11 +742,17 @@ export class RuntimeRunner {
|
|
|
670
742
|
const d = findDone(obs);
|
|
671
743
|
if (d)
|
|
672
744
|
done = d;
|
|
673
|
-
// Persist node completion for resume recovery.
|
|
745
|
+
// Persist node completion for resume recovery. W-1: the result-borne control signals ride
|
|
746
|
+
// along (a resumed classifier re-prunes; a recorded loop stop is honored) plus the output
|
|
747
|
+
// text (post-resume dependents/reduce still see this node's output).
|
|
674
748
|
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
|
|
675
749
|
turn: runtime.turn(),
|
|
676
750
|
agentId: result.agentId,
|
|
677
751
|
termination: result.result.termination,
|
|
752
|
+
classifyBranch: result.result.classifyBranch,
|
|
753
|
+
tournamentWinner: result.result.tournamentWinner,
|
|
754
|
+
loopContinue: result.result.loopContinue,
|
|
755
|
+
...(result.result.termination === "completed" && outText ? { output: outText } : {}),
|
|
678
756
|
}));
|
|
679
757
|
}
|
|
680
758
|
if (done && nextNodes.length === 0) {
|
|
@@ -685,8 +763,9 @@ export class RuntimeRunner {
|
|
|
685
763
|
}
|
|
686
764
|
/**
|
|
687
765
|
* Resume a workflow from the parent session's completed nodes.
|
|
688
|
-
* Reads the session log, extracts completed workflow node
|
|
689
|
-
* calls runWorkflow
|
|
766
|
+
* Reads the session log, extracts completed workflow node records (with their W-1 control
|
|
767
|
+
* signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
|
|
768
|
+
* flow (classify prune / loop stop), and the driver re-seeds its outputs map.
|
|
690
769
|
*/
|
|
691
770
|
async resumeWorkflow(spec, opts) {
|
|
692
771
|
// Standalone resume: a stateless handler passes the prior `sessionId` to pick up an interrupted
|
|
@@ -696,9 +775,34 @@ export class RuntimeRunner {
|
|
|
696
775
|
throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
|
|
697
776
|
}
|
|
698
777
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
|
|
778
|
+
const resumedResults = recoverCompletedWorkflowNodes(events);
|
|
779
|
+
const completedIds = new Set(resumedResults.map(r => r.agentId));
|
|
780
|
+
const recovered = recoverSubmittedWorkflowNodes(events);
|
|
781
|
+
// W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
|
|
782
|
+
// re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
|
|
783
|
+
// Only safe with exact bases (the dropped batch's slots become inert placeholders); a legacy
|
|
784
|
+
// order-only log keeps every batch, since dropping would shift all later indices.
|
|
785
|
+
let { submissions, bases } = recovered;
|
|
786
|
+
if (bases.length === submissions.length && submissions.length > 0) {
|
|
787
|
+
const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
|
|
788
|
+
submissions = submissions.filter((_, i) => keep[i]);
|
|
789
|
+
bases = bases.filter((_, i) => keep[i]);
|
|
790
|
+
}
|
|
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
|
+
return this.runWorkflow(spec, {
|
|
800
|
+
resumedResults,
|
|
801
|
+
resumedSubmissions: submissions,
|
|
802
|
+
resumedSubmissionBases: bases,
|
|
803
|
+
resumedOutputs,
|
|
804
|
+
sessionId,
|
|
805
|
+
});
|
|
702
806
|
}
|
|
703
807
|
interrupt() { this.interrupted = true; this.abortController?.abort(); }
|
|
704
808
|
/** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
|
|
@@ -715,6 +819,12 @@ export class RuntimeRunner {
|
|
|
715
819
|
payload: { goal: text },
|
|
716
820
|
});
|
|
717
821
|
}
|
|
822
|
+
/** The most recent kernel session-entropy sample (one per completed turn), or `null` before the
|
|
823
|
+
* first boundary. A pull companion to the streamed `entropy_sample` events — hosts polling from
|
|
824
|
+
* outside the stream (e.g. a heartbeat supervisor) read the latest measurement here. */
|
|
825
|
+
latestEntropy() {
|
|
826
|
+
return this.lastEntropySample;
|
|
827
|
+
}
|
|
718
828
|
/** Injected-note drain shared by the main loop's per-turn poll: injected notes first (FIFO), then
|
|
719
829
|
* the configured `signalSource`. Keeps the two inbound channels on one code path so they never drift. */
|
|
720
830
|
async nextInboundSignal() {
|
|
@@ -1148,9 +1258,9 @@ export class RuntimeRunner {
|
|
|
1148
1258
|
if (this.opts.runGroup) {
|
|
1149
1259
|
const g = this.opts.runGroup;
|
|
1150
1260
|
groupLedger = await g.budgetStore.read(g.id);
|
|
1151
|
-
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId });
|
|
1261
|
+
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId, kind: "vehicle" });
|
|
1152
1262
|
}
|
|
1153
|
-
this.applyKernelPolicies(runtime, groupLedger?.tokensSpent, groupLedger?.subagentsSpawned);
|
|
1263
|
+
this.applyKernelPolicies(runtime, groupLedger?.tokensSpent, groupLedger?.subagentsSpawned, groupLedger?.roundsCompleted);
|
|
1154
1264
|
// Multimodal upload: seed the user's attachments (images/audio) as a history
|
|
1155
1265
|
// message before start_run pushes the "[TASK STATE]" anchor. init_task does not
|
|
1156
1266
|
// clear history, so order becomes [attachment user msg, "Proceed…"] — both land
|
|
@@ -1621,10 +1731,27 @@ export class RuntimeRunner {
|
|
|
1621
1731
|
}
|
|
1622
1732
|
catch { /* malformed skill args — skip activation */ }
|
|
1623
1733
|
}
|
|
1734
|
+
const entropyObsStart = this.pendingObservations.length;
|
|
1624
1735
|
action = kernelAction(runtime, this.pendingObservations, {
|
|
1625
1736
|
kind: "tool_results",
|
|
1626
1737
|
results: toolResults.map(toolResultToKernel),
|
|
1627
1738
|
});
|
|
1739
|
+
// Surface the boundary's entropy measurement live (the heartbeat watch source) —
|
|
1740
|
+
// the session-log record lands via the normal appendObservations path.
|
|
1741
|
+
for (const obs of this.pendingObservations.slice(entropyObsStart)) {
|
|
1742
|
+
if (obs.kind === "entropy_sample") {
|
|
1743
|
+
this.lastEntropySample = entropySampleFromObservation(obs);
|
|
1744
|
+
yield { type: "entropy_sample", sample: this.lastEntropySample };
|
|
1745
|
+
}
|
|
1746
|
+
else if (obs.kind === "entropy_alert") {
|
|
1747
|
+
yield {
|
|
1748
|
+
type: "entropy_alert",
|
|
1749
|
+
turn: obs.turn ?? 0,
|
|
1750
|
+
score: obs.score ?? 0,
|
|
1751
|
+
threshold: obs.threshold ?? 0,
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1628
1755
|
}
|
|
1629
1756
|
else if (action.kind === "evaluate_milestone") {
|
|
1630
1757
|
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
@@ -1738,7 +1865,14 @@ export class RuntimeRunner {
|
|
|
1738
1865
|
catch { /* non-fatal */ }
|
|
1739
1866
|
}
|
|
1740
1867
|
}
|
|
1741
|
-
yield {
|
|
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
|
+
};
|
|
1742
1876
|
this.activeKernel = null;
|
|
1743
1877
|
this.currentSessionId = null;
|
|
1744
1878
|
this.dashboard = null;
|
|
@@ -1749,10 +1883,14 @@ export class RuntimeRunner {
|
|
|
1749
1883
|
* after each sprint renewal (`phase: "renewal"`): renewal drops the old history INCLUDING the
|
|
1750
1884
|
* earlier memory hits, so the new sprint gets a fresh recall pass. Errs-open throughout. */
|
|
1751
1885
|
async prefetchMemoryIntoHistory(runtime, phase) {
|
|
1752
|
-
if (!this.opts.
|
|
1886
|
+
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
1753
1887
|
return;
|
|
1888
|
+
// P10: recall is default-on (CC session-start recall) — with no hook configured,
|
|
1889
|
+
// the goal itself is the query. preQueryMemory stays as the targeting override.
|
|
1890
|
+
const preQuery = this.opts.preQueryMemory
|
|
1891
|
+
?? ((ctx) => [ctx.goal]);
|
|
1754
1892
|
try {
|
|
1755
|
-
const queries = await
|
|
1893
|
+
const queries = await preQuery({
|
|
1756
1894
|
goal: this.currentGoal,
|
|
1757
1895
|
runSpec: this.opts.runSpec,
|
|
1758
1896
|
phase,
|
|
@@ -1829,14 +1967,24 @@ export class RuntimeRunner {
|
|
|
1829
1967
|
nextArchiveStart = compressedSeq + 1;
|
|
1830
1968
|
const archived = obs.kind === "compressed" ? obs.archived : undefined;
|
|
1831
1969
|
if (this.opts.asyncSummarizer && archived && archived.length > 0) {
|
|
1832
|
-
void this.upgradeCompressedSummary(sessionId, compressedSeq, archived, compressionAction(obs.action) ?? "auto_compact");
|
|
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
|
+
}
|
|
1833
1987
|
}
|
|
1834
|
-
}
|
|
1835
|
-
if (obs.kind === "page_out"
|
|
1836
|
-
&& obs.tier_hint === "semantic"
|
|
1837
|
-
&& Array.isArray(obs.archived)
|
|
1838
|
-
&& obs.archived.length > 0) {
|
|
1839
|
-
void this.archiveSemanticPageOut(obs.archived, compressionAction(obs.action));
|
|
1840
1988
|
}
|
|
1841
1989
|
// K4: a sprint renewal dropped the old history — including any earlier memory hits — so
|
|
1842
1990
|
// re-run the preQueryMemory prefetch for the new sprint (live observations only: this
|
|
@@ -1854,23 +2002,25 @@ export class RuntimeRunner {
|
|
|
1854
2002
|
const summary = this.opts.dreamSummarizer
|
|
1855
2003
|
? await this.opts.dreamSummarizer.summarize(archived, { action })
|
|
1856
2004
|
: await summarizeForLongTermMemory(this.opts.dreamProvider ?? this.opts.provider, archived, this.opts.dreamSystemPrompt);
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2005
|
+
// P2 write-funnel: route through the ONE gated WriteMemory syscall so validation,
|
|
2006
|
+
// the rolling write quota, dedup, and the memory_written audit all apply. Score is
|
|
2007
|
+
// advisory (0.6) — an automatic summary must never outrank curated content.
|
|
2008
|
+
await this.writeMemory({
|
|
2009
|
+
content: summary,
|
|
2010
|
+
metadata: {
|
|
2011
|
+
name: `page-out-${Date.now()}`,
|
|
2012
|
+
description: `auto summary of ${action ?? "compaction"} archive`,
|
|
2013
|
+
source: "semantic_page_out",
|
|
2014
|
+
action,
|
|
2015
|
+
score: 0.6,
|
|
1866
2016
|
},
|
|
1867
|
-
}
|
|
2017
|
+
});
|
|
1868
2018
|
}
|
|
1869
2019
|
catch {
|
|
1870
2020
|
// non-fatal: in-context compression summary remains; long-term layer is best-effort
|
|
1871
2021
|
}
|
|
1872
2022
|
}
|
|
1873
|
-
async upgradeCompressedSummary(sessionId, compressedSeq, archived, action) {
|
|
2023
|
+
async upgradeCompressedSummary(sessionId, compressedSeq, archived, action, runtime) {
|
|
1874
2024
|
try {
|
|
1875
2025
|
const summary = await this.opts.asyncSummarizer.summarize(archived, action);
|
|
1876
2026
|
await this.opts.sessionLog.append(sessionId, {
|
|
@@ -1878,6 +2028,20 @@ export class RuntimeRunner {
|
|
|
1878
2028
|
compressed_seq: compressedSeq,
|
|
1879
2029
|
summary,
|
|
1880
2030
|
});
|
|
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
|
+
}
|
|
1881
2045
|
}
|
|
1882
2046
|
catch {
|
|
1883
2047
|
// non-fatal: rule-based summary stays in place
|
|
@@ -1885,7 +2049,20 @@ export class RuntimeRunner {
|
|
|
1885
2049
|
}
|
|
1886
2050
|
}
|
|
1887
2051
|
function isMidRun(events) {
|
|
1888
|
-
|
|
2052
|
+
// Mid-run ⇔ the LAST run_started has no run_terminal after it. Pairing (not mere
|
|
2053
|
+
// presence) matters on multi-round loop sessions: round 1's terminal must not make a
|
|
2054
|
+
// crashed round 2 look fresh, and driver-level round_* records must not make a fresh
|
|
2055
|
+
// round look interrupted.
|
|
2056
|
+
let lastStarted = -1;
|
|
2057
|
+
let lastTerminal = -1;
|
|
2058
|
+
for (let i = 0; i < events.length; i++) {
|
|
2059
|
+
const k = events[i].event.kind;
|
|
2060
|
+
if (k === "run_started")
|
|
2061
|
+
lastStarted = i;
|
|
2062
|
+
else if (k === "run_terminal")
|
|
2063
|
+
lastTerminal = i;
|
|
2064
|
+
}
|
|
2065
|
+
return lastStarted >= 0 && lastStarted > lastTerminal;
|
|
1889
2066
|
}
|
|
1890
2067
|
/**
|
|
1891
2068
|
* Build a kernel `add_history_message` payload from user attachments: a `user`
|
|
@@ -1940,6 +2117,50 @@ async function summarizeForLongTermMemory(provider, archived, systemPrompt) {
|
|
|
1940
2117
|
}
|
|
1941
2118
|
return text.trim() || transcript.slice(0, 2000);
|
|
1942
2119
|
}
|
|
2120
|
+
/** Kernel-consumed meta-tools (e.g. `pace`) are answered by a synthetic tool result the kernel keeps
|
|
2121
|
+
* in its OWN history but never emits as a `tool_completed` session event (they never reach the
|
|
2122
|
+
* execution plane). On replay that leaves an assistant `tool_call` with no following tool result —
|
|
2123
|
+
* which strict OpenAI-compatible providers reject ("every tool_call must be answered by a tool
|
|
2124
|
+
* message"). This pass re-pairs any such orphan by inserting a synthetic tool-result message right
|
|
2125
|
+
* after its assistant message, reproducing the pair the kernel had all along.
|
|
2126
|
+
*
|
|
2127
|
+
* Discriminator: only pair an orphan when the run **continued past it** — i.e. a later non-tool
|
|
2128
|
+
* message exists. A tail assistant tool_call with nothing after it is a genuinely PENDING tool the
|
|
2129
|
+
* run stopped in front of (the wake/recovery case), which must stay unpaired so wake executes it.
|
|
2130
|
+
* Pure. */
|
|
2131
|
+
export function pairOrphanToolCalls(messages) {
|
|
2132
|
+
const out = [];
|
|
2133
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2134
|
+
const m = messages[i];
|
|
2135
|
+
out.push(m);
|
|
2136
|
+
if (m.role !== "assistant" || !m.toolCalls?.length)
|
|
2137
|
+
continue;
|
|
2138
|
+
// Collect ids answered by the immediately-following run of tool messages; note where it ends.
|
|
2139
|
+
const answered = new Set();
|
|
2140
|
+
let j = i + 1;
|
|
2141
|
+
for (; j < messages.length && messages[j].role === "tool"; j++) {
|
|
2142
|
+
for (const p of messages[j].contentParts ?? []) {
|
|
2143
|
+
if (p.type === "tool_result")
|
|
2144
|
+
answered.add(p.callId);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
// If nothing follows the tool run, this tool_call is a pending tail (wake case) — leave it.
|
|
2148
|
+
if (j >= messages.length)
|
|
2149
|
+
continue;
|
|
2150
|
+
for (const c of m.toolCalls) {
|
|
2151
|
+
if (answered.has(c.id))
|
|
2152
|
+
continue;
|
|
2153
|
+
out.push({
|
|
2154
|
+
role: "tool",
|
|
2155
|
+
content: "",
|
|
2156
|
+
toolCalls: [],
|
|
2157
|
+
contentParts: [{ type: "tool_result", callId: c.id, output: `[${c.name} handled by kernel]`, isError: false }],
|
|
2158
|
+
tokenCount: 1,
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return out;
|
|
2163
|
+
}
|
|
1943
2164
|
export function replayMessages(events, maxBytes) {
|
|
1944
2165
|
// Build upgraded-summary index: compressed_seq -> upgraded summary
|
|
1945
2166
|
const upgradedSummaries = new Map();
|
|
@@ -1998,7 +2219,7 @@ export function replayMessages(events, maxBytes) {
|
|
|
1998
2219
|
}
|
|
1999
2220
|
}
|
|
2000
2221
|
}
|
|
2001
|
-
return messages;
|
|
2222
|
+
return pairOrphanToolCalls(messages);
|
|
2002
2223
|
}
|
|
2003
2224
|
export async function replayMessagesAsync(events, maxBytes, loadArchive) {
|
|
2004
2225
|
// Build upgraded-summary index: compressed_seq -> upgraded summary
|
|
@@ -2078,7 +2299,7 @@ export async function replayMessagesAsync(events, maxBytes, loadArchive) {
|
|
|
2078
2299
|
}
|
|
2079
2300
|
}
|
|
2080
2301
|
}
|
|
2081
|
-
return messages;
|
|
2302
|
+
return pairOrphanToolCalls(messages);
|
|
2082
2303
|
}
|
|
2083
2304
|
function nextArchivedSeqStart(events) {
|
|
2084
2305
|
let next = 0;
|
|
@@ -2181,6 +2402,19 @@ function authoredWorkflowOutcomeNote(outcome) {
|
|
|
2181
2402
|
}
|
|
2182
2403
|
/** Lower a host `RuntimeSignal` to the kernel's snake_case `signal` input event. Shared by the main
|
|
2183
2404
|
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
2405
|
+
/** Word-set jaccard similarity — the curator's dedup rule as a pure helper at the write funnel. */
|
|
2406
|
+
function jaccardSimilarity(a, b) {
|
|
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
|
+
}
|
|
2184
2418
|
function signalToKernelEvent(sig) {
|
|
2185
2419
|
return {
|
|
2186
2420
|
kind: "signal",
|