@deepstrike/wasm 0.2.63 → 0.2.65

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 CHANGED
@@ -1,4 +1,4 @@
1
- export { RuntimeRunner, collectText, runAgent, runFanout, InMemorySessionLog, SESSION_EVENT_KINDS, LocalExecutionPlane, DEFAULT_NATIVE_SIGNAL_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, ReplayProvider, extractRecordedMessages, judge, buildEvalMessages, parseVerdict, verdictOutputSchema, } from "./runtime/index.js";
1
+ export { RuntimeRunner, collectText, runAgent, runFanout, InMemorySessionLog, assertCanonicalProviderAttemptRecord, SESSION_EVENT_KINDS, LocalExecutionPlane, DEFAULT_NATIVE_SIGNAL_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, ReplayProvider, extractRecordedMessages, judge, buildEvalMessages, parseVerdict, verdictOutputSchema, } from "./runtime/index.js";
2
2
  export { DriverKernelJournal, InMemoryJournalDriver, InMemoryKernelJournal, JournalCasConflictError, JournalIntegrityError, JournalIoError, } from "./runtime/kernel-journal.js";
3
3
  export type { CheckpointCandidate, InstalledCheckpoint, JournalAppendReceipt, JournalEntry, JournalHead, JournalPruneReceipt, JournalRecordInput, JournalStorageDriver, KernelJournal, } from "./runtime/kernel-journal.js";
4
4
  export { CANONICAL_CONTENT_PARTS_PREFIX, encodeCanonicalContentParts, decodeCanonicalContentParts, } from "./runtime/kernel-step.js";
@@ -42,7 +42,7 @@ export type { RuntimeSignal, SignalSource } from "./signals/index.js";
42
42
  export { PermissionManager, PermissionMode } from "./safety/index.js";
43
43
  export { createProviderRequestPlan, createProviderRequestPlanForProvider, estimateProviderPromptTokens, measurementForPlan, normalizeProviderUsage, priceProviderUsage, recordPromptMeasurement } from "./providers/request-plan.js";
44
44
  export type { CostObservation, NormalizedProviderUsage, PricingSnapshot, ProviderRequestEndpoint, ProviderRequestPlan, ProviderUsage, RecordedPromptMeasurement, ResolvedProviderRoute } from "./providers/request-plan.js";
45
- export type { ProviderAttempt, ProviderAttemptRecord, ProviderAttemptStatus, ModelInvocation, InvocationOutcome, UsageSettlement, UsageAccountingPolicy, } from "./runtime/execution-evidence.js";
45
+ export type { ProviderAttempt, ProviderAttemptRecord, ProviderAttemptStatus, ModelInvocation, InvocationOutcome, ModelUsageSettlement, UsageAccountingPolicy, } from "./runtime/execution-evidence.js";
46
46
  export { FULL_FOOTPRINT_USAGE_ACCOUNTING_POLICY, providerAttemptToRecord, tryNormalizeProviderUsage } from "./runtime/execution-evidence.js";
47
47
  export { decodeCanonicalStopReason, normalizeProviderStopReason } from "./providers/stop-reason.js";
48
48
  export type { CanonicalStopReason } from "./providers/stop-reason.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { RuntimeRunner, collectText, runAgent, runFanout, InMemorySessionLog, SESSION_EVENT_KINDS, LocalExecutionPlane, DEFAULT_NATIVE_SIGNAL_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, ReplayProvider, extractRecordedMessages, judge, buildEvalMessages, parseVerdict, verdictOutputSchema, } from "./runtime/index.js";
1
+ export { RuntimeRunner, collectText, runAgent, runFanout, InMemorySessionLog, assertCanonicalProviderAttemptRecord, SESSION_EVENT_KINDS, LocalExecutionPlane, DEFAULT_NATIVE_SIGNAL_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, ReplayProvider, extractRecordedMessages, judge, buildEvalMessages, parseVerdict, verdictOutputSchema, } from "./runtime/index.js";
2
2
  // ── Durable transaction capability (Canonical Kernel ABI §9.1) ──────────────
3
3
  export { DriverKernelJournal, InMemoryJournalDriver, InMemoryKernelJournal, JournalCasConflictError, JournalIntegrityError, JournalIoError, } from "./runtime/kernel-journal.js";
4
4
  // ── content-parts-v1 registered encoding (F14/B5; byte-pinned by sdk-conformance) ──
