@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
|
@@ -2,6 +2,7 @@ import { normalizeProviderStopReason } from "../providers/stop-reason.js";
|
|
|
2
2
|
import { sha256Hex } from "./sha256.js";
|
|
3
3
|
import { JournalCasConflictError, MAX_CHAIN_POSITION as JOURNAL_MAX_CHAIN_POSITION, } from "./kernel-journal.js";
|
|
4
4
|
import { encodeCanonicalContentParts, kernelMessageToSdk, renderedContextToSdk, } from "./kernel-step.js";
|
|
5
|
+
import { toolArgumentsFromWire } from "./tool-arguments.js";
|
|
5
6
|
export const MAX_CHAIN_POSITION = JOURNAL_MAX_CHAIN_POSITION;
|
|
6
7
|
const utf8ByteLength = (value) => new TextEncoder().encode(value).byteLength;
|
|
7
8
|
function asObject(value) {
|
|
@@ -46,7 +47,7 @@ function canonicalDoneFromTerminal(terminal) {
|
|
|
46
47
|
content: String(finalMessage.content ?? ""),
|
|
47
48
|
toolCalls: (Array.isArray(finalMessage.tool_calls) ? finalMessage.tool_calls : []).map(value => {
|
|
48
49
|
const call = asObject(value);
|
|
49
|
-
return { id: String(call.call_id ?? ""), name: String(call.name ?? ""), arguments:
|
|
50
|
+
return { id: String(call.call_id ?? ""), name: String(call.name ?? ""), arguments: toolArgumentsFromWire(call.arguments) };
|
|
50
51
|
}),
|
|
51
52
|
} } : {}),
|
|
52
53
|
...(Object.keys(pace).length > 0 ? { paceDecision: {
|
|
@@ -95,7 +96,7 @@ export function canonicalActionFromProjectionJson(raw) {
|
|
|
95
96
|
return {
|
|
96
97
|
id: String(call.call_id ?? ""),
|
|
97
98
|
name: String(call.name ?? ""),
|
|
98
|
-
arguments:
|
|
99
|
+
arguments: toolArgumentsFromWire(call.arguments),
|
|
99
100
|
};
|
|
100
101
|
}),
|
|
101
102
|
};
|
|
@@ -126,7 +127,7 @@ export function canonicalActionFromProjectionJson(raw) {
|
|
|
126
127
|
return {
|
|
127
128
|
callId: String(request.call_id ?? ""),
|
|
128
129
|
tool: String(request.tool_name ?? ""),
|
|
129
|
-
arguments:
|
|
130
|
+
arguments: toolArgumentsFromWire(request.arguments),
|
|
130
131
|
reason: String(request.reason ?? ""),
|
|
131
132
|
};
|
|
132
133
|
}),
|
|
@@ -224,6 +225,18 @@ export class CanonicalKernelRejectedError extends Error {
|
|
|
224
225
|
this.fault = fault;
|
|
225
226
|
}
|
|
226
227
|
}
|
|
228
|
+
/** Fault codes that name a malformed input — the input was wrong, not the run. */
|
|
229
|
+
const INPUT_SHAPE_FAULTS = new Set(["malformed_envelope", "invalid_config"]);
|
|
230
|
+
/**
|
|
231
|
+
* Whether a run-loop failure was the kernel refusing a malformed input (`invalid_arg`) rather than
|
|
232
|
+
* any other error. Decided by the fault the kernel named, or a binding's argument-conversion
|
|
233
|
+
* status — never by searching the message text.
|
|
234
|
+
*/
|
|
235
|
+
export function isInvalidInputError(error) {
|
|
236
|
+
if (error instanceof CanonicalKernelRejectedError)
|
|
237
|
+
return INPUT_SHAPE_FAULTS.has(String(error.fault.code));
|
|
238
|
+
return error?.code === "InvalidArg";
|
|
239
|
+
}
|
|
227
240
|
/**
|
|
228
241
|
* A record is already authoritative once the journal append returns. A later native commit failure
|
|
229
242
|
* is therefore a rebuild boundary, never an abort boundary.
|
|
@@ -289,28 +302,41 @@ export class CanonicalKernelHost {
|
|
|
289
302
|
if (!operationId)
|
|
290
303
|
throw new TypeError("canonical kernel operationId must not be empty");
|
|
291
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* The journal holds one outbound envelope slot per operation, so two transitions of the same
|
|
307
|
+
* operation must never overlap: the second would overwrite the first's staged bytes before its
|
|
308
|
+
* append was acknowledged. Every entry point that stages or drains goes through this queue.
|
|
309
|
+
*/
|
|
310
|
+
exclusiveTail = Promise.resolve();
|
|
311
|
+
exclusive(work) {
|
|
312
|
+
const run = this.exclusiveTail.then(work, work);
|
|
313
|
+
this.exclusiveTail = run.catch(() => undefined);
|
|
314
|
+
return run;
|
|
315
|
+
}
|
|
292
316
|
async transition(input, options = {}) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return transition;
|
|
304
|
-
}
|
|
305
|
-
catch (error) {
|
|
306
|
-
// Append-acked records own the input; rejected inputs will not retry this envelope.
|
|
307
|
-
// Append-before failures leave the staged bytes so wake can drain byte-identical retries.
|
|
308
|
-
if (error instanceof CanonicalKernelRebuildRequiredError
|
|
309
|
-
|| error instanceof CanonicalKernelRejectedError) {
|
|
317
|
+
return this.exclusive(async () => {
|
|
318
|
+
const inputJson = JSON.stringify({
|
|
319
|
+
operation_id: this.operationId,
|
|
320
|
+
input_id: options.inputId ?? `wasm-input-${crypto.randomUUID()}`,
|
|
321
|
+
observed_at_ms: options.observedAtMs ?? String(Date.now()),
|
|
322
|
+
input,
|
|
323
|
+
});
|
|
324
|
+
await this.journal.stageOutboundEnvelope(this.operationId, inputJson);
|
|
325
|
+
try {
|
|
326
|
+
const transition = await this.transitionEnvelope(inputJson, 1, 1);
|
|
310
327
|
await this.journal.clearOutboundEnvelope(this.operationId);
|
|
328
|
+
return transition;
|
|
311
329
|
}
|
|
312
|
-
|
|
313
|
-
|
|
330
|
+
catch (error) {
|
|
331
|
+
// Append-acked records own the input; rejected inputs will not retry this envelope.
|
|
332
|
+
// Append-before failures leave the staged bytes so wake can drain byte-identical retries.
|
|
333
|
+
if (error instanceof CanonicalKernelRebuildRequiredError
|
|
334
|
+
|| error instanceof CanonicalKernelRejectedError) {
|
|
335
|
+
await this.journal.clearOutboundEnvelope(this.operationId);
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
314
340
|
}
|
|
315
341
|
/** Restore the latest installed checkpoint and its authoritative record tail in place. */
|
|
316
342
|
async restore() {
|
|
@@ -323,21 +349,23 @@ export class CanonicalKernelHost {
|
|
|
323
349
|
* No-op when nothing is staged. Clears the stage after commit/replay/reject.
|
|
324
350
|
*/
|
|
325
351
|
async drainOutboundEnvelope() {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return transition;
|
|
333
|
-
}
|
|
334
|
-
catch (error) {
|
|
335
|
-
if (error instanceof CanonicalKernelRebuildRequiredError
|
|
336
|
-
|| error instanceof CanonicalKernelRejectedError) {
|
|
352
|
+
return this.exclusive(async () => {
|
|
353
|
+
const pending = await this.journal.readOutboundEnvelope(this.operationId);
|
|
354
|
+
if (!pending)
|
|
355
|
+
return undefined;
|
|
356
|
+
try {
|
|
357
|
+
const transition = await this.transitionEnvelope(pending, 1, 1);
|
|
337
358
|
await this.journal.clearOutboundEnvelope(this.operationId);
|
|
359
|
+
return transition;
|
|
338
360
|
}
|
|
339
|
-
|
|
340
|
-
|
|
361
|
+
catch (error) {
|
|
362
|
+
if (error instanceof CanonicalKernelRebuildRequiredError
|
|
363
|
+
|| error instanceof CanonicalKernelRejectedError) {
|
|
364
|
+
await this.journal.clearOutboundEnvelope(this.operationId);
|
|
365
|
+
}
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
341
369
|
}
|
|
342
370
|
/** Execute the full §12.3 install/ack/reclaim boundary. */
|
|
343
371
|
async checkpoint() {
|
|
@@ -446,7 +474,35 @@ export class CanonicalKernelHost {
|
|
|
446
474
|
return transition;
|
|
447
475
|
}
|
|
448
476
|
}
|
|
449
|
-
|
|
477
|
+
/** Host-reported per-task failures: `[{ agent_id, error, kind? }]`. */
|
|
478
|
+
function launchFailures(raw) {
|
|
479
|
+
const failures = new Map();
|
|
480
|
+
for (const value of Array.isArray(raw) ? raw : []) {
|
|
481
|
+
const failure = asObject(value);
|
|
482
|
+
const taskId = String(failure.agent_id ?? failure.task_id ?? "");
|
|
483
|
+
if (!taskId)
|
|
484
|
+
continue;
|
|
485
|
+
failures.set(taskId, {
|
|
486
|
+
kind: String(failure.kind ?? "unknown"),
|
|
487
|
+
message: String(failure.error ?? failure.message ?? ""),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return failures;
|
|
491
|
+
}
|
|
492
|
+
/** Child usage as observed: the split only when the host measured it, never an invented zero. */
|
|
493
|
+
function childUsage(result) {
|
|
494
|
+
const usage = {};
|
|
495
|
+
if (result.input_tokens !== undefined)
|
|
496
|
+
usage.input_tokens = String(result.input_tokens);
|
|
497
|
+
if (result.output_tokens !== undefined)
|
|
498
|
+
usage.output_tokens = String(result.output_tokens);
|
|
499
|
+
if (result.total_tokens_used !== undefined)
|
|
500
|
+
usage.total_tokens = String(result.total_tokens_used);
|
|
501
|
+
if (result.turns_used !== undefined)
|
|
502
|
+
usage.turns = Number(result.turns_used);
|
|
503
|
+
return usage;
|
|
504
|
+
}
|
|
505
|
+
function canonicalProviderMessage(raw, appendBase = 0) {
|
|
450
506
|
const content = raw.content;
|
|
451
507
|
return {
|
|
452
508
|
role: String(raw.role ?? "assistant"),
|
|
@@ -458,7 +514,9 @@ function canonicalProviderMessage(raw) {
|
|
|
458
514
|
return {
|
|
459
515
|
call_id: String(call.call_id ?? call.id ?? ""),
|
|
460
516
|
name: String(call.name ?? ""),
|
|
461
|
-
arguments:
|
|
517
|
+
arguments: typeof call.arguments === "string"
|
|
518
|
+
? call.arguments
|
|
519
|
+
: canonicalProviderToolArguments(String(call.name ?? ""), asObject(call.arguments), appendBase),
|
|
462
520
|
};
|
|
463
521
|
}),
|
|
464
522
|
}
|
|
@@ -466,7 +524,18 @@ function canonicalProviderMessage(raw) {
|
|
|
466
524
|
...(raw.token_count !== undefined ? { tokens: Number(raw.token_count) } : {}),
|
|
467
525
|
};
|
|
468
526
|
}
|
|
469
|
-
function canonicalProviderToolArguments(name, argumentsValue) {
|
|
527
|
+
function canonicalProviderToolArguments(name, argumentsValue, appendBase) {
|
|
528
|
+
try {
|
|
529
|
+
return canonicalWorkflowToolArguments(name, argumentsValue, appendBase);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// The model wrote a workflow the canonical DAG cannot express. That is the model's bad
|
|
533
|
+
// arguments, not a host protocol violation: forward them verbatim so the kernel's syscall
|
|
534
|
+
// decoder answers with a model-visible rejection instead of the host failing the whole run.
|
|
535
|
+
return argumentsValue;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function canonicalWorkflowToolArguments(name, argumentsValue, appendBase) {
|
|
470
539
|
if (name === "start_workflow") {
|
|
471
540
|
const wrapped = asObject(argumentsValue.spec);
|
|
472
541
|
return canonicalWorkflowSpec(Object.keys(wrapped).length > 0 ? wrapped : argumentsValue);
|
|
@@ -474,22 +543,53 @@ function canonicalProviderToolArguments(name, argumentsValue) {
|
|
|
474
543
|
if (name === "submit_workflow_nodes") {
|
|
475
544
|
const spec = canonicalWorkflowSpec({
|
|
476
545
|
nodes: Array.isArray(argumentsValue.nodes) ? argumentsValue.nodes : [],
|
|
477
|
-
});
|
|
546
|
+
}, false, appendBase);
|
|
478
547
|
return { nodes: spec.nodes };
|
|
479
548
|
}
|
|
480
549
|
return argumentsValue;
|
|
481
550
|
}
|
|
551
|
+
function anonymousNodeId(base, index) {
|
|
552
|
+
return typeof base === "number" ? `wf-node${base + index}` : `${base}-${index}`;
|
|
553
|
+
}
|
|
482
554
|
function canonicalInitialMessage(raw) {
|
|
555
|
+
const tokens = raw.token_count !== undefined ? { tokens: Number(raw.token_count) } : {};
|
|
483
556
|
if (Array.isArray(raw.content)) {
|
|
557
|
+
// A lone tool result is a structural pairing, not opaque parts: send it as `tool_call_id` so
|
|
558
|
+
// the kernel keeps it bound to the assistant call that produced it.
|
|
559
|
+
const parts = raw.content.map(asObject);
|
|
560
|
+
if (parts.length === 1 && parts[0].type === "tool_result" && typeof parts[0].call_id === "string") {
|
|
561
|
+
return {
|
|
562
|
+
role: String(raw.role ?? "tool"),
|
|
563
|
+
content: String(parts[0].output ?? ""),
|
|
564
|
+
tool_call_id: parts[0].call_id,
|
|
565
|
+
...(parts[0].is_error === true ? { is_error: true } : {}),
|
|
566
|
+
...tokens,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
484
569
|
return {
|
|
485
570
|
role: String(raw.role ?? "user"),
|
|
486
571
|
content: encodeCanonicalContentParts(raw.content),
|
|
487
|
-
...
|
|
572
|
+
...tokens,
|
|
488
573
|
};
|
|
489
574
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
575
|
+
// History tool calls are replayed facts, not new syscalls: keep them verbatim.
|
|
576
|
+
const content = raw.content;
|
|
577
|
+
const toolCalls = Array.isArray(raw.tool_calls) ? raw.tool_calls.map(asObject) : [];
|
|
578
|
+
return {
|
|
579
|
+
role: String(raw.role ?? "assistant"),
|
|
580
|
+
content: typeof content === "string" ? content : JSON.stringify(content ?? ""),
|
|
581
|
+
...(toolCalls.length > 0
|
|
582
|
+
? {
|
|
583
|
+
tool_calls: toolCalls.map(call => ({
|
|
584
|
+
call_id: String(call.call_id ?? call.id ?? ""),
|
|
585
|
+
name: String(call.name ?? ""),
|
|
586
|
+
arguments: asObject(call.arguments),
|
|
587
|
+
})),
|
|
588
|
+
}
|
|
589
|
+
: {}),
|
|
590
|
+
...(typeof raw.tool_call_id === "string" ? { tool_call_id: raw.tool_call_id } : {}),
|
|
591
|
+
...tokens,
|
|
592
|
+
};
|
|
493
593
|
}
|
|
494
594
|
function logicalRunSpec(raw, goal) {
|
|
495
595
|
if (!raw)
|
|
@@ -538,9 +638,17 @@ function logicalRunSpec(raw, goal) {
|
|
|
538
638
|
...(raw.metadata && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}),
|
|
539
639
|
};
|
|
540
640
|
}
|
|
541
|
-
function canonicalWorkflowSpec(raw, allowHostSchedulingFactors = false) {
|
|
641
|
+
function canonicalWorkflowSpec(raw, allowHostSchedulingFactors = false, nodeIdBase = 0) {
|
|
542
642
|
const nodes = Array.isArray(raw.nodes) ? raw.nodes.map(asObject) : [];
|
|
543
|
-
|
|
643
|
+
// A caller-declared id is the node's wire identity. Only an anonymous node gets a positional id,
|
|
644
|
+
// offset by the DAG's current size so ids stay unique across appended batches — the kernel
|
|
645
|
+
// refuses an append whose id is already declared.
|
|
646
|
+
const nodeIds = nodes.map((node, index) => {
|
|
647
|
+
const declared = node.node_id ?? node.nodeId;
|
|
648
|
+
return typeof declared === "string" && declared.length > 0
|
|
649
|
+
? declared
|
|
650
|
+
: anonymousNodeId(nodeIdBase, index);
|
|
651
|
+
});
|
|
544
652
|
return {
|
|
545
653
|
nodes: nodes.map((node, index) => {
|
|
546
654
|
const unsupported = [];
|
|
@@ -654,9 +762,17 @@ export class CanonicalRunnerRuntime {
|
|
|
654
762
|
started = false;
|
|
655
763
|
turns = 0;
|
|
656
764
|
lastAction = null;
|
|
765
|
+
/** Live policy revision (§13.2): 0 at genesis, then each `live_policy_changed`. Unknown after a
|
|
766
|
+
* restore, when a patch must name the revision it expects explicitly. */
|
|
767
|
+
policyRevision = 0;
|
|
768
|
+
/** Kernel-minted attempt per launched task, learned from each launch acknowledgement. */
|
|
769
|
+
taskAttempts = new Map();
|
|
657
770
|
newMessages = [];
|
|
658
771
|
hostObservations = [];
|
|
659
772
|
spawnedTasks = 0;
|
|
773
|
+
/** Nodes in the active workflow DAG, mirrored from committed facts so anonymous appended nodes get
|
|
774
|
+
* unique positional ids. `undefined` after a restore until the next committed submission. */
|
|
775
|
+
workflowNodeCount = 0;
|
|
660
776
|
memoryBindingId;
|
|
661
777
|
payloadInlineThreshold = 50 * 1024;
|
|
662
778
|
payloadPreviewBytes = 2 * 1024;
|
|
@@ -714,6 +830,10 @@ export class CanonicalRunnerRuntime {
|
|
|
714
830
|
isTerminal() {
|
|
715
831
|
return ["completed", "cancelled", "failed"].includes(this.host.kernel.lifecycle());
|
|
716
832
|
}
|
|
833
|
+
/** Whether the operation has a root — the precondition for any live host control command. */
|
|
834
|
+
hasStarted() {
|
|
835
|
+
return this.started;
|
|
836
|
+
}
|
|
717
837
|
recoveryContentBytes() {
|
|
718
838
|
return Math.max(1_024, this.options.maxContextTokens * 4);
|
|
719
839
|
}
|
|
@@ -735,11 +855,16 @@ export class CanonicalRunnerRuntime {
|
|
|
735
855
|
}
|
|
736
856
|
async restore() {
|
|
737
857
|
await this.host.restore();
|
|
858
|
+
this.workflowNodeCount = undefined;
|
|
859
|
+
this.policyRevision = undefined;
|
|
738
860
|
this.configured = this.host.kernel.lifecycle() !== "created";
|
|
739
861
|
this.started = !["created", "configured"].includes(this.host.kernel.lifecycle());
|
|
740
862
|
// A crash between stage and append-ack leaves a byte-identical envelope; drain it before
|
|
741
863
|
// the host effect loop so retries never remint observed_at_ms.
|
|
742
|
-
|
|
864
|
+
// The crash-window envelope commits here, so its observations are this run's facts too.
|
|
865
|
+
const drained = await this.host.drainOutboundEnvelope();
|
|
866
|
+
if (drained)
|
|
867
|
+
this.absorbTransition(drained);
|
|
743
868
|
this.lastAction = this.currentAction();
|
|
744
869
|
}
|
|
745
870
|
resumeAction() {
|
|
@@ -768,25 +893,35 @@ export class CanonicalRunnerRuntime {
|
|
|
768
893
|
}
|
|
769
894
|
async startWorkflow(specValue) {
|
|
770
895
|
await this.ensureConfigured();
|
|
896
|
+
const spec = canonicalWorkflowSpec(specValue, true);
|
|
771
897
|
const action = await this.commit({
|
|
772
898
|
kind: "start_operation",
|
|
773
899
|
entry: {
|
|
774
900
|
kind: "workflow",
|
|
775
|
-
spec
|
|
901
|
+
spec,
|
|
776
902
|
},
|
|
777
903
|
initial_context: this.initialContext,
|
|
778
904
|
});
|
|
779
905
|
this.started = true;
|
|
906
|
+
this.workflowNodeCount = spec.nodes.length;
|
|
780
907
|
return action;
|
|
781
908
|
}
|
|
909
|
+
appendBase() {
|
|
910
|
+
return this.workflowNodeCount ?? `wf-append-${crypto.randomUUID().slice(0, 8)}`;
|
|
911
|
+
}
|
|
782
912
|
async applyHostEvent(event) {
|
|
783
913
|
if (!this.started && this.applyBootstrapEvent(event))
|
|
784
914
|
return null;
|
|
785
915
|
let input;
|
|
916
|
+
// What this input adds to the host's view (new messages, the turn count) is staged and only
|
|
917
|
+
// applied once the kernel commits it: a refused input must leave the host exactly where the
|
|
918
|
+
// kernel still is.
|
|
919
|
+
const staged = [];
|
|
920
|
+
let stagedTurns = 0;
|
|
786
921
|
switch (event.kind) {
|
|
787
922
|
case "provider_result": {
|
|
788
|
-
const message = canonicalProviderMessage(asObject(event.message));
|
|
789
|
-
|
|
923
|
+
const message = canonicalProviderMessage(asObject(event.message), this.appendBase());
|
|
924
|
+
staged.push({
|
|
790
925
|
role: message.role,
|
|
791
926
|
content: String(message.content ?? ""),
|
|
792
927
|
toolCalls: (Array.isArray(message.tool_calls) ? message.tool_calls : []).map(raw => {
|
|
@@ -794,11 +929,11 @@ export class CanonicalRunnerRuntime {
|
|
|
794
929
|
return {
|
|
795
930
|
id: String(call.call_id ?? ""),
|
|
796
931
|
name: String(call.name ?? ""),
|
|
797
|
-
arguments:
|
|
932
|
+
arguments: toolArgumentsFromWire(call.arguments),
|
|
798
933
|
};
|
|
799
934
|
}),
|
|
800
935
|
});
|
|
801
|
-
|
|
936
|
+
stagedTurns += 1;
|
|
802
937
|
input = {
|
|
803
938
|
kind: "resolve_effect",
|
|
804
939
|
effect_id: String(event.effect_id ?? ""),
|
|
@@ -881,7 +1016,7 @@ export class CanonicalRunnerRuntime {
|
|
|
881
1016
|
},
|
|
882
1017
|
});
|
|
883
1018
|
}
|
|
884
|
-
|
|
1019
|
+
staged.push({ role: "tool", content: output, toolCalls: [] });
|
|
885
1020
|
}
|
|
886
1021
|
input = this.succeededEffect(event, {
|
|
887
1022
|
kind: "tools",
|
|
@@ -898,26 +1033,51 @@ export class CanonicalRunnerRuntime {
|
|
|
898
1033
|
});
|
|
899
1034
|
break;
|
|
900
1035
|
case "workflow_spawn_result": {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1036
|
+
// The launch set comes from the kernel's own pending effect (by id, never "the last
|
|
1037
|
+
// action"), and each task's outcome from what the host reports it actually started.
|
|
1038
|
+
const tasks = this.pendingEffectPayload(String(event.effect_id ?? ""), "spawn_tasks", "tasks");
|
|
1039
|
+
const started = Array.isArray(event.started_agent_ids)
|
|
1040
|
+
? new Set(event.started_agent_ids.map(String))
|
|
1041
|
+
: undefined;
|
|
1042
|
+
const failures = launchFailures(event.failures);
|
|
1043
|
+
const attempts = tasks.map(task => {
|
|
1044
|
+
const taskId = String(task.task_id ?? "");
|
|
1045
|
+
const attemptId = String(task.attempt_id ?? "");
|
|
1046
|
+
const failure = failures.get(taskId) ??
|
|
1047
|
+
(started && !started.has(taskId)
|
|
1048
|
+
? { kind: "protocol_error", message: "the host did not report launching this task" }
|
|
1049
|
+
: undefined);
|
|
1050
|
+
if (!failure)
|
|
1051
|
+
this.taskAttempts.set(taskId, attemptId);
|
|
1052
|
+
return {
|
|
1053
|
+
task_id: taskId,
|
|
1054
|
+
attempt_id: attemptId,
|
|
1055
|
+
outcome: failure ? { status: "failed", failure } : { status: "started" },
|
|
1056
|
+
};
|
|
910
1057
|
});
|
|
1058
|
+
this.spawnedTasks += attempts.filter(attempt => attempt.outcome.status === "started").length;
|
|
1059
|
+
input = this.succeededEffect(event, { kind: "tasks_spawned", attempts });
|
|
911
1060
|
break;
|
|
912
1061
|
}
|
|
913
1062
|
case "preempt_result": {
|
|
914
|
-
const
|
|
1063
|
+
const refs = this.pendingEffectPayload(String(event.effect_id ?? ""), "preempt_tasks", "attempts");
|
|
1064
|
+
const finished = new Set(Array.isArray(event.already_finished_agent_ids)
|
|
1065
|
+
? event.already_finished_agent_ids.map(String)
|
|
1066
|
+
: []);
|
|
1067
|
+
const failures = launchFailures(event.failures);
|
|
915
1068
|
input = this.succeededEffect(event, {
|
|
916
1069
|
kind: "tasks_preempted",
|
|
917
|
-
attempts:
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1070
|
+
attempts: refs.map(ref => {
|
|
1071
|
+
const taskId = String(ref.task_id ?? "");
|
|
1072
|
+
const failure = failures.get(taskId);
|
|
1073
|
+
return {
|
|
1074
|
+
task_id: taskId,
|
|
1075
|
+
attempt_id: String(ref.attempt_id ?? ""),
|
|
1076
|
+
outcome: failure
|
|
1077
|
+
? { status: "failed", failure }
|
|
1078
|
+
: { status: finished.has(taskId) ? "already_finished" : "preempted" },
|
|
1079
|
+
};
|
|
1080
|
+
}),
|
|
921
1081
|
});
|
|
922
1082
|
break;
|
|
923
1083
|
}
|
|
@@ -928,40 +1088,36 @@ export class CanonicalRunnerRuntime {
|
|
|
928
1088
|
? raw.submitted_nodes.map(asObject)
|
|
929
1089
|
: [];
|
|
930
1090
|
const taskId = String(raw.agent_id ?? "");
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1091
|
+
// Child identity is the kernel's: the attempt it minted for this launch, carried by the
|
|
1092
|
+
// host from the spawned node or remembered from the launch acknowledgement.
|
|
1093
|
+
const attemptId = typeof event.attempt_id === "string" && event.attempt_id
|
|
1094
|
+
? event.attempt_id
|
|
1095
|
+
: this.taskAttempts.get(taskId);
|
|
1096
|
+
if (!attemptId) {
|
|
1097
|
+
throw new Error(`sub_agent_completed for ${taskId} names no kernel-minted attempt id`);
|
|
1098
|
+
}
|
|
1099
|
+
const termination = String(result.termination ?? "error");
|
|
1100
|
+
const status = termination === "completed"
|
|
1101
|
+
? "completed"
|
|
1102
|
+
: termination === "user_abort" ? "cancelled" : "failed";
|
|
1103
|
+
const output = asObject(result.final_message).content;
|
|
940
1104
|
input = {
|
|
941
1105
|
kind: "deliver_external_event",
|
|
942
1106
|
event: {
|
|
943
1107
|
kind: "child_completed",
|
|
944
1108
|
task_id: taskId,
|
|
945
|
-
attempt_id:
|
|
1109
|
+
attempt_id: attemptId,
|
|
946
1110
|
result: {
|
|
947
|
-
status
|
|
948
|
-
...(
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
...(!["completed", "max_turns", "token_budget"].includes(String(result.termination))
|
|
952
|
-
? { error: String(result.termination ?? "failed") }
|
|
953
|
-
: {}),
|
|
954
|
-
usage: {
|
|
955
|
-
input_tokens: "0",
|
|
956
|
-
output_tokens: String(result.total_tokens_used ?? 0),
|
|
957
|
-
turns: Number(result.turns_used ?? 0),
|
|
958
|
-
},
|
|
1111
|
+
status,
|
|
1112
|
+
...(output ? { output: String(output) } : {}),
|
|
1113
|
+
...(status !== "completed" ? { error: termination } : {}),
|
|
1114
|
+
usage: childUsage(result),
|
|
959
1115
|
},
|
|
960
1116
|
...(submittedNodes.length > 0
|
|
961
1117
|
? {
|
|
962
1118
|
parent_requests: [{
|
|
963
1119
|
kind: "append_workflow_nodes",
|
|
964
|
-
nodes: asObject(canonicalWorkflowSpec({ nodes: submittedNodes })).nodes,
|
|
1120
|
+
nodes: asObject(canonicalWorkflowSpec({ nodes: submittedNodes }, false, this.appendBase())).nodes,
|
|
965
1121
|
}],
|
|
966
1122
|
}
|
|
967
1123
|
: {}),
|
|
@@ -970,31 +1126,43 @@ export class CanonicalRunnerRuntime {
|
|
|
970
1126
|
break;
|
|
971
1127
|
}
|
|
972
1128
|
case "memory_persist_result":
|
|
1129
|
+
// A receipt names what the store actually holds. Without the write's record ref and
|
|
1130
|
+
// content digest there is nothing to attest: fail the effect rather than resolve it with
|
|
1131
|
+
// an identity minted here (non-retryable — re-publishing cannot conjure a receipt).
|
|
973
1132
|
input = event.error
|
|
974
1133
|
? this.failedEffect(event, "storage_unavailable", String(event.error), true)
|
|
975
|
-
:
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1134
|
+
: !event.record_ref || !event.digest
|
|
1135
|
+
? this.failedEffect(event, "storage_unavailable", "memory store returned no record_ref/digest receipt", false)
|
|
1136
|
+
: this.succeededEffect(event, {
|
|
1137
|
+
kind: "memory_persisted",
|
|
1138
|
+
receipt: {
|
|
1139
|
+
binding_id: this.memoryBindingId,
|
|
1140
|
+
record_ref: String(event.record_ref),
|
|
1141
|
+
digest: String(event.digest),
|
|
1142
|
+
},
|
|
1143
|
+
});
|
|
983
1144
|
break;
|
|
984
1145
|
case "memory_query_result":
|
|
985
1146
|
input = event.error
|
|
986
1147
|
? this.failedEffect(event, "storage_unavailable", String(event.error), true)
|
|
987
1148
|
: this.succeededEffect(event, {
|
|
988
1149
|
kind: "memory_queried",
|
|
989
|
-
|
|
1150
|
+
// A recall without a store identity cannot be attributed or re-read; drop it
|
|
1151
|
+
// instead of minting one.
|
|
1152
|
+
recalls: (Array.isArray(event.hits) ? event.hits : []).filter(value => Boolean(asObject(asObject(value).record).record_id)).map(value => {
|
|
990
1153
|
const hit = asObject(value);
|
|
991
1154
|
const record = asObject(hit.record);
|
|
992
1155
|
return {
|
|
993
|
-
record_ref: String(record.record_id
|
|
1156
|
+
record_ref: String(record.record_id),
|
|
994
1157
|
name: String(record.name ?? ""),
|
|
995
1158
|
kind: String(record.kind ?? "reference"),
|
|
996
1159
|
content: String(record.content ?? ""),
|
|
997
1160
|
...(typeof hit.score === "number" ? { score: hit.score } : {}),
|
|
1161
|
+
// M3: the store reports the count it holds; the kernel derives the next one.
|
|
1162
|
+
...(Number(record.recall_count) > 0
|
|
1163
|
+
? { recall_count: String(Math.trunc(Number(record.recall_count))) }
|
|
1164
|
+
: {}),
|
|
1165
|
+
...(record.pinned === true ? { pinned: true } : {}),
|
|
998
1166
|
};
|
|
999
1167
|
}),
|
|
1000
1168
|
});
|
|
@@ -1065,6 +1233,54 @@ export class CanonicalRunnerRuntime {
|
|
|
1065
1233
|
},
|
|
1066
1234
|
};
|
|
1067
1235
|
break;
|
|
1236
|
+
case "force_compact":
|
|
1237
|
+
input = { kind: "host_control", command: { kind: "force_compact" } };
|
|
1238
|
+
break;
|
|
1239
|
+
case "admit_memory_write":
|
|
1240
|
+
input = {
|
|
1241
|
+
kind: "host_control",
|
|
1242
|
+
command: {
|
|
1243
|
+
kind: "admit_memory_write",
|
|
1244
|
+
record_id: String(event.record_id ?? ""),
|
|
1245
|
+
name: String(event.name ?? ""),
|
|
1246
|
+
content_bytes: Number(event.content_bytes ?? 0),
|
|
1247
|
+
},
|
|
1248
|
+
};
|
|
1249
|
+
break;
|
|
1250
|
+
case "update_deadline":
|
|
1251
|
+
input = {
|
|
1252
|
+
kind: "host_control",
|
|
1253
|
+
command: {
|
|
1254
|
+
kind: "update_deadline",
|
|
1255
|
+
...(event.deadline_ms !== undefined && event.deadline_ms !== null
|
|
1256
|
+
? { deadline_ms: String(event.deadline_ms) }
|
|
1257
|
+
: {}),
|
|
1258
|
+
},
|
|
1259
|
+
};
|
|
1260
|
+
break;
|
|
1261
|
+
case "apply_policy_patch": {
|
|
1262
|
+
const expected = event.expected_revision ?? this.policyRevision;
|
|
1263
|
+
if (expected === undefined) {
|
|
1264
|
+
throw new Error("the live policy revision is unknown after a restore; pass expectedRevision explicitly");
|
|
1265
|
+
}
|
|
1266
|
+
input = {
|
|
1267
|
+
kind: "host_control",
|
|
1268
|
+
command: { kind: "apply_policy_patch", expected_revision: String(expected), patch: event.patch },
|
|
1269
|
+
};
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
case "skill_activation":
|
|
1273
|
+
input = {
|
|
1274
|
+
kind: "host_control",
|
|
1275
|
+
command: {
|
|
1276
|
+
kind: "apply_skill_activation",
|
|
1277
|
+
activate: [{
|
|
1278
|
+
name: String(event.name ?? ""),
|
|
1279
|
+
...(event.lease_turns !== undefined ? { lease_turns: Number(event.lease_turns) } : {}),
|
|
1280
|
+
}],
|
|
1281
|
+
},
|
|
1282
|
+
};
|
|
1283
|
+
break;
|
|
1068
1284
|
case "skill_deactivated":
|
|
1069
1285
|
input = {
|
|
1070
1286
|
kind: "host_control",
|
|
@@ -1085,7 +1301,19 @@ export class CanonicalRunnerRuntime {
|
|
|
1085
1301
|
default:
|
|
1086
1302
|
throw new Error(`WASM host fact has no canonical ABI input: ${String(event.kind)}`);
|
|
1087
1303
|
}
|
|
1088
|
-
|
|
1304
|
+
const action = await this.commit(input);
|
|
1305
|
+
this.newMessages.push(...staged);
|
|
1306
|
+
this.turns += stagedTurns;
|
|
1307
|
+
// A model-authored `start_workflow` replaces the DAG (§10.2); an admitted one publishes its spawn.
|
|
1308
|
+
if (event.kind === "provider_result" && action?.kind === "spawn_workflow") {
|
|
1309
|
+
const message = input.outcome;
|
|
1310
|
+
const started = (message.result?.outcome?.message?.tool_calls ?? [])
|
|
1311
|
+
.map(asObject)
|
|
1312
|
+
.find(call => call.name === "start_workflow");
|
|
1313
|
+
if (started)
|
|
1314
|
+
this.workflowNodeCount = asObject(started.arguments).nodes?.length ?? 0;
|
|
1315
|
+
}
|
|
1316
|
+
return action;
|
|
1089
1317
|
}
|
|
1090
1318
|
async ensureConfigured() {
|
|
1091
1319
|
if (this.configured)
|
|
@@ -1113,36 +1341,7 @@ export class CanonicalRunnerRuntime {
|
|
|
1113
1341
|
this.lastAction = this.currentAction();
|
|
1114
1342
|
}
|
|
1115
1343
|
if (transition) {
|
|
1116
|
-
|
|
1117
|
-
for (const raw of transition.plannedStep.observations ?? []) {
|
|
1118
|
-
const kind = String(raw.kind ?? "");
|
|
1119
|
-
if (!kind)
|
|
1120
|
-
throw new Error("canonical observation is missing kind");
|
|
1121
|
-
this.hostObservations.push({ ...raw, kind });
|
|
1122
|
-
}
|
|
1123
|
-
// §7.11 · a committed step that publishes effects is a fact worth recording in the
|
|
1124
|
-
// host event log: the journal stores only a digest of the step, so this manifest is
|
|
1125
|
-
// what makes the published effect ids + kinds recoverable post-hoc without replaying.
|
|
1126
|
-
const published = JSON.parse(this.host.kernel.publishedEffectsManifestJson(JSON.stringify(transition.plannedStep)));
|
|
1127
|
-
if (published.length > 0) {
|
|
1128
|
-
this.hostObservations.push({
|
|
1129
|
-
kind: "step_published_effects",
|
|
1130
|
-
effects: published,
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
if (transition.checkpointAdvice) {
|
|
1134
|
-
this.hostObservations.push({
|
|
1135
|
-
kind: "checkpoint_advised",
|
|
1136
|
-
...transition.checkpointAdvice,
|
|
1137
|
-
});
|
|
1138
|
-
}
|
|
1139
|
-
if (transition.checkpointFailure) {
|
|
1140
|
-
this.hostObservations.push({
|
|
1141
|
-
kind: "checkpoint_deferred",
|
|
1142
|
-
reason: transition.checkpointFailure,
|
|
1143
|
-
});
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1344
|
+
this.absorbTransition(transition);
|
|
1146
1345
|
this.lastAction = canonicalActionFromProjectionJson(this.host.kernel.projectPlannedStepJson(JSON.stringify(transition.plannedStep)));
|
|
1147
1346
|
}
|
|
1148
1347
|
if (this.lastAction?.kind !== "unsupported_effect")
|
|
@@ -1150,12 +1349,59 @@ export class CanonicalRunnerRuntime {
|
|
|
1150
1349
|
nextInput = canonicalUnsupportedEffectResolution(this.lastAction.effectId, this.lastAction.effectKind);
|
|
1151
1350
|
}
|
|
1152
1351
|
}
|
|
1352
|
+
/** Record what a committed transition tells the host: its observations and published effects. */
|
|
1353
|
+
absorbTransition(transition) {
|
|
1354
|
+
if (!transition.replayed) {
|
|
1355
|
+
for (const raw of transition.plannedStep.observations ?? []) {
|
|
1356
|
+
const kind = String(raw.kind ?? "");
|
|
1357
|
+
if (!kind)
|
|
1358
|
+
throw new Error("canonical observation is missing kind");
|
|
1359
|
+
if (kind === "workflow_nodes_submitted") {
|
|
1360
|
+
this.workflowNodeCount = Number(raw.base ?? 0) + Number(raw.count ?? 0);
|
|
1361
|
+
}
|
|
1362
|
+
if (kind === "live_policy_changed")
|
|
1363
|
+
this.policyRevision = Number(raw.revision ?? 0);
|
|
1364
|
+
this.hostObservations.push({ ...raw, kind });
|
|
1365
|
+
}
|
|
1366
|
+
// §7.11 · a committed step that publishes effects is a fact worth recording in the
|
|
1367
|
+
// host event log: the journal stores only a digest of the step, so this manifest is
|
|
1368
|
+
// what makes the published effect ids + kinds recoverable post-hoc without replaying.
|
|
1369
|
+
const published = JSON.parse(this.host.kernel.publishedEffectsManifestJson(JSON.stringify(transition.plannedStep)));
|
|
1370
|
+
if (published.length > 0) {
|
|
1371
|
+
this.hostObservations.push({
|
|
1372
|
+
kind: "step_published_effects",
|
|
1373
|
+
effects: published,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
if (transition.checkpointAdvice) {
|
|
1377
|
+
this.hostObservations.push({
|
|
1378
|
+
kind: "checkpoint_advised",
|
|
1379
|
+
...transition.checkpointAdvice,
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
if (transition.checkpointFailure) {
|
|
1383
|
+
this.hostObservations.push({
|
|
1384
|
+
kind: "checkpoint_deferred",
|
|
1385
|
+
reason: transition.checkpointFailure,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1153
1390
|
currentAction() {
|
|
1154
1391
|
return canonicalActionFromProjectionJson(this.host.kernel.projectionJson());
|
|
1155
1392
|
}
|
|
1156
1393
|
pendingEffects() {
|
|
1157
1394
|
return JSON.parse(this.host.kernel.pendingEffectsJson());
|
|
1158
1395
|
}
|
|
1396
|
+
/** One list field of the pending effect `effectId`, which must be of kind `kind`. */
|
|
1397
|
+
pendingEffectPayload(effectId, kind, field) {
|
|
1398
|
+
const pending = this.pendingEffects().find(effect => String(effect.effect_id ?? "") === effectId);
|
|
1399
|
+
const effect = asObject(pending?.effect);
|
|
1400
|
+
if (effect.kind !== kind) {
|
|
1401
|
+
throw new Error(`effect ${effectId} is not a pending ${kind} effect`);
|
|
1402
|
+
}
|
|
1403
|
+
return (Array.isArray(effect[field]) ? effect[field] : []).map(asObject);
|
|
1404
|
+
}
|
|
1159
1405
|
succeededEffect(event, result) {
|
|
1160
1406
|
return {
|
|
1161
1407
|
kind: "resolve_effect",
|
|
@@ -1182,11 +1428,16 @@ export class CanonicalRunnerRuntime {
|
|
|
1182
1428
|
const effect = asObject(pending?.effect);
|
|
1183
1429
|
const payload = asObject(effect.payload);
|
|
1184
1430
|
const content = String(payload.content ?? "");
|
|
1431
|
+
const payloadRef = event.payload_ref ?? event.archive_ref;
|
|
1432
|
+
// The kernel later `load_payload`s this ref; a minted ref names bytes no store holds.
|
|
1433
|
+
if (!payloadRef) {
|
|
1434
|
+
return this.failedEffect(event, "storage_unavailable", "archive store returned no payload_ref; nothing was persisted", false);
|
|
1435
|
+
}
|
|
1185
1436
|
return this.succeededEffect(event, {
|
|
1186
1437
|
kind: "page_out_archived",
|
|
1187
1438
|
receipt: {
|
|
1188
1439
|
handle_id: String(effect.handle_id ?? ""),
|
|
1189
|
-
payload_ref: String(
|
|
1440
|
+
payload_ref: String(payloadRef),
|
|
1190
1441
|
digest: String(payload.digest ?? sha256(content)),
|
|
1191
1442
|
original_size: String(payload.original_size ?? utf8ByteLength(content)),
|
|
1192
1443
|
},
|
|
@@ -1194,24 +1445,23 @@ export class CanonicalRunnerRuntime {
|
|
|
1194
1445
|
}
|
|
1195
1446
|
canonicalSignal(event) {
|
|
1196
1447
|
const signal = asObject(event.signal);
|
|
1197
|
-
const deliveryId = String(event.delivery_id ?? crypto.randomUUID());
|
|
1198
|
-
const payload = deliveryId.startsWith("injected-") && typeof signal.summary === "string"
|
|
1199
|
-
? signal.summary
|
|
1200
|
-
: signal.payload ?? {};
|
|
1201
1448
|
return {
|
|
1202
1449
|
kind: "deliver_signal",
|
|
1203
|
-
delivery_id:
|
|
1450
|
+
delivery_id: String(event.delivery_id ?? crypto.randomUUID()),
|
|
1204
1451
|
attempt: Number(event.attempt ?? 1),
|
|
1205
1452
|
signal: {
|
|
1206
1453
|
signal_id: String(signal.signal_id ?? signal.id ?? crypto.randomUUID()),
|
|
1207
1454
|
...(signal.source ? { source: signal.source } : {}),
|
|
1208
|
-
target: signal.
|
|
1209
|
-
?
|
|
1210
|
-
:
|
|
1455
|
+
target: signal.target && typeof signal.target === "object"
|
|
1456
|
+
? signal.target
|
|
1457
|
+
: signal.recipient
|
|
1458
|
+
? { kind: "task", task_id: String(signal.recipient) }
|
|
1459
|
+
: { kind: "operation" },
|
|
1211
1460
|
...(signal.urgency ? { urgency: signal.urgency } : {}),
|
|
1212
|
-
payload,
|
|
1213
|
-
...(signal.
|
|
1461
|
+
payload: signal.payload ?? {},
|
|
1462
|
+
...(signal.source_timestamp_ms !== undefined ? { source_timestamp_ms: String(signal.source_timestamp_ms) } : {}),
|
|
1214
1463
|
...(signal.dedupe_key ? { dedupe_key: signal.dedupe_key } : {}),
|
|
1464
|
+
...(signal.escalate_after_ms !== undefined ? { escalate_after_ms: String(signal.escalate_after_ms) } : {}),
|
|
1215
1465
|
},
|
|
1216
1466
|
};
|
|
1217
1467
|
}
|