@arnilo/prism 0.9.0 → 0.11.0

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/README.md +19 -16
  3. package/dist/agent-approval.d.ts +7 -1
  4. package/dist/agent-approval.js +15 -6
  5. package/dist/agent-run-lifecycle.d.ts +2 -1
  6. package/dist/agent-run-lifecycle.js +20 -6
  7. package/dist/agent-run-state.d.ts +26 -5
  8. package/dist/agent-run-state.js +97 -1
  9. package/dist/agent-session/event-subscriber.d.ts +2 -0
  10. package/dist/agent-session/event-subscriber.js +3 -0
  11. package/dist/agent-session/session/assemble.js +165 -16
  12. package/dist/agent-session/session/persist.js +11 -5
  13. package/dist/agent-session/session/provider-round.js +54 -13
  14. package/dist/agent-session/session/tool-round.d.ts +2 -2
  15. package/dist/agent-session/session/tool-round.js +86 -23
  16. package/dist/agent-session/session/types.d.ts +21 -2
  17. package/dist/agent-session/session.d.ts +66 -4
  18. package/dist/agent-session/session.js +159 -18
  19. package/dist/checkpoint-restore.d.ts +50 -14
  20. package/dist/checkpoint-restore.js +104 -28
  21. package/dist/context-budget.d.ts +11 -0
  22. package/dist/context-budget.js +33 -2
  23. package/dist/contracts-core/agent.d.ts +26 -5
  24. package/dist/contracts-core/extensions.d.ts +3 -0
  25. package/dist/contracts-core/guardrail-packs.d.ts +8 -3
  26. package/dist/contracts-core/loop.d.ts +36 -0
  27. package/dist/contracts-core/provider.d.ts +6 -1
  28. package/dist/contracts-core/run-limits.d.ts +10 -1
  29. package/dist/contracts-core/session.d.ts +2 -1
  30. package/dist/contracts-protocol.d.ts +6 -4
  31. package/dist/contracts-run-state.d.ts +48 -6
  32. package/dist/contributions.d.ts +2 -1
  33. package/dist/contributions.js +1 -0
  34. package/dist/extensions.d.ts +15 -1
  35. package/dist/extensions.js +68 -0
  36. package/dist/guardrail-packs/types.d.ts +10 -0
  37. package/dist/guardrail-packs/validation-respect.js +16 -0
  38. package/dist/guardrails.d.ts +42 -1
  39. package/dist/guardrails.js +124 -15
  40. package/dist/index.d.ts +7 -7
  41. package/dist/index.js +4 -4
  42. package/dist/leases.js +32 -6
  43. package/dist/middleware.d.ts +1 -1
  44. package/dist/node/contribution-discovery.d.ts +16 -1
  45. package/dist/node/contribution-discovery.js +47 -0
  46. package/dist/node/session-store-jsonl.js +67 -17
  47. package/dist/run-bundle.d.ts +6 -1
  48. package/dist/run-bundle.js +4 -1
  49. package/dist/run-limits.d.ts +11 -5
  50. package/dist/run-limits.js +13 -0
  51. package/dist/session-stores.js +61 -12
  52. package/dist/testing/prefix-stability-conformance.d.ts +73 -1
  53. package/dist/testing/prefix-stability-conformance.js +158 -27
  54. package/dist/tools.js +10 -3
  55. package/dist/usage-estimation.d.ts +7 -1
  56. package/dist/usage-estimation.js +16 -10
  57. package/docs/acp.md +2 -2
  58. package/docs/agent-events.md +15 -10
  59. package/docs/agent-session-runtime.md +10 -7
  60. package/docs/coding-agent-tools.md +1 -1
  61. package/docs/coding-tools.md +7 -11
  62. package/docs/compaction-llm.md +2 -0
  63. package/docs/compaction-observational-memory.md +21 -1
  64. package/docs/context-and-skills.md +6 -7
  65. package/docs/contribution-discovery.md +13 -0
  66. package/docs/durable-runs.md +14 -6
  67. package/docs/embeddings.md +7 -1
  68. package/docs/execution-timeline.md +9 -2
  69. package/docs/extensions.md +21 -5
  70. package/docs/guardrails.md +16 -6
  71. package/docs/hooks.md +282 -0
  72. package/docs/impeccable.md +1 -2
  73. package/docs/index.md +28 -21
  74. package/docs/input-and-prompt-assembly.md +1 -1
  75. package/docs/instruction-injection.md +1 -0
  76. package/docs/live-testing.md +3 -2
  77. package/docs/memory-fabric.md +29 -0
  78. package/docs/middleware-hooks.md +54 -4
  79. package/docs/migrate-to-0.11.md +65 -0
  80. package/docs/migration.md +24 -0
  81. package/docs/node-jsonl-session-store.md +4 -3
  82. package/docs/operations.md +1 -1
  83. package/docs/options-index.md +3 -1
  84. package/docs/peer-dependencies.md +3 -5
  85. package/docs/policy-and-audit.md +15 -2
  86. package/docs/prefix-stability-conformance.md +82 -9
  87. package/docs/provider-packages.md +20 -20
  88. package/docs/public-contracts.md +2 -1
  89. package/docs/rag.md +94 -7
  90. package/docs/release-and-install.md +62 -59
  91. package/docs/runs-and-usage.md +21 -10
  92. package/docs/scoped-agent-memory.md +17 -9
  93. package/docs/scoped-memory.md +138 -0
  94. package/docs/session-stores.md +2 -2
  95. package/docs/supervisors.md +14 -6
  96. package/docs/testing.md +17 -9
  97. package/docs/tools.md +1 -1
  98. package/docs/wiki.md +4 -2
  99. package/docs/workflows.md +2 -2
  100. package/package.json +8 -5
  101. package/docs/caveman.md +0 -130
  102. package/docs/graft.md +0 -149
  103. package/docs/ponytail.md +0 -129
