@deepstrike/wasm 0.2.74 → 0.2.76
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/agent.js +5 -0
- package/dist/governance.d.ts +33 -9
- package/dist/governance.js +25 -28
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/memory/authority.d.ts +30 -0
- package/dist/memory/authority.js +34 -0
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/openai.js +2 -1
- package/dist/runtime/canonical-kernel-step.d.ts +32 -0
- package/dist/runtime/canonical-kernel-step.js +391 -141
- package/dist/runtime/execution-plane.d.ts +2 -0
- package/dist/runtime/execution-plane.js +10 -3
- package/dist/runtime/kernel-step.js +14 -3
- package/dist/runtime/replay-provider.js +4 -3
- package/dist/runtime/runner.d.ts +55 -3
- package/dist/runtime/runner.js +405 -228
- package/dist/runtime/tool-arguments.d.ts +24 -0
- package/dist/runtime/tool-arguments.js +61 -0
- package/dist/runtime/types/agent.d.ts +4 -0
- package/dist/runtime/types/agent.js +6 -55
- package/dist/types.d.ts +2 -1
- package/package.json +3 -3
package/dist/runtime/runner.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { prepareProviderRequest } from "../providers/prepared-request.js";
|
|
2
2
|
import { createNativeContextPreparationAdapter } from "./context.js";
|
|
3
|
+
import { sha256Hex } from "./sha256.js";
|
|
3
4
|
import { extractSessionMemories } from "../memory/extraction.js";
|
|
4
5
|
import { resolvePermissionRequest } from "./execution-plane.js";
|
|
5
|
-
import { governancePolicyToKernelEvent
|
|
6
|
+
import { governancePolicyToKernelEvent } from "../governance.js";
|
|
7
|
+
import { parseToolCallArguments } from "./tool-arguments.js";
|
|
8
|
+
import { stripSkillFrontmatter } from "./execution-plane.js";
|
|
6
9
|
import { getKernel } from "./kernel.js";
|
|
7
|
-
import {
|
|
10
|
+
import { checkMemoryWrite, deriveMemoryRecall } from "../memory/authority.js";
|
|
11
|
+
import { CanonicalRunnerRuntime, canonicalKernelAction, canonicalKernelApply, isInvalidInputError, canonicalKernelMaybeAction, canonicalStartAgent, canonicalStartWorkflow, sha256 as canonicalSha256, } from "./canonical-kernel-step.js";
|
|
8
12
|
import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-replay.js";
|
|
9
13
|
import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
10
14
|
import { decodeDurableContent, decodeDurableToolResult, durableBlocksToToolOutput, toolOutputBlocksToDurable } from "./durable-content.js";
|
|
11
15
|
import { formatToolError } from "../tools/errors.js";
|
|
12
16
|
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, } from "./session-repair.js";
|
|
13
|
-
import { messageToKernelMessage, archivePresentationFromObservations, skillMetadataToKernel,
|
|
17
|
+
import { messageToKernelMessage, archivePresentationFromObservations, skillMetadataToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
14
18
|
import { agentRunSpecToKernel, MILESTONE_UNVERIFIED_REASON, milestoneCheckFail, milestoneCheckPass, milestoneCheckResultToKernel, subAgentResultToKernel, workflowBudgetNote, workflowNodeSpecToKernel, workflowNodeOutcomeFromKernel, workflowNodeStatusFromTermination, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
|
|
15
19
|
import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
16
20
|
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
@@ -65,6 +69,7 @@ function memoryPolicyToKernel(policy) {
|
|
|
65
69
|
"validationEnabled",
|
|
66
70
|
"maxContentBytes",
|
|
67
71
|
"maxNameLength",
|
|
72
|
+
"promotionRecallThreshold",
|
|
68
73
|
]);
|
|
69
74
|
const unknown = Object.keys(policy).filter(key => !allowed.has(key));
|
|
70
75
|
if (unknown.length > 0) {
|
|
@@ -76,16 +81,19 @@ function memoryPolicyToKernel(policy) {
|
|
|
76
81
|
...(policy.validationEnabled !== undefined ? { validation_enabled: policy.validationEnabled } : {}),
|
|
77
82
|
...(policy.maxContentBytes !== undefined ? { max_content_bytes: policy.maxContentBytes } : {}),
|
|
78
83
|
...(policy.maxNameLength !== undefined ? { max_name_length: policy.maxNameLength } : {}),
|
|
84
|
+
...(policy.promotionRecallThreshold !== undefined
|
|
85
|
+
? { promotion_recall_threshold: policy.promotionRecallThreshold }
|
|
86
|
+
: {}),
|
|
79
87
|
};
|
|
80
88
|
}
|
|
89
|
+
/** Logical tool calls the cancellation abandons. The kernel's `pending_call_ids` is the call-id
|
|
90
|
+
* namespace only: provider/host effect ids and sub-agent ids are not calls and are settled by the
|
|
91
|
+
* cancel transition itself. */
|
|
81
92
|
function pendingCallIds(action) {
|
|
82
93
|
switch (action.kind) {
|
|
83
|
-
case "call_provider": return [action.effectId];
|
|
84
94
|
case "execute_tool": return action.calls.map(call => call.id);
|
|
85
95
|
case "request_approval": return action.requests.map(request => request.callId);
|
|
86
|
-
|
|
87
|
-
case "preempt_sub_agents": return action.agentIds;
|
|
88
|
-
default: return "effectId" in action ? [action.effectId] : [];
|
|
96
|
+
default: return [];
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
function controlRequestRejection(observations, operation) {
|
|
@@ -111,6 +119,9 @@ export class RuntimeRunner {
|
|
|
111
119
|
fallbackPayloadStore = null;
|
|
112
120
|
/** O2 (system-reminder channel): host-pushed notes awaiting the next turn-boundary drain. */
|
|
113
121
|
injectedSignals = [];
|
|
122
|
+
/** Host control commands queued by the public control methods; the main loop applies them
|
|
123
|
+
* between effects so a command that publishes an effect never races an in-flight one. */
|
|
124
|
+
pendingControl = [];
|
|
114
125
|
/** Skill names whose content has already been pushed into the durable `knowledge` slot this
|
|
115
126
|
* run — guards against re-pushing a duplicate entry if the model calls `skill(name)` again for
|
|
116
127
|
* an already-active skill (loading is idempotent; the knowledge push should be too). */
|
|
@@ -206,7 +217,10 @@ export class RuntimeRunner {
|
|
|
206
217
|
* the kernel disposition ladder: `"normal"` queues (default), `"high"` soft-interrupts, `"critical"`
|
|
207
218
|
* preempts. */
|
|
208
219
|
injectNote(text, urgency = "normal") {
|
|
209
|
-
this.injectedSignals.push({
|
|
220
|
+
this.injectedSignals.push({
|
|
221
|
+
signal: { source: "custom", signalType: "event", urgency, payload: { goal: text } },
|
|
222
|
+
note: text,
|
|
223
|
+
});
|
|
210
224
|
}
|
|
211
225
|
/** The most recent kernel session-entropy sample (one per completed turn), or `null` before the
|
|
212
226
|
* first boundary. A pull companion to the streamed `entropy_sample` events. */
|
|
@@ -222,7 +236,8 @@ export class RuntimeRunner {
|
|
|
222
236
|
signalId: crypto.randomUUID(),
|
|
223
237
|
deliveryId: `injected-${crypto.randomUUID()}`,
|
|
224
238
|
deliveryAttempt: 1,
|
|
225
|
-
signal: injected,
|
|
239
|
+
signal: injected.signal,
|
|
240
|
+
note: injected.note,
|
|
226
241
|
ack: async () => true,
|
|
227
242
|
nack: async () => true,
|
|
228
243
|
};
|
|
@@ -327,25 +342,87 @@ export class RuntimeRunner {
|
|
|
327
342
|
}
|
|
328
343
|
yield* this.execute(sessionId, start.run_id, start.goal, start.criteria, extensions, events, true, start.attachments);
|
|
329
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* §22.13 · a host-authored memory write. The kernel decides whether it may happen: a live
|
|
347
|
+
* operation answers through `admit_memory_write` (the model's validation rule and rolling write
|
|
348
|
+
* quota); with no live operation the same kernel rule answers statelessly. Resolves `true` when
|
|
349
|
+
* the record was persisted.
|
|
350
|
+
*/
|
|
330
351
|
async writeMemory(memory, sessionId) {
|
|
331
352
|
if (!this.opts.memoryStore || !this.opts.agentId)
|
|
332
|
-
return;
|
|
353
|
+
return false;
|
|
354
|
+
const turn = this.activeKernel?.turn() ?? 0;
|
|
355
|
+
const error = await this.admitMemoryWrite(memory);
|
|
356
|
+
if (error) {
|
|
357
|
+
if (sessionId) {
|
|
358
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
359
|
+
kind: "memory_validation_failed", turn, record_id: memory.record_id, error,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
333
364
|
try {
|
|
334
365
|
await this.opts.memoryStore.put(this.opts.agentId, memory);
|
|
335
366
|
}
|
|
336
367
|
catch (cause) {
|
|
337
368
|
throw new Error(formatToolError(cause));
|
|
338
369
|
}
|
|
370
|
+
if (sessionId) {
|
|
371
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
372
|
+
kind: "memory_written", turn, record_id: memory.record_id, scope: memory.scope,
|
|
373
|
+
memory_kind: memory.kind, name: memory.name,
|
|
374
|
+
size_bytes: new TextEncoder().encode(memory.content).byteLength,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
async admitMemoryWrite(memory) {
|
|
380
|
+
const runtime = this.activeKernel;
|
|
381
|
+
if (runtime && runtime.hasStarted() && !runtime.isTerminal()) {
|
|
382
|
+
// The caller logs the verdict, so the fact is consumed here rather than queued for the
|
|
383
|
+
// per-turn drain (which would log it a second time).
|
|
384
|
+
const observations = await canonicalKernelApply(runtime, [], {
|
|
385
|
+
kind: "admit_memory_write",
|
|
386
|
+
record_id: memory.record_id,
|
|
387
|
+
name: memory.name,
|
|
388
|
+
content_bytes: new TextEncoder().encode(memory.content).byteLength,
|
|
389
|
+
});
|
|
390
|
+
let refusal;
|
|
391
|
+
for (const observation of observations) {
|
|
392
|
+
if (observation.kind === "memory_validation_failed" && observation.record_id === memory.record_id) {
|
|
393
|
+
refusal = String(observation.error ?? "memory write refused");
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
this.pendingObservations.push(observation);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return refusal;
|
|
400
|
+
}
|
|
401
|
+
return checkMemoryWrite(this.kernelMemoryPolicyWire(), memory.name, memory.content);
|
|
402
|
+
}
|
|
403
|
+
/** The run's memory policy as the kernel's canonical wire carries it. */
|
|
404
|
+
kernelMemoryPolicyWire() {
|
|
405
|
+
if (!this.opts.memoryPolicy)
|
|
406
|
+
return undefined;
|
|
407
|
+
const { promotion_recall_threshold: threshold, ...rest } = memoryPolicyToKernel(this.opts.memoryPolicy);
|
|
408
|
+
return {
|
|
409
|
+
...rest,
|
|
410
|
+
...(threshold !== undefined ? { promotion_recall_threshold: String(threshold) } : {}),
|
|
411
|
+
};
|
|
339
412
|
}
|
|
340
|
-
|
|
341
|
-
|
|
413
|
+
/**
|
|
414
|
+
* M3/M4 · a host-initiated recall (prefetch). The store reports the counts it holds; the kernel
|
|
415
|
+
* derives the next counts and the promotion edge, and the host only mirrors them — the same
|
|
416
|
+
* derivation a model `query_memory` resolution journals as `memory_recalled`.
|
|
417
|
+
*/
|
|
418
|
+
async recordHostMemoryRecall(hits) {
|
|
419
|
+
if (!this.opts.agentId)
|
|
342
420
|
return;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
await this.opts.sessionLog.append(sessionId, event);
|
|
421
|
+
const { recalls, promotions } = await deriveMemoryRecall(this.kernelMemoryPolicyWire(), Date.now(), hits);
|
|
422
|
+
if (recalls.length > 0)
|
|
423
|
+
await this.opts.memoryStore?.recordRecall?.(this.opts.agentId, recalls);
|
|
424
|
+
for (const promotion of promotions) {
|
|
425
|
+
this.opts.onPromotionSuggested?.({ recordId: promotion.record_id, recallCount: promotion.recall_count });
|
|
349
426
|
}
|
|
350
427
|
}
|
|
351
428
|
/** Push content into Slot 2 (system_knowledge) via add_knowledge_message.
|
|
@@ -378,6 +455,41 @@ export class RuntimeRunner {
|
|
|
378
455
|
}
|
|
379
456
|
/** K3: host-driven skill deactivation — toolset re-widens at the next provider call, the
|
|
380
457
|
* skill's knowledge pin drops at the next boundary. Errs-open: not-active is a no-op. */
|
|
458
|
+
/** §13.2 live policy patch (signal / governance / resource quota / recovery), guarded by the
|
|
459
|
+
* policy revision. Mirrors Node: the runtime tracks the committed revision; after a restore
|
|
460
|
+
* `expectedRevision` must be given. Rejects when the kernel refuses the patch. */
|
|
461
|
+
applyPolicyPatch(patch, opts = {}) {
|
|
462
|
+
return this.enqueueControl({
|
|
463
|
+
kind: "apply_policy_patch",
|
|
464
|
+
patch,
|
|
465
|
+
...(opts.expectedRevision !== undefined ? { expected_revision: opts.expectedRevision } : {}),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/** Move the operation's absolute deadline (epoch ms), or clear it with `null`. */
|
|
469
|
+
updateDeadline(deadlineMs) {
|
|
470
|
+
return this.enqueueControl({ kind: "update_deadline", deadline_ms: deadlineMs });
|
|
471
|
+
}
|
|
472
|
+
/** Ask the kernel to compact context now; any archive it owes runs as a normal page-out effect. */
|
|
473
|
+
forceCompact() {
|
|
474
|
+
return this.enqueueControl({ kind: "force_compact" });
|
|
475
|
+
}
|
|
476
|
+
/** Host-driven skill activation, admitted by the kernel against the operation's skill catalog. */
|
|
477
|
+
activateSkill(name, opts = {}) {
|
|
478
|
+
return this.enqueueControl({
|
|
479
|
+
kind: "skill_activation",
|
|
480
|
+
name,
|
|
481
|
+
...(opts.leaseTurns !== undefined ? { lease_turns: opts.leaseTurns } : {}),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
enqueueControl(event) {
|
|
485
|
+
if (!this.activeKernel)
|
|
486
|
+
return Promise.reject(new Error(`${String(event.kind)} requires an active run`));
|
|
487
|
+
return new Promise((resolve, reject) => this.pendingControl.push({ event, resolve, reject }));
|
|
488
|
+
}
|
|
489
|
+
rejectPendingControl(reason) {
|
|
490
|
+
for (const pending of this.pendingControl.splice(0))
|
|
491
|
+
pending.reject(new Error(reason));
|
|
492
|
+
}
|
|
381
493
|
async deactivateSkill(name) {
|
|
382
494
|
if (!this.activeKernel)
|
|
383
495
|
return;
|
|
@@ -627,6 +739,11 @@ export class RuntimeRunner {
|
|
|
627
739
|
? { ...baseSpec, capabilityFilter: { ...baseSpec.capabilityFilter, allowedIds: allowedToolIds } }
|
|
628
740
|
: baseSpec;
|
|
629
741
|
spec = { ...spec, exposureBaseline: baselineToolIds };
|
|
742
|
+
// The loaded milestone contract only gates a run whose spec names it; without this binding
|
|
743
|
+
// the kernel never publishes `evaluate_milestone` and the whole phase gate is inert.
|
|
744
|
+
if (this.opts.milestoneContract && !spec.verificationContractId) {
|
|
745
|
+
spec = { ...spec, verificationContractId: "node-default" };
|
|
746
|
+
}
|
|
630
747
|
startRunSpec = agentRunSpecToKernel(spec);
|
|
631
748
|
}
|
|
632
749
|
if (!resumeMidRun)
|
|
@@ -656,6 +773,22 @@ export class RuntimeRunner {
|
|
|
656
773
|
});
|
|
657
774
|
break;
|
|
658
775
|
}
|
|
776
|
+
if (this.pendingControl.length > 0) {
|
|
777
|
+
for (const pending of this.pendingControl.splice(0)) {
|
|
778
|
+
try {
|
|
779
|
+
await this.commitKernelMaybeAction(runtime, this.pendingObservations, pending.event);
|
|
780
|
+
pending.resolve();
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
pending.reject(error);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// A command can publish an effect ahead of the one in hand (a forced compaction's page-out);
|
|
787
|
+
// the kernel's projection decides what runs next.
|
|
788
|
+
action = runtime.resumeAction() ?? action;
|
|
789
|
+
if (runtime.isTerminal())
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
659
792
|
if (this.opts.signalSource || this.injectedSignals.length > 0) {
|
|
660
793
|
const delivery = await this.nextInboundSignal();
|
|
661
794
|
if (delivery) {
|
|
@@ -683,22 +816,10 @@ export class RuntimeRunner {
|
|
|
683
816
|
const attemptStartedAtMs = Date.now();
|
|
684
817
|
const finalToolCalls = [];
|
|
685
818
|
let finalText = "";
|
|
686
|
-
// I5:
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const { allowed, denied } = governanceFilterSchema(tools, this.opts.governancePolicy);
|
|
691
|
-
if (denied.length > 0) {
|
|
692
|
-
tools = allowed;
|
|
693
|
-
const note = `[governance] the following tools are denied for this run and will fail if called: ${denied.join(", ")}.`;
|
|
694
|
-
context = {
|
|
695
|
-
...context,
|
|
696
|
-
systemKnowledge: context.systemKnowledge
|
|
697
|
-
? `${context.systemKnowledge}\n\n${note}`
|
|
698
|
-
: note,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
}
|
|
819
|
+
// I5: the kernel itself withholds statically denied tools (`hide_denied_tools`) and renders
|
|
820
|
+
// the denial note — the host sends exactly the context and surface the kernel committed.
|
|
821
|
+
const context = action.context;
|
|
822
|
+
const tools = action.tools;
|
|
702
823
|
let turnTokens = 0;
|
|
703
824
|
let turnInputTokens = 0;
|
|
704
825
|
let turnOutputTokens = 0;
|
|
@@ -802,7 +923,7 @@ export class RuntimeRunner {
|
|
|
802
923
|
finalText += evt.delta;
|
|
803
924
|
else if (evt.type === "tool_call") {
|
|
804
925
|
const tc = evt;
|
|
805
|
-
finalToolCalls.push({ id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) });
|
|
926
|
+
finalToolCalls.push({ id: tc.id, name: tc.name, arguments: tc.rawArguments ?? JSON.stringify(tc.arguments) });
|
|
806
927
|
}
|
|
807
928
|
}
|
|
808
929
|
}
|
|
@@ -879,7 +1000,6 @@ export class RuntimeRunner {
|
|
|
879
1000
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
880
1001
|
kind: "cancel_operation",
|
|
881
1002
|
reason: this.cancellationReason ?? "user",
|
|
882
|
-
pending_call_ids: [providerEffectId],
|
|
883
1003
|
});
|
|
884
1004
|
break;
|
|
885
1005
|
}
|
|
@@ -932,7 +1052,29 @@ export class RuntimeRunner {
|
|
|
932
1052
|
...(attemptUsage !== undefined ? { usage: attemptUsage } : {}),
|
|
933
1053
|
wireEvidence,
|
|
934
1054
|
});
|
|
1055
|
+
// Skill content is STAGED before the provider resolution commits, so the kernel's next render
|
|
1056
|
+
// can include it. The kernel adjudicates the `skill` syscall in that commit and withdraws the
|
|
1057
|
+
// staged `skill:<name>` entry when it refuses the activation — the host never decides.
|
|
1058
|
+
if (this.opts.skillContentMap?.size) {
|
|
1059
|
+
for (const call of finalToolCalls) {
|
|
1060
|
+
if (call.name !== "skill")
|
|
1061
|
+
continue;
|
|
1062
|
+
const parsed = parseToolCallArguments(call.arguments);
|
|
1063
|
+
const name = parsed.ok ? String(parsed.args.name ?? "") : "";
|
|
1064
|
+
const raw = name ? this.opts.skillContentMap.get(name) : undefined;
|
|
1065
|
+
if (raw == null)
|
|
1066
|
+
continue;
|
|
1067
|
+
const content = stripSkillFrontmatter(raw);
|
|
1068
|
+
await this.pushKnowledge({ role: "system", content }, undefined, { key: `skill:${name}`, pinned: true });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const providerObservationStart = this.pendingObservations.length;
|
|
935
1072
|
action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
|
|
1073
|
+
for (const observation of this.pendingObservations.slice(providerObservationStart)) {
|
|
1074
|
+
if (observation.kind === "skill_admitted" && typeof observation.name === "string") {
|
|
1075
|
+
this.knowledgePushedSkills.add(observation.name);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
936
1078
|
await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
|
|
937
1079
|
turn: runtime.turn(),
|
|
938
1080
|
content: finalText,
|
|
@@ -980,11 +1122,34 @@ export class RuntimeRunner {
|
|
|
980
1122
|
});
|
|
981
1123
|
}
|
|
982
1124
|
else if (action.kind === "persist_memory") {
|
|
1125
|
+
// The kernel hands over the validated canonical write, which carries no store identity:
|
|
1126
|
+
// build the full record here, and let the receipt name exactly what the store now holds.
|
|
1127
|
+
const canonicalMemory = action.memory;
|
|
1128
|
+
const acceptedAt = Number(canonicalMemory.accepted_at_ms ?? Date.now());
|
|
1129
|
+
const record = {
|
|
1130
|
+
record_id: `memory:${crypto.randomUUID()}`,
|
|
1131
|
+
scope: this.opts.memoryScope ?? { tenant_id: "default", namespace: this.opts.agentId ?? "default" },
|
|
1132
|
+
name: String(canonicalMemory.name ?? ""),
|
|
1133
|
+
kind: String(canonicalMemory.kind ?? "reference"),
|
|
1134
|
+
content: String(canonicalMemory.content ?? ""),
|
|
1135
|
+
description: String(canonicalMemory.description ?? ""),
|
|
1136
|
+
provenance: {
|
|
1137
|
+
author: "model",
|
|
1138
|
+
trust: "untrusted",
|
|
1139
|
+
evidence_refs: Array.isArray(canonicalMemory.evidence_refs) ? canonicalMemory.evidence_refs.map(String) : [],
|
|
1140
|
+
},
|
|
1141
|
+
created_at: acceptedAt,
|
|
1142
|
+
updated_at: acceptedAt,
|
|
1143
|
+
recall_count: 0,
|
|
1144
|
+
confidence: 1,
|
|
1145
|
+
links: [],
|
|
1146
|
+
pinned: false,
|
|
1147
|
+
};
|
|
983
1148
|
let error;
|
|
984
1149
|
try {
|
|
985
1150
|
if (!this.opts.memoryStore || !this.opts.agentId)
|
|
986
1151
|
throw new Error("WASM memory persistence requires memoryStore and agentId");
|
|
987
|
-
await this.opts.memoryStore.put(this.opts.agentId,
|
|
1152
|
+
await this.opts.memoryStore.put(this.opts.agentId, record);
|
|
988
1153
|
}
|
|
989
1154
|
catch (cause) {
|
|
990
1155
|
error = formatToolError(cause);
|
|
@@ -992,7 +1157,10 @@ export class RuntimeRunner {
|
|
|
992
1157
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
993
1158
|
kind: "memory_persist_result",
|
|
994
1159
|
effect_id: action.effectId,
|
|
995
|
-
...(error ? { error } : {
|
|
1160
|
+
...(error ? { error } : {
|
|
1161
|
+
record_ref: record.record_id,
|
|
1162
|
+
digest: `sha256:${sha256Hex(record.content)}`,
|
|
1163
|
+
}),
|
|
996
1164
|
});
|
|
997
1165
|
}
|
|
998
1166
|
else if (action.kind === "query_memory") {
|
|
@@ -1092,23 +1260,12 @@ export class RuntimeRunner {
|
|
|
1092
1260
|
onPermissionRequest: this.opts.onPermissionRequest,
|
|
1093
1261
|
};
|
|
1094
1262
|
const toolResults = [];
|
|
1095
|
-
// Syscall tools are consumed by core from the provider result
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
const
|
|
1263
|
+
// Syscall tools (`update_plan` included) are consumed by core from the provider result: the
|
|
1264
|
+
// kernel decodes and applies them itself. One reaching this host effect projection is
|
|
1265
|
+
// boundary drift and fails closed — the host never re-applies a model plan as `update_task`.
|
|
1266
|
+
const submitCalls = allCalls.filter(c => c.name === "update_plan" || c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
1099
1267
|
const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes" && c.name !== "start_workflow"
|
|
1100
1268
|
&& c.name !== "update_plan");
|
|
1101
|
-
// `update_plan` is a kernel meta-tool (exposed via `enablePlanTool`), not a registered
|
|
1102
|
-
// plane tool — resolve it here as an `update_task` apply, mirroring the node/python runners.
|
|
1103
|
-
for (const call of planCalls) {
|
|
1104
|
-
const update = parseUpdatePlanArgs(call.arguments);
|
|
1105
|
-
await this.commitKernelApply(runtime, this.pendingObservations, {
|
|
1106
|
-
kind: "update_task",
|
|
1107
|
-
update: taskUpdateToKernel(update),
|
|
1108
|
-
});
|
|
1109
|
-
toolResults.push({ callId: call.id, output: "success", isError: false });
|
|
1110
|
-
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
1111
|
-
}
|
|
1112
1269
|
for (const call of submitCalls) {
|
|
1113
1270
|
throw new Error(`canonical kernel published model syscall ${call.name} as a host tool effect`);
|
|
1114
1271
|
}
|
|
@@ -1213,8 +1370,12 @@ export class RuntimeRunner {
|
|
|
1213
1370
|
}
|
|
1214
1371
|
if (!decision)
|
|
1215
1372
|
continue;
|
|
1216
|
-
if (typeof decision.replaceOutput === "string")
|
|
1373
|
+
if (typeof decision.replaceOutput === "string") {
|
|
1217
1374
|
r.output = decision.replaceOutput;
|
|
1375
|
+
// The replacement is what every consumer sees: drop the raw structured blocks so the
|
|
1376
|
+
// durable session record cannot resurface content the host just redacted.
|
|
1377
|
+
delete r.contentParts;
|
|
1378
|
+
}
|
|
1218
1379
|
if (decision.note)
|
|
1219
1380
|
this.injectNote(decision.note);
|
|
1220
1381
|
}
|
|
@@ -1231,34 +1392,8 @@ export class RuntimeRunner {
|
|
|
1231
1392
|
content: { blocks: toolOutputBlocksToDurable(r.contentParts?.length ? r.contentParts : [{ type: "text", text: r.output }]) },
|
|
1232
1393
|
})),
|
|
1233
1394
|
});
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
// Strict dynamic context control: the skill text
|
|
1238
|
-
// for the rest of the run, unlike a one-off memory/knowledge lookup (fact content, relevant
|
|
1239
|
-
// for the moment it's used). So its text ALSO goes into the durable `knowledge` slot here
|
|
1240
|
-
// (in addition to the ordinary tool_result already headed for `history`, where it will decay
|
|
1241
|
-
// with the compression pyramid like any other tool output). First activation only.
|
|
1242
|
-
for (const call of allCalls) {
|
|
1243
|
-
if (call.name !== "skill")
|
|
1244
|
-
continue;
|
|
1245
|
-
const res = toolResults.find(r => r.callId === call.id);
|
|
1246
|
-
if (!res || res.isError)
|
|
1247
|
-
continue;
|
|
1248
|
-
try {
|
|
1249
|
-
const name = JSON.parse(call.arguments || "{}").name;
|
|
1250
|
-
if (!name)
|
|
1251
|
-
continue;
|
|
1252
|
-
// With a lease configured, skip the Set optimization: an expired-then-reloaded skill
|
|
1253
|
-
// must re-pin — only the kernel knows the lease state; its put dedupes anyway.
|
|
1254
|
-
if (this.opts.skillLeaseTurns !== undefined || !this.knowledgePushedSkills.has(name)) {
|
|
1255
|
-
this.knowledgePushedSkills.add(name);
|
|
1256
|
-
// K1: keyed `skill:<name>` — the kernel-side put dedupes across runner instances.
|
|
1257
|
-
this.pushKnowledge({ role: "system", content: res.output }, undefined, { key: `skill:${name}` });
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
catch { /* skip */ }
|
|
1261
|
-
}
|
|
1395
|
+
// `skill` is a kernel syscall: it never reaches this host tool effect. The kernel admits or
|
|
1396
|
+
// refuses it at the provider resolution (and withdraws any content staged for a refusal).
|
|
1262
1397
|
const entropyObsStart = this.pendingObservations.length;
|
|
1263
1398
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1264
1399
|
kind: "tool_results",
|
|
@@ -1292,6 +1427,8 @@ export class RuntimeRunner {
|
|
|
1292
1427
|
else if (action.kind === "evaluate_milestone") {
|
|
1293
1428
|
const milestoneEffectId = action.effectId;
|
|
1294
1429
|
const milestonePhaseId = action.phaseId;
|
|
1430
|
+
// Host-owned phase data: the kernel names the phase, the host contract says what it demands.
|
|
1431
|
+
const milestonePhase = this.opts.milestoneContract?.phases.find(phase => phase.id === milestonePhaseId);
|
|
1295
1432
|
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
1296
1433
|
if (milestonePolicy === "auto_pass") {
|
|
1297
1434
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
@@ -1304,8 +1441,8 @@ export class RuntimeRunner {
|
|
|
1304
1441
|
else if (this.opts.onMilestoneEvaluate) {
|
|
1305
1442
|
const check = await this.opts.onMilestoneEvaluate({
|
|
1306
1443
|
phaseId: action.phaseId,
|
|
1307
|
-
criteria: action.criteria ?? [],
|
|
1308
|
-
requiredEvidence: action.requiredEvidence ?? [],
|
|
1444
|
+
criteria: milestonePhase?.criteria ?? action.criteria ?? [],
|
|
1445
|
+
requiredEvidence: milestonePhase?.requiredEvidence ?? action.requiredEvidence ?? [],
|
|
1309
1446
|
});
|
|
1310
1447
|
action = await this.commitKernelAction(runtime, this.pendingObservations, {
|
|
1311
1448
|
kind: "milestone_result",
|
|
@@ -1334,6 +1471,7 @@ export class RuntimeRunner {
|
|
|
1334
1471
|
totalTokens: 0,
|
|
1335
1472
|
}));
|
|
1336
1473
|
yield { type: "done", iterations: turnsUsed, totalTokens: 0, status: "milestone_pending" };
|
|
1474
|
+
this.rejectPendingControl("the run ended before the control command was applied");
|
|
1337
1475
|
this.activeKernel = null;
|
|
1338
1476
|
this.currentSessionId = null;
|
|
1339
1477
|
return;
|
|
@@ -1370,11 +1508,7 @@ export class RuntimeRunner {
|
|
|
1370
1508
|
// I0b: kernel rejection (or any other thrown error inside the loop) is observable here —
|
|
1371
1509
|
// emit run_terminal so downstream code sees a clean end rather than mid-loop EOF.
|
|
1372
1510
|
const errMsg = formatToolError(err);
|
|
1373
|
-
const
|
|
1374
|
-
const isInvalidArg = code === "InvalidArg" ||
|
|
1375
|
-
errMsg.toLowerCase().includes("invalidarg") ||
|
|
1376
|
-
errMsg.toLowerCase().includes("invalid argument");
|
|
1377
|
-
const reason = isInvalidArg ? "invalid_arg" : "error";
|
|
1511
|
+
const reason = isInvalidInputError(err) ? "invalid_arg" : "error";
|
|
1378
1512
|
yield { type: "error", message: errMsg };
|
|
1379
1513
|
try {
|
|
1380
1514
|
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
@@ -1385,6 +1519,7 @@ export class RuntimeRunner {
|
|
|
1385
1519
|
}
|
|
1386
1520
|
catch { /* session log failure must not mask the original error */ }
|
|
1387
1521
|
yield { type: "done", iterations: runtime.turn() || 0, totalTokens: 0, status: reason };
|
|
1522
|
+
this.rejectPendingControl("the run ended before the control command was applied");
|
|
1388
1523
|
this.activeKernel = null;
|
|
1389
1524
|
this.currentSessionId = null;
|
|
1390
1525
|
return;
|
|
@@ -1434,6 +1569,7 @@ export class RuntimeRunner {
|
|
|
1434
1569
|
// ③ loop-agent: surface the kernel-adjudicated after-round decision to the driver.
|
|
1435
1570
|
...(result?.paceDecision ? { paceDecision: result.paceDecision } : {}),
|
|
1436
1571
|
};
|
|
1572
|
+
this.rejectPendingControl("the run ended before the control command was applied");
|
|
1437
1573
|
this.activeKernel = null;
|
|
1438
1574
|
this.currentSessionId = null;
|
|
1439
1575
|
}
|
|
@@ -1707,6 +1843,7 @@ export class RuntimeRunner {
|
|
|
1707
1843
|
}
|
|
1708
1844
|
finally {
|
|
1709
1845
|
if (bootstrapped) {
|
|
1846
|
+
this.rejectPendingControl("the run ended before the control command was applied");
|
|
1710
1847
|
this.activeKernel = null;
|
|
1711
1848
|
this.currentSessionId = null;
|
|
1712
1849
|
this.abortController = null;
|
|
@@ -1720,7 +1857,7 @@ export class RuntimeRunner {
|
|
|
1720
1857
|
* `WorkflowRun` down), and we abort the matching children's in-flight LLM calls. Returns the
|
|
1721
1858
|
* torn-down outcome on preemption, else `null`. No-op without a signal source.
|
|
1722
1859
|
*/
|
|
1723
|
-
async monitorWorkflowPreemption(runtime, controllers, batchState) {
|
|
1860
|
+
async monitorWorkflowPreemption(runtime, controllers, batchState, exclusive) {
|
|
1724
1861
|
const source = this.opts.signalSource;
|
|
1725
1862
|
if (!source && this.injectedSignals.length === 0)
|
|
1726
1863
|
return null;
|
|
@@ -1733,29 +1870,36 @@ export class RuntimeRunner {
|
|
|
1733
1870
|
await new Promise(resolve => setTimeout(resolve, 5));
|
|
1734
1871
|
continue;
|
|
1735
1872
|
}
|
|
1736
|
-
const
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
if (signalAction
|
|
1740
|
-
|
|
1873
|
+
const preemptedBySignal = await exclusive(async () => {
|
|
1874
|
+
const observationStart = this.pendingObservations.length;
|
|
1875
|
+
const signalAction = await this.consumeInboundSignal(delivery, claimed => this.commitKernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(claimed)));
|
|
1876
|
+
if (signalAction) {
|
|
1877
|
+
if (signalAction.kind !== "preempt_sub_agents") {
|
|
1878
|
+
throw new Error(`workflow signal returned unexpected effect: ${signalAction.kind}`);
|
|
1879
|
+
}
|
|
1880
|
+
for (const id of signalAction.agentIds)
|
|
1881
|
+
controllers.get(id)?.abort();
|
|
1882
|
+
const continuation = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1883
|
+
kind: "preempt_result", effect_id: signalAction.effectId,
|
|
1884
|
+
already_finished_agent_ids: signalAction.agentIds.filter(id => batchState.finished.has(id)),
|
|
1885
|
+
});
|
|
1886
|
+
if (continuation && continuation.kind !== "call_provider" && continuation.kind !== "done") {
|
|
1887
|
+
throw new Error(`workflow preemption returned unexpected effect: ${continuation.kind}`);
|
|
1888
|
+
}
|
|
1741
1889
|
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1890
|
+
const obs = this.pendingObservations.slice(observationStart);
|
|
1891
|
+
const preempted = obs.find(o => o.kind === "agent_preempted");
|
|
1892
|
+
if (preempted) {
|
|
1893
|
+
batchState.stopping = true;
|
|
1894
|
+
for (const id of preempted.agent_ids ?? [])
|
|
1895
|
+
controllers.get(id)?.abort();
|
|
1896
|
+
const wc = obs.find(o => o.kind === "workflow_completed");
|
|
1897
|
+
return (wc?.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel);
|
|
1749
1898
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
for (const id of preempted.agent_ids ?? [])
|
|
1755
|
-
controllers.get(id)?.abort();
|
|
1756
|
-
const wc = obs.find(o => o.kind === "workflow_completed");
|
|
1757
|
-
return (wc?.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel);
|
|
1758
|
-
}
|
|
1899
|
+
return null;
|
|
1900
|
+
});
|
|
1901
|
+
if (preemptedBySignal)
|
|
1902
|
+
return preemptedBySignal;
|
|
1759
1903
|
}
|
|
1760
1904
|
return null;
|
|
1761
1905
|
}
|
|
@@ -1802,98 +1946,144 @@ export class RuntimeRunner {
|
|
|
1802
1946
|
// W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
|
|
1803
1947
|
// still see their (pre-crash) dependencies' outputs.
|
|
1804
1948
|
const outputs = new Map(seedOutputs ?? []);
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1949
|
+
if (nodes.length === 0)
|
|
1950
|
+
return { nodeOutcomes: [], outputs: Object.fromEntries(outputs) };
|
|
1951
|
+
// Nodes run as soon as the kernel spawns them and each completion is fed back the moment it
|
|
1952
|
+
// lands — no round barrier, so a fast node's dependents never wait on a slow sibling. A child
|
|
1953
|
+
// that fails is an ordinary completion; a driver that throws aborts every sibling and propagates.
|
|
1954
|
+
// #2-B-ii: per-node abort controllers + a concurrent preemption monitor (see node runner). The
|
|
1955
|
+
// monitor and the completion feeds share one kernel queue; after a teardown nothing is fed.
|
|
1956
|
+
const controllers = new Map();
|
|
1957
|
+
const batchState = { settled: false, stopping: false, finished: new Set() };
|
|
1958
|
+
let kernelQueue = Promise.resolve();
|
|
1959
|
+
const exclusive = (work) => {
|
|
1960
|
+
const run = kernelQueue.then(work, work);
|
|
1961
|
+
kernelQueue = run.catch(() => undefined);
|
|
1962
|
+
return run;
|
|
1963
|
+
};
|
|
1964
|
+
const monitor = this.monitorWorkflowPreemption(runtime, controllers, batchState, exclusive);
|
|
1965
|
+
const inFlight = new Map();
|
|
1966
|
+
const launch = (node) => {
|
|
1967
|
+
const dependencyOutputs = node.dependency_outputs ?? {};
|
|
1968
|
+
for (const [agentId, output] of Object.entries(dependencyOutputs)) {
|
|
1969
|
+
if (!outputs.has(agentId))
|
|
1970
|
+
outputs.set(agentId, output);
|
|
1814
1971
|
}
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
const
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1972
|
+
const controller = new AbortController();
|
|
1973
|
+
controllers.set(node.agent_id, controller);
|
|
1974
|
+
const running = this.runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs, controller.signal)
|
|
1975
|
+
.then(result => ({ kind: "node", node, result }))
|
|
1976
|
+
.finally(() => batchState.finished.add(node.agent_id));
|
|
1977
|
+
// A node can fail while the driver is busy feeding another completion; the race observes
|
|
1978
|
+
// it on the next pass, so it must not count as unhandled in between.
|
|
1979
|
+
running.catch(() => undefined);
|
|
1980
|
+
inFlight.set(node.agent_id, running);
|
|
1981
|
+
};
|
|
1982
|
+
const stopAll = async () => {
|
|
1821
1983
|
batchState.settled = true;
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
const completionAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
1840
|
-
kind: "sub_agent_completed",
|
|
1841
|
-
result: subAgentResultToKernel(result),
|
|
1842
|
-
});
|
|
1843
|
-
let obs = this.pendingObservations.slice(observationStart);
|
|
1844
|
-
if (completionAction?.kind === "spawn_workflow") {
|
|
1845
|
-
nextNodes.push(...completionAction.nodes);
|
|
1846
|
-
budget = completionAction.budget ?? budget;
|
|
1847
|
-
obs = [...obs, ...await acceptSpawn(completionAction)];
|
|
1848
|
-
}
|
|
1849
|
-
else if (completionAction?.kind === "call_provider") {
|
|
1850
|
-
this.workflowContinuation = completionAction;
|
|
1851
|
-
}
|
|
1852
|
-
else if (completionAction?.kind === "done") {
|
|
1853
|
-
// The correlated child completion may terminalize the workflow in the same canonical
|
|
1854
|
-
// step. Its workflow_completed observation below remains the typed outcome source.
|
|
1855
|
-
}
|
|
1856
|
-
else if (completionAction) {
|
|
1857
|
-
throw new Error(`workflow completion returned unexpected effect: ${completionAction.kind}`);
|
|
1984
|
+
for (const controller of controllers.values())
|
|
1985
|
+
controller.abort();
|
|
1986
|
+
await Promise.allSettled(inFlight.values());
|
|
1987
|
+
await monitor;
|
|
1988
|
+
};
|
|
1989
|
+
let watching = monitor.then(outcome => ({ kind: "monitor", outcome }));
|
|
1990
|
+
for (const node of nodes)
|
|
1991
|
+
launch(node);
|
|
1992
|
+
try {
|
|
1993
|
+
while (inFlight.size > 0) {
|
|
1994
|
+
const landed = await Promise.race([...inFlight.values(), ...(watching ? [watching] : [])]);
|
|
1995
|
+
if (landed.kind === "monitor") {
|
|
1996
|
+
watching = undefined;
|
|
1997
|
+
if (landed.outcome === null)
|
|
1998
|
+
continue;
|
|
1999
|
+
await stopAll();
|
|
2000
|
+
return { nodeOutcomes: landed.outcome, outputs: Object.fromEntries(outputs) };
|
|
1858
2001
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2002
|
+
inFlight.delete(landed.node.agent_id);
|
|
2003
|
+
const settled = await exclusive(async () => {
|
|
2004
|
+
if (batchState.stopping)
|
|
2005
|
+
return { spawned: [], done: undefined };
|
|
2006
|
+
const { node, result } = landed;
|
|
2007
|
+
// G2: record this node's output so a downstream reduce node can consume it.
|
|
2008
|
+
const outContent = result.result.finalMessage?.content;
|
|
2009
|
+
const outText = typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "";
|
|
2010
|
+
outputs.set(result.agentId, outText);
|
|
2011
|
+
// A loop iteration completes under `wf-node{N}-i{k}` but its dependents consume the STABLE
|
|
2012
|
+
// node id `wf-node{N}` — alias it so the LAST iteration's output is what dependents see.
|
|
2013
|
+
const stableId = result.agentId.replace(/-i\d+$/, "");
|
|
2014
|
+
if (stableId !== result.agentId)
|
|
2015
|
+
outputs.set(stableId, outText);
|
|
2016
|
+
const observationStart = this.pendingObservations.length;
|
|
2017
|
+
const completionAction = await this.commitKernelMaybeAction(runtime, this.pendingObservations, {
|
|
2018
|
+
kind: "sub_agent_completed",
|
|
2019
|
+
result: subAgentResultToKernel(result),
|
|
2020
|
+
...(node.attempt_id ? { attempt_id: node.attempt_id } : {}),
|
|
2021
|
+
});
|
|
2022
|
+
let obs = this.pendingObservations.slice(observationStart);
|
|
2023
|
+
const spawned = [];
|
|
2024
|
+
if (completionAction?.kind === "spawn_workflow") {
|
|
2025
|
+
spawned.push(...completionAction.nodes);
|
|
2026
|
+
budget = completionAction.budget ?? budget;
|
|
2027
|
+
obs = [...obs, ...await acceptSpawn(completionAction)];
|
|
2028
|
+
}
|
|
2029
|
+
else if (completionAction?.kind === "call_provider") {
|
|
2030
|
+
this.workflowContinuation = completionAction;
|
|
2031
|
+
}
|
|
2032
|
+
else if (completionAction?.kind === "done") {
|
|
2033
|
+
// The correlated child completion may terminalize the workflow in the same canonical
|
|
2034
|
+
// step. Its workflow_completed observation below remains the typed outcome source.
|
|
2035
|
+
}
|
|
2036
|
+
else if (completionAction) {
|
|
2037
|
+
throw new Error(`workflow completion returned unexpected effect: ${completionAction.kind}`);
|
|
1870
2038
|
}
|
|
2039
|
+
// Child-authored DAG additions are admitted only as part of the canonical child-completion
|
|
2040
|
+
// resolution. Persist the projection only after core reports the admitted base index.
|
|
2041
|
+
if (result.submittedNodes?.length) {
|
|
2042
|
+
const submitted = obs.find(o => o.kind === "workflow_nodes_submitted");
|
|
2043
|
+
if (submitted) {
|
|
2044
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
2045
|
+
turn: runtime.turn(),
|
|
2046
|
+
nodes: result.submittedNodes.map(workflowNodeSpecToKernel),
|
|
2047
|
+
baseIndex: submitted.base,
|
|
2048
|
+
submitterAgentId: result.agentId,
|
|
2049
|
+
}));
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
// Persist node completion for resume recovery. W-1: the result-borne control signals ride
|
|
2053
|
+
// along (a resumed classifier re-prunes; a recorded loop stop is honored) plus the output
|
|
2054
|
+
// text (post-resume dependents/reduce still see this node's output).
|
|
2055
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
|
|
2056
|
+
turn: runtime.turn(),
|
|
2057
|
+
agentId: result.agentId,
|
|
2058
|
+
status: workflowNodeStatusFromTermination(result.result.termination),
|
|
2059
|
+
termination: result.result.termination,
|
|
2060
|
+
classifyBranch: result.result.classifyBranch,
|
|
2061
|
+
tournamentWinner: result.result.tournamentWinner,
|
|
2062
|
+
loopContinue: result.result.loopContinue,
|
|
2063
|
+
...(result.result.finalMessage ? { output: result.result.finalMessage } : {}),
|
|
2064
|
+
}));
|
|
2065
|
+
return { spawned, done: findDone(obs) };
|
|
2066
|
+
});
|
|
2067
|
+
if (settled.done && settled.spawned.length === 0 && inFlight.size === 0) {
|
|
2068
|
+
await stopAll();
|
|
2069
|
+
return {
|
|
2070
|
+
nodeOutcomes: (settled.done.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel),
|
|
2071
|
+
outputs: Object.fromEntries(outputs),
|
|
2072
|
+
};
|
|
1871
2073
|
}
|
|
1872
|
-
const
|
|
1873
|
-
|
|
1874
|
-
done = d;
|
|
1875
|
-
// Persist node completion for resume recovery. W-1: the result-borne control signals ride
|
|
1876
|
-
// along (a resumed classifier re-prunes; a recorded loop stop is honored) plus the output
|
|
1877
|
-
// text (post-resume dependents/reduce still see this node's output).
|
|
1878
|
-
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
|
|
1879
|
-
turn: runtime.turn(),
|
|
1880
|
-
agentId: result.agentId,
|
|
1881
|
-
status: workflowNodeStatusFromTermination(result.result.termination),
|
|
1882
|
-
termination: result.result.termination,
|
|
1883
|
-
classifyBranch: result.result.classifyBranch,
|
|
1884
|
-
tournamentWinner: result.result.tournamentWinner,
|
|
1885
|
-
loopContinue: result.result.loopContinue,
|
|
1886
|
-
...(result.result.finalMessage ? { output: result.result.finalMessage } : {}),
|
|
1887
|
-
}));
|
|
2074
|
+
for (const node of settled.spawned)
|
|
2075
|
+
launch(node);
|
|
1888
2076
|
}
|
|
1889
|
-
if (done && nextNodes.length === 0) {
|
|
1890
|
-
return {
|
|
1891
|
-
nodeOutcomes: (done.node_outcomes ?? []).map(workflowNodeOutcomeFromKernel),
|
|
1892
|
-
outputs: Object.fromEntries(outputs),
|
|
1893
|
-
};
|
|
1894
|
-
}
|
|
1895
|
-
nodes = nextNodes;
|
|
1896
2077
|
}
|
|
2078
|
+
catch (error) {
|
|
2079
|
+
await stopAll();
|
|
2080
|
+
throw error;
|
|
2081
|
+
}
|
|
2082
|
+
await stopAll();
|
|
2083
|
+
const preempted = await monitor;
|
|
2084
|
+
if (preempted)
|
|
2085
|
+
return { nodeOutcomes: preempted, outputs: Object.fromEntries(outputs) };
|
|
2086
|
+
return { nodeOutcomes: [], outputs: Object.fromEntries(outputs) };
|
|
1897
2087
|
}
|
|
1898
2088
|
async appendObservations(sessionId, runtime, nextArchiveStart) {
|
|
1899
2089
|
const turn = runtime.turn();
|
|
@@ -1945,11 +2135,17 @@ export class RuntimeRunner {
|
|
|
1945
2135
|
try {
|
|
1946
2136
|
const queries = await preQuery({ goal: this.currentGoal, phase });
|
|
1947
2137
|
const lines = [];
|
|
2138
|
+
const seen = new Set();
|
|
2139
|
+
const recalled = [];
|
|
1948
2140
|
for (const q of queries ?? []) {
|
|
1949
2141
|
if (!q.query.trim())
|
|
1950
2142
|
continue;
|
|
1951
2143
|
const hits = await this.opts.memoryStore.search(this.opts.agentId, q);
|
|
1952
2144
|
for (const hit of hits) {
|
|
2145
|
+
if (seen.has(hit.record.record_id))
|
|
2146
|
+
continue;
|
|
2147
|
+
seen.add(hit.record.record_id);
|
|
2148
|
+
recalled.push(hit);
|
|
1953
2149
|
lines.push(`[memory record_id=${hit.record.record_id} trust=${hit.record.provenance.trust} score=${hit.score.toFixed(3)}] ${hit.record.content}`);
|
|
1954
2150
|
}
|
|
1955
2151
|
}
|
|
@@ -1959,6 +2155,7 @@ export class RuntimeRunner {
|
|
|
1959
2155
|
message: { role: "user", content: lines.join("\n") },
|
|
1960
2156
|
});
|
|
1961
2157
|
}
|
|
2158
|
+
await this.recordHostMemoryRecall(recalled);
|
|
1962
2159
|
}
|
|
1963
2160
|
catch { /* errs-open */ }
|
|
1964
2161
|
}
|
|
@@ -2132,47 +2329,27 @@ async function withTimeout(promise, timeoutMs) {
|
|
|
2132
2329
|
clearTimeout(timer);
|
|
2133
2330
|
}
|
|
2134
2331
|
}
|
|
2135
|
-
/**
|
|
2136
|
-
* the
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
}
|
|
2142
|
-
catch {
|
|
2143
|
-
// Ignore parse error → empty update.
|
|
2144
|
-
}
|
|
2145
|
-
return {
|
|
2146
|
-
plan: parsed.plan,
|
|
2147
|
-
currentStep: parsed.currentStep !== undefined ? Number(parsed.currentStep) : parsed.current_step !== undefined ? Number(parsed.current_step) : undefined,
|
|
2148
|
-
progress: parsed.progress,
|
|
2149
|
-
scratchpad: parsed.scratchpad,
|
|
2150
|
-
blockedOn: parsed.blockedOn !== undefined
|
|
2151
|
-
? parsed.blockedOn
|
|
2152
|
-
: parsed.blocked_on,
|
|
2153
|
-
};
|
|
2154
|
-
}
|
|
2155
|
-
/** Lower a claimed signal delivery to the kernel's `deliver_signal` input event. Shared by the main
|
|
2156
|
-
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
2157
|
-
function signalToKernelEvent(delivery) {
|
|
2332
|
+
/** Lower a claimed signal delivery straight into the kernel's `LogicalSignal` vocabulary. Shared by
|
|
2333
|
+
* the main loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never
|
|
2334
|
+
* drift). `signalType` / `coalesceKey` / `coalescedCount` are signal-source concepts the canonical
|
|
2335
|
+
* wire deliberately does not carry (§5n); the absolute `deadlineMs` becomes the duration the kernel
|
|
2336
|
+
* anchors to its own accepted time. */
|
|
2337
|
+
export function signalToKernelEvent(delivery, nowMs = Date.now()) {
|
|
2158
2338
|
const sig = delivery.signal;
|
|
2159
2339
|
return {
|
|
2160
2340
|
kind: "deliver_signal",
|
|
2161
2341
|
delivery_id: delivery.deliveryId,
|
|
2162
2342
|
attempt: delivery.deliveryAttempt,
|
|
2163
2343
|
signal: {
|
|
2164
|
-
|
|
2344
|
+
signal_id: delivery.signalId,
|
|
2165
2345
|
source: sig.source ?? "custom",
|
|
2166
|
-
|
|
2346
|
+
target: sig.recipient ? { kind: "task", task_id: sig.recipient } : { kind: "operation" },
|
|
2167
2347
|
urgency: sig.urgency ?? "normal",
|
|
2168
|
-
|
|
2169
|
-
payload: sig.payload ?? {},
|
|
2348
|
+
payload: delivery.note ?? sig.payload ?? {},
|
|
2170
2349
|
...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
|
|
2171
|
-
...(sig.
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
coalesced_count: Math.max(1, sig.coalescedCount ?? 1),
|
|
2175
|
-
timestamp_ms: Date.now(),
|
|
2350
|
+
...(sig.deadlineMs !== undefined
|
|
2351
|
+
? { escalate_after_ms: String(Math.max(0, Math.floor(sig.deadlineMs - nowMs))) }
|
|
2352
|
+
: {}),
|
|
2176
2353
|
},
|
|
2177
2354
|
};
|
|
2178
2355
|
}
|