@deepstrike/sdk 0.2.62 → 0.2.64

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.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * P4 (0.2.64 Execution Evidence Plane): the host-side object model connecting a kernel-minted
3
+ * effect to the real provider execution it drove — ModelInvocation / ProviderAttempt /
4
+ * ResolvedProviderRoute / UsageAccountingPolicy. Everything here is L2 host evidence
5
+ * (B7): it lands in SessionLog for cross-verification against the journal (C6/C8) and is
6
+ * NEVER fed back as kernel input (B4/DEC-2 discipline: wall-clock and wire facts stay host-side).
7
+ *
8
+ * Kernel ABI is untouched — the kernel projection of an invocation outcome remains the
9
+ * existing ProviderCompleted/HostEffectFailure shapes (D2).
10
+ */
11
+ import type { ProviderUsage, ProviderWireEvidence, UsageEvent } from "../types.js";
12
+ import { type NormalizedProviderUsage, type ResolvedProviderRoute } from "../providers/request-plan.js";
13
+ /** Canonical stop-reason vocabulary already carried on the wire usage frame (types.ts). */
14
+ export type CanonicalStopReason = NonNullable<UsageEvent["stopReason"]>;
15
+ /**
16
+ * P4 §1.1: one logical model call = the fact-connected chain of CallProvider effects the
17
+ * kernel walked to obtain one turn of model output. Derived identity, zero minting:
18
+ * `invocationId` IS the first effect's effect_id. Authority = journal; the SessionLog
19
+ * projection (`llm_completed.invocation_id`) is evidence only.
20
+ */
21
+ export interface ModelInvocation {
22
+ invocationId: string;
23
+ turn: number;
24
+ /** Every effect_id on the chain, in order. Length 1 = first attempt succeeded. */
25
+ effectChain: string[];
26
+ outcome?: InvocationOutcome;
27
+ }
28
+ /** P4 §1.4: the invocation's terminal projection. */
29
+ export interface InvocationOutcome {
30
+ invocationId: string;
31
+ /** The effect the kernel adopted as the outcome (the successful one). */
32
+ selectedEffectId: string;
33
+ stopReason?: CanonicalStopReason;
34
+ /** Absent on a failed chain (nothing settled). */
35
+ settlement?: ModelUsageSettlement;
36
+ }
37
+ export type ProviderAttemptStatus = "success" | "transport_exhausted" | "aborted" | "rejected";
38
+ /**
39
+ * P4 §1.2: one effect's execution against one route, one physical attempt. The kernel-minted
40
+ * `effectId` is the primary key (H7 — no parallel id minting). Transport-ladder rungs are
41
+ * summarized as a count plus the final error class; rung-level evidence belongs to adapter
42
+ * debug logs, not SessionLog.
43
+ */
44
+ export interface ProviderAttempt {
45
+ effectId: string;
46
+ /** Failover sequence within one effect execution (1-based). Always 1 today (P4 §0.2). */
47
+ attemptSeq: number;
48
+ route: ResolvedProviderRoute;
49
+ /** → ProviderRequestPlan.fingerprint (G2). */
50
+ requestFingerprint: string;
51
+ status: ProviderAttemptStatus;
52
+ transportRungs: number;
53
+ /** classifyProviderError's class, never the raw vendor text (B1 spirit). */
54
+ lastErrorClass?: string;
55
+ /** Host wall-clock, pure evidence, never kernel input (DEC-2 discipline). */
56
+ startedAtMs: number;
57
+ finishedAtMs: number;
58
+ /** Full measurement fields (P4 §2); only the settlement crosses the kernel boundary (B4). */
59
+ usage?: NormalizedProviderUsage;
60
+ wireEvidence?: ProviderWireEvidence;
61
+ }
62
+ /**
63
+ * The SessionLog wire payload of a ProviderAttempt (P4 §3): the §1.2 fields flattened into
64
+ * the event, snake_case per SessionLog convention. Nested objects keep their native shape
65
+ * (same convention as `prompt_measured.measurement`).
66
+ */
67
+ export interface ProviderAttemptRecord {
68
+ effect_id: string;
69
+ attempt_seq: number;
70
+ route: ResolvedProviderRoute;
71
+ request_fingerprint: string;
72
+ status: ProviderAttemptStatus;
73
+ transport_rungs: number;
74
+ last_error_class?: string;
75
+ started_at_ms: number;
76
+ finished_at_ms: number;
77
+ usage?: NormalizedProviderUsage;
78
+ wire_evidence?: ProviderWireEvidence;
79
+ /** P4 §2.1: the accounting policy this attempt's settlement was/will be derived with —
80
+ * pinned on the attempt so settlement is deterministically recomputable from
81
+ * (measurement, policy_id). */
82
+ accounting_policy_id?: string;
83
+ }
84
+ export declare function providerAttemptToRecord(attempt: ProviderAttempt, accountingPolicyId?: string): ProviderAttemptRecord;
85
+ /**
86
+ * P4 §2: the only two numbers that cross the kernel boundary (B4). Field names match the
87
+ * existing ResolveEffect wire shape — this is what the runner already feeds the kernel as
88
+ * `observed_input_tokens` / `observed_output_tokens`.
89
+ */
90
+ export interface ModelUsageSettlement {
91
+ observed_input_tokens: number;
92
+ observed_output_tokens: number;
93
+ }
94
+ /**
95
+ * P4 §2.1: the named, pinnable, replayable policy turning a measurement into a settlement.
96
+ * `settle` must be a pure function of the measurement — given (usage, policyId) any auditor
97
+ * recomputes the identical settlement.
98
+ */
99
+ export interface UsageAccountingPolicy {
100
+ policyId: string;
101
+ settle(usage: NormalizedProviderUsage): ModelUsageSettlement;
102
+ }
103
+ /**
104
+ * The default policy = today's implicit runner behavior exactly (full input footprint + full
105
+ * output footprint — the two numbers the runner has always fed `observed_*`). P4 changes no
106
+ * default numbers; it only makes the conversion a named object. The date stamp is the
107
+ * policy's identity: any future semantics change MUST ship under a new policyId.
108
+ */
109
+ export declare const FULL_FOOTPRINT_USAGE_ACCOUNTING_POLICY: UsageAccountingPolicy;
110
+ /**
111
+ * Defensive measurement assembly for the attempt evidence path: an invalid frame (cache
112
+ * subsets exceeding input, etc.) degrades to NO measurement instead of breaking the run —
113
+ * evidence is never worth a run failure, and the settlement falls back to the raw counts.
114
+ * Telemetry fields normalizeProviderUsage drops are re-attached so `usage` stays full-field.
115
+ */
116
+ export declare function tryNormalizeProviderUsage(usage: ProviderUsage): NormalizedProviderUsage | undefined;
@@ -0,0 +1,51 @@
1
+ import { normalizeProviderUsage, } from "../providers/request-plan.js";
2
+ export function providerAttemptToRecord(attempt, accountingPolicyId) {
3
+ return {
4
+ effect_id: attempt.effectId,
5
+ attempt_seq: attempt.attemptSeq,
6
+ route: attempt.route,
7
+ request_fingerprint: attempt.requestFingerprint,
8
+ status: attempt.status,
9
+ transport_rungs: attempt.transportRungs,
10
+ ...(attempt.lastErrorClass !== undefined ? { last_error_class: attempt.lastErrorClass } : {}),
11
+ started_at_ms: attempt.startedAtMs,
12
+ finished_at_ms: attempt.finishedAtMs,
13
+ ...(attempt.usage !== undefined ? { usage: attempt.usage } : {}),
14
+ ...(attempt.wireEvidence !== undefined ? { wire_evidence: attempt.wireEvidence } : {}),
15
+ ...(accountingPolicyId !== undefined ? { accounting_policy_id: accountingPolicyId } : {}),
16
+ };
17
+ }
18
+ /**
19
+ * The default policy = today's implicit runner behavior exactly (full input footprint + full
20
+ * output footprint — the two numbers the runner has always fed `observed_*`). P4 changes no
21
+ * default numbers; it only makes the conversion a named object. The date stamp is the
22
+ * policy's identity: any future semantics change MUST ship under a new policyId.
23
+ */
24
+ export const FULL_FOOTPRINT_USAGE_ACCOUNTING_POLICY = {
25
+ policyId: "deepstrike.full-footprint@2026-09-15",
26
+ settle(usage) {
27
+ return {
28
+ observed_input_tokens: usage.inputTokens,
29
+ observed_output_tokens: usage.outputTokens,
30
+ };
31
+ },
32
+ };
33
+ /**
34
+ * Defensive measurement assembly for the attempt evidence path: an invalid frame (cache
35
+ * subsets exceeding input, etc.) degrades to NO measurement instead of breaking the run —
36
+ * evidence is never worth a run failure, and the settlement falls back to the raw counts.
37
+ * Telemetry fields normalizeProviderUsage drops are re-attached so `usage` stays full-field.
38
+ */
39
+ export function tryNormalizeProviderUsage(usage) {
40
+ try {
41
+ const normalized = normalizeProviderUsage(usage);
42
+ return {
43
+ ...normalized,
44
+ ...(usage.cacheTelemetryStatus !== undefined ? { cacheTelemetryStatus: usage.cacheTelemetryStatus } : {}),
45
+ ...(usage.cacheTelemetrySource !== undefined ? { cacheTelemetrySource: usage.cacheTelemetrySource } : {}),
46
+ };
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ }
@@ -1,7 +1,9 @@
1
1
  import type { EntropySample, Message, RenderedContext, TaskUpdate, ToolCall, ToolResult, ToolSchema } from "../types.js";
2
2
  import type { SkillMetadata } from "../skills/loader.js";
3
3
  import type { RollbackReason } from "./session-log.js";
4
+ export declare const CANONICAL_CONTENT_PARTS_PREFIX = "[[deepstrike-content-parts]]";
4
5
  export declare function encodeCanonicalContentParts(parts: unknown[]): string;
6
+ export declare function decodeCanonicalContentParts(content: string): Array<Record<string, unknown>> | undefined;
5
7
  export interface PaceDecision {
6
8
  action: "continue" | "sleep" | "stop";
7
9
  delayMs?: number;
@@ -1,8 +1,8 @@
1
- const CANONICAL_CONTENT_PARTS_PREFIX = "[[deepstrike-content-parts]]";
1
+ export const CANONICAL_CONTENT_PARTS_PREFIX = "[[deepstrike-content-parts]]";
2
2
  export function encodeCanonicalContentParts(parts) {
3
3
  return `${CANONICAL_CONTENT_PARTS_PREFIX}${Buffer.from(JSON.stringify(parts)).toString("base64url")}`;
4
4
  }
5
- function decodeCanonicalContentParts(content) {
5
+ export function decodeCanonicalContentParts(content) {
6
6
  if (!content.startsWith(CANONICAL_CONTENT_PARTS_PREFIX))
7
7
  return undefined;
8
8
  try {
@@ -13,6 +13,7 @@ export declare function stableSemanticArchiveName(effectId: string): string;
13
13
  import { type SubAgentOrchestrator } from "./sub-agent-orchestrator.js";
14
14
  import { type ReducerRegistry } from "./reducers.js";
15
15
  import { type GovernancePolicy } from "../governance.js";
16
+ import { type UsageAccountingPolicy } from "./execution-evidence.js";
16
17
  import { type NativeOsProfile, type OsProfileId, type SignalPolicy } from "./os-profile.js";
17
18
  import { PayloadStore } from "./payload-store.js";
18
19
  import type { BackgroundTaskErrorHandler } from "./reliability.js";
@@ -176,6 +177,12 @@ export interface RuntimeOptions {
176
177
  signalPolicy?: SignalPolicy;
177
178
  /** Provider-envelope overhead plus output and safety reserves journaled before start. */
178
179
  promptBudget?: PromptBudget;
180
+ /**
181
+ * P4-S2: the measurement→settlement conversion policy. Absent ⇒ the default full-footprint
182
+ * policy — byte-identical numbers to what the runner has always fed `observed_*`; the policy
183
+ * only makes the conversion named, pinnable on `provider_attempt`, and replayable.
184
+ */
185
+ usageAccountingPolicy?: UsageAccountingPolicy;
179
186
  /** Stable replayable context behavior; SDK ratios are normalized to integer ppm on the ABI wire. */
180
187
  contextPolicy?: ContextPolicyOverrides;
181
188
  /** Deterministic DAG scheduling policy installed atomically through ConfigureRun. */
@@ -403,6 +410,16 @@ export declare class RuntimeRunner {
403
410
  private readonly composedSystemPrompt;
404
411
  /** H1.2: present only when `opts.nudges` is non-empty; else null and the append funnel is untouched. */
405
412
  private readonly nudgeEngine;
413
+ /** P4-S1: the run's resolved provider route, assembled once at construction (P4 §0.2 — the
414
+ * provider is fixed for the run today; every attempt references this same route object). */
415
+ private readonly providerRoute;
416
+ /** P4-S2: measurement→settlement policy; default = the exact implicit behavior (numbers unchanged). */
417
+ private readonly usageAccountingPolicy;
418
+ /** P4 §1.1: the active invocation's derived identity = its chain's FIRST effect_id. Tracked
419
+ * across kernel-driven provider retries (a provider_error commit arms `providerRetryPending`;
420
+ * the next call_provider adopts the pending invocation instead of opening a new one). */
421
+ private activeProviderInvocationId;
422
+ private providerRetryPending;
406
423
  constructor(opts: RuntimeOptions);
407
424
  /** Host configuration (for coordinator / sub-agent spawn). */
408
425
  get hostOptions(): RuntimeOptions;
@@ -412,6 +429,13 @@ export declare class RuntimeRunner {
412
429
  private commitKernelAction;
413
430
  private startKernelAgent;
414
431
  private payloadStore;
432
+ /**
433
+ * P4-S1 (G1): land one provider_attempt evidence record per effect execution. Pure host
434
+ * evidence (B7) — appended AFTER the transport fact exists (success/failure/abort), never
435
+ * consulted for kernel input. The accounting policy id pins only when a measurement exists,
436
+ * so (usage, policy_id) deterministically recomputes the settlement that crossed the wire.
437
+ */
438
+ private appendProviderAttempt;
415
439
  private persistMemoryToStore;
416
440
  private retrieveMemoryFromStore;
417
441
  /**
@@ -24,7 +24,8 @@ import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAg
24
24
  import { resolveReducer } from "./reducers.js";
25
25
  import { loopInstruction, classifyInstruction, judgeGoal, dependencyOutputsNote, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
26
26
  import { governancePolicyToKernelEvent, governanceFilterSchema } from "../governance.js";
27
- import { createProviderRequestPlanForProvider, estimateProviderPromptTokens, measurementForPlan, recordPromptMeasurement, } from "../providers/request-plan.js";
27
+ import { createProviderRequestPlanForProvider, estimateProviderPromptTokens, measurementForPlan, recordPromptMeasurement, resolveProviderRoute, } from "../providers/request-plan.js";
28
+ import { FULL_FOOTPRINT_USAGE_ACCOUNTING_POLICY, providerAttemptToRecord, tryNormalizeProviderUsage, } from "./execution-evidence.js";
28
29
  import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
29
30
  import { assertNativeProfile } from "./os-profile.js";
30
31
  import { PayloadStore } from "./payload-store.js";
@@ -164,6 +165,16 @@ export class RuntimeRunner {
164
165
  composedSystemPrompt;
165
166
  /** H1.2: present only when `opts.nudges` is non-empty; else null and the append funnel is untouched. */
166
167
  nudgeEngine;
168
+ /** P4-S1: the run's resolved provider route, assembled once at construction (P4 §0.2 — the
169
+ * provider is fixed for the run today; every attempt references this same route object). */
170
+ providerRoute;
171
+ /** P4-S2: measurement→settlement policy; default = the exact implicit behavior (numbers unchanged). */
172
+ usageAccountingPolicy;
173
+ /** P4 §1.1: the active invocation's derived identity = its chain's FIRST effect_id. Tracked
174
+ * across kernel-driven provider retries (a provider_error commit arms `providerRetryPending`;
175
+ * the next call_provider adopts the pending invocation instead of opening a new one). */
176
+ activeProviderInvocationId;
177
+ providerRetryPending = false;
167
178
  constructor(opts) {
168
179
  this.opts = opts;
169
180
  const schemaAttempts = opts.workflowSchemaValidationAttempts ?? 2;
@@ -175,6 +186,8 @@ export class RuntimeRunner {
175
186
  if (opts.memoryPolicy)
176
187
  memoryPolicyToKernel(opts.memoryPolicy);
177
188
  this.composedSystemPrompt = composeSystemPrompt(opts.systemPrompt, opts.instructions);
189
+ this.providerRoute = resolveProviderRoute(opts.provider);
190
+ this.usageAccountingPolicy = opts.usageAccountingPolicy ?? FULL_FOOTPRINT_USAGE_ACCOUNTING_POLICY;
178
191
  if (opts.enableDiagnosticsDashboard) {
179
192
  const originalAppend = opts.sessionLog.append.bind(opts.sessionLog);
180
193
  opts.sessionLog.append = async (sessionId, event) => {
@@ -230,6 +243,18 @@ export class RuntimeRunner {
230
243
  this.fallbackPayloadStore ??= new PayloadStore();
231
244
  return this.fallbackPayloadStore;
232
245
  }
246
+ /**
247
+ * P4-S1 (G1): land one provider_attempt evidence record per effect execution. Pure host
248
+ * evidence (B7) — appended AFTER the transport fact exists (success/failure/abort), never
249
+ * consulted for kernel input. The accounting policy id pins only when a measurement exists,
250
+ * so (usage, policy_id) deterministically recomputes the settlement that crossed the wire.
251
+ */
252
+ async appendProviderAttempt(sessionId, attempt) {
253
+ await this.opts.sessionLog.append(sessionId, {
254
+ kind: "provider_attempt",
255
+ ...providerAttemptToRecord(attempt, attempt.usage ? this.usageAccountingPolicy.policyId : undefined),
256
+ });
257
+ }
233
258
  async persistMemoryToStore(memory, agentId) {
234
259
  if (!this.opts.memoryStore)
235
260
  throw new Error("memory persistence requires memoryStore");
@@ -754,6 +779,7 @@ export class RuntimeRunner {
754
779
  goal: `workflow:${spec.nodes.length} nodes`,
755
780
  criteria: [],
756
781
  agent_id: this.opts.agentId,
782
+ route: this.providerRoute,
757
783
  });
758
784
  await this.initializeWorkflowKernel(sessionId, runId, groupBudgetScope);
759
785
  }
@@ -1204,6 +1230,7 @@ export class RuntimeRunner {
1204
1230
  agent_id: this.opts.agentId,
1205
1231
  system_prompt: this.composedSystemPrompt,
1206
1232
  ...(attachments ? { attachments } : {}),
1233
+ route: this.providerRoute,
1207
1234
  });
1208
1235
  }
1209
1236
  yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, attachments, runId);
@@ -1233,7 +1260,10 @@ export class RuntimeRunner {
1233
1260
  yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true, start.attachments, start.run_id);
1234
1261
  }
1235
1262
  /** Execute a kernel-owned approval effect and return the correlated decision lists. */
1236
- async resolveApprovalRequests(requests, runtime, sessionId) {
1263
+ async resolveApprovalRequests(requests, runtime, sessionId,
1264
+ /** P3-S2 (G4): the request_approval effect's id — pinned on the denial's tool_completed
1265
+ * so the denial evidence joins the journal effect chain like an executed tool's does. */
1266
+ effectId) {
1237
1267
  const approved = [];
1238
1268
  const denied = [];
1239
1269
  const events = [];
@@ -1306,6 +1336,7 @@ export class RuntimeRunner {
1306
1336
  error_kind: "governance_denied",
1307
1337
  content: { blocks: [{ type: "text", text: `permission denied: ${denyReason}` }] },
1308
1338
  }],
1339
+ ...(effectId !== undefined ? { effect_id: effectId } : {}),
1309
1340
  });
1310
1341
  }
1311
1342
  }
@@ -1319,6 +1350,8 @@ export class RuntimeRunner {
1319
1350
  this.pendingPageOutArchives = [];
1320
1351
  this.activePageOutArchive = undefined;
1321
1352
  this.currentSessionId = sessionId;
1353
+ this.activeProviderInvocationId = undefined;
1354
+ this.providerRetryPending = false;
1322
1355
  if (this.opts.enableDiagnosticsDashboard) {
1323
1356
  this.dashboard = new KernelPrimitivesDashboard(sessionId);
1324
1357
  }
@@ -1557,6 +1590,18 @@ export class RuntimeRunner {
1557
1590
  break;
1558
1591
  if (action.kind === "call_provider") {
1559
1592
  const providerEffectId = action.effectId;
1593
+ // P4 §1.1: invocation identity is derived, never minted — the chain's FIRST effect_id.
1594
+ // A provider_error commit armed `providerRetryPending`, so this effect CONTINUES the
1595
+ // pending invocation; otherwise it opens a new one. The chain itself lives in the
1596
+ // journal (each hop a Failed resolution input → new effect); this is the SessionLog
1597
+ // evidence projection of it.
1598
+ if (!this.providerRetryPending || this.activeProviderInvocationId === undefined) {
1599
+ this.activeProviderInvocationId = providerEffectId;
1600
+ }
1601
+ this.providerRetryPending = false;
1602
+ const invocationId = this.activeProviderInvocationId;
1603
+ // Host wall-clock, pure evidence (B7/DEC-2): never crosses into kernel input.
1604
+ const attemptStartedAtMs = Date.now();
1560
1605
  const finalToolCalls = [];
1561
1606
  let finalText = "";
1562
1607
  // I5: governance schema-level pre-filter. When a declarative GovernancePolicy is loaded
@@ -1588,6 +1633,9 @@ export class RuntimeRunner {
1588
1633
  let turnCacheTelemetrySource;
1589
1634
  let turnCacheReadBySlot;
1590
1635
  let turnStopReason;
1636
+ // P4 §2: the raw postflight provider usage frame, kept whole so the attempt's
1637
+ // measurement carries full fields (only the settlement crosses the kernel boundary, B4).
1638
+ let turnProviderUsage;
1591
1639
  const providerPlan = createProviderRequestPlanForProvider(this.opts.provider, context, tools, ext);
1592
1640
  const recorded = measurementForPlan(providerPlan, recordedMeasurements.get(providerPlan.fingerprint));
1593
1641
  let promptMeasurement = recorded;
@@ -1614,6 +1662,7 @@ export class RuntimeRunner {
1614
1662
  kind: "prompt_measured",
1615
1663
  turn: runtime.turn(),
1616
1664
  measurement: promptMeasurement,
1665
+ effect_id: providerEffectId,
1617
1666
  });
1618
1667
  }
1619
1668
  const reservedPromptTokens = (this.opts.promptBudget?.promptOverheadTokens ?? 0)
@@ -1626,6 +1675,23 @@ export class RuntimeRunner {
1626
1675
  && promptMeasurement.source.kind !== "heuristic"
1627
1676
  && promptMeasurement.inputTokens + reservedPromptTokens > this.opts.maxTokens;
1628
1677
  if (context.budgetOverflow || measuredOverflow) {
1678
+ // P4 §1.2: blocked BEFORE any transport — zero rungs, status rejected. The
1679
+ // fingerprint still binds the would-be request to its prompt_measured record (G2).
1680
+ await this.appendProviderAttempt(sessionId, {
1681
+ effectId: providerEffectId,
1682
+ attemptSeq: 1,
1683
+ route: this.providerRoute,
1684
+ requestFingerprint: providerPlan.fingerprint,
1685
+ status: "rejected",
1686
+ transportRungs: 0,
1687
+ lastErrorClass: "context_overflow",
1688
+ startedAtMs: attemptStartedAtMs,
1689
+ finishedAtMs: Date.now(),
1690
+ wireEvidence: {
1691
+ protocol: this.providerRoute.protocol,
1692
+ request_fingerprint: providerPlan.fingerprint,
1693
+ },
1694
+ });
1629
1695
  action = await this.commitKernelAction(runtime, this.pendingObservations, {
1630
1696
  kind: "provider_error",
1631
1697
  effect_id: providerEffectId,
@@ -1633,6 +1699,7 @@ export class RuntimeRunner {
1633
1699
  error_kind: "context_overflow",
1634
1700
  retryable: false,
1635
1701
  });
1702
+ this.providerRetryPending = action.kind === "call_provider";
1636
1703
  continue;
1637
1704
  }
1638
1705
  const abortSignal = this.abortController?.signal;
@@ -1648,6 +1715,7 @@ export class RuntimeRunner {
1648
1715
  turnTokens = usageEvt.totalTokens;
1649
1716
  turnInputTokens = usageEvt.inputTokens ?? 0;
1650
1717
  turnOutputTokens = usageEvt.outputTokens ?? 0;
1718
+ turnProviderUsage = usageEvt.providerUsage ?? turnProviderUsage;
1651
1719
  // P0-C: capture the prompt-cache split for the tool-gating hit-rate baseline.
1652
1720
  turnCacheReadTokens = usageEvt.cacheReadInputTokens ?? 0;
1653
1721
  turnCacheCreationTokens = usageEvt.cacheCreationInputTokens ?? 0;
@@ -1679,6 +1747,7 @@ export class RuntimeRunner {
1679
1747
  source: postflight.source,
1680
1748
  confidence: postflight.confidence,
1681
1749
  },
1750
+ effect_id: providerEffectId,
1682
1751
  });
1683
1752
  }
1684
1753
  // Phase 4: stop_reason drives the kernel's max-output-tokens recovery. The closing
@@ -1706,6 +1775,26 @@ export class RuntimeRunner {
1706
1775
  const provider = this.opts.provider.descriptor?.().provider ?? "unknown";
1707
1776
  const providerError = classifyProviderError(provider, err);
1708
1777
  const message = providerError.message;
1778
+ // P4 §1.2: the transport ladder is exhausted — one attempt record, rung count from
1779
+ // provider telemetry (1 on the single-shot stream path), error CLASS only (B1:
1780
+ // never the raw vendor text).
1781
+ const telemetry = this.opts.provider.peekTransportTelemetry?.();
1782
+ await this.appendProviderAttempt(sessionId, {
1783
+ effectId: providerEffectId,
1784
+ attemptSeq: 1,
1785
+ route: this.providerRoute,
1786
+ requestFingerprint: providerPlan.fingerprint,
1787
+ status: "transport_exhausted",
1788
+ transportRungs: telemetry?.rungs ?? 1,
1789
+ lastErrorClass: providerError.kind,
1790
+ startedAtMs: attemptStartedAtMs,
1791
+ finishedAtMs: Date.now(),
1792
+ wireEvidence: {
1793
+ protocol: this.providerRoute.protocol,
1794
+ request_fingerprint: providerPlan.fingerprint,
1795
+ ...(telemetry?.responseId !== undefined ? { response_id: telemetry.responseId } : {}),
1796
+ },
1797
+ });
1709
1798
  // Reactive recovery is now a kernel decision. Forward the raw provider error and
1710
1799
  // dispatch whatever the kernel returns: `call_provider` to retry with a freshly
1711
1800
  // compacted context, or `done` to terminate with an honest `ContextOverflow`. The
@@ -1719,6 +1808,9 @@ export class RuntimeRunner {
1719
1808
  message,
1720
1809
  ...providerErrorEventFields(providerError),
1721
1810
  });
1811
+ // P4 §1.1: a kernel-recovered retry CONTINUES this invocation (the journal holds the
1812
+ // causation hop); a terminal closes it.
1813
+ this.providerRetryPending = action.kind === "call_provider";
1722
1814
  // Withholding (query.ts parity): surface the raw provider error only when the kernel
1723
1815
  // could NOT recover (it returned a terminal). On a recovered retry (`call_provider`)
1724
1816
  // the error stays hidden, so embedders that terminate on `error` events don't see a
@@ -1731,6 +1823,23 @@ export class RuntimeRunner {
1731
1823
  }
1732
1824
  // Do not commit partial provider output after host cancellation.
1733
1825
  if (abortSignal?.aborted) {
1826
+ // P4 §1.2: host cancellation mid-stream — the attempt is evidence too.
1827
+ const telemetry = this.opts.provider.peekTransportTelemetry?.();
1828
+ await this.appendProviderAttempt(sessionId, {
1829
+ effectId: providerEffectId,
1830
+ attemptSeq: 1,
1831
+ route: this.providerRoute,
1832
+ requestFingerprint: providerPlan.fingerprint,
1833
+ status: "aborted",
1834
+ transportRungs: telemetry?.rungs ?? 1,
1835
+ startedAtMs: attemptStartedAtMs,
1836
+ finishedAtMs: Date.now(),
1837
+ wireEvidence: {
1838
+ protocol: this.providerRoute.protocol,
1839
+ request_fingerprint: providerPlan.fingerprint,
1840
+ ...(telemetry?.responseId !== undefined ? { response_id: telemetry.responseId } : {}),
1841
+ },
1842
+ });
1734
1843
  action = await this.commitKernelAction(runtime, this.pendingObservations, {
1735
1844
  kind: "cancel_operation",
1736
1845
  reason: this.cancellationReason ?? "user",
@@ -1762,12 +1871,37 @@ export class RuntimeRunner {
1762
1871
  toolCalls: canonicalToolCalls,
1763
1872
  tokenCount: turnOutputTokens || turnTokens || undefined,
1764
1873
  };
1874
+ // P4 §2: assemble the measurement from the exact numbers that cross the boundary today
1875
+ // (inputTokens/outputTokens turn counters), enriched with the raw provider frame's cache
1876
+ // split and reasoning fields. An invalid frame degrades to no measurement (evidence
1877
+ // never breaks a run); the settlement then falls back to the raw counts below.
1878
+ const attemptUsage = (turnInputTokens > 0 || turnOutputTokens > 0)
1879
+ ? tryNormalizeProviderUsage({
1880
+ inputTokens: turnInputTokens,
1881
+ outputTokens: turnOutputTokens,
1882
+ ...(turnProviderUsage?.cacheReadInputTokens !== undefined
1883
+ ? { cacheReadInputTokens: turnProviderUsage.cacheReadInputTokens }
1884
+ : turnCacheReadTokens > 0 ? { cacheReadInputTokens: turnCacheReadTokens } : {}),
1885
+ ...(turnProviderUsage?.cacheCreationInputTokens !== undefined
1886
+ ? { cacheCreationInputTokens: turnProviderUsage.cacheCreationInputTokens }
1887
+ : turnCacheCreationTokens > 0 ? { cacheCreationInputTokens: turnCacheCreationTokens } : {}),
1888
+ ...(turnProviderUsage?.reasoningTokens !== undefined
1889
+ ? { reasoningTokens: turnProviderUsage.reasoningTokens } : {}),
1890
+ cacheTelemetryStatus: turnCacheTelemetryStatus,
1891
+ ...(turnCacheTelemetrySource !== undefined
1892
+ ? { cacheTelemetrySource: turnCacheTelemetrySource } : {}),
1893
+ })
1894
+ : undefined;
1895
+ const settlement = attemptUsage ? this.usageAccountingPolicy.settle(attemptUsage) : undefined;
1765
1896
  const providerEvent = {
1766
1897
  kind: "provider_result",
1767
1898
  effect_id: providerEffectId,
1768
1899
  message: messageToKernelMessage(assistantMessage),
1769
- ...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
1770
- ...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
1900
+ // P4-S2: observed_* now comes from the pinned policy's settlement of the measurement.
1901
+ // Under the default full-footprint policy these are provably the numbers the runner
1902
+ // has always fed (inputTokens/outputTokens verbatim under the same >0 gates).
1903
+ ...(turnInputTokens > 0 ? { observed_input_tokens: settlement?.observed_input_tokens ?? turnInputTokens } : {}),
1904
+ ...(turnOutputTokens > 0 ? { observed_output_tokens: settlement?.observed_output_tokens ?? turnOutputTokens } : {}),
1771
1905
  ...(turnStopReason ? { stop_reason: turnStopReason } : {}),
1772
1906
  };
1773
1907
  if (this.opts.skillDir) {
@@ -1798,14 +1932,38 @@ export class RuntimeRunner {
1798
1932
  }
1799
1933
  }
1800
1934
  }
1801
- action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
1935
+ // P4-S1: land the attempt evidence BEFORE the kernel resolution commits — the record
1936
+ // describes the transport fact, which exists regardless of what the kernel decides next.
1937
+ const attemptTelemetry = this.opts.provider.peekTransportTelemetry?.();
1802
1938
  const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
1939
+ const wireEvidence = {
1940
+ protocol: this.providerRoute.protocol,
1941
+ request_fingerprint: providerPlan.fingerprint,
1942
+ ...(attemptTelemetry?.responseId !== undefined ? { response_id: attemptTelemetry.responseId } : {}),
1943
+ ...(providerReplay !== undefined ? { replay_state: providerReplay } : {}),
1944
+ };
1945
+ await this.appendProviderAttempt(sessionId, {
1946
+ effectId: providerEffectId,
1947
+ attemptSeq: 1,
1948
+ route: this.providerRoute,
1949
+ requestFingerprint: providerPlan.fingerprint,
1950
+ status: "success",
1951
+ transportRungs: attemptTelemetry?.rungs ?? 1,
1952
+ startedAtMs: attemptStartedAtMs,
1953
+ finishedAtMs: Date.now(),
1954
+ ...(attemptUsage !== undefined ? { usage: attemptUsage } : {}),
1955
+ wireEvidence,
1956
+ });
1957
+ action = await this.commitKernelAction(runtime, this.pendingObservations, providerEvent);
1803
1958
  await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
1804
1959
  turn: runtime.turn(),
1805
1960
  content: finalText,
1806
1961
  tokenCount: turnOutputTokens || turnTokens || undefined,
1807
1962
  toolCalls: finalToolCalls,
1808
1963
  providerReplay,
1964
+ effectId: providerEffectId,
1965
+ invocationId,
1966
+ wireEvidence,
1809
1967
  }));
1810
1968
  // P0-C: emit per-turn tool-gating telemetry. `activeSkill` reflects the skill in effect
1811
1969
  // GOING INTO this turn; a `skill` call here only takes effect next turn, so emit first, then
@@ -1840,7 +1998,7 @@ export class RuntimeRunner {
1840
1998
  }
1841
1999
  }
1842
2000
  else if (action.kind === "request_approval") {
1843
- const resolved = await this.resolveApprovalRequests(action.requests, runtime, sessionId);
2001
+ const resolved = await this.resolveApprovalRequests(action.requests, runtime, sessionId, action.effectId);
1844
2002
  for (const event of resolved.events)
1845
2003
  yield event;
1846
2004
  action = await this.commitKernelAction(runtime, this.pendingObservations, {
@@ -2170,6 +2328,7 @@ export class RuntimeRunner {
2170
2328
  token_count: r.tokenCount,
2171
2329
  content: { blocks: toolOutputBlocksToDurable(r.contentParts?.length ? r.contentParts : [{ type: "text", text: r.output }]) },
2172
2330
  })),
2331
+ effect_id: toolEffectId,
2173
2332
  });
2174
2333
  // The canonical provider resolution already activates a successfully resolved `skill` call.
2175
2334
  // The host's remaining responsibility is to pin the resolved METHOD content — how to do
@@ -1,6 +1,7 @@
1
1
  import type { KernelPrimitive } from "./kernel-event-log.js";
2
- import type { ContentPart, ProviderReplay, ToolCall, ToolErrorKind } from "../types.js";
3
- import type { RecordedPromptMeasurement } from "../providers/request-plan.js";
2
+ import type { ContentPart, ProviderReplay, ProviderWireEvidence, ToolCall, ToolErrorKind } from "../types.js";
3
+ import type { RecordedPromptMeasurement, ResolvedProviderRoute } from "../providers/request-plan.js";
4
+ import type { ProviderAttemptRecord } from "./execution-evidence.js";
4
5
  import type { MemoryRecall, MemoryScope } from "../memory/protocols.js";
5
6
  import type { KernelJournal } from "./kernel-journal.js";
6
7
  export type RollbackReason = {
@@ -30,6 +31,7 @@ export type SessionEvent = {
30
31
  agent_id?: string;
31
32
  system_prompt?: string;
32
33
  attachments?: ContentPart[];
34
+ route?: ResolvedProviderRoute;
33
35
  } | {
34
36
  kind: "llm_completed";
35
37
  turn: number;
@@ -37,11 +39,17 @@ export type SessionEvent = {
37
39
  token_count?: number;
38
40
  tool_calls: ToolCall[];
39
41
  provider_replay?: ProviderReplay;
42
+ effect_id?: string;
43
+ invocation_id?: string;
44
+ wire_evidence?: ProviderWireEvidence;
40
45
  } | {
41
46
  kind: "prompt_measured";
42
47
  turn: number;
43
48
  measurement: RecordedPromptMeasurement;
44
- } | {
49
+ effect_id?: string;
50
+ } | ({
51
+ kind: "provider_attempt";
52
+ } & ProviderAttemptRecord) | {
45
53
  kind: "tool_requested";
46
54
  turn: number;
47
55
  calls: ToolCall[];
@@ -59,6 +67,7 @@ export type SessionEvent = {
59
67
  blocks: Record<string, unknown>[];
60
68
  };
61
69
  }>;
70
+ effect_id?: string;
62
71
  } | {
63
72
  kind: "tool_argument_repaired";
64
73
  turn: number;
@@ -326,6 +335,14 @@ export type SessionEvent = {
326
335
  reason: string;
327
336
  coerced_from?: string;
328
337
  };
338
+ export type SessionEventKind = SessionEvent["kind"];
339
+ /**
340
+ * The registered session-event vocabulary (F9 / S3, P7-S4). This list is the single authority
341
+ * the cross-SDK manifest fixture pins: a kind added here without the same-commit update to
342
+ * `tests/fixtures/sdk-conformance/canonical/session-event-vocabulary.json` and the python/wasm
343
+ * vocabularies turns cross-SDK conformance red. Declared in `SessionEvent` union order.
344
+ */
345
+ export declare const SESSION_EVENT_KINDS: readonly ["run_started", "llm_completed", "prompt_measured", "provider_attempt", "tool_requested", "tool_completed", "tool_argument_repaired", "tool_denied", "permission_requested", "permission_resolved", "compressed", "page_out", "semantic_archive_pending", "semantic_archive_completed", "semantic_archive_failed", "page_in", "rollbacked", "capability_changed", "context_renewed", "suspended", "resumed", "tool_gated", "signal_delivery_disposed", "budget_exceeded", "budget_usage_reported", "operation_cancelled", "milestone_advanced", "milestone_blocked", "checkpoint_taken", "entropy_sample", "entropy_alert", "agent_process_changed", "memory_written", "memory_queried", "memory_validation_failed", "memory_write_failed", "memory_query_failed", "memory_retrieval_result", "workflow_node_completed", "workflow_nodes_submitted", "workflow_batch_spawned", "workflow_completed", "kernel_observation", "run_terminal", "summary_upgraded", "group_member_joined", "group_budget_charged", "round_started", "round_paced"];
329
346
  /**
330
347
  * The business-projection log (spec §9.2): run started/terminal, stream events, observations,
331
348
  * provider/tool presentation, audit metadata.