@@ -1,8 +1,8 @@
1
1
  /** Shared host/round types for runInternal phase split (plan 059). Internal only. */
2
2
  import type { ActiveDurableRun } from "../../agent-approval.js";
3
- import type { PendingToolCall } from "../../agent-run-state.js";
3
+ import type { PendingToolCall, PersistedGuardrailPacks } from "../../agent-run-state.js";
4
4
  import type { AttentionFoldLedger, AttentionStickyFrontier, PersistedAttentionFoldLedger, PersistedAttentionStickyFrontier } from "../../attention-compiler.js";
5
- import type { Agent, AgentEvent, AgentFinishReason, AgentLoopStrategy, AgentRunResult, AIProvider, ErrorInfo, Guardrails, LoopContext, Message, ModelConfig, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunLedger, RunOptions, SessionEntry, SessionStore, Skill, ToolCallSummary, ToolDefinition, ToolEffectStore, ToolRegistry, ToolResult, Usage } from "../../contracts.js";
5
+ import type { Agent, AgentEvent, AgentFinishReason, AgentLoopStrategy, AgentRunResult, AIProvider, ErrorInfo, Guardrails, LoopContext, Message, ModelConfig, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunLedger, RunOptions, SessionEntry, SessionStore, Skill, StopHook, ToolCallSummary, ToolDefinition, ToolEffectStore, ToolRegistry, ToolResult, Usage } from "../../contracts.js";
6
6
  import type { AgentIdentity } from "../../identity.js";
7
7
  import type { AgentInput } from "../../input.js";
8
8
  import type { SecretRedactor } from "../../redaction.js";
@@ -35,6 +35,12 @@ export type SessionHost = {
35
35
  activeGuardrails?: Guardrails;
36
36
  /** Plan 092 Task 2: packs compiled once at session construction; read-only for phases. */
37
37
  readonly packGuardrails?: Guardrails;
38
+ /** Plan 104 T3: `ask` rules as the durable charge-time gate (`interrupt` records) and as plain
39
+ * blocks for a run that cannot suspend. */
40
+ readonly packAskGate?: Guardrails;
41
+ readonly packAskBlocks?: Guardrails;
42
+ /** Plan 104 T2: pack refs + live pack-owned state for a durable checkpoint. */
43
+ serializedGuardrailPackState(): PersistedGuardrailPacks | undefined;
38
44
  activeMetadata?: Readonly<Record<string, unknown>>;
39
45
  activePromptVersion?: PromptVersionRef;
40
46
  activeLimits?: RunLimitTracker;
@@ -83,6 +89,8 @@ export type SessionHost = {
83
89
  emit(event: AgentEvent): void;
84
90
  rebuildHistory(): Promise<void>;
85
91
  resolveRunSkills(options: RunOptions, tools: readonly ToolDefinition[]): readonly Skill[];
92
+ /** Redacted, cap-checked steer queue push (plan 106 R1 uses it for stop-hook continuations). */
93
+ steer(input: AgentInput): void;
86
94
  appendEntry(entry: SessionEntry): Promise<void>;
87
95
  redact<T>(value: T): T;
88
96
  appendMessage(message: Message, runId: string): Promise<void>;
@@ -99,12 +107,21 @@ export type SessionHost = {
99
107
  readonly status: AgentRunResult["status"];
100
108
  readonly usage?: Usage;
101
109
  readonly limit?: import("../../contracts.js").RunLimitBreach;
110
+ readonly attribution?: import("../../run-limits.js").BudgetExhaustionAttribution;
102
111
  readonly error?: ErrorInfo;
103
112
  readonly abortReason?: string;
104
113
  readonly runState?: import("../../contracts.js").AgentRunState;
105
114
  readonly interruption?: import("../../contracts.js").AgentRunInterruption;
106
115
  }): AgentRunResult;
116
+ /**
117
+ * Plan 106 R2: dispatch `session_start` middleware once per session (first run start), awaited by
118
+ * the run assembler after the `agent_started`/`agent_resumed` emits. No-op on every later call.
119
+ */
120
+ openSession(runId: string): Promise<void>;
121
+ /** Session teardown: close every subscriber, run-scoped and `acrossRuns` alike. */
107
122
  closeSubscribers(): void;
123
+ /** Run end (finish, suspend, or deny): close only the subscribers that do not opt into `acrossRuns`. */
124
+ closeRunSubscribers(): void;
108
125
  snapshot(): Promise<SessionContextSnapshot>;
109
126
  };
110
127
  export declare function asSessionHost(session: unknown): SessionHost;
