@arnilo/prism 0.8.0 → 0.10.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 (141) hide show
  1. package/CHANGELOG.md +62 -1
  2. package/README.md +13 -12
  3. package/dist/agent-approval.d.ts +17 -2
  4. package/dist/agent-approval.js +15 -6
  5. package/dist/agent-event-source.d.ts +9 -1
  6. package/dist/agent-event-source.js +10 -3
  7. package/dist/agent-loops.js +7 -4
  8. package/dist/agent-run-lifecycle.d.ts +15 -1
  9. package/dist/agent-run-lifecycle.js +82 -11
  10. package/dist/agent-run-state.d.ts +47 -6
  11. package/dist/agent-run-state.js +154 -6
  12. package/dist/agent-session/event-subscriber.d.ts +2 -0
  13. package/dist/agent-session/event-subscriber.js +3 -0
  14. package/dist/agent-session/helpers.js +14 -0
  15. package/dist/agent-session/session/assemble.js +281 -32
  16. package/dist/agent-session/session/persist.d.ts +11 -0
  17. package/dist/agent-session/session/persist.js +48 -16
  18. package/dist/agent-session/session/provider-round.d.ts +14 -4
  19. package/dist/agent-session/session/provider-round.js +226 -19
  20. package/dist/agent-session/session/tool-round.d.ts +2 -2
  21. package/dist/agent-session/session/tool-round.js +78 -6
  22. package/dist/agent-session/session/types.d.ts +44 -3
  23. package/dist/agent-session/session.d.ts +100 -5
  24. package/dist/agent-session/session.js +224 -13
  25. package/dist/attention-compiler.d.ts +51 -2
  26. package/dist/attention-compiler.js +282 -21
  27. package/dist/cache-helpers.d.ts +4 -2
  28. package/dist/cache-helpers.js +8 -6
  29. package/dist/checkpoint-restore.d.ts +45 -0
  30. package/dist/checkpoint-restore.js +54 -0
  31. package/dist/context-budget.d.ts +13 -1
  32. package/dist/context-budget.js +57 -4
  33. package/dist/contracts-core/agent.d.ts +52 -1
  34. package/dist/contracts-core/attention.d.ts +95 -0
  35. package/dist/contracts-core/content.d.ts +10 -0
  36. package/dist/contracts-core/extensions.d.ts +3 -0
  37. package/dist/contracts-core/guardrail-packs.d.ts +46 -0
  38. package/dist/contracts-core/guardrail-packs.js +2 -0
  39. package/dist/contracts-core/loop.d.ts +36 -0
  40. package/dist/contracts-core/provider.d.ts +30 -0
  41. package/dist/contracts-core/run-limits.d.ts +29 -1
  42. package/dist/contracts-core/session.d.ts +23 -5
  43. package/dist/contracts-core/session.js +21 -2
  44. package/dist/contracts-core/usage.d.ts +40 -0
  45. package/dist/contracts-core/usage.js +8 -0
  46. package/dist/contracts-core.d.ts +2 -0
  47. package/dist/contracts-core.js +2 -0
  48. package/dist/contracts-protocol.d.ts +81 -5
  49. package/dist/contracts-run-state.d.ts +91 -2
  50. package/dist/contributions.d.ts +2 -1
  51. package/dist/contributions.js +1 -0
  52. package/dist/extensions.d.ts +15 -1
  53. package/dist/extensions.js +68 -0
  54. package/dist/guardrail-packs/coding-standard.d.ts +3 -0
  55. package/dist/guardrail-packs/coding-standard.js +63 -0
  56. package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
  57. package/dist/guardrail-packs/destructive-commands.js +46 -0
  58. package/dist/guardrail-packs/errors.d.ts +7 -0
  59. package/dist/guardrail-packs/errors.js +9 -0
  60. package/dist/guardrail-packs/index.d.ts +4 -0
  61. package/dist/guardrail-packs/index.js +15 -0
  62. package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
  63. package/dist/guardrail-packs/secrets-hygiene.js +23 -0
  64. package/dist/guardrail-packs/types.d.ts +26 -0
  65. package/dist/guardrail-packs/types.js +2 -0
  66. package/dist/guardrail-packs/validation-respect.d.ts +3 -0
  67. package/dist/guardrail-packs/validation-respect.js +69 -0
  68. package/dist/guardrails.d.ts +61 -1
  69. package/dist/guardrails.js +377 -0
  70. package/dist/index.d.ts +16 -11
  71. package/dist/index.js +10 -7
  72. package/dist/input.d.ts +8 -1
  73. package/dist/input.js +68 -6
  74. package/dist/middleware.d.ts +37 -2
  75. package/dist/middleware.js +41 -0
  76. package/dist/node/session-store-jsonl.js +18 -3
  77. package/dist/observability.js +6 -0
  78. package/dist/provider-events.d.ts +8 -2
  79. package/dist/provider-events.js +60 -2
  80. package/dist/providers/openai-compatible.js +6 -3
  81. package/dist/run-bundle.d.ts +6 -1
  82. package/dist/run-bundle.js +5 -1
  83. package/dist/run-limits.d.ts +11 -1
  84. package/dist/run-limits.js +59 -0
  85. package/dist/session-stores.d.ts +12 -1
  86. package/dist/session-stores.js +21 -4
  87. package/dist/testing/agent-event-source-conformance.js +41 -2
  88. package/dist/testing/prefix-stability-conformance.d.ts +59 -0
  89. package/dist/testing/prefix-stability-conformance.js +172 -0
  90. package/dist/testing/session-store-conformance.d.ts +3 -2
  91. package/dist/testing/session-store-conformance.js +48 -0
  92. package/dist/tools.d.ts +5 -0
  93. package/dist/tools.js +21 -6
  94. package/dist/usage-estimation.d.ts +29 -0
  95. package/dist/usage-estimation.js +79 -0
  96. package/docs/agent-events.md +75 -4
  97. package/docs/agent-session-runtime.md +10 -6
  98. package/docs/attention-compiler.md +89 -8
  99. package/docs/caveman.md +1 -1
  100. package/docs/coding-agent-tools.md +1 -1
  101. package/docs/compaction-and-retry.md +1 -1
  102. package/docs/compaction-llm.md +2 -0
  103. package/docs/compaction-observational-memory.md +54 -7
  104. package/docs/durable-runs.md +46 -3
  105. package/docs/embeddings.md +9 -0
  106. package/docs/evaluations.md +5 -0
  107. package/docs/execution-timeline.md +79 -1
  108. package/docs/extensions.md +20 -3
  109. package/docs/guardrails.md +50 -4
  110. package/docs/hooks.md +282 -0
  111. package/docs/index.md +37 -15
  112. package/docs/input-and-prompt-assembly.md +4 -4
  113. package/docs/instruction-injection.md +1 -0
  114. package/docs/knowledge-sync.md +4 -0
  115. package/docs/live-testing.md +3 -1
  116. package/docs/memory-fabric.md +28 -0
  117. package/docs/middleware-hooks.md +90 -4
  118. package/docs/migrate-to-0.9.md +210 -0
  119. package/docs/migration.md +26 -0
  120. package/docs/multi-agent-patterns.md +25 -2
  121. package/docs/node-jsonl-session-store.md +7 -1
  122. package/docs/observability.md +7 -3
  123. package/docs/options-index.md +4 -1
  124. package/docs/policy-and-audit.md +26 -1
  125. package/docs/prefix-stability-conformance.md +143 -0
  126. package/docs/provider-caching.md +4 -4
  127. package/docs/provider-conformance.md +16 -0
  128. package/docs/provider-packages.md +20 -20
  129. package/docs/public-contracts.md +3 -2
  130. package/docs/rag.md +188 -3
  131. package/docs/release-and-install.md +45 -40
  132. package/docs/runs-and-usage.md +56 -10
  133. package/docs/scoped-agent-memory.md +270 -0
  134. package/docs/scoped-memory.md +138 -0
  135. package/docs/session-store-conformance.md +1 -2
  136. package/docs/session-stores.md +17 -17
  137. package/docs/supervisors.md +32 -12
  138. package/docs/tools.md +18 -1
  139. package/docs/wiki.md +4 -2
  140. package/docs/workflows.md +5 -0
  141. package/package.json +8 -2