@@ -1,4 +1,4 @@
1
- import type { RenderedContext, ToolSchema } from "../types.js";
1
+ import type { ProviderProtocol, RenderedContext, ToolSchema } from "../types.js";
2
2
  export interface ProviderRequestEndpoint {
3
3
  id: string;
4
4
  protocol: string;
@@ -40,7 +40,7 @@ export interface ProviderUsage {
40
40
  export interface ResolvedProviderRoute {
41
41
  routeId: string;
42
42
  provider: string;
43
- protocol: string;
43
+ protocol: ProviderProtocol;
44
44
  model: string;
45
45
  endpoint: ProviderRequestEndpoint;
46
46
  adapterVersion: string;
@@ -95,3 +95,26 @@ export declare function recordPromptMeasurement(plan: Pick<ProviderRequestPlan,
95
95
  export declare function measurementForPlan(plan: Pick<ProviderRequestPlan, "fingerprint">, record: RecordedPromptMeasurement | undefined): RecordedPromptMeasurement | undefined;
96
96
  export declare function normalizeProviderUsage(usage: ProviderUsage): NormalizedProviderUsage;
97
97
  export declare function priceProviderUsage(usage: NormalizedProviderUsage, snapshot: PricingSnapshot, observedAt?: string | Date): CostObservation;
98
+ /**
99
+ * P4-S1: assemble the run's route once at runner construction (P4 §0.2 — the provider is
100
+ * fixed for the run today; a future failover resolver re-evaluates per attempt with the same
101
+ * shape). Never throws: evidence assembly degrades to "unknown" fields rather than breaking a
102
+ * run. The wasm bundle cannot read its own package manifest at runtime (browser targets), so
103
+ * adapterVersion is honestly "unknown" — the field is a report, not a gate.
104
+ */
105
+ export declare function resolveProviderRoute(provider: {
106
+ descriptor?(): {
107
+ provider: string;
108
+ protocol: string;
109
+ model: string;
110
+ };
111
+ requestPlanIdentity?(): {
112
+ providerId?: string;
113
+ modelId?: string;
114
+ endpoint?: {
115
+ id?: string;
116
+ protocol?: string;
117
+ baseURL?: string;
118
+ };
119
+ };
120
+ }): ResolvedProviderRoute;
@@ -106,6 +106,55 @@ function sanitizeEndpoint(endpoint) {
106
106
  return { ...clone(endpoint), baseURL: "" };
107
107
  }
108
108
  }
109
+ const KNOWN_GENERATION_PROTOCOLS = new Set([
110
+ "anthropic-messages",
111
+ "openai-chat",
112
+ "openai-responses",
113
+ "gemini",
114
+ "ollama-chat",
115
+ ]);
116
+ /**
117
+ * P4-S1: assemble the run's route once at runner construction (P4 §0.2 — the provider is
118
+ * fixed for the run today; a future failover resolver re-evaluates per attempt with the same
119
+ * shape). Never throws: evidence assembly degrades to "unknown" fields rather than breaking a
120
+ * run. The wasm bundle cannot read its own package manifest at runtime (browser targets), so
121
+ * adapterVersion is honestly "unknown" — the field is a report, not a gate.
122
+ */
123
+ export function resolveProviderRoute(provider) {
124
+ try {
125
+ const descriptor = provider.descriptor?.() ?? { provider: "unknown", protocol: "unknown", model: "unknown" };
126
+ const identity = provider.requestPlanIdentity?.();
127
+ const protocolRaw = identity?.endpoint?.protocol ?? descriptor.protocol;
128
+ // Evidence records what the descriptor said, even when a foreign provider speaks a protocol
129
+ // outside the in-tree vocabulary — the field is a report, not a gate.
130
+ const protocol = protocolRaw;
131
+ const route = {
132
+ provider: identity?.providerId ?? descriptor.provider,
133
+ protocol,
134
+ model: identity?.modelId ?? descriptor.model,
135
+ endpoint: sanitizeEndpoint({
136
+ id: identity?.endpoint?.id ?? `${descriptor.provider}.${descriptor.protocol}`,
137
+ protocol: protocolRaw,
138
+ baseURL: identity?.endpoint?.baseURL ?? "",
139
+ }),
140
+ adapterVersion: "unknown",
141
+ capabilitiesRef: KNOWN_GENERATION_PROTOCOLS.has(protocolRaw) ? protocolRaw : "unknown",
142
+ };
143
+ // §1.3: content addressing covers every field EXCEPT capabilitiesRef (a reference, not content).
144
+ const { capabilitiesRef: _ref, ...addressed } = route;
145
+ return { ...route, routeId: sha256Hex(stableJson(addressed)) };
146
+ }
147
+ catch {
148
+ const fallback = {
149
+ provider: "unknown",
150
+ protocol: "unknown",
151
+ model: "unknown",
152
+ endpoint: { id: "unknown", protocol: "unknown", baseURL: "" },
153
+ adapterVersion: "unknown",
154
+ };
155
+ return { ...fallback, capabilitiesRef: "unknown", routeId: sha256Hex(stableJson(fallback)) };
156
+ }
157
+ }
109
158
  function sanitizeMaterialValue(value) {
110
159
  if (value === undefined || typeof value === "function")
111
160
  return undefined;
@@ -29,7 +29,7 @@ export interface InvocationOutcome {
29
29
  selectedEffectId: string;
30
30
  stopReason?: CanonicalStopReason;
31
31
  /** Absent on a failed chain (nothing settled). */
32
- settlement?: UsageSettlement;
32
+ settlement?: ModelUsageSettlement;
33
33
  }
34
34
  export type ProviderAttemptStatus = "success" | "transport_exhausted" | "aborted" | "rejected";
35
35
  /**
@@ -84,7 +84,7 @@ export declare function providerAttemptToRecord(attempt: ProviderAttempt, accoun
84
84
  * existing ResolveEffect wire shape — this is what the runner already feeds the kernel as
85
85
  * `observed_input_tokens` / `observed_output_tokens`.
86
86
  */
87
- export interface UsageSettlement {
87
+ export interface ModelUsageSettlement {
88
88
  observed_input_tokens: number;
89
89
  observed_output_tokens: number;
90
90
  }
@@ -95,7 +95,7 @@ export interface UsageSettlement {
95
95
  */
96
96
  export interface UsageAccountingPolicy {
97
97
  policyId: string;
98
- settle(usage: NormalizedProviderUsage): UsageSettlement;
98
+ settle(usage: NormalizedProviderUsage): ModelUsageSettlement;
99
99
  }
100
100
  /**
101
101
  * The default policy = today's implicit runner behavior exactly (full input footprint + full
@@ -1,5 +1,5 @@
1
1
  export type { SessionEvent, SessionEventKind, SessionLog } from "./session-log.js";
2
- export { InMemorySessionLog } from "./session-log.js";
2
+ export { InMemorySessionLog, assertCanonicalProviderAttemptRecord } from "./session-log.js";
3
3
  export { SESSION_EVENT_KINDS } from "./session-log.js";
4
4
  export { DriverKernelJournal, InMemoryJournalDriver, InMemoryKernelJournal, JournalCasConflictError, JournalIntegrityError, JournalIoError, } from "./kernel-journal.js";
5
5
  export type { CheckpointCandidate, InstalledCheckpoint, JournalAppendReceipt, JournalEntry, JournalHead, JournalPruneReceipt, JournalRecordInput, JournalStorageDriver, KernelJournal, } from "./kernel-journal.js";
@@ -1,4 +1,4 @@
1
- export { InMemorySessionLog } from "./session-log.js";
1
+ export { InMemorySessionLog, assertCanonicalProviderAttemptRecord } from "./session-log.js";
2
2
  // Registered session-event vocabulary (F9/S3; manifest-pinned by sdk-conformance, P7-S4)
3
3
  export { SESSION_EVENT_KINDS } from "./session-log.js";
4
4
  // Durable transaction capability (Canonical Kernel ABI §9.1)
@@ -13,6 +13,7 @@ import { type ReducerRegistry } from "./reducers.js";
13
13
  import { type NativeOsProfile, type OsProfileId, type SignalPolicy } from "./os-profile.js";
14
14
  import { PayloadStore } from "./payload-store.js";
15
15
  import { type ContextPolicyOverrides } from "./context-policy.js";
16
+ import { type UsageAccountingPolicy } from "./execution-evidence.js";
16
17
  export interface MemoryWriteRateLimit {
17
18
  maxWrites: number;
18
19
  windowMs: number;
@@ -140,6 +141,10 @@ export interface RuntimeOptions {
140
141
  governancePolicy?: GovernancePolicy;
141
142
  signalPolicy?: SignalPolicy;
142
143
  promptBudget?: PromptBudget;
144
+ /** P4 §2.1: the named policy turning a provider measurement into the two observed_* numbers
145
+ * that cross the kernel boundary. Undefined ⇒ FULL_FOOTPRINT (the numbers the runner has
146
+ * always fed); only makes the conversion named, pinnable on `provider_attempt`, replayable. */
147
+ usageAccountingPolicy?: UsageAccountingPolicy;
143
148
  /** Stable replayable context behavior; ratios are normalized to integer ppm. */
144
149
  contextPolicy?: ContextPolicyOverrides;
145
150
  schedulerPolicy?: SchedulerPolicy;
@@ -281,7 +286,24 @@ export declare class RuntimeRunner {
281
286
  private activePageOutArchive;
282
287
  /** Provider continuation emitted after a canonical nested workflow completes. */
283
288
  private workflowContinuation;
289
+ /** P4 (0.2.64 Evidence Plane): the resolved route every provider attempt is pinned to —
290
+ * resolved once so evidence identity is byte-stable across the run (C6.3 pins in-run
291
+ * route stability). */
292
+ private readonly providerRoute;
293
+ private readonly usageAccountingPolicy;
294
+ /** P4 §1.1: the active invocation's derived identity = its chain's FIRST effect_id. Tracked
295
+ * across the kernel-recovered retry loop; a fresh call_provider with no pending retry opens
296
+ * a new invocation. */
297
+ private providerRetryPending;
298
+ private activeProviderInvocationId;
284
299
  constructor(opts: RuntimeOptions);
300
+ /**
301
+ * P4-S1 (G1): land one provider_attempt evidence record per effect execution. Pure host
302
+ * evidence (B7) — appended AFTER the transport fact exists (success/failure/abort), never
303
+ * consulted for kernel input. The accounting policy id pins only when a measurement exists,
304
+ * so (usage, policy_id) deterministically recomputes the settlement that crossed the wire.
305
+ */
306
+ private appendProviderAttempt;
285
307
  private resolveKernelJournal;
286
308
  private createCanonicalRuntime;
287
309
  private commitKernelApply;
@@ -19,7 +19,8 @@ import { assertNativeProfile } from "./os-profile.js";
19
19
  import { classifyProviderError, providerErrorEventFields } from "../providers/provider-error.js";
20
20
  import { PayloadStore } from "./payload-store.js";
21
21
  import { contextPolicy, normalizeContextPolicy, } from "./context-policy.js";
22
- import { createProviderRequestPlanForProvider, estimateProviderPromptTokens, measurementForPlan, recordPromptMeasurement, } from "../providers/request-plan.js";
22
+ import { createProviderRequestPlanForProvider, estimateProviderPromptTokens, measurementForPlan, recordPromptMeasurement, resolveProviderRoute, } from "../providers/request-plan.js";
23
+ import { FULL_FOOTPRINT_USAGE_ACCOUNTING_POLICY, providerAttemptToRecord, tryNormalizeProviderUsage, } from "./execution-evidence.js";
23
24
  export function schedulerPolicyToKernel(policy) {
24
25
  const allowed = new Set([
25
26
  "criticalPathWeight", "fanoutWeight", "ageWeight", "tokenCostWeight",
@@ -121,12 +122,36 @@ export class RuntimeRunner {
121
122
  activePageOutArchive;
122
123
  /** Provider continuation emitted after a canonical nested workflow completes. */
123
124
  workflowContinuation = null;
125
+ /** P4 (0.2.64 Evidence Plane): the resolved route every provider attempt is pinned to —
126
+ * resolved once so evidence identity is byte-stable across the run (C6.3 pins in-run
127
+ * route stability). */
128
+ providerRoute;
129
+ usageAccountingPolicy;
130
+ /** P4 §1.1: the active invocation's derived identity = its chain's FIRST effect_id. Tracked
131
+ * across the kernel-recovered retry loop; a fresh call_provider with no pending retry opens
132
+ * a new invocation. */
133
+ providerRetryPending = false;
134
+ activeProviderInvocationId;
124
135
  constructor(opts) {
125
136
  this.opts = opts;
126
137
  const schemaAttempts = opts.workflowSchemaValidationAttempts ?? 2;
127
138
  if (!Number.isInteger(schemaAttempts) || schemaAttempts < 1 || schemaAttempts > 16) {
128
139
  throw new RangeError("workflowSchemaValidationAttempts must be an integer between 1 and 16");
129
140
  }
141
+ this.providerRoute = resolveProviderRoute(opts.provider);
142
+ this.usageAccountingPolicy = opts.usageAccountingPolicy ?? FULL_FOOTPRINT_USAGE_ACCOUNTING_POLICY;
143
+ }
144
+ /**
145
+ * P4-S1 (G1): land one provider_attempt evidence record per effect execution. Pure host
146
+ * evidence (B7) — appended AFTER the transport fact exists (success/failure/abort), never
147
+ * consulted for kernel input. The accounting policy id pins only when a measurement exists,
148
+ * so (usage, policy_id) deterministically recomputes the settlement that crossed the wire.
149
+ */
150
+ async appendProviderAttempt(sessionId, attempt) {
151
+ await this.opts.sessionLog.append(sessionId, {
152
+ kind: "provider_attempt",
153
+ ...providerAttemptToRecord(attempt, attempt.usage ? this.usageAccountingPolicy.policyId : undefined),
154
+ });
130
155
  }
131
156
  resolveKernelJournal() {
132
157
  const embedded = this.opts.sessionLog;
@@ -269,6 +294,7 @@ export class RuntimeRunner {
269
294
  agent_id: this.opts.agentId,
270
295
  system_prompt: this.opts.systemPrompt,
271
296
  ...(attachments ? { attachments } : {}),
297
+ route: this.providerRoute,
272
298
  });
273
299
  }
274
300
  yield* this.execute(req.sessionId, runId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, attachments);
@@ -355,7 +381,7 @@ export class RuntimeRunner {
355
381
  await this.commitKernelApply(this.activeKernel, this.pendingObservations, { kind: "skill_deactivated", name });
356
382
  this.knowledgePushedSkills.delete(name);
357
383
  }
358
- async resolveKernelSuspend(requests, runtime, sessionId) {
384
+ async resolveKernelSuspend(requests, runtime, sessionId, effectId) {
359
385
  const approved = [];
360
386
  const denied = [];
361
387
  const events = [];
@@ -421,6 +447,8 @@ export class RuntimeRunner {
421
447
  await this.opts.sessionLog.append(sessionId, {
422
448
  kind: "tool_completed",
423
449
  turn: runtime.turn(),
450
+ // P4 §1.1: governance-denied completions carry the requesting effect's identity.
451
+ ...(effectId !== undefined ? { effect_id: effectId } : {}),
424
452
  results: [{
425
453
  call_id: requestAction.callId,
426
454
  output: `permission denied: ${denyReason}`,
@@ -638,6 +666,18 @@ export class RuntimeRunner {
638
666
  break;
639
667
  if (action.kind === "call_provider") {
640
668
  const providerEffectId = action.effectId;
669
+ // P4 §1.1: invocation identity is derived, never minted — the chain's FIRST effect_id.
670
+ // A provider_error commit armed `providerRetryPending`, so this effect CONTINUES the
671
+ // pending invocation; otherwise it opens a new one. The chain itself lives in the
672
+ // journal (each hop a resolution input → new effect); this is the SessionLog evidence
673
+ // projection of it.
674
+ if (!this.providerRetryPending || this.activeProviderInvocationId === undefined) {
675
+ this.activeProviderInvocationId = providerEffectId;
676
+ }
677
+ this.providerRetryPending = false;
678
+ const invocationId = this.activeProviderInvocationId;
679
+ // Host wall-clock, pure evidence (B7/DEC-2): never crosses into kernel input.
680
+ const attemptStartedAtMs = Date.now();
641
681
  const finalToolCalls = [];
642
682
  let finalText = "";
643
683
  // I5: governance schema-level pre-filter — see Node runner for full rationale.
@@ -684,7 +724,7 @@ export class RuntimeRunner {
684
724
  });
685
725
  }
686
726
  recordedMeasurements.set(providerPlan.fingerprint, promptMeasurement);
687
- await this.opts.sessionLog.append(sessionId, { kind: "prompt_measured", turn: runtime.turn(), measurement: promptMeasurement });
727
+ await this.opts.sessionLog.append(sessionId, { kind: "prompt_measured", turn: runtime.turn(), measurement: promptMeasurement, effect_id: providerEffectId });
688
728
  }
689
729
  const reservedPromptTokens = (this.opts.promptBudget?.promptOverheadTokens ?? 0)
690
730
  + (this.opts.promptBudget?.outputReserveTokens ?? 0)
@@ -693,11 +733,29 @@ export class RuntimeRunner {
693
733
  && promptMeasurement.source.kind !== "heuristic"
694
734
  && promptMeasurement.inputTokens + reservedPromptTokens > this.opts.maxTokens;
695
735
  if (context.budgetOverflow || measuredOverflow) {
736
+ // P4 §1.2: blocked BEFORE any transport — zero rungs, status rejected. The
737
+ // fingerprint still binds the would-be request to its prompt_measured record (G2).
738
+ await this.appendProviderAttempt(sessionId, {
739
+ effectId: providerEffectId,
740
+ attemptSeq: 1,
741
+ route: this.providerRoute,
742
+ requestFingerprint: providerPlan.fingerprint,
743
+ status: "rejected",
744
+ transportRungs: 0,
745
+ lastErrorClass: "context_overflow",
746
+ startedAtMs: attemptStartedAtMs,
747
+ finishedAtMs: Date.now(),
748
+ wireEvidence: {
749
+ protocol: this.providerRoute.protocol,
750
+ request_fingerprint: providerPlan.fingerprint,
751
+ },
752
+ });
696
753
  action = await this.commitKernelAction(runtime, this.pendingObservations, {
697
754
  kind: "provider_error", effect_id: providerEffectId,
698
755
  message: "provider-visible prompt exceeds the configured context budget",
699
756
  error_kind: "context_overflow", retryable: false,
700
757
  });
758
+ this.providerRetryPending = action.kind === "call_provider";
701
759
  continue;
702
760
  }
703
761
  const abortSignal = this.abortController?.signal;
@@ -741,6 +799,23 @@ export class RuntimeRunner {
741
799
  else {
742
800
  const descriptor = this.opts.provider.descriptor?.();
743
801
  const providerError = classifyProviderError(descriptor?.provider ?? "unknown", err);
802
+ // P4 §1.2: the transport ladder is exhausted — one attempt record, error CLASS only
803
+ // (B1: never the raw vendor text). The single-shot stream path reports 1 rung.
804
+ await this.appendProviderAttempt(sessionId, {
805
+ effectId: providerEffectId,
806
+ attemptSeq: 1,
807
+ route: this.providerRoute,
808
+ requestFingerprint: providerPlan.fingerprint,
809
+ status: "transport_exhausted",
810
+ transportRungs: 1,
811
+ lastErrorClass: providerError.kind,
812
+ startedAtMs: attemptStartedAtMs,
813
+ finishedAtMs: Date.now(),
814
+ wireEvidence: {
815
+ protocol: this.providerRoute.protocol,
816
+ request_fingerprint: providerPlan.fingerprint,
817
+ },
818
+ });
744
819
  // Reactive recovery is now a kernel decision. Forward the raw provider error and
745
820
  // dispatch whatever the kernel returns: `call_provider` to retry with a freshly
746
821
  // compacted context, or `done` to terminate with an honest `ContextOverflow`. The
@@ -752,6 +827,9 @@ export class RuntimeRunner {
752
827
  message: providerError.message,
753
828
  ...providerErrorEventFields(providerError),
754
829
  });
830
+ // P4 §1.1: a kernel-recovered retry CONTINUES this invocation (the journal holds the
831
+ // causation hop); a terminal closes it.
832
+ this.providerRetryPending = action.kind === "call_provider";
755
833
  // Withholding (query.ts parity): surface the raw provider error only when the kernel
756
834
  // could NOT recover (it returned a terminal). On a recovered retry (`call_provider`)
757
835
  // the error stays hidden. `continue` re-enters the loop: a recovered turn persists its
@@ -765,6 +843,21 @@ export class RuntimeRunner {
765
843
  }
766
844
  // #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
767
845
  if (abortSignal?.aborted) {
846
+ // P4 §1.2: host cancellation mid-stream — the attempt is evidence too.
847
+ await this.appendProviderAttempt(sessionId, {
848
+ effectId: providerEffectId,
849
+ attemptSeq: 1,
850
+ route: this.providerRoute,
851
+ requestFingerprint: providerPlan.fingerprint,
852
+ status: "aborted",
853
+ transportRungs: 1,
854
+ startedAtMs: attemptStartedAtMs,
855
+ finishedAtMs: Date.now(),
856
+ wireEvidence: {
857
+ protocol: this.providerRoute.protocol,
858
+ request_fingerprint: providerPlan.fingerprint,
859
+ },
860
+ });
768
861
  action = await this.commitKernelAction(runtime, this.pendingObservations, {
769
862
  kind: "cancel_operation",
770
863
  reason: this.cancellationReason ?? "user",
@@ -778,22 +871,60 @@ export class RuntimeRunner {
778
871
  toolCalls: finalToolCalls,
779
872
  tokenCount: turnOutputTokens || turnTokens || undefined,
780
873
  };
874
+ // P4 §2: assemble the measurement from the exact numbers that cross the boundary today
875
+ // (input/output turn counters + the cache split). An invalid frame degrades to no
876
+ // measurement (evidence never breaks a run); the settlement then falls back to the raw
877
+ // counts below.
878
+ const attemptUsage = (turnInputTokens > 0 || turnOutputTokens > 0)
879
+ ? tryNormalizeProviderUsage({
880
+ inputTokens: turnInputTokens,
881
+ outputTokens: turnOutputTokens,
882
+ ...(turnCacheReadTokens > 0 ? { cacheReadInputTokens: turnCacheReadTokens } : {}),
883
+ ...(turnCacheCreationTokens > 0 ? { cacheCreationInputTokens: turnCacheCreationTokens } : {}),
884
+ })
885
+ : undefined;
886
+ const settlement = attemptUsage ? this.usageAccountingPolicy.settle(attemptUsage) : undefined;
781
887
  const providerEvent = {
782
888
  kind: "provider_result",
783
889
  effect_id: providerEffectId,
784
890
  message: messageToKernelMessage(assistantMessage),
785
- ...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
786
- ...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
891
+ // P4-S2: observed_* now comes from the pinned policy's settlement of the measurement.
892
+ // Under the default full-footprint policy these are provably the numbers the runner
893
+ // has always fed (inputTokens/outputTokens verbatim under the same >0 gates).
894
+ ...(turnInputTokens > 0 ? { observed_input_tokens: settlement?.observed_input_tokens ?? turnInputTokens } : {}),
895
+ ...(turnOutputTokens > 0 ? { observed_output_tokens: settlement?.observed_output_tokens ?? turnOutputTokens } : {}),
787
896
  ...(turnStopReason ? { stop_reason: turnStopReason } : {}),
788
897
  };
789
- action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
898
+ // P4-S1: land the attempt evidence BEFORE the kernel resolution commits — the record
899
+ // describes the transport fact, which exists regardless of what the kernel decides next.
790
900
  const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
901
+ const wireEvidence = {
902
+ protocol: this.providerRoute.protocol,
903
+ request_fingerprint: providerPlan.fingerprint,
904
+ ...(providerReplay !== undefined ? { replay_state: providerReplay } : {}),
905
+ };
906
+ await this.appendProviderAttempt(sessionId, {
907
+ effectId: providerEffectId,
908
+ attemptSeq: 1,
909
+ route: this.providerRoute,
910
+ requestFingerprint: providerPlan.fingerprint,
911
+ status: "success",
912
+ transportRungs: 1,
913
+ startedAtMs: attemptStartedAtMs,
914
+ finishedAtMs: Date.now(),
915
+ ...(attemptUsage !== undefined ? { usage: attemptUsage } : {}),
916
+ wireEvidence,
917
+ });
918
+ action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
791
919
  await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
792
920
  turn: runtime.turn(),
793
921
  content: finalText,
794
922
  tokenCount: turnOutputTokens || turnTokens || undefined,
795
923
  toolCalls: finalToolCalls,
796
924
  providerReplay,
925
+ effectId: providerEffectId,
926
+ invocationId,
927
+ wireEvidence,
797
928
  }));
798
929
  // P0-C: per-turn tool-gating telemetry. `activeSkill` reflects the skill in effect GOING INTO
799
930
  // this turn; a `skill` call here only takes effect next turn — emit first, then advance.
@@ -823,7 +954,7 @@ export class RuntimeRunner {
823
954
  }
824
955
  }
825
956
  else if (action.kind === "request_approval") {
826
- const resolved = await this.resolveKernelSuspend(action.requests, runtime, sessionId);
957
+ const resolved = await this.resolveKernelSuspend(action.requests, runtime, sessionId, action.effectId);
827
958
  for (const evt of resolved.events)
828
959
  yield evt;
829
960
  action = await this.commitKernelAction(runtime, this.pendingObservations, {
@@ -1076,6 +1207,8 @@ export class RuntimeRunner {
1076
1207
  await this.opts.sessionLog.append(sessionId, {
1077
1208
  kind: "tool_completed",
1078
1209
  turn: runtime.turn(),
1210
+ // P4 §1.1: the completion carries the execute_tools effect's identity.
1211
+ effect_id: toolEffectId,
1079
1212
  results: toolResults.map(r => ({
1080
1213
  call_id: r.callId,
1081
1214
  output: r.output,
@@ -1530,6 +1663,7 @@ export class RuntimeRunner {
1530
1663
  goal: `workflow:${spec.nodes.length} nodes`,
1531
1664
  criteria: [],
1532
1665
  ...(this.opts.agentId ? { agent_id: this.opts.agentId } : {}),
1666
+ route: this.providerRoute,
1533
1667
  });
1534
1668
  await this.initializeWorkflowKernel(sessionId, runId);
1535
1669
  }
@@ -395,3 +395,11 @@ export declare class InMemorySessionLog implements SessionLog {
395
395
  }>>;
396
396
  latestSeq(sessionId: string): Promise<number>;
397
397
  }
398
+ /**
399
+ * P4 §3 teeth, wasm mirror of the node/python persisted-record validator. The wasm SessionLog is
400
+ * in-memory only (no durable read path where validation would otherwise live), so the teeth are
401
+ * exported for hosts that bridge these events to durable storage — and for the conformance
402
+ * runner, which pins the rejection contract (`invalid_provider_attempt`) across all three
403
+ * JSON-projection SDKs. Same checks and message wording as node `assertCanonicalProviderAttempt`.
404
+ */
405
+ export declare function assertCanonicalProviderAttemptRecord(event: Record<string, unknown>): void;
@@ -99,3 +99,75 @@ export class InMemorySessionLog {
99
99
  return (this.seqCounters.get(sessionId) ?? 0) - 1;
100
100
  }
101
101
  }
102
+ const PROVIDER_ATTEMPT_KEYS = new Set([
103
+ "kind", "effect_id", "attempt_seq", "route", "request_fingerprint", "status", "transport_rungs",
104
+ "last_error_class", "started_at_ms", "finished_at_ms", "usage", "wire_evidence", "accounting_policy_id",
105
+ ]);
106
+ const PROVIDER_ATTEMPT_STATUSES = new Set(["success", "transport_exhausted", "aborted", "rejected"]);
107
+ const WIRE_EVIDENCE_KEYS = new Set(["protocol", "request_fingerprint", "response_id", "raw_usage", "replay_state"]);
108
+ function assertCanonicalRoute(value) {
109
+ if (!value || typeof value !== "object" || Array.isArray(value))
110
+ throw new Error("route must be an object");
111
+ const route = value;
112
+ for (const field of ["routeId", "provider", "protocol", "model", "adapterVersion", "capabilitiesRef"]) {
113
+ if (typeof route[field] !== "string" || route[field].length === 0) {
114
+ throw new Error(`route ${field} must be a non-empty string`);
115
+ }
116
+ }
117
+ if (!route.endpoint || typeof route.endpoint !== "object" || Array.isArray(route.endpoint)) {
118
+ throw new Error("route endpoint must be an object");
119
+ }
120
+ }
121
+ function assertCanonicalWireEvidence(value) {
122
+ if (!value || typeof value !== "object" || Array.isArray(value))
123
+ throw new Error("wire evidence must be an object");
124
+ const evidence = value;
125
+ for (const key of Object.keys(evidence))
126
+ if (!WIRE_EVIDENCE_KEYS.has(key))
127
+ throw new Error(`wire evidence has unknown field ${key}`);
128
+ if (typeof evidence.protocol !== "string" || evidence.protocol.length === 0)
129
+ throw new Error("wire evidence protocol is required");
130
+ // G2: the fingerprint is mandatory non-empty — it is what binds this evidence to a request plan.
131
+ if (typeof evidence.request_fingerprint !== "string" || evidence.request_fingerprint.length === 0) {
132
+ throw new Error("wire evidence request_fingerprint is required");
133
+ }
134
+ }
135
+ /**
136
+ * P4 §3 teeth, wasm mirror of the node/python persisted-record validator. The wasm SessionLog is
137
+ * in-memory only (no durable read path where validation would otherwise live), so the teeth are
138
+ * exported for hosts that bridge these events to durable storage — and for the conformance
139
+ * runner, which pins the rejection contract (`invalid_provider_attempt`) across all three
140
+ * JSON-projection SDKs. Same checks and message wording as node `assertCanonicalProviderAttempt`.
141
+ */
142
+ export function assertCanonicalProviderAttemptRecord(event) {
143
+ for (const key of Object.keys(event))
144
+ if (!PROVIDER_ATTEMPT_KEYS.has(key))
145
+ throw new Error(`provider_attempt has unknown field ${key}`);
146
+ if (typeof event.effect_id !== "string" || event.effect_id.length === 0)
147
+ throw new Error("provider_attempt effect_id is required");
148
+ if (!Number.isInteger(event.attempt_seq) || event.attempt_seq < 1)
149
+ throw new Error("provider_attempt attempt_seq must be a positive integer");
150
+ assertCanonicalRoute(event.route);
151
+ if (typeof event.request_fingerprint !== "string" || event.request_fingerprint.length === 0) {
152
+ throw new Error("provider_attempt request_fingerprint is required");
153
+ }
154
+ if (typeof event.status !== "string" || !PROVIDER_ATTEMPT_STATUSES.has(event.status)) {
155
+ throw new Error("provider_attempt status must be success|transport_exhausted|aborted|rejected");
156
+ }
157
+ if (!Number.isInteger(event.transport_rungs) || event.transport_rungs < 0) {
158
+ throw new Error("provider_attempt transport_rungs must be a non-negative integer");
159
+ }
160
+ if (event.last_error_class !== undefined && typeof event.last_error_class !== "string") {
161
+ throw new Error("provider_attempt last_error_class must be a string");
162
+ }
163
+ for (const field of ["started_at_ms", "finished_at_ms"]) {
164
+ if (typeof event[field] !== "number" || !Number.isFinite(event[field])) {
165
+ throw new Error(`provider_attempt ${field} must be a finite number`);
166
+ }
167
+ }
168
+ if (event.wire_evidence !== undefined)
169
+ assertCanonicalWireEvidence(event.wire_evidence);
170
+ if (event.accounting_policy_id !== undefined && typeof event.accounting_policy_id !== "string") {
171
+ throw new Error("provider_attempt accounting_policy_id must be a string");
172
+ }
173
+ }
@@ -1,4 +1,4 @@
1
- import type { Message, ProviderReplay, ToolCall } from "../types.js";
1
+ import type { Message, ProviderReplay, ProviderWireEvidence, ToolCall } from "../types.js";
2
2
  import type { SessionEvent } from "./session-log.js";
3
3
  import type { WorkflowNodeStatus } from "./types/agent.js";
4
4
  export { REPLAY_CONTENT_MAX_BYTES as RECOVERY_CONTENT_MAX_BYTES } from "./replay-sanitize.js";
@@ -27,6 +27,9 @@ export declare function buildLlmCompletedEvent(input: {
27
27
  tokenCount?: number;
28
28
  toolCalls: ToolCall[];
29
29
  providerReplay?: ProviderReplay;
30
+ effectId?: string;
31
+ invocationId?: string;
32
+ wireEvidence?: ProviderWireEvidence;
30
33
  }): Extract<SessionEvent, {
31
34
  kind: "llm_completed";
32
35
  }>;
@@ -21,6 +21,11 @@ export function normalizeLlmCompleted(event, maxBytes) {
21
21
  tool_calls: toolCalls,
22
22
  token_count: event.token_count ?? estimateTokenCount(content),
23
23
  ...(providerReplay ? { provider_replay: providerReplay } : {}),
24
+ // P4 §3: the evidence-plane fields ride along verbatim — recovery never reads them
25
+ // (SessionLog is evidence, not authority), so normalize must neither synthesize nor drop them.
26
+ ...(event.effect_id !== undefined ? { effect_id: event.effect_id } : {}),
27
+ ...(event.invocation_id !== undefined ? { invocation_id: event.invocation_id } : {}),
28
+ ...(event.wire_evidence !== undefined ? { wire_evidence: event.wire_evidence } : {}),
24
29
  };
25
30
  }
26
31
  export function repairEventsForRecovery(events, maxBytes) {
@@ -38,6 +43,9 @@ export function buildLlmCompletedEvent(input) {
38
43
  tool_calls: input.toolCalls ?? [],
39
44
  token_count: input.tokenCount,
40
45
  provider_replay: input.providerReplay,
46
+ ...(input.effectId !== undefined ? { effect_id: input.effectId } : {}),
47
+ ...(input.invocationId !== undefined ? { invocation_id: input.invocationId } : {}),
48
+ ...(input.wireEvidence !== undefined ? { wire_evidence: input.wireEvidence } : {}),
41
49
  });
42
50
  }
43
51
  export function buildRunTerminalEvent(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/wasm",
3
- "version": "0.2.63",
3
+ "version": "0.2.65",
4
4
  "description": "DeepStrike WASM SDK — browser, Cloudflare Workers, Deno Deploy",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "test:canonical-binding": "wasm-pack build ../crates/deepstrike-wasm --target nodejs --dev --out-dir ../../wasm/pkg-node && node tests/canonical-binding.node.cjs && node tests/golden-fixtures-sweep.node.cjs"
18
18
  },
19
19
  "dependencies": {
20
- "@deepstrike/wasm-kernel": "0.2.63"
20
+ "@deepstrike/wasm-kernel": "0.2.65"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/jest": "^30.0.0",