@@ -151,6 +168,8 @@ export type RoundContext = {
151
168
  toolResults: ToolResult[];
152
169
  /** Set when a `RunOptions.turnPolicy` stop ended the loop (plan 084 Task 2). */
153
170
  runStop?: RunStopInfo;
171
+ /** Merged agent + run stop hooks, in invocation order (plan 106 R1). Empty = wrapper skipped. */
172
+ stopHooks: readonly StopHook[];
154
173
  runUsage: {
155
174
  add(usage: Usage): void;
156
175
  value(): Usage | undefined;
@@ -1,8 +1,8 @@
1
1
  /** session (0.2.5 plan 025 Task 1 split). Moved verbatim from agent-session.ts; public surface unchanged behind the barrel. */
2
2
  import { ActiveDurableRun, ActiveDurableRunExtras } from "../agent-approval.js";
3
- import type { PendingToolCall, StoredAgentRunState } from "../agent-run-state.js";
3
+ import type { PendingToolCall, PersistedGuardrailPacks, StoredAgentRunState } from "../agent-run-state.js";
4
4
  import { type AttentionFoldLedger, type AttentionStickyFrontier, type PersistedAttentionFoldLedger, type PersistedAttentionStickyFrontier } from "../attention-compiler.js";
5
- import type { Agent, AgentEvent, AgentRunResult, AgentRunState, AgentRunStateOptions, AgentSession, AgentSessionConfig, AIProvider, CompactionOptions, CompactionResult, ContextMeter, ErrorInfo, Guardrails, Message, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunDecision, RunOptions, SessionEntry, Skill, SteerOptions, SubscribeOptions, ToolCallSummary, ToolDefinition, ToolEffectStore, Usage } from "../contracts.js";
5
+ import type { Agent, AgentEvent, AgentRunResult, AgentRunState, AgentRunStateOptions, AgentSession, AgentSessionConfig, AIProvider, CompactionOptions, CompactionResult, ContextMeter, ErrorInfo, GuardrailPackRef, Guardrails, Message, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunDecision, RunOptions, SessionEntry, Skill, SteerOptions, SubscribeOptions, ToolCallSummary, ToolDefinition, ToolEffectStore, Usage } from "../contracts.js";
6
6
  import type { AgentIdentity } from "../identity.js";
7
7
  import type { AgentInput } from "../input.js";
8
8
  import type { RunLimitTracker } from "../run-limits.js";
@@ -14,6 +14,10 @@ export declare class RuntimeAgentSession implements AgentSession {
14
14
  private readonly metadata?;
15
15
  private readonly store;
16
16
  private readonly subscribers;
17
+ /** Plan 106 R2: `session_start` is dispatched at the first run start, once per runtime session. */
18
+ private sessionOpened;
19
+ /** Plan 106 R2: `close()` dispatches `session_shutdown` and closes subscribers exactly once. */
20
+ private closed;
17
21
  private currentLeafId?;
18
22
  private history;
19
23
  private activeRun?;
@@ -31,9 +35,15 @@ export declare class RuntimeAgentSession implements AgentSession {
31
35
  private activeIdempotencyKey?;
32
36
  private activeGuardrails?;
33
37
  /** Plan 092 Task 2: guardrail packs compiled once in the constructor; merged into every run's `activeGuardrails`. */
34
- readonly packGuardrails?: Guardrails;
38
+ packGuardrails?: Guardrails;
39
+ /** Plan 104 Task 3: `ask` rules as the durable charge-time gate (a match records `interrupt`). */
40
+ packAskGate?: Guardrails;
41
+ /** Plan 104 Task 3: the same `ask` rules as plain blocks for a run that cannot suspend. */
42
+ packAskBlocks?: Guardrails;
35
43
  /** Original pack refs, carried into `fork()`/`clone()` so a branch cannot silently lose its policy. */
36
- private readonly guardrailPackRefs?;
44
+ private packRefs?;
45
+ /** Plan 104 Task 2: compiled refs + live pack state, replaced by `restoreGuardrailPacks` on resume. */
46
+ private compiledGuardrailPacks?;
37
47
  activeMetadata?: Readonly<Record<string, unknown>>;
38
48
  activePromptVersion?: PromptVersionRef;
39
49
  activeLimits?: RunLimitTracker;
@@ -95,11 +105,28 @@ export declare class RuntimeAgentSession implements AgentSession {
95
105
  clearActivatedTools(): void;
96
106
  /** Plan 018 Task 6: restore persisted loaded-skill bodies (already validated fail-closed at load). */
97
107
  restoreLoadedSkillBodies(bodies: readonly LoadedSkillBodiesEntry[]): void;
108
+ /** Plan 104 T2: the refs this session actually enforces (restored ones after a resume). */
109
+ get guardrailPackRefs(): readonly GuardrailPackRef[] | undefined;
110
+ /** Plan 104 T2: pack refs + live pack-owned state for a durable checkpoint (opt-in with `persistSessionState`). */
111
+ serializedGuardrailPackState(): PersistedGuardrailPacks | undefined;
112
+ /**
113
+ * Plan 104 T2: recompile checkpoint packs before the resumed run's first turn. `state` present
114
+ * (even empty) marks a restore, so unknown ids, version mismatches, and codec-less state fail
115
+ * closed as `AgentRunStateError` — never a session that silently enforces less than it did.
116
+ */
117
+ restoreGuardrailPacks(refs: readonly GuardrailPackRef[], state?: Readonly<Record<string, unknown>>): void;
98
118
  private ledgerChain;
99
119
  private ledgerFailure;
100
120
  private snapshotGeneration;
101
121
  private snapshotCache?;
102
122
  private readonly snapshotCacheTtlMs;
123
+ /**
124
+ * Plan 103 T4: identity-keyed meter cache. Holds only the last public meter value
125
+ * plus the identity of everything the cold read consumed (`snapshotGeneration`,
126
+ * leaf, active meter/limits, and the history array reference + length, which catches
127
+ * in-place `history.push` during a run) — never history content, never an estimator.
128
+ */
129
+ private meterCache?;
103
130
  constructor(config: AgentSessionConfig & {
104
131
  readonly agent: Agent;
105
132
  });
@@ -111,15 +138,44 @@ export declare class RuntimeAgentSession implements AgentSession {
111
138
  * `provider_turn_finished.budgets` resolves them. Before any provider turn in
112
139
  * this session it estimates stored history, so a non-reporting model still
113
140
  * shows a working meter instead of zero. Never billing; estimates are labeled.
141
+ *
142
+ * Plan 103 T4: reads are cached until the history generation, leaf, history
143
+ * length, or active-run identity changes, so a per-frame poll pays one estimate
144
+ * per mutation instead of one per read. The cached value is frozen and is
145
+ * identical (`===`) to the previous read while nothing changed.
114
146
  */
115
147
  contextMeter(): ContextMeter;
148
+ /** Cold path of `contextMeter()`: one estimate over stored history plus cap/budget resolution. */
149
+ private measureContextMeter;
150
+ /**
151
+ * Live events for this session. A run-scoped subscriber (the default) is closed when the run ends,
152
+ * suspends, or is denied; `SubscribeOptions.acrossRuns: true` keeps one subscriber open across runs
153
+ * of the same session until the host closes it, the session tears it down, or its bounded queue
154
+ * overflows under the default policy. Subscribe before `run()`; the consumer loop and `run()` must
155
+ * run concurrently, since events are only emitted during a live run.
156
+ */
116
157
  subscribe(options?: SubscribeOptions): AsyncIterable<AgentEvent>;
158
+ private createSubscriber;
117
159
  run(input: AgentInput, options?: RunOptions): Promise<AgentRunResult>;
118
160
  steer(input: AgentInput, options?: SteerOptions): void;
119
161
  resumeDurable(state: StoredAgentRunState, runState: AgentRunStateOptions, ownership?: OwnershipScope, signal?: AbortSignal, decisions?: ReadonlyMap<string, RunDecision>, extras?: ActiveDurableRunExtras): Promise<AgentRunResult>;
120
162
  recordDurableResumption(runId: string, interruption: import("../contracts.js").AgentRunInterruption, version: number, ownership?: OwnershipScope): Promise<void>;
121
163
  recordDurableDenial(runId: string, interruption: import("../contracts.js").AgentRunInterruption, version: number, ownership?: OwnershipScope): Promise<void>;
122
164
  private runInternal;
165
+ /**
166
+ * Plan 106 R2: dispatch `session_start` once per session, at its first run start (including the
167
+ * first run of a session rebuilt from a durable checkpoint). The run assembler awaits it right
168
+ * after `agent_started`/`agent_resumed`, so session-scoped provisioning is done before the first
169
+ * turn while the runtime's synchronous emit burst stays intact. Middleware error policy decides
170
+ * whether a failure surfaces or becomes an `extension_error` event.
171
+ */
172
+ openSession(runId: string): Promise<void>;
173
+ /**
174
+ * Plan 106 R2: session teardown. Dispatches `session_shutdown` middleware once (idempotent) and
175
+ * then closes every subscriber, run-scoped and `acrossRuns` alike. Call it after the active run
176
+ * settles; `closeSubscribers()` remains the subscriber-only seam.
177
+ */
178
+ close(): Promise<void>;
123
179
  prompt(input: string, options?: RunOptions): Promise<AgentRunResult>;
124
180
  stream(input: AgentInput, options?: RunOptions & SubscribeOptions): AsyncGenerator<AgentEvent>;
125
181
  buildRunResult(input: {
@@ -127,6 +183,7 @@ export declare class RuntimeAgentSession implements AgentSession {
127
183
  readonly status: AgentRunResult["status"];
128
184
  readonly usage?: Usage;
129
185
  readonly limit?: import("../contracts.js").RunLimitBreach;
186
+ readonly attribution?: import("../run-limits.js").BudgetExhaustionAttribution;
130
187
  readonly error?: ErrorInfo;
131
188
  readonly abortReason?: string;
132
189
  readonly stopReason?: import("../contracts.js").AgentFinishReason;
@@ -149,6 +206,11 @@ export declare class RuntimeAgentSession implements AgentSession {
149
206
  resolveRunProvider(options: RunOptions): void;
150
207
  resolveRunSkills(options: RunOptions, tools: readonly ToolDefinition[]): readonly Skill[];
151
208
  emit(event: AgentEvent): void;
209
+ /**
210
+ * Run end (finish, suspension, or denial): closes the run-scoped subscribers only. Subscribers that
211
+ * opted into `SubscribeOptions.acrossRuns` stay open for the next run of this session.
212
+ */
213
+ closeRunSubscribers(): void;
152
214
  closeSubscribers(): void;
153
215
  drainLedger(): Promise<void>;
154
216
  applyPendingSteers(runId: string, metadata: Readonly<Record<string, unknown>>, signal: AbortSignal): Promise<boolean>;
@@ -3,8 +3,8 @@ import { policyList } from "../agent-tool-dispatch.js";
3
3
  import { createAttentionFoldLedger, createAttentionStickyFrontier, resolveInputCap, restoreAttentionStickyFrontier, serializeAttentionFoldLedger, serializeAttentionStickyFrontier, } from "../attention-compiler.js";
4
4
  import { createDefaultCompactionStrategy, isCompactionEntryData } from "../compaction.js";
5
5
  import { estimateAssemblyTokens, estimateMessageTokens, estimateTextTokens } from "../context-budget.js";
6
- import { DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_SNAPSHOT_CACHE_TTL_MS, HARD_MAX_SNAPSHOT_CACHE_TTL_MS, resolveShouldCompact, } from "../contracts.js";
7
- import { compileGuardrailPacks, GuardrailError, runGuardrails } from "../guardrails.js";
6
+ import { AgentRunStateError, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_SNAPSHOT_CACHE_TTL_MS, HARD_MAX_SNAPSHOT_CACHE_TTL_MS, resolveShouldCompact, } from "../contracts.js";
7
+ import { compileGuardrailPacksWithState, GuardrailError, GuardrailPackError, runGuardrails } from "../guardrails.js";
8
8
  import { applyDefaultProviderRequestOptions, createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult, } from "../provider-request-policy.js";
9
9
  import { redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "../redaction.js";
10
10
  import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext } from "../session-stores.js";
@@ -23,6 +23,10 @@ export class RuntimeAgentSession {
23
23
  metadata;
24
24
  store;
25
25
  subscribers = new Set();
26
+ /** Plan 106 R2: `session_start` is dispatched at the first run start, once per runtime session. */
27
+ sessionOpened = false;
28
+ /** Plan 106 R2: `close()` dispatches `session_shutdown` and closes subscribers exactly once. */
29
+ closed = false;
26
30
  currentLeafId;
27
31
  history = [];
28
32
  activeRun;
@@ -41,8 +45,14 @@ export class RuntimeAgentSession {
41
45
  activeGuardrails;
42
46
  /** Plan 092 Task 2: guardrail packs compiled once in the constructor; merged into every run's `activeGuardrails`. */
43
47
  packGuardrails;
48
+ /** Plan 104 Task 3: `ask` rules as the durable charge-time gate (a match records `interrupt`). */
49
+ packAskGate;
50
+ /** Plan 104 Task 3: the same `ask` rules as plain blocks for a run that cannot suspend. */
51
+ packAskBlocks;
44
52
  /** Original pack refs, carried into `fork()`/`clone()` so a branch cannot silently lose its policy. */
45
- guardrailPackRefs;
53
+ packRefs;
54
+ /** Plan 104 Task 2: compiled refs + live pack state, replaced by `restoreGuardrailPacks` on resume. */
55
+ compiledGuardrailPacks;
46
56
  activeMetadata;
47
57
  activePromptVersion;
48
58
  activeLimits;
@@ -127,11 +137,51 @@ export class RuntimeAgentSession {
127
137
  for (const entry of bodies)
128
138
  this.loadedSkills.add(entry.name);
129
139
  }
140
+ /** Plan 104 T2: the refs this session actually enforces (restored ones after a resume). */
141
+ get guardrailPackRefs() {
142
+ return this.packRefs;
143
+ }
144
+ /** Plan 104 T2: pack refs + live pack-owned state for a durable checkpoint (opt-in with `persistSessionState`). */
145
+ serializedGuardrailPackState() {
146
+ const compiled = this.compiledGuardrailPacks;
147
+ if (!compiled || compiled.packs.length === 0)
148
+ return undefined;
149
+ const state = compiled.snapshotState();
150
+ return { packs: compiled.packs, ...(state ? { state } : {}) };
151
+ }
152
+ /**
153
+ * Plan 104 T2: recompile checkpoint packs before the resumed run's first turn. `state` present
154
+ * (even empty) marks a restore, so unknown ids, version mismatches, and codec-less state fail
155
+ * closed as `AgentRunStateError` — never a session that silently enforces less than it did.
156
+ */
157
+ restoreGuardrailPacks(refs, state) {
158
+ let compiled;
159
+ try {
160
+ compiled = compileGuardrailPacksWithState(refs, undefined, state ?? {});
161
+ }
162
+ catch (error) {
163
+ if (error instanceof GuardrailPackError)
164
+ throw new AgentRunStateError(`Cannot restore guardrail packs: ${error.message}`);
165
+ throw error;
166
+ }
167
+ this.compiledGuardrailPacks = compiled;
168
+ this.packGuardrails = compiled.guardrails;
169
+ this.packAskGate = compiled.askGate;
170
+ this.packAskBlocks = compiled.askBlocks;
171
+ this.packRefs = refs;
172
+ }
130
173
  ledgerChain = Promise.resolve();
131
174
  ledgerFailure;
132
175
  snapshotGeneration = 0;
133
176
  snapshotCache;
134
177
  snapshotCacheTtlMs;
178
+ /**
179
+ * Plan 103 T4: identity-keyed meter cache. Holds only the last public meter value
180
+ * plus the identity of everything the cold read consumed (`snapshotGeneration`,
181
+ * leaf, active meter/limits, and the history array reference + length, which catches
182
+ * in-place `history.push` during a run) — never history content, never an estimator.
183
+ */
184
+ meterCache;
135
185
  constructor(config) {
136
186
  this.id = config.id ?? randomId("session");
137
187
  this.agent = config.agent;
@@ -139,11 +189,14 @@ export class RuntimeAgentSession {
139
189
  this.store = config.store ?? config.agent.config.store ?? createMemorySessionStore();
140
190
  this.currentLeafId = config.leafId;
141
191
  this.snapshotCacheTtlMs = resolveSnapshotCacheTtlMs(config.snapshotCacheTtlMs);
142
- this.packGuardrails = compileGuardrailPacks(config.guardrailPacks);
143
- this.guardrailPackRefs = config.guardrailPacks;
192
+ this.compiledGuardrailPacks = compileGuardrailPacksWithState(config.guardrailPacks);
193
+ this.packGuardrails = this.compiledGuardrailPacks.guardrails;
194
+ this.packAskGate = this.compiledGuardrailPacks.askGate;
195
+ this.packAskBlocks = this.compiledGuardrailPacks.askBlocks;
196
+ this.packRefs = config.guardrailPacks;
144
197
  const usageEstimation = config.agent.config.usageEstimation;
145
- if (usageEstimation !== undefined && usageEstimation !== "fallback" && usageEstimation !== "off") {
146
- throw new TypeError('usageEstimation must be "fallback" or "off"');
198
+ if (usageEstimation !== undefined && usageEstimation !== "fallback" && usageEstimation !== "off" && usageEstimation !== "strict") {
199
+ throw new TypeError('usageEstimation must be "fallback", "off", or "strict"');
147
200
  }
148
201
  }
149
202
  get leafId() {
@@ -156,8 +209,37 @@ export class RuntimeAgentSession {
156
209
  * `provider_turn_finished.budgets` resolves them. Before any provider turn in
157
210
  * this session it estimates stored history, so a non-reporting model still
158
211
  * shows a working meter instead of zero. Never billing; estimates are labeled.
212
+ *
213
+ * Plan 103 T4: reads are cached until the history generation, leaf, history
214
+ * length, or active-run identity changes, so a per-frame poll pays one estimate
215
+ * per mutation instead of one per read. The cached value is frozen and is
216
+ * identical (`===`) to the previous read while nothing changed.
159
217
  */
160
218
  contextMeter() {
219
+ const cached = this.meterCache;
220
+ if (cached &&
221
+ cached.leafId === this.currentLeafId &&
222
+ cached.generation === this.snapshotGeneration &&
223
+ cached.meter === this.activeInputMeter &&
224
+ cached.limits === this.activeLimits &&
225
+ cached.history === this.history &&
226
+ cached.historyLength === this.history.length) {
227
+ return cached.value;
228
+ }
229
+ const value = Object.freeze(this.measureContextMeter());
230
+ this.meterCache = {
231
+ leafId: this.currentLeafId,
232
+ generation: this.snapshotGeneration,
233
+ meter: this.activeInputMeter,
234
+ limits: this.activeLimits,
235
+ history: this.history,
236
+ historyLength: this.history.length,
237
+ value,
238
+ };
239
+ return value;
240
+ }
241
+ /** Cold path of `contextMeter()`: one estimate over stored history plus cap/budget resolution. */
242
+ measureContextMeter() {
161
243
  const model = this.agent.config.model;
162
244
  const inputTokens = this.activeInputMeter?.tokens ?? estimateMessageTokens(this.history, model.model).tokens;
163
245
  const source = this.activeInputMeter?.source ?? "estimated";
@@ -179,7 +261,17 @@ export class RuntimeAgentSession {
179
261
  ...(inputCap === undefined ? {} : { usedRatio: inputTokens / inputCap }),
180
262
  };
181
263
  }
264
+ /**
265
+ * Live events for this session. A run-scoped subscriber (the default) is closed when the run ends,
266
+ * suspends, or is denied; `SubscribeOptions.acrossRuns: true` keeps one subscriber open across runs
267
+ * of the same session until the host closes it, the session tears it down, or its bounded queue
268
+ * overflows under the default policy. Subscribe before `run()`; the consumer loop and `run()` must
269
+ * run concurrently, since events are only emitted during a live run.
270
+ */
182
271
  subscribe(options = {}) {
272
+ return this.createSubscriber(options);
273
+ }
274
+ createSubscriber(options) {
183
275
  const subscriber = new EventSubscriber(this.id, options, () => this.subscribers.delete(subscriber));
184
276
  this.subscribers.add(subscriber);
185
277
  return subscriber;
@@ -233,7 +325,7 @@ export class RuntimeAgentSession {
233
325
  this.activeLedger = undefined;
234
326
  this.activeOwnership = undefined;
235
327
  this.activeRedactor = undefined;
236
- this.closeSubscribers();
328
+ this.closeRunSubscribers();
237
329
  }
238
330
  }
239
331
  async recordDurableDenial(runId, interruption, version, ownership) {
@@ -248,25 +340,58 @@ export class RuntimeAgentSession {
248
340
  this.activeLedger = undefined;
249
341
  this.activeOwnership = undefined;
250
342
  this.activeRedactor = undefined;
251
- this.closeSubscribers();
343
+ this.closeRunSubscribers();
252
344
  }
253
345
  }
254
346
  async runInternal(input, options, runId, resumed) {
255
347
  return executeRun(asSessionHost(this), input, options, runId, resumed);
256
348
  }
349
+ /**
350
+ * Plan 106 R2: dispatch `session_start` once per session, at its first run start (including the
351
+ * first run of a session rebuilt from a durable checkpoint). The run assembler awaits it right
352
+ * after `agent_started`/`agent_resumed`, so session-scoped provisioning is done before the first
353
+ * turn while the runtime's synchronous emit burst stays intact. Middleware error policy decides
354
+ * whether a failure surfaces or becomes an `extension_error` event.
355
+ */
356
+ async openSession(runId) {
357
+ if (this.sessionOpened)
358
+ return;
359
+ this.sessionOpened = true;
360
+ await this.agent.config.middleware?.run("session_start", { sessionId: this.id, runId });
361
+ }
362
+ /**
363
+ * Plan 106 R2: session teardown. Dispatches `session_shutdown` middleware once (idempotent) and
364
+ * then closes every subscriber, run-scoped and `acrossRuns` alike. Call it after the active run
365
+ * settles; `closeSubscribers()` remains the subscriber-only seam.
366
+ */
367
+ async close() {
368
+ if (this.closed)
369
+ return;
370
+ this.closed = true;
371
+ try {
372
+ await this.agent.config.middleware?.run("session_shutdown", { sessionId: this.id });
373
+ }
374
+ finally {
375
+ this.closeSubscribers();
376
+ }
377
+ }
257
378
  prompt(input, options) {
258
379
  return this.run(input, options);
259
380
  }
260
381
  async *stream(input, options = {}) {
261
382
  const { maxQueuedEvents, overflow, ...runOptions } = options;
262
- const subscription = this.subscribe({ maxQueuedEvents, overflow });
383
+ const subscriber = this.createSubscriber({ maxQueuedEvents, overflow });
263
384
  let runOwnedId;
264
385
  let settled = false;
386
+ // This subscription is stream()'s own, so it does not depend on the run-end close: settling the
387
+ // run closes it too, which also unblocks the consumer loop when the run fails before it ever
388
+ // emits (a pre-flight validation rejection returns before run-end cleanup).
265
389
  const runPromise = this.run(input, runOptions).finally(() => {
266
390
  settled = true;
391
+ subscriber.close();
267
392
  });
268
393
  try {
269
- for await (const event of subscription) {
394
+ for await (const event of subscriber) {
270
395
  if ("runId" in event && typeof event.runId === "string") {
271
396
  if (runOwnedId === undefined && event.type === "agent_started")
272
397
  runOwnedId = event.runId;
@@ -278,6 +403,7 @@ export class RuntimeAgentSession {
278
403
  await runPromise;
279
404
  }
280
405
  finally {
406
+ subscriber.close();
281
407
  if (!settled) {
282
408
  this.abort(new Error("stream consumer closed"));
283
409
  await runPromise.catch(() => undefined);
@@ -296,6 +422,7 @@ export class RuntimeAgentSession {
296
422
  message: final.message,
297
423
  usage: input.usage,
298
424
  limit: input.limit,
425
+ attribution: input.attribution,
299
426
  error: input.error,
300
427
  abortReason: input.abortReason,
301
428
  stopReason: input.stopReason,
@@ -333,7 +460,7 @@ export class RuntimeAgentSession {
333
460
  store: this.store,
334
461
  leafId: options.leafId ?? this.currentLeafId,
335
462
  metadata: this.metadata,
336
- ...(this.guardrailPackRefs ? { guardrailPacks: this.guardrailPackRefs } : {}),
463
+ ...(this.packRefs ? { guardrailPacks: this.packRefs } : {}),
337
464
  });
338
465
  }
339
466
  async clone(options = {}) {
@@ -356,12 +483,12 @@ export class RuntimeAgentSession {
356
483
  store: this.store,
357
484
  leafId: branch.length ? remap.get(branch[branch.length - 1].id) : undefined,
358
485
  metadata: this.metadata,
359
- ...(this.guardrailPackRefs ? { guardrailPacks: this.guardrailPackRefs } : {}),
486
+ ...(this.packRefs ? { guardrailPacks: this.packRefs } : {}),
360
487
  });
361
488
  }
362
489
  branchReader() {
363
- // ponytail: prefer the store's readBranchPath (one ancestor-chain query) when present so a
364
- // DB-backed store never loads the full session; else fall back to list() + in-memory walk.
490
+ // ponytail: prefer readBranchPath when present (memory, SQLite, Postgres) so snapshot does
491
+ // not list() the whole session. JSONL and other omitters fall back to list() + in-memory walk.
365
492
  const read = this.store.readBranchPath;
366
493
  return read ? (query) => read.call(this.store, query) : undefined;
367
494
  }
@@ -418,6 +545,15 @@ export class RuntimeAgentSession {
418
545
  });
419
546
  }
420
547
  }
548
+ /**
549
+ * Run end (finish, suspension, or denial): closes the run-scoped subscribers only. Subscribers that
550
+ * opted into `SubscribeOptions.acrossRuns` stay open for the next run of this session.
551
+ */
552
+ closeRunSubscribers() {
553
+ for (const subscriber of this.subscribers)
554
+ if (!subscriber.acrossRuns)
555
+ subscriber.close();
556
+ }
421
557
  closeSubscribers() {
422
558
  for (const subscriber of this.subscribers)
423
559
  subscriber.close();
@@ -528,10 +664,15 @@ export class RuntimeAgentSession {
528
664
  signal,
529
665
  };
530
666
  this.emit({ type: "compaction_started", sessionId: this.id, runId });
531
- let result = await strategy.compact(context);
667
+ // Plan 106 R3: pre-compaction seam — the strategy compacts exactly the context this returns.
668
+ const requested = (await this.agent.config.middleware?.run("compaction_request", context)) ?? context;
669
+ let result = await strategy.compact(requested);
532
670
  result = { ...result, summary: redactSecrets(result.summary, secrets) };
533
- const payload = (await this.agent.config.middleware?.run("compaction", { context, result })) ?? {
534
- context,
671
+ const payload = (await this.agent.config.middleware?.run("compaction", {
672
+ context: requested,
673
+ result,
674
+ })) ?? {
675
+ context: requested,
535
676
  result,
536
677
  };
537
678
  result = { ...payload.result, summary: redactSecrets(payload.result.summary, secrets) };
@@ -1,12 +1,17 @@
1
1
  /**
2
- * Checkpoint restore hooks (plan 094 Task 3). A hook restores one external layer (git commit,
3
- * document version, workspace fingerprint) recorded in a checkpoint's sidecar metadata.
2
+ * Checkpoint restore hooks (plan 094 Task 3; reverse compensation plan 109 Task 2). A hook restores
3
+ * one external layer (git commit, document version, workspace fingerprint) recorded in a
4
+ * checkpoint's sidecar metadata.
4
5
  *
5
- * All-or-nothing: hooks run before the resume claims the checkpoint. The first hook that fails
6
- * or times out aborts the resume with `CheckpointRestoreError` naming that hook, so the
7
- * conversation restore never applies on top of a half-restored external world. Hosts that need
8
- * every layer back where they were re-run the whole restore after fixing the failing layer.
6
+ * All-or-nothing: hooks run before the resume claims the checkpoint. The first hook that fails or
7
+ * times out aborts the resume with `CheckpointRestoreError` naming that hook, so the conversation
8
+ * restore never applies on top of a half-restored external world. A handler may also declare a
9
+ * `compensate` direction: on failure the applied layers — including the failing one, which may be
10
+ * half-applied — are undone in reverse order and the error carries a best-effort
11
+ * `CheckpointRestoreCompensation` report. Compensation never runs after the caller aborted, never
12
+ * masks the original failure, and the checkpoint stays resumable either way.
9
13
  */
14
+ import type { SecretRedactor } from "./redaction.js";
10
15
  /** Per-hook ceiling for a restore (plan 094 Task 3 default). */
11
16
  export declare const DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS = 10000;
12
17
  /** One hook that completed during a restore. */
@@ -20,26 +25,57 @@ export interface CheckpointRestoreAudit {
20
25
  readonly durationMs: number;
21
26
  }
22
27
  /**
23
- * Host code restoring one external layer. `signal` aborts on the per-hook timeout and on the
24
- * caller's abort, so a hook that talks to a remote system can cancel instead of dangling.
28
+ * Host code restoring or compensating one external layer. `signal` aborts on the per-hook timeout
29
+ * and on the caller's abort, so a hook that talks to a remote system can cancel instead of dangling.
25
30
  */
26
31
  export type CheckpointRestoreHook<Context> = (checkpoint: Context, signal: AbortSignal) => void | Promise<void>;
32
+ /** Object form of a restore handler: an identity plus the layer's undo direction (plan 109 Task 2). */
33
+ interface CheckpointRestoreHandlerObject<Context> {
34
+ /** Stable name reported in the audit and in `CheckpointRestoreError.compensation`; defaults to the restore hook's name, then `hook[i]`. */
35
+ readonly id?: string;
36
+ readonly restore: CheckpointRestoreHook<Context>;
37
+ /** Undo for this layer, run when a later hook — or this one — fails. Absent = the layer has no undo. */
38
+ readonly compensate?: CheckpointRestoreHook<Context>;
39
+ }
40
+ /**
41
+ * A restore handler: today's bare function, or `{ id?, restore, compensate? }` to declare an undo
42
+ * direction. Bare functions behave exactly as in plan 094 and are never compensated.
43
+ */
44
+ export type CheckpointRestoreHandler<Context> = CheckpointRestoreHook<Context> | CheckpointRestoreHandlerObject<Context>;
45
+ /** Best-effort record of the reverse compensation pass after a failed restore (plan 109 Task 2). */
46
+ export interface CheckpointRestoreCompensation {
47
+ /** Names of the hooks whose `compensate` ran, most recently applied layer first. */
48
+ readonly ran: readonly string[];
49
+ /** First compensation that failed; the pass still continues with the remaining layers. */
50
+ readonly failed?: {
51
+ readonly hook: string;
52
+ readonly reason: string;
53
+ };
54
+ }
27
55
  /** Thrown when a restore hook fails or times out; the checkpoint and conversation are untouched. */
28
56
  export declare class CheckpointRestoreError extends Error {
29
57
  readonly code = "ERR_PRISM_CHECKPOINT_RESTORE";
30
- /** Name of the failing hook (`fn.name` or `hook[i]`). */
58
+ /** Name of the failing hook (`id`, `hook.name`, or `hook[i]`). */
31
59
  readonly hook: string;
32
- constructor(hook: string, cause: unknown);
60
+ /** Present only when at least one `compensate` ran; omitted otherwise, as in plan 094. */
61
+ readonly compensation?: CheckpointRestoreCompensation;
62
+ constructor(hook: string, cause: unknown, compensation?: CheckpointRestoreCompensation);
33
63
  }
34
64
  export interface RunCheckpointRestoreHooksOptions {
35
65
  /** Per-hook timeout; defaults to `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`. */
36
66
  readonly timeoutMs?: number;
37
67
  /** Caller abort: checked between hooks and combined into each hook's signal. */
38
68
  readonly signal?: AbortSignal;
69
+ /** Applied to the bounded compensation reason; absent = the message is reported as-is (still capped). */
70
+ readonly redactor?: SecretRedactor;
39
71
  }
40
72
  /**
41
- * Run restore hooks sequentially and report the audit. A hook failure throws
42
- * `CheckpointRestoreError` immediately (later hooks do not run); an already-aborted caller signal
43
- * throws its own abort reason so the resume reads as cancelled rather than as a restore failure.
73
+ * Run restore handlers sequentially and report the audit. A handler failure throws
74
+ * `CheckpointRestoreError` immediately (later handlers do not run) after a best-effort reverse
75
+ * compensation pass over the applied layers; with no `compensate` declared anywhere, the failure
76
+ * shape is exactly plan 094's. An already-aborted caller signal throws its own abort reason so the
77
+ * resume reads as cancelled rather than as a restore failure, and a caller abort during the pass
78
+ * stops it before the next handler runs.
44
79
  */
45
- export declare function runCheckpointRestoreHooks<Context>(hooks: readonly CheckpointRestoreHook<Context>[], context: Context, options?: RunCheckpointRestoreHooksOptions): Promise<CheckpointRestoreAudit>;
80
+ export declare function runCheckpointRestoreHooks<Context>(hooks: readonly CheckpointRestoreHandler<Context>[], context: Context, options?: RunCheckpointRestoreHooksOptions): Promise<CheckpointRestoreAudit>;
81
+ export {};