@@ -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
- import { ActiveDurableRun } from "../agent-approval.js";
3
- import type { PendingToolCall, StoredAgentRunState } from "../agent-run-state.js";
4
- import { type AttentionStickyFrontier, type PersistedAttentionStickyFrontier } from "../attention-compiler.js";
5
- import type { Agent, AgentEvent, AgentRunResult, AgentRunState, AgentRunStateOptions, AgentSession, AgentSessionConfig, AIProvider, CompactionOptions, CompactionResult, ErrorInfo, Message, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunDecision, RunOptions, SessionEntry, Skill, SteerOptions, SubscribeOptions, ToolDefinition, ToolEffectStore, Usage } from "../contracts.js";
2
+ import { ActiveDurableRun, ActiveDurableRunExtras } from "../agent-approval.js";
3
+ import type { PendingToolCall, PersistedGuardrailPacks, StoredAgentRunState } from "../agent-run-state.js";
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, 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?;
@@ -30,9 +34,27 @@ export declare class RuntimeAgentSession implements AgentSession {
30
34
  activeIdentity?: AgentIdentity;
31
35
  private activeIdempotencyKey?;
32
36
  private activeGuardrails?;
37
+ /** Plan 092 Task 2: guardrail packs compiled once in the constructor; merged into every run's `activeGuardrails`. */
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;
43
+ /** Original pack refs, carried into `fork()`/`clone()` so a branch cannot silently lose its policy. */
44
+ private packRefs?;
45
+ /** Plan 104 Task 2: compiled refs + live pack state, replaced by `restoreGuardrailPacks` on resume. */
46
+ private compiledGuardrailPacks?;
33
47
  activeMetadata?: Readonly<Record<string, unknown>>;
34
48
  activePromptVersion?: PromptVersionRef;
35
49
  activeLimits?: RunLimitTracker;
50
+ /** Plan 091 T2: input tokens of the latest provider turn plus whether the provider reported
51
+ * them; set by the usage seam, read by `contextMeter()`. */
52
+ activeInputMeter?: {
53
+ readonly tokens: number;
54
+ readonly source: "reported" | "estimated";
55
+ };
56
+ /** Bounded last-N tool-call summaries of the active run (plan 087 T2): ids, names, arg hashes. */
57
+ activeRecentToolCalls?: ToolCallSummary[];
36
58
  activeLimitOutputBuffer: boolean;
37
59
  activeDurable?: ActiveDurableRun;
38
60
  activeLoop?: import("../contracts.js").AgentLoopStrategy;
@@ -43,6 +65,8 @@ export declare class RuntimeAgentSession implements AgentSession {
43
65
  }>;
44
66
  activeLoopTurn: number;
45
67
  private readonly loadedSkills;
68
+ /** Run-owned only: loaded bodies and URI resources retain first insertion order within one provider loop. */
69
+ readonly tailSegments: Map<string, Message>;
46
70
  /** Tools activated via `search_tools` this session (plan 041); names-only in persistence. */
47
71
  readonly activatedTools: import("../tool-search.js").ActiveToolSet;
48
72
  /** Plan 018 Task 6 (closeout `checkpoint-bodies`): persisted exact instructions, registry-independent. */
@@ -62,6 +86,17 @@ export declare class RuntimeAgentSession implements AgentSession {
62
86
  /** Plan 074 P3: restore a frontier validated at checkpoint load, so a resumed run keeps its
63
87
  * stubs instead of re-deciding its first turn from the ratio. */
64
88
  restoreAttentionSticky(persisted: PersistedAttentionStickyFrontier): void;
89
+ /** Session-owned folded bodies (plan 086 T3); created on first use like the frontier, so a
90
+ * compiler-off session allocates nothing. */
91
+ private attentionFold?;
92
+ /** Set per run from the resolved compiler: `durable: true` opts the fold ledger and its
93
+ * frontier into checkpoints even when `persistSessionState` is off. */
94
+ attentionDurable: boolean;
95
+ attentionFoldFor(): AttentionFoldLedger;
96
+ /** Plan 086 T3: bounded ledger snapshot for a durable checkpoint; `undefined` before any fold. */
97
+ serializedAttentionFold(): PersistedAttentionFoldLedger | undefined;
98
+ /** Plan 086 T3: adopt a ledger validated at checkpoint load, so a resumed fold is byte-identical. */
99
+ restoreAttentionFold(ledger: AttentionFoldLedger): void;
65
100
  /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
66
101
  restoreLoadedSkills(names: readonly string[]): void;
67
102
  /** Plan 041: re-add persisted activated-tool names (names only; inert for absent tools). */
@@ -70,22 +105,77 @@ export declare class RuntimeAgentSession implements AgentSession {
70
105
  clearActivatedTools(): void;
71
106
  /** Plan 018 Task 6: restore persisted loaded-skill bodies (already validated fail-closed at load). */
72
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;
73
118
  private ledgerChain;
74
119
  private ledgerFailure;
75
120
  private snapshotGeneration;
76
121
  private snapshotCache?;
77
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?;
78
130
  constructor(config: AgentSessionConfig & {
79
131
  readonly agent: Agent;
80
132
  });
81
133
  get leafId(): string | undefined;
134
+ /**
135
+ * Context-fill read (plan 091 T2): the latest provider turn's input tokens —
136
+ * provider-reported when it reported, else a labeled estimate — plus the
137
+ * per-request cap and cumulative run input budget, resolved exactly as
138
+ * `provider_turn_finished.budgets` resolves them. Before any provider turn in
139
+ * this session it estimates stored history, so a non-reporting model still
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.
146
+ */
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
+ */
82
157
  subscribe(options?: SubscribeOptions): AsyncIterable<AgentEvent>;
158
+ private createSubscriber;
83
159
  run(input: AgentInput, options?: RunOptions): Promise<AgentRunResult>;
84
160
  steer(input: AgentInput, options?: SteerOptions): void;
85
- resumeDurable(state: StoredAgentRunState, runState: AgentRunStateOptions, ownership?: OwnershipScope, signal?: AbortSignal, decisions?: ReadonlyMap<string, RunDecision>): Promise<AgentRunResult>;
161
+ resumeDurable(state: StoredAgentRunState, runState: AgentRunStateOptions, ownership?: OwnershipScope, signal?: AbortSignal, decisions?: ReadonlyMap<string, RunDecision>, extras?: ActiveDurableRunExtras): Promise<AgentRunResult>;
86
162
  recordDurableResumption(runId: string, interruption: import("../contracts.js").AgentRunInterruption, version: number, ownership?: OwnershipScope): Promise<void>;
87
163
  recordDurableDenial(runId: string, interruption: import("../contracts.js").AgentRunInterruption, version: number, ownership?: OwnershipScope): Promise<void>;
88
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>;
89
179
  prompt(input: string, options?: RunOptions): Promise<AgentRunResult>;
90
180
  stream(input: AgentInput, options?: RunOptions & SubscribeOptions): AsyncGenerator<AgentEvent>;
91
181
  buildRunResult(input: {
@@ -115,6 +205,11 @@ export declare class RuntimeAgentSession implements AgentSession {
115
205
  resolveRunProvider(options: RunOptions): void;
116
206
  resolveRunSkills(options: RunOptions, tools: readonly ToolDefinition[]): readonly Skill[];
117
207
  emit(event: AgentEvent): void;
208
+ /**
209
+ * Run end (finish, suspension, or denial): closes the run-scoped subscribers only. Subscribers that
210
+ * opted into `SubscribeOptions.acrossRuns` stay open for the next run of this session.
211
+ */
212
+ closeRunSubscribers(): void;
118
213
  closeSubscribers(): void;
119
214
  drainLedger(): Promise<void>;
120
215
  applyPendingSteers(runId: string, metadata: Readonly<Record<string, unknown>>, signal: AbortSignal): Promise<boolean>;
@@ -1,10 +1,10 @@
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 { policyList } from "../agent-tool-dispatch.js";
3
- import { createAttentionStickyFrontier, resolveInputCap, restoreAttentionStickyFrontier, serializeAttentionStickyFrontier, } from "../attention-compiler.js";
3
+ import { createAttentionFoldLedger, createAttentionStickyFrontier, resolveInputCap, restoreAttentionStickyFrontier, serializeAttentionFoldLedger, serializeAttentionStickyFrontier, } from "../attention-compiler.js";
4
4
  import { createDefaultCompactionStrategy, isCompactionEntryData } from "../compaction.js";
5
- import { estimateAssemblyTokens, 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 { GuardrailError, runGuardrails } from "../guardrails.js";
5
+ import { estimateAssemblyTokens, estimateMessageTokens, estimateTextTokens } from "../context-budget.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;
@@ -39,9 +43,24 @@ export class RuntimeAgentSession {
39
43
  activeIdentity;
40
44
  activeIdempotencyKey;
41
45
  activeGuardrails;
46
+ /** Plan 092 Task 2: guardrail packs compiled once in the constructor; merged into every run's `activeGuardrails`. */
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;
52
+ /** Original pack refs, carried into `fork()`/`clone()` so a branch cannot silently lose its policy. */
53
+ packRefs;
54
+ /** Plan 104 Task 2: compiled refs + live pack state, replaced by `restoreGuardrailPacks` on resume. */
55
+ compiledGuardrailPacks;
42
56
  activeMetadata;
43
57
  activePromptVersion;
44
58
  activeLimits;
59
+ /** Plan 091 T2: input tokens of the latest provider turn plus whether the provider reported
60
+ * them; set by the usage seam, read by `contextMeter()`. */
61
+ activeInputMeter;
62
+ /** Bounded last-N tool-call summaries of the active run (plan 087 T2): ids, names, arg hashes. */
63
+ activeRecentToolCalls;
45
64
  activeLimitOutputBuffer = false;
46
65
  activeDurable;
47
66
  activeLoop;
@@ -49,6 +68,8 @@ export class RuntimeAgentSession {
49
68
  activeGatedRound;
50
69
  activeLoopTurn = 1;
51
70
  loadedSkills = createLoadedSkillSet();
71
+ /** Run-owned only: loaded bodies and URI resources retain first insertion order within one provider loop. */
72
+ tailSegments = new Map();
52
73
  /** Tools activated via `search_tools` this session (plan 041); names-only in persistence. */
53
74
  activatedTools = createActiveToolSet();
54
75
  /** Plan 018 Task 6 (closeout `checkpoint-bodies`): persisted exact instructions, registry-independent. */
@@ -77,6 +98,24 @@ export class RuntimeAgentSession {
77
98
  restoreAttentionSticky(persisted) {
78
99
  this.attentionSticky = restoreAttentionStickyFrontier(persisted);
79
100
  }
101
+ /** Session-owned folded bodies (plan 086 T3); created on first use like the frontier, so a
102
+ * compiler-off session allocates nothing. */
103
+ attentionFold;
104
+ /** Set per run from the resolved compiler: `durable: true` opts the fold ledger and its
105
+ * frontier into checkpoints even when `persistSessionState` is off. */
106
+ attentionDurable = false;
107
+ attentionFoldFor() {
108
+ this.attentionFold ??= createAttentionFoldLedger();
109
+ return this.attentionFold;
110
+ }
111
+ /** Plan 086 T3: bounded ledger snapshot for a durable checkpoint; `undefined` before any fold. */
112
+ serializedAttentionFold() {
113
+ return this.attentionFold && this.attentionFold.bodies.size > 0 ? serializeAttentionFoldLedger(this.attentionFold) : undefined;
114
+ }
115
+ /** Plan 086 T3: adopt a ledger validated at checkpoint load, so a resumed fold is byte-identical. */
116
+ restoreAttentionFold(ledger) {
117
+ this.attentionFold = ledger;
118
+ }
80
119
  /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
81
120
  restoreLoadedSkills(names) {
82
121
  for (const name of names)
@@ -98,11 +137,51 @@ export class RuntimeAgentSession {
98
137
  for (const entry of bodies)
99
138
  this.loadedSkills.add(entry.name);
100
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
+ }
101
173
  ledgerChain = Promise.resolve();
102
174
  ledgerFailure;
103
175
  snapshotGeneration = 0;
104
176
  snapshotCache;
105
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;
106
185
  constructor(config) {
107
186
  this.id = config.id ?? randomId("session");
108
187
  this.agent = config.agent;
@@ -110,11 +189,89 @@ export class RuntimeAgentSession {
110
189
  this.store = config.store ?? config.agent.config.store ?? createMemorySessionStore();
111
190
  this.currentLeafId = config.leafId;
112
191
  this.snapshotCacheTtlMs = resolveSnapshotCacheTtlMs(config.snapshotCacheTtlMs);
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;
197
+ const usageEstimation = config.agent.config.usageEstimation;
198
+ if (usageEstimation !== undefined && usageEstimation !== "fallback" && usageEstimation !== "off" && usageEstimation !== "strict") {
199
+ throw new TypeError('usageEstimation must be "fallback", "off", or "strict"');
200
+ }
113
201
  }
114
202
  get leafId() {
115
203
  return this.currentLeafId;
116
204
  }
205
+ /**
206
+ * Context-fill read (plan 091 T2): the latest provider turn's input tokens —
207
+ * provider-reported when it reported, else a labeled estimate — plus the
208
+ * per-request cap and cumulative run input budget, resolved exactly as
209
+ * `provider_turn_finished.budgets` resolves them. Before any provider turn in
210
+ * this session it estimates stored history, so a non-reporting model still
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.
217
+ */
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() {
243
+ const model = this.agent.config.model;
244
+ const inputTokens = this.activeInputMeter?.tokens ?? estimateMessageTokens(this.history, model.model).tokens;
245
+ const source = this.activeInputMeter?.source ?? "estimated";
246
+ let inputCap;
247
+ try {
248
+ const setting = this.agent.config.attentionCompiler;
249
+ const options = typeof setting === "object" && setting !== null ? setting : undefined;
250
+ inputCap = resolveInputCap(options ? { maxInputTokens: options.maxInputTokens, reserveTokens: options.reserveTokens } : {}, model);
251
+ }
252
+ catch {
253
+ inputCap = undefined; // undialed model: omit instead of throwing a state read
254
+ }
255
+ const runInputBudget = this.activeLimits?.limits.maxInputTokens ?? undefined;
256
+ return {
257
+ inputTokens,
258
+ source,
259
+ ...(inputCap === undefined ? {} : { inputCap }),
260
+ ...(runInputBudget == null ? {} : { runInputBudget }),
261
+ ...(inputCap === undefined ? {} : { usedRatio: inputTokens / inputCap }),
262
+ };
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
+ */
117
271
  subscribe(options = {}) {
272
+ return this.createSubscriber(options);
273
+ }
274
+ createSubscriber(options) {
118
275
  const subscriber = new EventSubscriber(this.id, options, () => this.subscribers.delete(subscriber));
119
276
  this.subscribers.add(subscriber);
120
277
  return subscriber;
@@ -147,12 +304,13 @@ export class RuntimeAgentSession {
147
304
  this.pendingSoftInterrupt = true;
148
305
  }
149
306
  }
150
- async resumeDurable(state, runState, ownership, signal, decisions) {
307
+ async resumeDurable(state, runState, ownership, signal, decisions, extras) {
151
308
  return this.runInternal(state.input ?? [], { runState, ownership, signal }, state.runId, {
152
309
  options: runState,
153
310
  state,
154
311
  version: state.version,
155
312
  decisions,
313
+ ...extras,
156
314
  });
157
315
  }
158
316
  async recordDurableResumption(runId, interruption, version, ownership) {
@@ -167,7 +325,7 @@ export class RuntimeAgentSession {
167
325
  this.activeLedger = undefined;
168
326
  this.activeOwnership = undefined;
169
327
  this.activeRedactor = undefined;
170
- this.closeSubscribers();
328
+ this.closeRunSubscribers();
171
329
  }
172
330
  }
173
331
  async recordDurableDenial(runId, interruption, version, ownership) {
@@ -182,25 +340,58 @@ export class RuntimeAgentSession {
182
340
  this.activeLedger = undefined;
183
341
  this.activeOwnership = undefined;
184
342
  this.activeRedactor = undefined;
185
- this.closeSubscribers();
343
+ this.closeRunSubscribers();
186
344
  }
187
345
  }
188
346
  async runInternal(input, options, runId, resumed) {
189
347
  return executeRun(asSessionHost(this), input, options, runId, resumed);
190
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
+ }
191
378
  prompt(input, options) {
192
379
  return this.run(input, options);
193
380
  }
194
381
  async *stream(input, options = {}) {
195
382
  const { maxQueuedEvents, overflow, ...runOptions } = options;
196
- const subscription = this.subscribe({ maxQueuedEvents, overflow });
383
+ const subscriber = this.createSubscriber({ maxQueuedEvents, overflow });
197
384
  let runOwnedId;
198
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).
199
389
  const runPromise = this.run(input, runOptions).finally(() => {
200
390
  settled = true;
391
+ subscriber.close();
201
392
  });
202
393
  try {
203
- for await (const event of subscription) {
394
+ for await (const event of subscriber) {
204
395
  if ("runId" in event && typeof event.runId === "string") {
205
396
  if (runOwnedId === undefined && event.type === "agent_started")
206
397
  runOwnedId = event.runId;
@@ -212,6 +403,7 @@ export class RuntimeAgentSession {
212
403
  await runPromise;
213
404
  }
214
405
  finally {
406
+ subscriber.close();
215
407
  if (!settled) {
216
408
  this.abort(new Error("stream consumer closed"));
217
409
  await runPromise.catch(() => undefined);
@@ -241,7 +433,10 @@ export class RuntimeAgentSession {
241
433
  async compact(options = {}) {
242
434
  if (this.activeRun)
243
435
  throw new Error("Agent session already has an active run");
244
- return this.compactBranch(options, undefined, options.signal, "manual");
436
+ const result = await this.compactBranch(options, undefined, options.signal, "manual");
437
+ // Plan 091 T2: history changed, so a pre-compaction meter reading would overstate the context.
438
+ this.activeInputMeter = undefined;
439
+ return result;
245
440
  }
246
441
  abort(reason) {
247
442
  this.activeRun?.abort(reason);
@@ -264,6 +459,7 @@ export class RuntimeAgentSession {
264
459
  store: this.store,
265
460
  leafId: options.leafId ?? this.currentLeafId,
266
461
  metadata: this.metadata,
462
+ ...(this.packRefs ? { guardrailPacks: this.packRefs } : {}),
267
463
  });
268
464
  }
269
465
  async clone(options = {}) {
@@ -286,6 +482,7 @@ export class RuntimeAgentSession {
286
482
  store: this.store,
287
483
  leafId: branch.length ? remap.get(branch[branch.length - 1].id) : undefined,
288
484
  metadata: this.metadata,
485
+ ...(this.packRefs ? { guardrailPacks: this.packRefs } : {}),
289
486
  });
290
487
  }
291
488
  branchReader() {
@@ -347,6 +544,15 @@ export class RuntimeAgentSession {
347
544
  });
348
545
  }
349
546
  }
547
+ /**
548
+ * Run end (finish, suspension, or denial): closes the run-scoped subscribers only. Subscribers that
549
+ * opted into `SubscribeOptions.acrossRuns` stay open for the next run of this session.
550
+ */
551
+ closeRunSubscribers() {
552
+ for (const subscriber of this.subscribers)
553
+ if (!subscriber.acrossRuns)
554
+ subscriber.close();
555
+ }
350
556
  closeSubscribers() {
351
557
  for (const subscriber of this.subscribers)
352
558
  subscriber.close();
@@ -457,10 +663,15 @@ export class RuntimeAgentSession {
457
663
  signal,
458
664
  };
459
665
  this.emit({ type: "compaction_started", sessionId: this.id, runId });
460
- let result = await strategy.compact(context);
666
+ // Plan 106 R3: pre-compaction seam — the strategy compacts exactly the context this returns.
667
+ const requested = (await this.agent.config.middleware?.run("compaction_request", context)) ?? context;
668
+ let result = await strategy.compact(requested);
461
669
  result = { ...result, summary: redactSecrets(result.summary, secrets) };
462
- const payload = (await this.agent.config.middleware?.run("compaction", { context, result })) ?? {
463
- context,
670
+ const payload = (await this.agent.config.middleware?.run("compaction", {
671
+ context: requested,
672
+ result,
673
+ })) ?? {
674
+ context: requested,
464
675
  result,
465
676
  };
466
677
  result = { ...payload.result, summary: redactSecrets(payload.result.summary, secrets) };
@@ -5,7 +5,7 @@
5
5
  * frozen prefix. The assembly branch lives in `input.ts`; opt-in agent wiring (Task 5) is not
6
6
  * part of this module. */
7
7
  import { type ContextBudgetMessageGroups } from "./context-budget.js";
8
- import type { AttentionCompiler, AttentionCompilerContext, AttentionCompilerOptions, AttentionCompilerSetting, AttentionInputCapOptions, AttentionReport, ContextBlock, Skill, ToolDefinition } from "./contracts.js";
8
+ import type { AttentionCompiler, AttentionCompilerContext, AttentionCompilerOptions, AttentionCompilerSetting, AttentionInputCapOptions, AttentionReport, AttentionTrigger, AttentionTriggerDecision, AttentionTriggerState, ContextBlock, Skill, ToolDefinition } from "./contracts.js";
9
9
  import { type CompactionTrigger } from "./contracts-core/compaction.js";
10
10
  import type { SecretRedactor } from "./redaction.js";
11
11
  import { type ResolvedToolResultFoldOptions } from "./tool-result-fold.js";
@@ -25,13 +25,29 @@ export declare function resolveAttentionReserveTokens(value: unknown): number;
25
25
  /** C2: host `maxInputTokens` wins; otherwise `contextWindow - (maxOutputTokens ?? 0) - reserve`.
26
26
  * Shared with the `input_ratio` compaction trigger (Task 4). Throws when no cap can be derived. */
27
27
  export declare function resolveInputCap(options?: AttentionInputCapOptions, model?: AttentionCompilerContext["model"]): number;
28
+ /**
29
+ * Build the frozen per-turn state the axes read: the compiler supplies the cap and the run budget
30
+ * (so a caller cannot silently drop the budget and fall back to the input cap), the caller
31
+ * supplies what only the turn knows.
32
+ */
33
+ export declare function attentionTriggerState(compiler: Pick<AttentionCompiler, "inputCap" | "runInputBudget">, turn: {
34
+ readonly estimatedInputTokens: number;
35
+ readonly runInputTokens?: number;
36
+ readonly turn?: number;
37
+ }): AttentionTriggerState;
38
+ /**
39
+ * Evaluate the fold axes in order against one turn's state; the first axis that fires wins, and
40
+ * its `targetTokens` (when it has one) is what the stages fold to. Called at most twice per turn —
41
+ * once at turn start, once after the stages — so a host predicate never runs per row.
42
+ */
43
+ export declare function evaluateAttentionTrigger(axes: readonly AttentionTrigger[], state: AttentionTriggerState): AttentionTriggerDecision;
28
44
  /** Validate compiler options + resolve the input cap. No provider I/O; unknown or
29
45
  * unresolvable input throws here rather than on the first turn. */
30
46
  export declare function createAttentionCompiler(options?: AttentionCompilerOptions, context?: AttentionCompilerContext): AttentionCompiler;
31
47
  /** Resolve the run's compiler from the agent setting plus an optional run overlay, validating
32
48
  * both eagerly (no provider I/O) so a typo fails at run start, not on some later turn (C12).
33
49
  * Returns `undefined` when the compiler is off — the assembly path then allocates nothing. */
34
- export declare function resolveRunAttentionCompiler(agent: AttentionCompilerSetting | undefined, run: AttentionCompilerSetting | undefined, model: AttentionCompilerContext["model"]): AttentionCompiler | undefined;
50
+ export declare function resolveRunAttentionCompiler(agent: AttentionCompilerSetting | undefined, run: AttentionCompilerSetting | undefined, model: AttentionCompilerContext["model"], runInputBudget?: number | null): AttentionCompiler | undefined;
35
51
  /** Caller-owned sticky frontier: what this session leaf already mutated (C10). Mutations are
36
52
  * monotonic, so a stubbed call stays stubbed and stripped thinking stays stripped even on a
37
53
  * later under-ratio turn — restoring either would rewrite the prompt-cache prefix. */
@@ -53,9 +69,15 @@ export interface AttentionCompileOptions {
53
69
  * decide fold-eligibility. Omitted → the deterministic stub and `keepLast` alone. */
54
70
  readonly fold?: ResolvedToolResultFoldOptions;
55
71
  readonly frontier?: AttentionStickyFrontier;
72
+ /** Session-owned folded bodies (plan 086 T3): a body stored here is re-applied instead of
73
+ * re-summarized, so a sticky row stays byte-identical and the host `summarize` runs once per
74
+ * row instead of once per turn. Omit for one-shot assemblies. */
75
+ readonly attentionFold?: AttentionFoldLedger;
56
76
  readonly redactor?: SecretRedactor;
57
77
  readonly signal?: AbortSignal;
58
78
  readonly turn?: number;
79
+ /** Run input tokens already charged this run; the `run_input_ratio` axis projects onto it. */
80
+ readonly runInputTokens?: number;
59
81
  readonly sessionId?: string;
60
82
  readonly runId?: string;
61
83
  }
@@ -90,6 +112,33 @@ export declare function serializeAttentionStickyFrontier(frontier: AttentionStic
90
112
  export declare function parseAttentionStickyFrontier(value: unknown): PersistedAttentionStickyFrontier | undefined;
91
113
  /** Rebuild a frontier from a restored snapshot; callers hold the session that owns it. */
92
114
  export declare function restoreAttentionStickyFrontier(persisted: PersistedAttentionStickyFrontier): AttentionStickyFrontier;
115
+ /** Session-owned folded bodies, keyed by tool call id. A row is summarized once: every later
116
+ * turn re-applies the stored body, so a sticky row stays byte-identical for the provider cache
117
+ * and the host `summarize` is not called again for it. Bodies are already redacted and capped by
118
+ * the fold that produced them, so nothing here needs redaction at persistence time. */
119
+ export interface AttentionFoldLedger {
120
+ /** toolCallId → stub body (never the payload); insertion order is fold order, oldest first. */
121
+ readonly bodies: Map<string, string>;
122
+ }
123
+ export declare function createAttentionFoldLedger(): AttentionFoldLedger;
124
+ /** Serialized fold ledger (plan 086 T3). Stub bodies only — no payload, and every entry was
125
+ * already capped and redacted when it was folded, so a durable resume restores it verbatim. */
126
+ export interface PersistedAttentionFoldLedger {
127
+ readonly v: 1;
128
+ /** Oldest-first, so a restored ledger is the tail of the folds the session made. */
129
+ readonly bodies: readonly {
130
+ readonly id: string;
131
+ readonly body: string;
132
+ }[];
133
+ }
134
+ /** Bounded snapshot of a live ledger; caller-owned (the runtime persists it, the compiler never
135
+ * writes anywhere). */
136
+ export declare function serializeAttentionFoldLedger(ledger: AttentionFoldLedger): PersistedAttentionFoldLedger;
137
+ /** Validate a persisted ledger from an untrusted store (plan 086 T3). Malformed *entries* are
138
+ * dropped one by one — a body the compiler cannot trust simply re-summarizes on the next turn —
139
+ * while a malformed *shape* yields `undefined` so the caller starts from an empty ledger.
140
+ * Never throws: a resume must not fail because a checkpoint was hand-edited. */
141
+ export declare function restoreAttentionFoldLedger(value: unknown): AttentionFoldLedger | undefined;
93
142
  /** Consecutive `truncated` turns that arm compaction by default. */
94
143
  export declare const DEFAULT_ATTENTION_TRUNCATION_THRESHOLD = 2;
95
144
  export interface AttentionTruncationTriggerOptions {