@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,4 +1,5 @@
1
1
  import { capSkillCatalog, selectSkillsForPrompt, skillHasRenderableBody, skillPromptText, } from "./skill-disclosure.js";
2
+ import { estimateTextTokensForFamily, MODEL_FAMILY_TOKENS, resolveModelFamily } from "./usage-estimation.js";
2
3
  export const CONTEXT_BUDGET_REPORT_METADATA_KEY = "contextBudgetReport";
3
4
  export const HARD_MAX_CONTEXT_BUDGET_TOKENS = 2_000_000;
4
5
  export const HARD_MAX_CONTEXT_BUDGET_BYTES = 32 * 1024 * 1024;
@@ -22,8 +23,29 @@ export function estimateTextTokens(text) {
22
23
  export function estimateTextBytes(text) {
23
24
  return Buffer.byteLength(text, "utf8");
24
25
  }
25
- export function estimateMessageTokens(message, estimateTokens = estimateTextTokens) {
26
- return estimateTokens(messageText(message));
26
+ /**
27
+ * Single message → flattened text → estimator (budget accounting, never billing).
28
+ *
29
+ * Plan 091 Task 1 overload: a message array plus a model id, provider id, or
30
+ * family name returns a labeled {@link TokenEstimate}. It reuses this same
31
+ * per-message flattening and adds the family's per-message chat-template
32
+ * overhead; `unknown` families fall back to the conservative table with
33
+ * `lowConfidence: true`.
34
+ */
35
+ export function estimateMessageTokens(input, estimatorOrFamily) {
36
+ if (isMessageArray(input)) {
37
+ const family = resolveModelFamily(estimatorOrFamily);
38
+ const table = MODEL_FAMILY_TOKENS[family];
39
+ let tokens = table.perMessageOverhead * input.length;
40
+ for (const message of input)
41
+ tokens += estimateTextTokensForFamily(messageText(message), family);
42
+ return { tokens, confidence: table.confidence, lowConfidence: table.confidence === "low" };
43
+ }
44
+ const estimateTokens = typeof estimatorOrFamily === "function" ? estimatorOrFamily : estimateTextTokens;
45
+ return estimateTokens(messageText(input));
46
+ }
47
+ function isMessageArray(value) {
48
+ return Array.isArray(value);
27
49
  }
28
50
  export function estimateMessageBytes(message) {
29
51
  return estimateTextBytes(messageText(message));
@@ -224,7 +246,7 @@ function measureAll(groups, context, skills, tools, skillContext, demotedBodies,
224
246
  for (const message of groups.toolResults)
225
247
  addMessage(message);
226
248
  for (const block of context) {
227
- const text = `${block.title ? `${block.title}:\n` : "Context:\n"}${contextBlockText(block)}`;
249
+ const text = contextBlockMeasureText(block);
228
250
  tokens += estimateTokens(text);
229
251
  bytes += estimateTextBytes(text);
230
252
  }
@@ -234,12 +256,35 @@ function measureAll(groups, context, skills, tools, skillContext, demotedBodies,
234
256
  bytes += estimateTextBytes(text);
235
257
  }
236
258
  if (tools?.length) {
237
- const text = `Available tools:\n${tools.map((tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ""}`).join("\n")}`;
259
+ const text = toolsMeasureText(tools);
238
260
  tokens += estimateTokens(text);
239
261
  bytes += estimateTextBytes(text);
240
262
  }
241
263
  return { tokens, bytes };
242
264
  }
265
+ /** Plan 103 T6: the host's `contextBudget.tokenEstimator`, validated exactly like the budget pass
266
+ * validates it (a non-function, or a non-finite/negative count, fails closed with `TypeError`).
267
+ * `undefined` when no host estimator is configured, so callers can fall through to the built-in
268
+ * heuristic. Exported for the usage seam (`provider-round.ts`) — deliberately not re-exported by
269
+ * `src/index.ts`, so the public surface is unchanged. */
270
+ export function resolveHostTokenEstimator(budget) {
271
+ if (budget?.tokenEstimator === undefined)
272
+ return undefined;
273
+ return resolveTokenEstimator(budget);
274
+ }
275
+ /** Plan 103 T6: tool declarations and context blocks projected with the assembler's own
276
+ * `measureAll` text shapes, so the usage-fallback estimate and the budget pass cannot drift
277
+ * (never `JSON.stringify` of the raw schemas). Exported for the usage seam — deliberately not
278
+ * re-exported by `src/index.ts`. */
279
+ export function estimateRequestExtrasTokens(tools, context, estimateTokens) {
280
+ let tokens = 0;
281
+ if (context?.length)
282
+ for (const block of context)
283
+ tokens += estimateTokens(contextBlockMeasureText(block));
284
+ if (tools?.length)
285
+ tokens += estimateTokens(toolsMeasureText(tools));
286
+ return tokens;
287
+ }
243
288
  function overBudget(cost, budget) {
244
289
  if (budget.maxInputTokens !== undefined && cost.tokens > budget.maxInputTokens)
245
290
  return true;
@@ -320,4 +365,12 @@ function contextBlockText(block) {
320
365
  })
321
366
  .join("\n");
322
367
  }
368
+ /** The context block exactly as `measureAll` measures it (plan 103 T6 shares this shape with the usage seam). */
369
+ function contextBlockMeasureText(block) {
370
+ return `${block.title ? `${block.title}:\n` : "Context:\n"}${contextBlockText(block)}`;
371
+ }
372
+ /** The tool list exactly as `measureAll` measures it (plan 103 T6 shares this shape with the usage seam). */
373
+ function toolsMeasureText(tools) {
374
+ return `Available tools:\n${tools.map((tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ""}`).join("\n")}`;
375
+ }
323
376
  //# sourceMappingURL=context-budget.js.map
@@ -10,7 +10,8 @@ import type { ToolValidator } from "../tools.js";
10
10
  import type { CompactionOptions, RetryOptions } from "./compaction.js";
11
11
  import type { ContentBlock, ErrorInfo, JsonObject, Message, ModelConfig } from "./content.js";
12
12
  import type { ProviderRequestPolicy, SystemPromptConfig } from "./extensions.js";
13
- import type { AgentLoopOptions, AgentLoopStrategy } from "./loop.js";
13
+ import type { GuardrailPackRef } from "./guardrail-packs.js";
14
+ import type { AgentLoopOptions, AgentLoopStrategy, StopHook } from "./loop.js";
14
15
  import type { OwnershipScope } from "./persistence.js";
15
16
  import type { AIProvider, ProviderRequestOptions, ProviderResolver } from "./provider.js";
16
17
  import type { ResourceLoader } from "./resources.js";
@@ -47,6 +48,17 @@ export interface AgentDefinitionResolutionContext {
47
48
  readonly activateAllCapabilities?: true;
48
49
  readonly overrides?: Partial<AgentConfig>;
49
50
  }
51
+ /** Per-turn tool menu. Called at `loopCtx.assemble` before each provider request. */
52
+ export interface ToolNarrowingContext {
53
+ /** 1-based provider turn this assemble precedes. */
54
+ readonly turn: number;
55
+ /** Text of the latest assistant message, when any. */
56
+ readonly lastAssistantText?: string;
57
+ /** Run-grant tool names (R11 snapshot, including generated `search_tools` when disclosure is search). */
58
+ readonly toolIds: readonly string[];
59
+ }
60
+ /** Host callback: return a subset of `toolIds`. Superset names are clamped; throw fails the turn. */
61
+ export type ToolNarrowing = (ctx: ToolNarrowingContext) => readonly string[] | Promise<readonly string[]>;
50
62
  export interface AgentConfig {
51
63
  readonly id?: string;
52
64
  readonly name?: string;
@@ -64,11 +76,34 @@ export interface AgentConfig {
64
76
  /** Tools disclosure: "all" (default) sends every active tool schema; "search" sends top-k + the generated `search_tools` tool. */
65
77
  readonly toolsDisclosure?: import("../tool-search.js").ToolsDisclosure;
66
78
  readonly toolsSearch?: import("../tool-search.js").ToolsSearchOptions;
79
+ /** Per-turn restrictive allow-list over the run grant. RunOptions override. */
80
+ readonly toolNarrowing?: ToolNarrowing;
81
+ /** Opt-in: tools hidden this turn stay callable by name (default off). */
82
+ readonly allowHiddenToolCalls?: true;
67
83
  /** Opt-in projection-only fold for aged large tool results in provider view; store untouched. */
68
84
  readonly toolResultFold?: import("../tool-result-fold.js").ToolResultFoldOptions;
69
85
  /** Opt-in attention compiler (plan 074): `true` for defaults, an object to tune ratios/depth.
70
86
  * Omitted keeps today's request bytes; per-run options may only relax this setting. */
71
87
  readonly attentionCompiler?: import("./attention.js").AttentionCompilerSetting;
88
+ /**
89
+ * Missing-usage handling (plan 091 T2, plan 103 T5): `"fallback"` (default) records a labeled
90
+ * estimate when a provider turn reports no usage; `"off"` leaves usage absent — never zero;
91
+ * `"strict"` refuses a usage-less turn instead, failing the run with `code: "usage_missing"`
92
+ * (`name: "UsageMissingError"`) so a host whose cost gates cannot tolerate approximations never
93
+ * runs on an estimate. A refusal is a harness decision, not a provider failure, so it carries no
94
+ * `failureClass` and is never retried. Estimates are marked `Usage.estimated` and never priced.
95
+ */
96
+ readonly usageEstimation?: "fallback" | "off" | "strict";
97
+ /**
98
+ * Session-turn context budget (plan 103 T6): forwarded to every `assembleProviderInput`
99
+ * call this agent's sessions make, so a session gets the same eviction, `tokenEstimator`,
100
+ * and `reportOmissions` semantics as a direct assembler caller. Mutually exclusive with
101
+ * `attentionCompiler` (rejected at assembly). With `usageEstimation: "fallback"`, the
102
+ * missing-usage estimate prefers this budget's own measurement: the request's
103
+ * `ContextBudgetReport.keptTokens` when `reportOmissions` is on, else the `tokenEstimator`
104
+ * projection — see [Runs and usage](../../docs/runs-and-usage.md).
105
+ */
106
+ readonly contextBudget?: import("../context-budget.js").ContextBudget;
72
107
  readonly inputBuilder?: InputBuilder;
73
108
  readonly promptBuilder?: PromptBuilder;
74
109
  readonly middleware?: MiddlewareRegistry;
@@ -102,6 +137,8 @@ export interface AgentConfig {
102
137
  readonly inputLayout?: InputAssemblyLayout;
103
138
  readonly loop?: AgentLoopStrategy | AgentLoopOptions;
104
139
  readonly guardrails?: Guardrails;
140
+ /** Run-end stop hooks (plan 106 R1); `RunOptions.stopHooks` appends to this list. */
141
+ readonly stopHooks?: readonly StopHook[];
105
142
  /** Opt-in durable interruption/checkpointing default for this agent. */
106
143
  readonly runState?: AgentRunStateOptions;
107
144
  /** Internal marker set by createSecureAgent(); makes security defaults immutable per run. */
@@ -134,6 +171,12 @@ export interface AgentSessionConfig {
134
171
  readonly store?: SessionStore;
135
172
  readonly leafId?: string;
136
173
  readonly metadata?: Readonly<Record<string, unknown>>;
174
+ /**
175
+ * Restrictive-only guardrail packs compiled once per session onto the existing tool interception
176
+ * seams (plan 092). Built-in ids are versioned; an input object with `rules` is an inline pack.
177
+ * Compiled rules can only deny or tripwire — they never grant permissions.
178
+ */
179
+ readonly guardrailPacks?: readonly GuardrailPackRef[];
137
180
  /**
138
181
  * TTL of the in-memory `session.snapshot()` branch cache in milliseconds.
139
182
  * Default `DEFAULT_SNAPSHOT_CACHE_TTL_MS`; `0` disables the cache (every snapshot read
@@ -155,6 +198,12 @@ export interface AgentSessionCloneOptions {
155
198
  }
156
199
  export type SubscriberOverflowPolicy = "close" | "drop_oldest" | "drop_newest";
157
200
  export interface SubscribeOptions {
201
+ /**
202
+ * Plan 104 T5: `true` keeps this subscriber open across runs of the same session; it is then
203
+ * closed only by the host (`subscription.close()` / `session.closeSubscribers()`) or by an
204
+ * overflow under the default `close` policy. Default `false` (closed at run end).
205
+ */
206
+ readonly acrossRuns?: boolean;
158
207
  /** Maximum queued events for a subscriber that is not actively awaiting `next()`. Defaults to 1024. */
159
208
  readonly maxQueuedEvents?: number;
160
209
  /** What to do when `maxQueuedEvents` is reached. Defaults to `close`. */
@@ -277,6 +326,8 @@ export interface PromptBuildRequest {
277
326
  readonly skills?: readonly Skill[];
278
327
  readonly skillsDisclosure?: import("../skill-disclosure.js").SkillsDisclosure;
279
328
  readonly loadedSkills?: import("../skill-disclosure.js").LoadedSkillSet;
329
+ /** Loaded skill bodies already appended to `messages` by the session tail allocator; render catalog entries only. */
330
+ readonly tailSkillBodies?: boolean;
280
331
  /** Tools disclosure: "all" (default) sends every active tool schema; "search" sends top-k + the generated `search_tools` tool. */
281
332
  readonly toolsDisclosure?: import("../tool-search.js").ToolsDisclosure;
282
333
  readonly toolsSearch?: import("../tool-search.js").ToolsSearchOptions;
@@ -12,9 +12,75 @@ export interface AttentionInputCapOptions {
12
12
  /** Output + next-turn headroom subtracted from the window (default 1024). */
13
13
  readonly reserveTokens?: number;
14
14
  }
15
+ /** Fold axes (plan 086 T2). `input_ratio` is the legacy `triggerRatio` axis; the rest are new. */
16
+ export type AttentionTriggerKind = "input_ratio" | "run_input_ratio" | "token_floor" | "predicate";
17
+ /** Per-turn inputs an axis reads. Frozen before a `predicate` sees it; estimates and ids only. */
18
+ export interface AttentionTriggerState {
19
+ /** Estimated tokens of the request this turn will send — what the ratio axes compare. */
20
+ readonly estimatedInputTokens: number;
21
+ /** Resolved per-request input cap (`resolveInputCap`). */
22
+ readonly inputCapTokens: number;
23
+ /** Cumulative run input budget when the run limits declare one; absent falls back to `inputCapTokens`. */
24
+ readonly runInputBudgetTokens?: number;
25
+ /** Run input tokens already charged by provider usage this run (0 when unknown). */
26
+ readonly runInputTokens: number;
27
+ /** 1-based provider turn index. */
28
+ readonly turn: number;
29
+ }
30
+ /** Host predicate axis. Runs host-supplied code under the same trust as `CompactionTrigger.custom`. */
31
+ export type AttentionTriggerFunction = (state: AttentionTriggerState) => boolean;
32
+ /** One fold gate. An array is any-of; the first axis that fires is the one attributed. */
33
+ export type AttentionTrigger =
34
+ /** Legacy axis: fires when the assembled request reaches `ratio` of the per-request input cap. */
35
+ {
36
+ readonly kind: "input_ratio";
37
+ readonly ratio: number;
38
+ }
39
+ /** Cumulative axis: fires when `runInputTokens + estimatedInputTokens` reaches `ratio` of the run
40
+ * input budget, so a run capped below the window folds before the cap kills it. Falls back to
41
+ * the `input_ratio` comparison when the run limits declare no input budget. */
42
+ | {
43
+ readonly kind: "run_input_ratio";
44
+ readonly ratio: number;
45
+ }
46
+ /** Absolute axis: fires when the assembled request reaches `tokens`, whatever the cap. */
47
+ | {
48
+ readonly kind: "token_floor";
49
+ readonly tokens: number;
50
+ }
51
+ /** Host axis: fires when `shouldFold` returns `true`. Called once per turn; must be synchronous. */
52
+ | {
53
+ readonly kind: "predicate";
54
+ readonly shouldFold: AttentionTriggerFunction;
55
+ };
56
+ /** Accepted `trigger` value: one axis, one predicate, or an any-of array of either. */
57
+ export type AttentionTriggerInput = AttentionTrigger | AttentionTriggerFunction | readonly (AttentionTrigger | AttentionTriggerFunction)[];
58
+ /** Result of evaluating the axes once against one turn's state. */
59
+ export interface AttentionTriggerDecision {
60
+ readonly shouldFold: boolean;
61
+ /** First axis that fired, in configured order (plan 087 attribution). */
62
+ readonly firedAxis?: AttentionTriggerKind;
63
+ /** Estimated-token target the sticky stages fold to; absent = fold every eligible row. */
64
+ readonly targetTokens?: number;
65
+ /** `true` when folding this request can settle the fired axis, so a still-firing axis after every
66
+ * eligible row throws `AttentionBudgetError`. Cumulative `run_input_ratio` axes are `false`: the
67
+ * spend is already booked, folding only slows the counter, and the run limit owns the cap. */
68
+ readonly failsClosed: boolean;
69
+ }
15
70
  export interface AttentionCompilerOptions extends AttentionInputCapOptions {
16
71
  /** Fraction of `inputCap` that triggers mutation; in `(0, 1)` (default 0.75). */
17
72
  readonly triggerRatio?: number;
73
+ /**
74
+ * Fold axes (plan 086 T2). Omitted keeps the `triggerRatio` axis alone, so requests gate
75
+ * exactly as before. Given, it **replaces** the `triggerRatio` axis: the gate is the any-of of
76
+ * the listed axes, and `triggerRatio` stays the input-ratio reference for `compactRatio` and
77
+ * the report. A run overlay may not set it (the gate is agent-config only).
78
+ *
79
+ * Predicate axes execute host-supplied code, trusted exactly like `CompactionTrigger.custom`:
80
+ * the function runs once per turn with a frozen `AttentionTriggerState` and must return a
81
+ * boolean synchronously (a `Promise` return fails closed with a `TypeError`).
82
+ */
83
+ readonly trigger?: AttentionTriggerInput;
18
84
  /** Where compaction should fire relative to `triggerRatio`; must exceed it (default 0.9). */
19
85
  readonly compactRatio?: number;
20
86
  /** Newest thinking-bearing assistant turns kept intact (default 1). */
@@ -23,6 +89,17 @@ export interface AttentionCompilerOptions extends AttentionInputCapOptions {
23
89
  readonly keepLast?: number;
24
90
  /** Tool names whose results are never stubbed, whatever the ratio. */
25
91
  readonly excludeTools?: readonly string[];
92
+ /**
93
+ * Durable folding (plan 086 T3): persist the fold ledger and its sticky frontier into the
94
+ * run's checkpoint at each fold, and restore them before the first turn after a resume.
95
+ * Requires a durable run (`runState` with a checkpoint store); independent of
96
+ * `persistSessionState`, which governs skill/tool session state instead.
97
+ *
98
+ * Off by default: durable folding costs one extra checkpoint write per fold (never per turn)
99
+ * and stores the folded bodies — already redacted, capped by `maxSummaryBytes` — so a resumed
100
+ * request carries the same folded rows a live run would. A run overlay may not set it.
101
+ */
102
+ readonly durable?: boolean;
26
103
  }
27
104
  /** Where the compiler is switched on: `true` uses the defaults, an object tunes them, `false`
28
105
  * (or omitted) leaves requests byte-for-byte as they are without the compiler. On
@@ -34,6 +111,10 @@ export interface AttentionCompilerContext {
34
111
  readonly model?: Pick<ModelConfig, "limits">;
35
112
  /** Validated at create so an unknown trigger type fails at config time, not on turn one. */
36
113
  readonly compactionTrigger?: CompactionTrigger;
114
+ /** Cumulative run input budget the `run_input_ratio` axis folds against (the resolved
115
+ * `RunLimits.maxInputTokens`); `null`/omitted means no run budget, so that axis falls back to
116
+ * the per-request input cap. Distinct from `maxInputTokens`, which caps one request. */
117
+ readonly runInputBudget?: number | null;
37
118
  }
38
119
  /** Validated, frozen configuration returned by `createAttentionCompiler`. */
39
120
  export interface AttentionCompiler {
@@ -44,6 +125,14 @@ export interface AttentionCompiler {
44
125
  readonly thinkingKeepTurns: number;
45
126
  readonly keepLast: number;
46
127
  readonly excludeTools: readonly string[];
128
+ /** Normalized, frozen fold axes in evaluation order: the `input_ratio` default when no
129
+ * `trigger` was configured, otherwise exactly the configured axes. */
130
+ readonly trigger: readonly AttentionTrigger[];
131
+ /** Run input budget resolved at create; absent when the run limits declare none. */
132
+ readonly runInputBudget?: number;
133
+ /** Durable folding resolved at create (plan 086 T3); `true` opts the run's fold state into
134
+ * checkpoint persistence, so `assembleProviderInput` callers should pass the ledger. */
135
+ readonly durable: boolean;
47
136
  }
48
137
  /** One mutated turn. Under-ratio turns emit nothing and produce no report (C14). */
49
138
  export interface AttentionReport {
@@ -53,12 +142,18 @@ export interface AttentionReport {
53
142
  readonly usedAfter: number;
54
143
  readonly inputCap: number;
55
144
  readonly triggerRatio: number;
145
+ /** Axis that opened the gate on this turn, when one did (plan 087 attribution). */
146
+ readonly firedAxis?: AttentionTriggerKind;
56
147
  /** Thinking turns absent from this request; rows re-applied from the sticky frontier count again. */
57
148
  readonly droppedThinkingTurns: number;
58
149
  /** Tool results stubbed in this request; rows re-applied from the sticky frontier count again. */
59
150
  readonly stubbedToolResults: number;
60
151
  /** Payload bytes the stubs took out of this request (never the stub text itself). */
61
152
  readonly stubbedBytes: number;
153
+ /** Folded bodies this turn added to the ledger — the summarize calls a cache saved, and the
154
+ * signal that a durable fold has new state to checkpoint (plan 086 T3). Zero on a turn that
155
+ * only re-applied bodies the ledger already held. */
156
+ readonly newFoldedBodies: number;
62
157
  /** True when the gate stopped with eligible rows left: the sticky frontier is partial. */
63
158
  readonly truncated: boolean;
64
159
  readonly runId?: string;
@@ -2,6 +2,7 @@
2
2
  * Moved verbatim from contracts-core.ts; public surface unchanged behind the barrel. */
3
3
  import type { AudioContent, DocumentContent, FileContent } from "../content.js";
4
4
  import type { ModelCacheCapabilities } from "./provider.js";
5
+ import type { TokenEstimateConfidence } from "./usage.js";
5
6
  export type JsonPrimitive = string | number | boolean | null;
6
7
  export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
7
8
  export interface JsonObject {
@@ -177,6 +178,15 @@ export interface Usage {
177
178
  readonly cacheWriteTokens?: number;
178
179
  readonly cost?: number;
179
180
  readonly currency?: string;
181
+ /**
182
+ * True when these token counts are a harness estimate, never provider truth
183
+ * (plan 091 T2). Estimated usage is never priced, and every accounting export
184
+ * (ledger rows, run totals, turn events) keeps this flag so a billing surface
185
+ * can always tell an estimate from a report. Absent means reported.
186
+ */
187
+ readonly estimated?: boolean;
188
+ /** Confidence label of an estimated usage; absent on reported usage. */
189
+ readonly confidence?: TokenEstimateConfidence;
180
190
  }
181
191
  /**
182
192
  * Host-supplied pricing adapter (plan 062): quotes cost rates per model id. Core
@@ -7,6 +7,7 @@ import type { Middleware, MiddlewareHookName, MiddlewareRegistry } from "../midd
7
7
  import type { AgentDefinition, CommandDefinition, ContextProvider, InputBuilder, InstructionInjector, PromptBuilder, Skill } from "./agent.js";
8
8
  import type { CompactionStrategy, RetryPolicy } from "./compaction.js";
9
9
  import type { ErrorInfo, ModelConfig } from "./content.js";
10
+ import type { StopHook } from "./loop.js";
10
11
  import type { StoreFactory } from "./persistence.js";
11
12
  import type { AIProvider, ProviderRequest } from "./provider.js";
12
13
  import type { Credential, CredentialResolver, ResourceLoader, SettingsProvider } from "./resources.js";
@@ -160,4 +161,6 @@ export interface ExtensionAPI {
160
161
  registerProviderRequestPolicy(policy: ProviderRequestPolicy): void;
161
162
  registerSystemPromptContribution(contribution: SystemPromptContribution): void;
162
163
  registerInstructionInjector(injector: InstructionInjector): void;
164
+ /** Contributes an inert run-end stop hook; activate it via `activateKernel()` → `AgentConfig.stopHooks` (plan 106 R1). */
165
+ registerStopHook(hook: StopHook): void;
163
166
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Guardrail packs (plan 092 Task 2): config-declared, restrictive-only rule sets compiled once per
3
+ * session onto the existing tool interception seams (`tool_input` / `tool_output`). Packs can only
4
+ * deny, tripwire, or ask for approval — they never grant permissions, widen arguments, or add a stage.
5
+ */
6
+ import type { JsonObject } from "./content.js";
7
+ export type GuardrailRuleAction = "deny" | "tripwire" | "ask";
8
+ /** Read-only identity view handed to a pack rule predicate (never carries a raw argument echo). */
9
+ export interface GuardrailRuleContext {
10
+ readonly toolName: string;
11
+ readonly toolCallId: string;
12
+ readonly sessionId: string;
13
+ readonly runId: string;
14
+ readonly metadata: Readonly<Record<string, unknown>>;
15
+ /** Pack-local state shared with the pack's result observer; treat as read-only. */
16
+ readonly state: Readonly<Record<string, unknown>>;
17
+ }
18
+ export interface GuardrailRule {
19
+ readonly id: string;
20
+ /** Tool names this rule applies to; omitted matches every tool. */
21
+ readonly tool?: string | readonly string[];
22
+ /** Regex source, compiled once; tested against matched argument strings. Exactly one of `pattern` / `deny`. */
23
+ readonly pattern?: string | RegExp;
24
+ /** Dot path(s) of arguments to test (e.g. `command`, `["from", "to"]`); omitted deep-scans argument strings. */
25
+ readonly argPath?: string | readonly string[];
26
+ /** Typed predicate escape hatch (host-trusted like all host code); deny when it returns true. Exactly one of `pattern` / `deny`. */
27
+ readonly deny?: (args: JsonObject, context: GuardrailRuleContext) => boolean;
28
+ /**
29
+ * Defaults to `deny`. `tripwire` also rejects the enclosing run. `ask` suspends a durable run
30
+ * before the call dispatches (the pending decision names this rule) and blocks the call in a run
31
+ * that cannot suspend; it requires `pattern` — an opaque predicate cannot raise an approval
32
+ * (plan 104 Task 3).
33
+ */
34
+ readonly action?: GuardrailRuleAction;
35
+ /** Bounded, redacted record reason; defaults to the pack/rule id. */
36
+ readonly reason?: string;
37
+ }
38
+ /** Built-in pack selection by `id` (optional `options`), or an inline pack when `rules` is present. */
39
+ export interface GuardrailPackInput {
40
+ readonly id: string;
41
+ readonly version?: number;
42
+ readonly options?: Readonly<Record<string, unknown>>;
43
+ readonly rules?: readonly GuardrailRule[];
44
+ }
45
+ /** A session's `guardrailPacks` entry: built-in pack id, or an inline/built-in pack input object. */
46
+ export type GuardrailPackRef = string | GuardrailPackInput;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=guardrail-packs.js.map
@@ -46,6 +46,36 @@ export interface TurnPolicyOptions {
46
46
  */
47
47
  readonly stop?: (context: TurnBoundaryContext) => TurnStopDecision;
48
48
  }
49
+ /**
50
+ * Run-end stop-hook contract (plan 106 R1). Stop hooks run at a natural loop end — never after a
51
+ * loop ceiling, a host turn-policy stop, or an artifact failure — and decide whether the run is
52
+ * done. The first `continue` queues `reason` (plus optional `steer`) through the same steer path a
53
+ * host would use and re-enters the loop; `stop` (or no hook continuing) ends the run normally.
54
+ */
55
+ export interface StopHookContext {
56
+ readonly sessionId: string;
57
+ readonly runId: string;
58
+ /** Provider turns already assembled in this run (resumption continues the run's counter). */
59
+ readonly turn: number;
60
+ /** Live transcript at loop end; hooks read it, never mutate it. */
61
+ readonly history: readonly Message[];
62
+ readonly metadata: Readonly<Record<string, unknown>>;
63
+ readonly signal: AbortSignal;
64
+ /** True on every invocation after the first continuation in this run (Claude Code `stop_hook_active`). */
65
+ readonly stopHookActive: boolean;
66
+ }
67
+ /** `continue` re-enters the loop with `reason` queued as a steer (optional extra `steer` message follows it). */
68
+ export type StopHookDecision = {
69
+ readonly action: "stop";
70
+ } | {
71
+ readonly action: "continue";
72
+ readonly reason: string;
73
+ readonly steer?: string | Message;
74
+ };
75
+ export interface StopHook {
76
+ readonly name: string;
77
+ decide(context: StopHookContext): StopHookDecision | Promise<StopHookDecision>;
78
+ }
49
79
  export interface LoopContext {
50
80
  readonly sessionId: string;
51
81
  readonly runId: string;
@@ -54,6 +84,12 @@ export interface LoopContext {
54
84
  readonly history: Message[];
55
85
  readonly input: AgentInput;
56
86
  readonly inputMessages: readonly Message[];
87
+ /**
88
+ * True when this `run()` call is a stop-hook continuation re-entry (plan 106 R1): `input` and
89
+ * `inputMessages` are empty because the continuation message is already in `history`. Custom
90
+ * strategies must not replay run-start input when this is set.
91
+ */
92
+ readonly continuation?: boolean;
57
93
  readonly maxToolRounds: number;
58
94
  /**
59
95
  * Why the loop stopped, when a limit/ceiling ends the run cleanly (F4). Strategies set
@@ -62,6 +62,36 @@ export interface AIProvider {
62
62
  readonly id: string;
63
63
  generate(request: ProviderRequest): AsyncIterable<ProviderEvent>;
64
64
  }
65
+ /**
66
+ * Closed taxonomy for why a provider turn stopped (plan 087 T1). Adapters map native wire
67
+ * reasons (`finish_reason`, `stop_reason`, `finishReason`, Converse `stopReason`) through the
68
+ * shared `mapProviderStopReason` table; `unknown` is the escape hatch for a new wire value.
69
+ */
70
+ export type ProviderStopReason = "end_turn" | "tool_calls" | "max_output_tokens" | "content_filter" | "abort" | "provider_error" | "unknown";
71
+ /**
72
+ * Effective budget snapshot at provider-turn end (plan 087 T1): current-turn input tokens
73
+ * against the per-request input cap, cumulative run input against its budget, and the turn
74
+ * axis, so a host can see which limit was closest without instrumenting the session.
75
+ */
76
+ export interface TurnBudgets {
77
+ /** Input tokens charged for this turn: provider-reported, or a labeled fallback estimate when the
78
+ * provider reported none; absent when neither exists. See `inputTokensSource`. */
79
+ readonly inputTokens?: number;
80
+ /** Provenance of `inputTokens`: `"reported"` from the provider, `"estimated"` from the
81
+ * `usageEstimation: "fallback"` seam. Absent together with `inputTokens`; `usage.estimated` stays
82
+ * the authoritative label for the turn's usage. */
83
+ readonly inputTokensSource?: "reported" | "estimated";
84
+ /** Resolved per-request input cap (attention compiler when enabled); absent when no cap derivable. */
85
+ readonly inputCap?: number;
86
+ /** Cumulative run input budget (`RunLimits.maxInputTokens`); absent when the axis is disabled. */
87
+ readonly runInputBudget?: number;
88
+ /** Cumulative input tokens charged this run (all provider turns). */
89
+ readonly runInputUsed: number;
90
+ /** Provider turns started this run (1-based current turn at turn end). */
91
+ readonly turns: number;
92
+ /** Resolved clean turn cap; `null` when disabled. */
93
+ readonly maxTurns: number | null;
94
+ }
65
95
  export type ProviderResolver = (model: ModelConfig) => AIProvider | undefined;
66
96
  /** Realtime audio/session event. Realtime is a bidirectional session, not a request/response
67
97
  * stream, so it is a separate neutral seam from `AIProvider.generate()`. Credentials are
@@ -22,6 +22,12 @@ export interface RunLimits {
22
22
  readonly amount: number;
23
23
  readonly currency: string;
24
24
  };
25
+ /**
26
+ * Clean cap on stop-hook continuations in one run (plan 106 R1). Default 3; `0` observes stop
27
+ * hooks but never continues; `null` disables the cap. Layers narrow by min like every other
28
+ * policy axis, and the cap ends the run with `stopReason: "hook_limit"` (no limit breach).
29
+ */
30
+ readonly maxStopContinuations?: number | null;
25
31
  }
26
32
  /** Fully resolved limits after `resolveRunLimits`: every policy axis is a finite cap or `null` (disabled). */
27
33
  export interface ResolvedRunLimits {
@@ -39,8 +45,11 @@ export interface ResolvedRunLimits {
39
45
  readonly amount: number;
40
46
  readonly currency: string;
41
47
  };
48
+ /** Stop-hook continuation cap (plan 106 R1); not a run-limit counter axis. */
49
+ readonly maxStopContinuations: number | null;
42
50
  }
43
- export type RunLimitName = keyof Required<RunLimits>;
51
+ /** Counter-backed limit axes; `maxStopContinuations` caps a clean stop instead of a breach. */
52
+ export type RunLimitName = Exclude<keyof Required<RunLimits>, "maxStopContinuations">;
44
53
  export interface RunLimitCounters {
45
54
  readonly turns: number;
46
55
  readonly providerAttempts: number;
@@ -60,6 +69,25 @@ export interface RunLimitBreach {
60
69
  readonly observed: number;
61
70
  readonly currency?: string;
62
71
  }
72
+ /** One dispatched host tool call, bounded to id + name + argument hash (plan 087 T2). */
73
+ export interface ToolCallSummary {
74
+ readonly id: string;
75
+ readonly name: string;
76
+ /** `sha256:<64 hex>` over the canonicalized arguments; raw arguments never enter events. */
77
+ readonly argHash: string;
78
+ }
79
+ /** One run-limit axis and how close it came to its cap: `used / cap` in [0, 1] (plan 087 T2). */
80
+ export interface BudgetAxisUsage {
81
+ readonly axis: RunLimitName;
82
+ readonly usedRatio: number;
83
+ }
84
+ /** Run counters at exhaustion (plan 087 T2): the axes a host reads first when attributing a death. */
85
+ export interface BudgetConsumedCounters {
86
+ readonly turns: number;
87
+ readonly inputTokens: number;
88
+ readonly providerAttempts: number;
89
+ readonly requestBytes: number;
90
+ }
63
91
  export type GuardrailStage = "input" | "output" | "tool_input" | "tool_output";
64
92
  export type GuardrailAction = "allow" | "block" | "tripwire" | "interrupt";
65
93
  export type GuardrailValue<S extends GuardrailStage> = S extends "input" ? readonly Message[] : S extends "output" ? ProviderTurnResult : S extends "tool_input" ? ToolCallContent : ToolResult;
@@ -35,8 +35,8 @@ export interface SessionStore {
35
35
  /**
36
36
  * Optional bounded session search. Prefer implementing this **or** returning a companion
37
37
  * `SessionIndex` from the adapter factory — hosts must not need both. Call
38
- * `resolveSessionSearchQuery` before scan/query. Memory defaults to capped linear
39
- * search (`sessionSearchMode: "unsupported"` throws). JSONL throws unsupported.
38
+ * `resolveSessionSearchQuery` before scan/query. Memory and JSONL default to capped linear
39
+ * search (memory `sessionSearchMode: "unsupported"` throws); DB adapters index.
40
40
  */
41
41
  searchSessions?(query: SessionSearchQuery): Promise<PersistencePage<SessionSearchHit>>;
42
42
  }
@@ -58,11 +58,18 @@ export declare const DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES: number;
58
58
  export declare const HARD_MAX_SESSION_SEARCH_LINEAR_BYTES: number;
59
59
  export declare const DEFAULT_MAX_SESSION_SEARCH_FTS_CANDIDATES = 1000;
60
60
  export declare const HARD_MAX_SESSION_SEARCH_FTS_CANDIDATES = 5000;
61
+ /** Entry-kind filter for `SessionSearchQuery.kind`; `"any"` (the default) matches every kind. */
62
+ export type SessionSearchKind = SessionEntryKind | "any";
61
63
  /** Bounded session search filters. Workspace matches host-written `metadata.workspaceRoot`. */
62
64
  export interface SessionSearchQuery extends PersistenceQuery, OwnershipScope {
63
65
  readonly workspaceRoot?: string;
64
66
  /** Optional full-text / message+summary query (adapter-defined matching). */
65
67
  readonly query?: string;
68
+ /**
69
+ * Restrict the text `query` to entries of these kinds (one kind or a list). Omitted or `"any"`
70
+ * matches every kind. Annotation search is `kind: ["label", "summary", "metadata", "custom"]`.
71
+ */
72
+ readonly kind?: SessionSearchKind | readonly SessionSearchKind[];
66
73
  readonly provider?: string;
67
74
  readonly model?: string;
68
75
  readonly label?: string;
@@ -73,11 +80,20 @@ export interface SessionSearchQuery extends PersistenceQuery, OwnershipScope {
73
80
  }
74
81
  /**
75
82
  * Safe search hit for resume/checkout. Never includes credentials or raw full transcripts.
76
- * `leafId` is the branch tip for `session.checkout` when known.
83
+ * `leafId` is the branch tip for `session.checkout` when known; when a text `query` matched,
84
+ * `entryId`/`runId`/`turn`/`score` point at the matched entry and `snippet` is its matched text.
77
85
  */
78
86
  export interface SessionSearchHit {
79
87
  readonly sessionId: string;
80
88
  readonly leafId?: string;
89
+ /** Transcript entry that matched the text `query` (absent for filter-only searches). */
90
+ readonly entryId?: string;
91
+ /** Run that wrote the matched entry. */
92
+ readonly runId?: string;
93
+ /** 1-based position of the matched entry in the session transcript (`(timestamp, id)` order). */
94
+ readonly turn?: number;
95
+ /** Matched-entry relevance from the store's full-text index; higher is better (0 is a valid score). */
96
+ readonly score?: number;
81
97
  readonly updatedAt?: string;
82
98
  readonly label?: string;
83
99
  readonly summary?: string;
@@ -89,10 +105,12 @@ export interface SessionSearchHit {
89
105
  export interface SessionIndex {
90
106
  search(query: SessionSearchQuery): Promise<PersistencePage<SessionSearchHit>>;
91
107
  }
92
- /** Validated search query with finite `limit` / `order` filled in. */
108
+ /** Validated search query with finite `limit` / `order` filled in and `kind` normalized. */
93
109
  export interface ResolvedSessionSearchQuery extends SessionSearchQuery {
94
110
  readonly limit: number;
95
111
  readonly order: "asc" | "desc";
112
+ /** Concrete kinds to match, or `undefined` for "any". */
113
+ readonly kind?: readonly SessionEntryKind[];
96
114
  }
97
115
  /**
98
116
  * O(1) validation before any scan/query. Applies default page limit; rejects NaN,
@@ -100,7 +118,7 @@ export interface ResolvedSessionSearchQuery extends SessionSearchQuery {
100
118
  */
101
119
  export declare function resolveSessionSearchQuery(query: SessionSearchQuery): ResolvedSessionSearchQuery;
102
120
  export declare const SESSION_SEARCH_UNSUPPORTED_CODE: "session_search_unsupported";
103
- /** Thrown when a store opts out of `searchSessions` (memory `unsupported`, JSONL). */
121
+ /** Thrown when a store opts out of `searchSessions` (memory `sessionSearchMode: "unsupported"`). */
104
122
  export declare class SessionSearchUnsupportedError extends Error {
105
123
  readonly code: "session_search_unsupported";
106
124
  constructor(message?: string);