@deepstrike/sdk 0.2.36 → 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 +2 -0
- 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 +38 -62
- package/dist/runtime/kernel-step.d.ts +9 -0
- package/dist/runtime/kernel-step.js +12 -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 +28 -5
- package/dist/runtime/runner.js +267 -70
- package/dist/runtime/session-log.d.ts +31 -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 +2 -0
- package/package.json +2 -2
package/dist/runtime/runner.js
CHANGED
|
@@ -10,7 +10,7 @@ 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";
|
|
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";
|
|
@@ -72,23 +72,30 @@ export class RuntimeRunner {
|
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
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
|
-
|
|
75
|
+
// Curator-style jaccard dedup at the single write path: a near-duplicate of an
|
|
76
|
+
// existing entry is dropped (the observation is still logged for audit).
|
|
77
|
+
const isDuplicate = existing.some(e => jaccardSimilarity(e.text, memory.content) >= 0.9);
|
|
78
|
+
if (!isDuplicate) {
|
|
79
|
+
const meta = memory.metadata;
|
|
80
|
+
const score = typeof meta?.score === "number" ? meta.score : 1.0;
|
|
81
|
+
await this.opts.dreamStore.commit(agentId, {
|
|
82
|
+
toAdd: [{
|
|
83
|
+
text: memory.content,
|
|
84
|
+
score,
|
|
85
|
+
metadata: {
|
|
86
|
+
...memory.metadata,
|
|
87
|
+
source: meta?.source ?? "write_memory_syscall",
|
|
88
|
+
},
|
|
89
|
+
}],
|
|
90
|
+
toRemoveIndices: [],
|
|
91
|
+
stats: {
|
|
92
|
+
insightsProcessed: 1,
|
|
93
|
+
duplicatesRemoved: 0,
|
|
94
|
+
conflictsResolved: 0,
|
|
95
|
+
entriesAdded: 1,
|
|
96
|
+
},
|
|
97
|
+
}, existing);
|
|
98
|
+
}
|
|
92
99
|
await this.appendMemorySyscallObservations(sessionId, observations);
|
|
93
100
|
}
|
|
94
101
|
async queryMemory(query, opts = {}) {
|
|
@@ -116,24 +123,19 @@ export class RuntimeRunner {
|
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
await this.appendMemorySyscallObservations(sessionId, observations);
|
|
119
|
-
await this.logMemoryRetrievalResult(sessionId,
|
|
126
|
+
await this.logMemoryRetrievalResult(sessionId, retrieval);
|
|
120
127
|
return hits;
|
|
121
128
|
}
|
|
122
|
-
async logMemoryRetrievalResult(sessionId,
|
|
129
|
+
async logMemoryRetrievalResult(sessionId, retrieval) {
|
|
123
130
|
if (!sessionId)
|
|
124
131
|
return;
|
|
132
|
+
// The session-log record is the durable audit artifact; the kernel needs no
|
|
133
|
+
// acknowledgment (the former kernel event was a no-op and was removed).
|
|
125
134
|
await this.opts.sessionLog.append(sessionId, {
|
|
126
135
|
kind: "memory_retrieval_result",
|
|
127
136
|
selected_memory_ids: retrieval.selected_memory_ids,
|
|
128
137
|
selection_rationale: retrieval.selection_rationale,
|
|
129
138
|
});
|
|
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
139
|
}
|
|
138
140
|
createSyscallRuntime() {
|
|
139
141
|
const { KernelRuntime } = getKernel();
|
|
@@ -152,7 +154,7 @@ export class RuntimeRunner {
|
|
|
152
154
|
* exactly as a mid-run spawn would be. Must run BEFORE `start_run` so the in-kernel gate enforces
|
|
153
155
|
* every policy from the first spawn. No config ⇒ the native-profile defaults (铁律: defaults only).
|
|
154
156
|
*/
|
|
155
|
-
applyKernelPolicies(runtime, groupTokensBase, groupSpawnsBase) {
|
|
157
|
+
applyKernelPolicies(runtime, groupTokensBase, groupSpawnsBase, groupRoundsBase) {
|
|
156
158
|
// K2: lower governance / attention / scheduler / quota in ONE `configure_run` event instead of
|
|
157
159
|
// the previous 2–4 separate `set_*` / `load_governance_policy` events. The kernel applies each
|
|
158
160
|
// present field via the same path its granular event uses; absent fields are left untouched.
|
|
@@ -187,6 +189,10 @@ export class RuntimeRunner {
|
|
|
187
189
|
if (groupSpawnsBase !== undefined && groupSpawnsBase > 0) {
|
|
188
190
|
config.group_spawns_base = groupSpawnsBase;
|
|
189
191
|
}
|
|
192
|
+
if (groupRoundsBase !== undefined && groupRoundsBase > 0) {
|
|
193
|
+
// ③ loop-agent: completed-round count seeds the pacing trap's max_rounds coercion.
|
|
194
|
+
config.group_rounds_base = groupRoundsBase;
|
|
195
|
+
}
|
|
190
196
|
// O6: tune/disable the in-kernel repeat fuse. `false` disables; an object overrides thresholds.
|
|
191
197
|
// Absent ⇒ kernel defaults (enabled, deny_after=5, terminate_after=8).
|
|
192
198
|
if (this.opts.repeatFuse !== undefined) {
|
|
@@ -330,7 +336,10 @@ export class RuntimeRunner {
|
|
|
330
336
|
// G4: surface the workflow's remaining budget to the node's agent so a coordinator can size its
|
|
331
337
|
// `submit_workflow_nodes` batch to what is available (empty string ⇒ unbounded, no note).
|
|
332
338
|
const budgetNote = workflowBudgetNote(budget);
|
|
333
|
-
|
|
339
|
+
// W-N2: a DAG edge carries data — every dependent node sees its dependencies' outputs (the
|
|
340
|
+
// kernel sends `input_agent_ids` for all dependents; judges/reduce keep their special paths).
|
|
341
|
+
const depsNote = dependencyOutputsNote(node.input_agent_ids, outputs);
|
|
342
|
+
const withBudget = (goal) => [goal, depsNote, budgetNote].filter(Boolean).join("\n\n");
|
|
334
343
|
const mkCtx = (goal) => ({
|
|
335
344
|
parentOpts: this.opts,
|
|
336
345
|
parentSessionId,
|
|
@@ -340,6 +349,10 @@ export class RuntimeRunner {
|
|
|
340
349
|
// M5 v2.1: this child IS a workflow node — its `start_workflow` flattens to this kernel (the
|
|
341
350
|
// workflow it would author joins the running DAG) rather than bootstrapping a nested pivot.
|
|
342
351
|
isWorkflowNode: true,
|
|
352
|
+
// W-N1: trusted workflow nodes run on the parent's execution plane (they carry no grant list
|
|
353
|
+
// by design — filtering on the missing list ran every DAG node TOOL-LESS); quarantined nodes
|
|
354
|
+
// stay deny-all filtered (they read untrusted content).
|
|
355
|
+
toolAccess: (node.trust === "quarantined" ? "filtered" : "inherit"),
|
|
343
356
|
// #2-B-ii: the per-node abort signal the driver fires when the kernel preempts this node.
|
|
344
357
|
...(abortSignal ? { abortSignal } : {}),
|
|
345
358
|
...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
|
|
@@ -361,10 +374,18 @@ export class RuntimeRunner {
|
|
|
361
374
|
const winnerId = winner === "right" ? node.judge_match.right : node.judge_match.left;
|
|
362
375
|
return withSignal(result, { tournamentWinner: winnerId });
|
|
363
376
|
}
|
|
364
|
-
// A#2 v2 loop iteration: run the increment
|
|
365
|
-
//
|
|
377
|
+
// A#2 v2 loop iteration: run the increment under the armed pacing trap (workflowNodeToSpec set
|
|
378
|
+
// `loopRound`, and the iteration resumes the loop's stable session — transcript-as-carry).
|
|
379
|
+
// DW-3 one vocabulary: the kernel-adjudicated `pace` verb IS the continuation signal
|
|
380
|
+
// (stop → loopContinue=false); the legacy text-sniffed JSON blob survives only as the fallback
|
|
381
|
+
// when no pace decision arrives (stub orchestrators, harness children), where no signal still
|
|
382
|
+
// means "run to max_iters" (v1).
|
|
366
383
|
if (node.loop_max_iters != null) {
|
|
367
|
-
const
|
|
384
|
+
const iteration = Number(/-i(\d+)$/.exec(node.agent_id)?.[1] ?? "0");
|
|
385
|
+
const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters, iteration)}`));
|
|
386
|
+
const pace = result.result.paceDecision;
|
|
387
|
+
if (pace)
|
|
388
|
+
return withSignal(result, { loopContinue: pace.action !== "stop" });
|
|
368
389
|
const cont = extractLoopContinue(textOf(result));
|
|
369
390
|
return cont === undefined ? result : withSignal(result, { loopContinue: cont });
|
|
370
391
|
}
|
|
@@ -450,7 +471,7 @@ export class RuntimeRunner {
|
|
|
450
471
|
if (this.opts.runGroup) {
|
|
451
472
|
const g = this.opts.runGroup;
|
|
452
473
|
groupLedger = await g.budgetStore.read(g.id);
|
|
453
|
-
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId });
|
|
474
|
+
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId, kind: "vehicle" });
|
|
454
475
|
}
|
|
455
476
|
this.bootstrapWorkflowKernel(sessionId, spec, groupLedger?.tokensSpent, groupLedger?.subagentsSpawned);
|
|
456
477
|
}
|
|
@@ -463,10 +484,22 @@ export class RuntimeRunner {
|
|
|
463
484
|
parent_session_id: parentSessionId,
|
|
464
485
|
// W0-ABI resume: skip nodes already completed before an interruption.
|
|
465
486
|
...(opts?.resumedCompleted?.length ? { resumed_completed: opts.resumedCompleted } : {}),
|
|
487
|
+
// W-1: signal-carrying completion records (classify branch / loop stop replay).
|
|
488
|
+
...(opts?.resumedResults?.length
|
|
489
|
+
? {
|
|
490
|
+
resumed_results: opts.resumedResults.map(r => ({
|
|
491
|
+
agent_id: r.agentId,
|
|
492
|
+
...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
|
|
493
|
+
...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
|
|
494
|
+
...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
|
|
495
|
+
})),
|
|
496
|
+
}
|
|
497
|
+
: {}),
|
|
466
498
|
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
467
499
|
...(opts?.resumedSubmissions?.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
500
|
+
...(opts?.resumedSubmissionBases?.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
|
|
468
501
|
});
|
|
469
|
-
return await this.driveWorkflow(observations, parentSessionId, runtime);
|
|
502
|
+
return await this.driveWorkflow(observations, parentSessionId, runtime, opts?.resumedOutputs);
|
|
470
503
|
}
|
|
471
504
|
finally {
|
|
472
505
|
if (bootstrapped) {
|
|
@@ -534,6 +567,18 @@ export class RuntimeRunner {
|
|
|
534
567
|
const parentSessionId = this.currentSessionId;
|
|
535
568
|
const runtime = this.activeKernel;
|
|
536
569
|
const observations = kernelApply(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
570
|
+
// W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
|
|
571
|
+
// announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
|
|
572
|
+
// had this spec, unlike the `runWorkflow` path.
|
|
573
|
+
const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
|
|
574
|
+
if (submitted) {
|
|
575
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
576
|
+
turn: runtime.turn(),
|
|
577
|
+
nodes: workflowSpecToKernel(spec).nodes ?? [],
|
|
578
|
+
baseIndex: submitted.base,
|
|
579
|
+
submitterAgentId: opts?.submitterAgentId,
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
537
582
|
return this.driveWorkflow(observations, parentSessionId, runtime);
|
|
538
583
|
}
|
|
539
584
|
/**
|
|
@@ -597,7 +642,7 @@ export class RuntimeRunner {
|
|
|
597
642
|
* batch in parallel, feed completions back (appending any agent-submitted nodes first), and loop
|
|
598
643
|
* until the kernel reports the workflow complete. Returns the completed / failed node agent-ids.
|
|
599
644
|
*/
|
|
600
|
-
async driveWorkflow(initial, parentSessionId, runtime) {
|
|
645
|
+
async driveWorkflow(initial, parentSessionId, runtime, seedOutputs) {
|
|
601
646
|
let observations = initial;
|
|
602
647
|
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
603
648
|
const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")
|
|
@@ -614,7 +659,9 @@ export class RuntimeRunner {
|
|
|
614
659
|
// G2: each completed node's output, keyed by agent id — a reduce node reads its dependencies'
|
|
615
660
|
// outputs from here. Deps always complete in an earlier round than the reduce node that needs
|
|
616
661
|
// them (the kernel keeps the reduce node un-ready until its deps finish), so this is populated.
|
|
617
|
-
|
|
662
|
+
// W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
|
|
663
|
+
// still see their (pre-crash) dependencies' outputs.
|
|
664
|
+
const outputs = new Map(seedOutputs ?? []);
|
|
618
665
|
for (;;) {
|
|
619
666
|
if (nodes.length === 0)
|
|
620
667
|
return { completed: [], failed: [], outputs: Object.fromEntries(outputs) }; // nothing to run (e.g. all gated)
|
|
@@ -643,7 +690,13 @@ export class RuntimeRunner {
|
|
|
643
690
|
for (const result of results) {
|
|
644
691
|
// G2: record this node's output so a downstream reduce node can consume it.
|
|
645
692
|
const outContent = result.result.finalMessage?.content;
|
|
646
|
-
|
|
693
|
+
const outText = typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "";
|
|
694
|
+
outputs.set(result.agentId, outText);
|
|
695
|
+
// A loop iteration completes under `wf-node{N}-i{k}` but its dependents consume the STABLE
|
|
696
|
+
// node id `wf-node{N}` — alias it so the LAST iteration's output is what dependents see.
|
|
697
|
+
const stableId = result.agentId.replace(/-i\d+$/, "");
|
|
698
|
+
if (stableId !== result.agentId)
|
|
699
|
+
outputs.set(stableId, outText);
|
|
647
700
|
// R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
|
|
648
701
|
// reporting the node's completion — the workflow is still active (the kernel hasn't seen this
|
|
649
702
|
// node finish), so even a submission from the last running node keeps the DAG alive. The
|
|
@@ -655,10 +708,15 @@ export class RuntimeRunner {
|
|
|
655
708
|
const subObs = kernelApply(runtime, this.pendingObservations, submitEvent);
|
|
656
709
|
nextNodes.push(...collectNodes(subObs));
|
|
657
710
|
budget = collectBudget(subObs) ?? budget;
|
|
658
|
-
// R3-1: persist the submission (kernel-shape nodes)
|
|
711
|
+
// R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
|
|
712
|
+
// so resume can re-apply the batch at the exact original graph position. W-N3: also the
|
|
713
|
+
// submitter, so resume drops batches whose submitter re-runs (it will re-submit).
|
|
714
|
+
const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
|
|
659
715
|
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
660
716
|
turn: runtime.turn(),
|
|
661
717
|
nodes: submitEvent.nodes ?? [],
|
|
718
|
+
baseIndex: submitted?.base,
|
|
719
|
+
submitterAgentId: result.agentId,
|
|
662
720
|
}));
|
|
663
721
|
}
|
|
664
722
|
const obs = kernelApply(runtime, this.pendingObservations, {
|
|
@@ -670,11 +728,17 @@ export class RuntimeRunner {
|
|
|
670
728
|
const d = findDone(obs);
|
|
671
729
|
if (d)
|
|
672
730
|
done = d;
|
|
673
|
-
// Persist node completion for resume recovery.
|
|
731
|
+
// Persist node completion for resume recovery. W-1: the result-borne control signals ride
|
|
732
|
+
// along (a resumed classifier re-prunes; a recorded loop stop is honored) plus the output
|
|
733
|
+
// text (post-resume dependents/reduce still see this node's output).
|
|
674
734
|
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
|
|
675
735
|
turn: runtime.turn(),
|
|
676
736
|
agentId: result.agentId,
|
|
677
737
|
termination: result.result.termination,
|
|
738
|
+
classifyBranch: result.result.classifyBranch,
|
|
739
|
+
tournamentWinner: result.result.tournamentWinner,
|
|
740
|
+
loopContinue: result.result.loopContinue,
|
|
741
|
+
...(result.result.termination === "completed" && outText ? { output: outText } : {}),
|
|
678
742
|
}));
|
|
679
743
|
}
|
|
680
744
|
if (done && nextNodes.length === 0) {
|
|
@@ -685,8 +749,9 @@ export class RuntimeRunner {
|
|
|
685
749
|
}
|
|
686
750
|
/**
|
|
687
751
|
* Resume a workflow from the parent session's completed nodes.
|
|
688
|
-
* Reads the session log, extracts completed workflow node
|
|
689
|
-
* calls runWorkflow
|
|
752
|
+
* Reads the session log, extracts completed workflow node records (with their W-1 control
|
|
753
|
+
* signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
|
|
754
|
+
* flow (classify prune / loop stop), and the driver re-seeds its outputs map.
|
|
690
755
|
*/
|
|
691
756
|
async resumeWorkflow(spec, opts) {
|
|
692
757
|
// Standalone resume: a stateless handler passes the prior `sessionId` to pick up an interrupted
|
|
@@ -696,9 +761,34 @@ export class RuntimeRunner {
|
|
|
696
761
|
throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
|
|
697
762
|
}
|
|
698
763
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
699
|
-
const
|
|
700
|
-
const
|
|
701
|
-
|
|
764
|
+
const resumedResults = recoverCompletedWorkflowNodes(events);
|
|
765
|
+
const completedIds = new Set(resumedResults.map(r => r.agentId));
|
|
766
|
+
const recovered = recoverSubmittedWorkflowNodes(events);
|
|
767
|
+
// W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
|
|
768
|
+
// re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
|
|
769
|
+
// Only safe with exact bases (the dropped batch's slots become inert placeholders); a legacy
|
|
770
|
+
// order-only log keeps every batch, since dropping would shift all later indices.
|
|
771
|
+
let { submissions, bases } = recovered;
|
|
772
|
+
if (bases.length === submissions.length && submissions.length > 0) {
|
|
773
|
+
const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
|
|
774
|
+
submissions = submissions.filter((_, i) => keep[i]);
|
|
775
|
+
bases = bases.filter((_, i) => keep[i]);
|
|
776
|
+
}
|
|
777
|
+
const resumedOutputs = new Map(resumedResults.filter(r => r.output).map(r => [r.agentId, r.output]));
|
|
778
|
+
// Alias loop iterations onto their stable node id (last iteration wins) — dependents consume
|
|
779
|
+
// `wf-node{N}`, not `wf-node{N}-i{k}`.
|
|
780
|
+
for (const r of resumedResults) {
|
|
781
|
+
const stableId = r.agentId.replace(/-i\d+$/, "");
|
|
782
|
+
if (stableId !== r.agentId && r.output)
|
|
783
|
+
resumedOutputs.set(stableId, r.output);
|
|
784
|
+
}
|
|
785
|
+
return this.runWorkflow(spec, {
|
|
786
|
+
resumedResults,
|
|
787
|
+
resumedSubmissions: submissions,
|
|
788
|
+
resumedSubmissionBases: bases,
|
|
789
|
+
resumedOutputs,
|
|
790
|
+
sessionId,
|
|
791
|
+
});
|
|
702
792
|
}
|
|
703
793
|
interrupt() { this.interrupted = true; this.abortController?.abort(); }
|
|
704
794
|
/** Push a contextual note into the run's signal stream (the system-reminder channel): it drains at
|
|
@@ -1148,9 +1238,9 @@ export class RuntimeRunner {
|
|
|
1148
1238
|
if (this.opts.runGroup) {
|
|
1149
1239
|
const g = this.opts.runGroup;
|
|
1150
1240
|
groupLedger = await g.budgetStore.read(g.id);
|
|
1151
|
-
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId });
|
|
1241
|
+
await g.budgetStore.join(g.id, { sessionId, role: this.opts.agentId, kind: "vehicle" });
|
|
1152
1242
|
}
|
|
1153
|
-
this.applyKernelPolicies(runtime, groupLedger?.tokensSpent, groupLedger?.subagentsSpawned);
|
|
1243
|
+
this.applyKernelPolicies(runtime, groupLedger?.tokensSpent, groupLedger?.subagentsSpawned, groupLedger?.roundsCompleted);
|
|
1154
1244
|
// Multimodal upload: seed the user's attachments (images/audio) as a history
|
|
1155
1245
|
// message before start_run pushes the "[TASK STATE]" anchor. init_task does not
|
|
1156
1246
|
// clear history, so order becomes [attachment user msg, "Proceed…"] — both land
|
|
@@ -1738,7 +1828,14 @@ export class RuntimeRunner {
|
|
|
1738
1828
|
catch { /* non-fatal */ }
|
|
1739
1829
|
}
|
|
1740
1830
|
}
|
|
1741
|
-
yield {
|
|
1831
|
+
yield {
|
|
1832
|
+
type: "done",
|
|
1833
|
+
iterations: turnsUsed,
|
|
1834
|
+
totalTokens,
|
|
1835
|
+
status,
|
|
1836
|
+
// ③ loop-agent: surface the kernel-adjudicated after-round decision to the driver.
|
|
1837
|
+
...(result?.paceDecision ? { paceDecision: result.paceDecision } : {}),
|
|
1838
|
+
};
|
|
1742
1839
|
this.activeKernel = null;
|
|
1743
1840
|
this.currentSessionId = null;
|
|
1744
1841
|
this.dashboard = null;
|
|
@@ -1749,10 +1846,14 @@ export class RuntimeRunner {
|
|
|
1749
1846
|
* after each sprint renewal (`phase: "renewal"`): renewal drops the old history INCLUDING the
|
|
1750
1847
|
* earlier memory hits, so the new sprint gets a fresh recall pass. Errs-open throughout. */
|
|
1751
1848
|
async prefetchMemoryIntoHistory(runtime, phase) {
|
|
1752
|
-
if (!this.opts.
|
|
1849
|
+
if (!this.opts.dreamStore || !this.opts.agentId)
|
|
1753
1850
|
return;
|
|
1851
|
+
// P10: recall is default-on (CC session-start recall) — with no hook configured,
|
|
1852
|
+
// the goal itself is the query. preQueryMemory stays as the targeting override.
|
|
1853
|
+
const preQuery = this.opts.preQueryMemory
|
|
1854
|
+
?? ((ctx) => [ctx.goal]);
|
|
1754
1855
|
try {
|
|
1755
|
-
const queries = await
|
|
1856
|
+
const queries = await preQuery({
|
|
1756
1857
|
goal: this.currentGoal,
|
|
1757
1858
|
runSpec: this.opts.runSpec,
|
|
1758
1859
|
phase,
|
|
@@ -1829,14 +1930,24 @@ export class RuntimeRunner {
|
|
|
1829
1930
|
nextArchiveStart = compressedSeq + 1;
|
|
1830
1931
|
const archived = obs.kind === "compressed" ? obs.archived : undefined;
|
|
1831
1932
|
if (this.opts.asyncSummarizer && archived && archived.length > 0) {
|
|
1832
|
-
void this.upgradeCompressedSummary(sessionId, compressedSeq, archived, compressionAction(obs.action) ?? "auto_compact");
|
|
1933
|
+
void this.upgradeCompressedSummary(sessionId, compressedSeq, archived, compressionAction(obs.action) ?? "auto_compact", runtime);
|
|
1934
|
+
}
|
|
1935
|
+
// One compaction = one kernel observation: the page_out session record (and the
|
|
1936
|
+
// semantic-archive branch) is DERIVED here from Compressed.tier_hint, preserving the
|
|
1937
|
+
// session-log format and OsSnapshot page_out_count.
|
|
1938
|
+
if (obs.tier_hint && Array.isArray(archived) && archived.length > 0) {
|
|
1939
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1940
|
+
kind: "page_out",
|
|
1941
|
+
turn: obs.turn ?? turn,
|
|
1942
|
+
action: compressionAction(obs.action),
|
|
1943
|
+
summary: obs.summary,
|
|
1944
|
+
tier_hint: obs.tier_hint ?? "durable",
|
|
1945
|
+
message_count: archived.length,
|
|
1946
|
+
});
|
|
1947
|
+
if (obs.tier_hint === "semantic") {
|
|
1948
|
+
void this.archiveSemanticPageOut(archived, compressionAction(obs.action));
|
|
1949
|
+
}
|
|
1833
1950
|
}
|
|
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
1951
|
}
|
|
1841
1952
|
// K4: a sprint renewal dropped the old history — including any earlier memory hits — so
|
|
1842
1953
|
// re-run the preQueryMemory prefetch for the new sprint (live observations only: this
|
|
@@ -1854,23 +1965,25 @@ export class RuntimeRunner {
|
|
|
1854
1965
|
const summary = this.opts.dreamSummarizer
|
|
1855
1966
|
? await this.opts.dreamSummarizer.summarize(archived, { action })
|
|
1856
1967
|
: await summarizeForLongTermMemory(this.opts.dreamProvider ?? this.opts.provider, archived, this.opts.dreamSystemPrompt);
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1968
|
+
// P2 write-funnel: route through the ONE gated WriteMemory syscall so validation,
|
|
1969
|
+
// the rolling write quota, dedup, and the memory_written audit all apply. Score is
|
|
1970
|
+
// advisory (0.6) — an automatic summary must never outrank curated content.
|
|
1971
|
+
await this.writeMemory({
|
|
1972
|
+
content: summary,
|
|
1973
|
+
metadata: {
|
|
1974
|
+
name: `page-out-${Date.now()}`,
|
|
1975
|
+
description: `auto summary of ${action ?? "compaction"} archive`,
|
|
1976
|
+
source: "semantic_page_out",
|
|
1977
|
+
action,
|
|
1978
|
+
score: 0.6,
|
|
1866
1979
|
},
|
|
1867
|
-
}
|
|
1980
|
+
});
|
|
1868
1981
|
}
|
|
1869
1982
|
catch {
|
|
1870
1983
|
// non-fatal: in-context compression summary remains; long-term layer is best-effort
|
|
1871
1984
|
}
|
|
1872
1985
|
}
|
|
1873
|
-
async upgradeCompressedSummary(sessionId, compressedSeq, archived, action) {
|
|
1986
|
+
async upgradeCompressedSummary(sessionId, compressedSeq, archived, action, runtime) {
|
|
1874
1987
|
try {
|
|
1875
1988
|
const summary = await this.opts.asyncSummarizer.summarize(archived, action);
|
|
1876
1989
|
await this.opts.sessionLog.append(sessionId, {
|
|
@@ -1878,6 +1991,20 @@ export class RuntimeRunner {
|
|
|
1878
1991
|
compressed_seq: compressedSeq,
|
|
1879
1992
|
summary,
|
|
1880
1993
|
});
|
|
1994
|
+
// P4: the LLM summary also re-enters the LIVE session as a keyed page-in entry —
|
|
1995
|
+
// K1 boundary-deferred upsert lands it with zero mid-generation cache churn and the
|
|
1996
|
+
// K2 budget governs its size. The RuleSummarizer text remains the synchronous label.
|
|
1997
|
+
if (runtime) {
|
|
1998
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
1999
|
+
kind: "page_in",
|
|
2000
|
+
entries: [{
|
|
2001
|
+
content: `[ARCHIVE SUMMARY] ${summary}`,
|
|
2002
|
+
key: `summary:seq-${compressedSeq}`,
|
|
2003
|
+
pinned: false,
|
|
2004
|
+
source: "async_summarizer",
|
|
2005
|
+
}],
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
1881
2008
|
}
|
|
1882
2009
|
catch {
|
|
1883
2010
|
// non-fatal: rule-based summary stays in place
|
|
@@ -1885,7 +2012,20 @@ export class RuntimeRunner {
|
|
|
1885
2012
|
}
|
|
1886
2013
|
}
|
|
1887
2014
|
function isMidRun(events) {
|
|
1888
|
-
|
|
2015
|
+
// Mid-run ⇔ the LAST run_started has no run_terminal after it. Pairing (not mere
|
|
2016
|
+
// presence) matters on multi-round loop sessions: round 1's terminal must not make a
|
|
2017
|
+
// crashed round 2 look fresh, and driver-level round_* records must not make a fresh
|
|
2018
|
+
// round look interrupted.
|
|
2019
|
+
let lastStarted = -1;
|
|
2020
|
+
let lastTerminal = -1;
|
|
2021
|
+
for (let i = 0; i < events.length; i++) {
|
|
2022
|
+
const k = events[i].event.kind;
|
|
2023
|
+
if (k === "run_started")
|
|
2024
|
+
lastStarted = i;
|
|
2025
|
+
else if (k === "run_terminal")
|
|
2026
|
+
lastTerminal = i;
|
|
2027
|
+
}
|
|
2028
|
+
return lastStarted >= 0 && lastStarted > lastTerminal;
|
|
1889
2029
|
}
|
|
1890
2030
|
/**
|
|
1891
2031
|
* Build a kernel `add_history_message` payload from user attachments: a `user`
|
|
@@ -1940,6 +2080,50 @@ async function summarizeForLongTermMemory(provider, archived, systemPrompt) {
|
|
|
1940
2080
|
}
|
|
1941
2081
|
return text.trim() || transcript.slice(0, 2000);
|
|
1942
2082
|
}
|
|
2083
|
+
/** Kernel-consumed meta-tools (e.g. `pace`) are answered by a synthetic tool result the kernel keeps
|
|
2084
|
+
* in its OWN history but never emits as a `tool_completed` session event (they never reach the
|
|
2085
|
+
* execution plane). On replay that leaves an assistant `tool_call` with no following tool result —
|
|
2086
|
+
* which strict OpenAI-compatible providers reject ("every tool_call must be answered by a tool
|
|
2087
|
+
* message"). This pass re-pairs any such orphan by inserting a synthetic tool-result message right
|
|
2088
|
+
* after its assistant message, reproducing the pair the kernel had all along.
|
|
2089
|
+
*
|
|
2090
|
+
* Discriminator: only pair an orphan when the run **continued past it** — i.e. a later non-tool
|
|
2091
|
+
* message exists. A tail assistant tool_call with nothing after it is a genuinely PENDING tool the
|
|
2092
|
+
* run stopped in front of (the wake/recovery case), which must stay unpaired so wake executes it.
|
|
2093
|
+
* Pure. */
|
|
2094
|
+
export function pairOrphanToolCalls(messages) {
|
|
2095
|
+
const out = [];
|
|
2096
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2097
|
+
const m = messages[i];
|
|
2098
|
+
out.push(m);
|
|
2099
|
+
if (m.role !== "assistant" || !m.toolCalls?.length)
|
|
2100
|
+
continue;
|
|
2101
|
+
// Collect ids answered by the immediately-following run of tool messages; note where it ends.
|
|
2102
|
+
const answered = new Set();
|
|
2103
|
+
let j = i + 1;
|
|
2104
|
+
for (; j < messages.length && messages[j].role === "tool"; j++) {
|
|
2105
|
+
for (const p of messages[j].contentParts ?? []) {
|
|
2106
|
+
if (p.type === "tool_result")
|
|
2107
|
+
answered.add(p.callId);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
// If nothing follows the tool run, this tool_call is a pending tail (wake case) — leave it.
|
|
2111
|
+
if (j >= messages.length)
|
|
2112
|
+
continue;
|
|
2113
|
+
for (const c of m.toolCalls) {
|
|
2114
|
+
if (answered.has(c.id))
|
|
2115
|
+
continue;
|
|
2116
|
+
out.push({
|
|
2117
|
+
role: "tool",
|
|
2118
|
+
content: "",
|
|
2119
|
+
toolCalls: [],
|
|
2120
|
+
contentParts: [{ type: "tool_result", callId: c.id, output: `[${c.name} handled by kernel]`, isError: false }],
|
|
2121
|
+
tokenCount: 1,
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return out;
|
|
2126
|
+
}
|
|
1943
2127
|
export function replayMessages(events, maxBytes) {
|
|
1944
2128
|
// Build upgraded-summary index: compressed_seq -> upgraded summary
|
|
1945
2129
|
const upgradedSummaries = new Map();
|
|
@@ -1998,7 +2182,7 @@ export function replayMessages(events, maxBytes) {
|
|
|
1998
2182
|
}
|
|
1999
2183
|
}
|
|
2000
2184
|
}
|
|
2001
|
-
return messages;
|
|
2185
|
+
return pairOrphanToolCalls(messages);
|
|
2002
2186
|
}
|
|
2003
2187
|
export async function replayMessagesAsync(events, maxBytes, loadArchive) {
|
|
2004
2188
|
// Build upgraded-summary index: compressed_seq -> upgraded summary
|
|
@@ -2078,7 +2262,7 @@ export async function replayMessagesAsync(events, maxBytes, loadArchive) {
|
|
|
2078
2262
|
}
|
|
2079
2263
|
}
|
|
2080
2264
|
}
|
|
2081
|
-
return messages;
|
|
2265
|
+
return pairOrphanToolCalls(messages);
|
|
2082
2266
|
}
|
|
2083
2267
|
function nextArchivedSeqStart(events) {
|
|
2084
2268
|
let next = 0;
|
|
@@ -2181,6 +2365,19 @@ function authoredWorkflowOutcomeNote(outcome) {
|
|
|
2181
2365
|
}
|
|
2182
2366
|
/** Lower a host `RuntimeSignal` to the kernel's snake_case `signal` input event. Shared by the main
|
|
2183
2367
|
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
2368
|
+
/** Word-set jaccard similarity — the curator's dedup rule as a pure helper at the write funnel. */
|
|
2369
|
+
function jaccardSimilarity(a, b) {
|
|
2370
|
+
const sa = new Set(a.split(/\s+/).filter(Boolean));
|
|
2371
|
+
const sb = new Set(b.split(/\s+/).filter(Boolean));
|
|
2372
|
+
if (sa.size === 0 && sb.size === 0)
|
|
2373
|
+
return 1;
|
|
2374
|
+
let inter = 0;
|
|
2375
|
+
for (const w of sa)
|
|
2376
|
+
if (sb.has(w))
|
|
2377
|
+
inter++;
|
|
2378
|
+
const union = sa.size + sb.size - inter;
|
|
2379
|
+
return union === 0 ? 0 : inter / union;
|
|
2380
|
+
}
|
|
2184
2381
|
function signalToKernelEvent(sig) {
|
|
2185
2382
|
return {
|
|
2186
2383
|
kind: "signal",
|