@arnilo/prism 0.9.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 (75) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +13 -12
  3. package/dist/agent-approval.d.ts +7 -1
  4. package/dist/agent-approval.js +15 -6
  5. package/dist/agent-run-lifecycle.js +19 -5
  6. package/dist/agent-run-state.d.ts +26 -5
  7. package/dist/agent-run-state.js +97 -1
  8. package/dist/agent-session/event-subscriber.d.ts +2 -0
  9. package/dist/agent-session/event-subscriber.js +3 -0
  10. package/dist/agent-session/session/assemble.js +156 -9
  11. package/dist/agent-session/session/persist.js +11 -5
  12. package/dist/agent-session/session/provider-round.js +54 -13
  13. package/dist/agent-session/session/tool-round.d.ts +2 -2
  14. package/dist/agent-session/session/tool-round.js +58 -5
  15. package/dist/agent-session/session/types.d.ts +20 -2
  16. package/dist/agent-session/session.d.ts +65 -4
  17. package/dist/agent-session/session.js +156 -16
  18. package/dist/context-budget.d.ts +11 -0
  19. package/dist/context-budget.js +33 -2
  20. package/dist/contracts-core/agent.d.ts +26 -5
  21. package/dist/contracts-core/extensions.d.ts +3 -0
  22. package/dist/contracts-core/guardrail-packs.d.ts +8 -3
  23. package/dist/contracts-core/loop.d.ts +36 -0
  24. package/dist/contracts-core/provider.d.ts +6 -1
  25. package/dist/contracts-core/run-limits.d.ts +10 -1
  26. package/dist/contracts-protocol.d.ts +6 -4
  27. package/dist/contracts-run-state.d.ts +37 -3
  28. package/dist/contributions.d.ts +2 -1
  29. package/dist/contributions.js +1 -0
  30. package/dist/extensions.d.ts +15 -1
  31. package/dist/extensions.js +68 -0
  32. package/dist/guardrail-packs/types.d.ts +10 -0
  33. package/dist/guardrail-packs/validation-respect.js +16 -0
  34. package/dist/guardrails.d.ts +42 -1
  35. package/dist/guardrails.js +124 -15
  36. package/dist/index.d.ts +6 -6
  37. package/dist/index.js +4 -4
  38. package/dist/middleware.d.ts +1 -1
  39. package/dist/run-bundle.d.ts +6 -1
  40. package/dist/run-bundle.js +4 -1
  41. package/dist/run-limits.js +13 -0
  42. package/dist/testing/prefix-stability-conformance.d.ts +29 -0
  43. package/dist/testing/prefix-stability-conformance.js +91 -23
  44. package/dist/tools.js +10 -3
  45. package/docs/agent-events.md +12 -8
  46. package/docs/agent-session-runtime.md +9 -6
  47. package/docs/caveman.md +1 -1
  48. package/docs/compaction-llm.md +2 -0
  49. package/docs/compaction-observational-memory.md +21 -1
  50. package/docs/durable-runs.md +4 -3
  51. package/docs/embeddings.md +5 -1
  52. package/docs/execution-timeline.md +3 -2
  53. package/docs/extensions.md +20 -3
  54. package/docs/guardrails.md +16 -6
  55. package/docs/hooks.md +282 -0
  56. package/docs/index.md +18 -15
  57. package/docs/input-and-prompt-assembly.md +1 -1
  58. package/docs/instruction-injection.md +1 -0
  59. package/docs/live-testing.md +3 -1
  60. package/docs/memory-fabric.md +28 -0
  61. package/docs/middleware-hooks.md +54 -4
  62. package/docs/migration.md +13 -0
  63. package/docs/options-index.md +3 -1
  64. package/docs/policy-and-audit.md +14 -1
  65. package/docs/prefix-stability-conformance.md +57 -7
  66. package/docs/provider-packages.md +20 -20
  67. package/docs/public-contracts.md +1 -0
  68. package/docs/rag.md +93 -6
  69. package/docs/release-and-install.md +42 -39
  70. package/docs/runs-and-usage.md +17 -8
  71. package/docs/scoped-agent-memory.md +17 -9
  72. package/docs/scoped-memory.md +138 -0
  73. package/docs/tools.md +1 -1
  74. package/docs/wiki.md +4 -2
  75. package/package.json +4 -2
@@ -74,8 +74,13 @@ export type ProviderStopReason = "end_turn" | "tool_calls" | "max_output_tokens"
74
74
  * axis, so a host can see which limit was closest without instrumenting the session.
75
75
  */
76
76
  export interface TurnBudgets {
77
- /** Provider-reported input tokens for this turn; absent when the provider reported none. */
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`. */
78
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";
79
84
  /** Resolved per-request input cap (attention compiler when enabled); absent when no cap derivable. */
80
85
  readonly inputCap?: number;
81
86
  /** Cumulative run input budget (`RunLimits.maxInputTokens`); absent when the axis is disabled. */
@@ -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;
@@ -1,6 +1,6 @@
1
- import type { AgentLoopOptions, AgentLoopStrategy, ArtifactValidation, BudgetAxisUsage, BudgetConsumedCounters, CompactionOptions, ContentBlock, ErrorInfo, GuardrailRecord, Guardrails, InstructionInjector, JsonObject, Message, ModelConfig, OwnershipScope, ProviderRequestOptions, ProviderRequestPolicy, ProviderResolver, ProviderStopReason, RetryOptions, RunLimitBreach, RunLimitName, RunLimits, Skill, SubscriberOverflowPolicy, SystemPromptConfig, ToolCallAuthority, ToolCallContent, ToolCallSummary, TurnBudgets, TurnPolicyOptions, Usage } from "./contracts-core.js";
2
1
  import type { CacheUsageReport } from "./cache-helpers.js";
3
2
  import type { CheckpointRestoreAudit } from "./checkpoint-restore.js";
3
+ import type { AgentLoopOptions, AgentLoopStrategy, ArtifactValidation, BudgetAxisUsage, BudgetConsumedCounters, CompactionOptions, ContentBlock, ErrorInfo, GuardrailRecord, Guardrails, InstructionInjector, JsonObject, Message, ModelConfig, OwnershipScope, ProviderRequestOptions, ProviderRequestPolicy, ProviderResolver, ProviderStopReason, RetryOptions, RunLimitBreach, RunLimitName, RunLimits, Skill, StopHook, SubscriberOverflowPolicy, SystemPromptConfig, ToolCallAuthority, ToolCallContent, ToolCallSummary, TurnBudgets, TurnPolicyOptions, Usage } from "./contracts-core.js";
4
4
  import type { AgentRunInterruption, AgentRunStateOptions } from "./contracts-run-state.js";
5
5
  import type { SecretRedactor } from "./redaction.js";
6
6
  import type { ToolValidator } from "./tools.js";
@@ -137,6 +137,8 @@ export interface RunOptions {
137
137
  * evaluated before every provider request. Omitted → no callback runs.
138
138
  */
139
139
  readonly turnPolicy?: TurnPolicyOptions;
140
+ /** Appended to agent-level stop hooks for this run (plan 106 R1). */
141
+ readonly stopHooks?: readonly StopHook[];
140
142
  }
141
143
  export interface ProviderTurnMetadata {
142
144
  readonly providerId: string;
@@ -193,8 +195,8 @@ export interface DelegatedAgentStep {
193
195
  readonly label?: string;
194
196
  };
195
197
  }
196
- /** Why a run stopped cleanly. `host_policy` is a `RunOptions.turnPolicy` stop; the rest are loop ceilings (F4). */
197
- export type AgentFinishReason = "turn_limit" | "token_limit" | "refusal" | "host_policy";
198
+ /** Why a run stopped cleanly. `host_policy` is a `RunOptions.turnPolicy` stop, `hook_limit` a stop-hook continuation cap; the rest are loop ceilings (F4). */
199
+ export type AgentFinishReason = "turn_limit" | "token_limit" | "refusal" | "host_policy" | "hook_limit";
198
200
  /**
199
201
  * Origin of an agent event forwarded from a delegated child (supervisor child-event passthrough).
200
202
  * Present only on child events routed onto a parent stream; absent on a session's own events.
@@ -215,7 +217,7 @@ type AgentEventPayload = {
215
217
  readonly sessionId: string;
216
218
  readonly runId: string;
217
219
  readonly usage?: Usage;
218
- /** Why the loop stopped, when a limit/ceiling or a host turn policy ended the run cleanly (F4). Absent = natural end. */
220
+ /** Why the loop stopped, when a limit/ceiling, a host turn policy, or a stop-hook continuation cap ended the run cleanly (F4). Absent = natural end. */
219
221
  readonly finishReason?: AgentFinishReason;
220
222
  /** Host stop detail from `TurnPolicyOptions.stop` (≤256 bytes, redacted). Present only with `finishReason: "host_policy"`. */
221
223
  readonly stopDetail?: string;
@@ -1,6 +1,6 @@
1
- import type { AgentSessionCloneOptions, AgentSessionForkOptions, CheckpointRecord, CheckpointStore, CompactionOptions, CompactionResult, ContentBlock, ContextMeter, ErrorInfo, JsonObject, JsonValue, Message, ModelConfig, OwnershipScope, RunLimitBreach, SessionEntry, SubscribeOptions, ToolCallContent, Usage } from "./contracts-core.js";
2
- import type { AgentEvent, AgentFinishReason, RunOptions, ToolEffectKind } from "./contracts-protocol.js";
3
1
  import type { CheckpointRestoreHook } from "./checkpoint-restore.js";
2
+ import type { AgentSessionCloneOptions, AgentSessionForkOptions, CheckpointRecord, CheckpointStore, CompactionOptions, CompactionResult, ContentBlock, ContextMeter, ErrorInfo, GuardrailPackRef, JsonObject, JsonValue, Message, ModelConfig, OwnershipScope, RunLimitBreach, SessionEntry, SubscribeOptions, ToolCallContent, Usage } from "./contracts-core.js";
3
+ import type { AgentEvent, AgentFinishReason, RunOptions, ToolEffectKind } from "./contracts-protocol.js";
4
4
  export type AgentRunStatus = "succeeded" | "failed" | "aborted" | "suspended" | "denied";
5
5
  export type AgentRunInterruptionKind = "input_guardrail" | "tool_approval" | "elicitation";
6
6
  export type ApprovalOutcome = "allow_once" | "allow_for_run" | "reject_once" | "reject_for_run";
@@ -27,6 +27,16 @@ export interface PendingDecision {
27
27
  readonly reason: string;
28
28
  /** Typed payload contract for elicitation decisions. */
29
29
  readonly elicitationSchema?: JsonObject;
30
+ /**
31
+ * Plan 104 T3: pack `ask` rule that gated this call as `pack:<pack>/<rule>` (bounded by the
32
+ * compile-time id limits), present only when a pack rule raised the decision.
33
+ */
34
+ readonly guardrail?: string;
35
+ /** Plan 104 T3: machine-readable ids behind `guardrail`, so a host never parses the name. */
36
+ readonly guardrailRule?: {
37
+ readonly pack: string;
38
+ readonly rule: string;
39
+ };
30
40
  /** Delegation chain, root-first; core-written, never client-supplied. */
31
41
  readonly attribution?: {
32
42
  readonly path: readonly string[];
@@ -38,6 +48,8 @@ export interface AgentRunInterruption {
38
48
  readonly reason: string;
39
49
  readonly toolCallId?: string;
40
50
  readonly toolName?: string;
51
+ /** Plan 104 T3: the pack rule that raised this suspension (`pack:<pack>/<rule>`), when one did. */
52
+ readonly guardrail?: string;
41
53
  /** All unresolved approval requests of this suspension; absent for legacy single approvals. */
42
54
  readonly pendingDecisions?: readonly PendingDecision[];
43
55
  }
@@ -350,6 +362,12 @@ export interface SteerOptions {
350
362
  }
351
363
  export interface AgentSession {
352
364
  readonly id: string;
365
+ /**
366
+ * Plan 104 Task 2: guardrail pack refs this session enforces (the restored rows after a durable
367
+ * resume); `undefined` when it enforces none. Pass to `snapshotRunBundle({ packs })` for the
368
+ * recorded identity that matches enforcement.
369
+ */
370
+ readonly guardrailPackRefs?: readonly GuardrailPackRef[];
353
371
  /** Current branch leaf entry id; advances on every append/run and is re-pointed by `checkout`.
354
372
  * Undefined until the first entry lands (a fresh session with no history). */
355
373
  readonly leafId: string | undefined;
@@ -361,10 +379,26 @@ export interface AgentSession {
361
379
  * Fails closed when no run is active or the pending queue exceeds caps.
362
380
  */
363
381
  steer(input: string | Message | readonly Message[], options?: SteerOptions): void;
364
- /** Subscribe first, then start exactly one run and yield only that run's events until it terminates. */
382
+ /**
383
+ * Subscribe first, then start exactly one run and yield only that run's events until it terminates.
384
+ * This subscription belongs to `stream()`: it is closed when the owned run settles (so a pre-flight
385
+ * rejection unblocks the consumer instead of parking it behind a run that never emits).
386
+ */
365
387
  stream(input: string | Message | readonly Message[], options?: RunOptions & SubscribeOptions): AsyncIterable<AgentEvent>;
366
388
  compact(options?: CompactionOptions): Promise<CompactionResult>;
389
+ /**
390
+ * Subscribe to this session's live events. A run-scoped subscriber (default) is closed when the run
391
+ * ends, suspends, or is denied; `SubscribeOptions.acrossRuns: true` keeps it open across runs until
392
+ * `subscription.close()`, `closeSubscribers()`, or a queue overflow closes it.
393
+ */
367
394
  subscribe(options?: SubscribeOptions): AsyncIterable<AgentEvent>;
395
+ /**
396
+ * Plan 106 R2: session teardown. Dispatches `session_shutdown` middleware once (idempotent), then
397
+ * closes every subscriber (`acrossRuns` included); calling it twice is a no-op. Call after the
398
+ * active run settles. Subscriber-only teardown stays `break`ing the `for await` (or calling the
399
+ * iterator's `return()`). See [Agent/session runtime](agent-session-runtime.md).
400
+ */
401
+ close(): Promise<void>;
368
402
  abort(reason?: unknown): void;
369
403
  entries(): Promise<readonly SessionEntry[]>;
370
404
  checkout(leafId?: string): Promise<void>;
@@ -1,4 +1,4 @@
1
- import type { AgentDefinition, AuthMethod, CommandDefinition, CompactionStrategy, ContextProvider, CredentialResolver, DiscoveredContribution, InputBuilder, InstructionInjector, PromptBuilder, ProviderPackage, ProviderRequestPolicy, ResourceLoader, RetryPolicy, SettingsProvider, Skill, StoreFactory, SystemPromptContribution, ToolDefinition } from "./contracts.js";
1
+ import type { AgentDefinition, AuthMethod, CommandDefinition, CompactionStrategy, ContextProvider, CredentialResolver, DiscoveredContribution, InputBuilder, InstructionInjector, PromptBuilder, ProviderPackage, ProviderRequestPolicy, ResourceLoader, RetryPolicy, SettingsProvider, Skill, StopHook, StoreFactory, SystemPromptContribution, ToolDefinition } from "./contracts.js";
2
2
  import { type ModelRegistry } from "./models.js";
3
3
  import { type ProviderRegistry } from "./providers.js";
4
4
  import { type DuplicateRegistrationOptions } from "./registry-options.js";
@@ -35,6 +35,7 @@ export interface ContributionRegistries {
35
35
  readonly providerRequestPolicies: ContributionRegistry<ProviderRequestPolicy>;
36
36
  readonly systemPromptContributions: ContributionRegistry<SystemPromptContribution>;
37
37
  readonly instructionInjectors: ContributionRegistry<InstructionInjector>;
38
+ readonly stopHooks: ContributionRegistry<StopHook>;
38
39
  }
39
40
  export interface ContributionRegistriesOptions extends DuplicateRegistrationOptions {
40
41
  }
@@ -49,6 +49,7 @@ export function createContributionRegistries(options = {}) {
49
49
  providerRequestPolicies: createContributionRegistry(registryOptions("provider request policy")),
50
50
  systemPromptContributions: createContributionRegistry(registryOptions("system prompt contribution")),
51
51
  instructionInjectors: createContributionRegistry(registryOptions("instruction injector")),
52
+ stopHooks: createContributionRegistry(registryOptions("stop hook")),
52
53
  };
53
54
  }
54
55
  /** Register discovered contributions into the given registries. Inert: skill
@@ -1,4 +1,4 @@
1
- import type { CommandDefinition, ContextProvider, Extension, ExtensionEvent, ExtensionLifecycleEventName, InstructionInjector, Skill, ToolDefinition } from "./contracts.js";
1
+ import type { AgentEvent, CommandDefinition, ContextProvider, Extension, ExtensionEvent, ExtensionLifecycleEventName, InstructionInjector, Skill, StopHook, ToolDefinition } from "./contracts.js";
2
2
  import { type ContributionRegistries } from "./contributions.js";
3
3
  import { type MiddlewareRegistry } from "./middleware.js";
4
4
  import { type PermissionPolicy } from "./security.js";
@@ -40,6 +40,18 @@ export interface ExtensionKernel {
40
40
  }
41
41
  export declare function createExtensionEventBus(options?: Pick<ExtensionKernelOptions, "errorPolicy" | "secrets">): ExtensionEventBus;
42
42
  export declare function createExtensionKernel(options?: ExtensionKernelOptions): ExtensionKernel;
43
+ export interface AgentEventBridgeOptions {
44
+ /** Bridge-side failure (a source error, or a bus listener that throws under `errorPolicy: "throw"`).
45
+ * Never rethrown — the bridge must not fail the run it is observing. */
46
+ readonly onError?: (error: unknown) => void;
47
+ }
48
+ /**
49
+ * Forward an `AgentEvent` iterable onto the extension bus, one mapped lifecycle event at a time
50
+ * (plan 106 R2). Handlers run in event order and never in the run's path, so a slow or throwing
51
+ * listener cannot stall or fail the observed run. Returns an unsubscribe that stops forwarding and
52
+ * releases the source iterator.
53
+ */
54
+ export declare function forwardAgentEvents(source: AsyncIterable<AgentEvent>, events: Pick<ExtensionEventBus, "emit">, options?: AgentEventBridgeOptions): () => void;
43
55
  /** Host-owned activation: copy contributed entries into the `createAgent()`
44
56
  * fields that accept plain arrays. Contributions stay inert until the host
45
57
  * passes the returned fields into runtime config. Array slots only —
@@ -51,6 +63,8 @@ export interface ActivatedKernelConfig {
51
63
  readonly skills: readonly Skill[];
52
64
  readonly instructionInjectors: readonly InstructionInjector[];
53
65
  readonly context: readonly ContextProvider[];
66
+ /** Run-end stop hooks (plan 106 R1); pass to `createAgent({ stopHooks })`. */
67
+ readonly stopHooks: readonly StopHook[];
54
68
  /** For host command surfaces (CLI/RPC/UI); not part of `AgentConfig`. */
55
69
  readonly commands: readonly CommandDefinition[];
56
70
  /** The kernel middleware registry itself; runs only when passed to runtime config. */
@@ -143,6 +143,10 @@ export function createExtensionKernel(options = {}) {
143
143
  registries.instructionInjectors.register(injector.name, injector);
144
144
  track?.(() => registries.instructionInjectors.unregister(injector.name));
145
145
  },
146
+ registerStopHook(hook) {
147
+ registries.stopHooks.register(hook.name, hook);
148
+ track?.(() => registries.stopHooks.unregister(hook.name));
149
+ },
146
150
  });
147
151
  const unwind = (undo) => {
148
152
  for (const fn of undo.reverse()) {
@@ -190,12 +194,76 @@ export function createExtensionKernel(options = {}) {
190
194
  },
191
195
  };
192
196
  }
197
+ /**
198
+ * Lifecycle hook each forwarded `AgentEvent` maps onto (plan 106 R2). Notification only: the bus
199
+ * receives the original event as `payload` and nothing is transformed. Events not listed here are
200
+ * ignored — notably `agent_finished` stays an AgentEvent and is not re-emitted as a bus event.
201
+ */
202
+ const AGENT_EVENT_BRIDGE = {
203
+ agent_started: "before_agent_start",
204
+ turn_started: "turn",
205
+ turn_finished: "turn",
206
+ tool_execution_started: "tool_call",
207
+ tool_execution_finished: "tool_result",
208
+ };
209
+ /**
210
+ * Forward an `AgentEvent` iterable onto the extension bus, one mapped lifecycle event at a time
211
+ * (plan 106 R2). Handlers run in event order and never in the run's path, so a slow or throwing
212
+ * listener cannot stall or fail the observed run. Returns an unsubscribe that stops forwarding and
213
+ * releases the source iterator.
214
+ */
215
+ export function forwardAgentEvents(source, events, options = {}) {
216
+ const iterator = source[Symbol.asyncIterator]();
217
+ const report = (error) => {
218
+ try {
219
+ options.onError?.(error);
220
+ }
221
+ catch {
222
+ // A throwing error callback must not become an unhandled rejection.
223
+ }
224
+ };
225
+ let stopped = false;
226
+ // Serially chained so a listener that awaits keeps event order; the run never awaits this chain.
227
+ let pending = Promise.resolve();
228
+ void (async () => {
229
+ try {
230
+ while (!stopped) {
231
+ const { value, done } = await iterator.next();
232
+ if (done || stopped)
233
+ return;
234
+ const type = AGENT_EVENT_BRIDGE[value.type];
235
+ if (type === undefined)
236
+ continue;
237
+ const event = { type, payload: value };
238
+ pending = pending.then(() => events.emit(event)).catch(report);
239
+ }
240
+ }
241
+ catch (error) {
242
+ if (!stopped)
243
+ report(error);
244
+ }
245
+ })();
246
+ return () => {
247
+ if (stopped)
248
+ return;
249
+ stopped = true;
250
+ void (async () => {
251
+ try {
252
+ await iterator.return?.();
253
+ }
254
+ catch (error) {
255
+ report(error);
256
+ }
257
+ })();
258
+ };
259
+ }
193
260
  export function activateKernel(kernel) {
194
261
  return {
195
262
  tools: kernel.registries.tools.list(),
196
263
  skills: kernel.registries.skills.list(),
197
264
  instructionInjectors: kernel.registries.instructionInjectors.list(),
198
265
  context: kernel.registries.contextProviders.list(),
266
+ stopHooks: kernel.registries.stopHooks.list(),
199
267
  commands: kernel.registries.commands.list(),
200
268
  middleware: kernel.middleware,
201
269
  };
@@ -1,11 +1,20 @@
1
1
  /** Internal guardrail-pack definition types (plan 092 Task 2). Not SDK surface; hosts configure packs via `AgentSessionConfig.guardrailPacks`. */
2
2
  import type { GuardrailRule, GuardrailRuleContext } from "../contracts-core/guardrail-packs.js";
3
3
  import type { ToolResult } from "../contracts-protocol.js";
4
+ /** Plan 104 Task 2: pack-owned durable-state codec. The pack decides what a checkpoint carries. */
5
+ interface GuardrailPackStateCodec {
6
+ /** Serialize the live state for a checkpoint; `undefined` when nothing needs persisting. */
7
+ readonly snapshot: (state: Record<string, unknown>) => Readonly<Record<string, unknown>> | undefined;
8
+ /** Parse a persisted snapshot back into state fields; throw `GuardrailPackError` on a malformed value. */
9
+ readonly parse: (json: unknown) => Record<string, unknown>;
10
+ }
4
11
  /** What a pack's `build` returns: restrictive rules plus an optional pure result observer. */
5
12
  export interface GuardrailPackRules {
6
13
  readonly rules: readonly GuardrailRule[];
7
14
  /** Records tool results into pack-local state (never denies); a throw fails closed as a guardrail tripwire. */
8
15
  readonly observe?: (state: Record<string, unknown>, result: ToolResult, context: GuardrailRuleContext) => void;
16
+ /** Optional codec making the pack's state survive a durable resume; a pack without one persists nothing. */
17
+ readonly state?: GuardrailPackStateCodec;
9
18
  }
10
19
  export interface GuardrailPackDefinition {
11
20
  readonly id: string;
@@ -14,3 +23,4 @@ export interface GuardrailPackDefinition {
14
23
  /** Pure factory: options in, rules out. No session access, no I/O. */
15
24
  readonly build: (options: Readonly<Record<string, unknown>>) => GuardrailPackRules;
16
25
  }
26
+ export {};
@@ -3,6 +3,7 @@ const MUTATING_TOOLS = ["write", "edit", "delete", "move"];
3
3
  /** Validation-style tool names. `shell` is opt-in (`validationTools`): a non-zero shell exit is a failure signal too. */
4
4
  const DEFAULT_VALIDATION_TOOLS = ["test", "run_tests", "validate", "validation", "lint", "typecheck", "check"];
5
5
  const MAX_VALIDATION_TOOLS = 16;
6
+ const MAX_VALIDATION_TOOL_CHARS = 128;
6
7
  /** A tool result failed when it carries an error, or reported a non-zero `exitCode` (the shell tool's shape). */
7
8
  function resultFailed(result) {
8
9
  if (result.error !== undefined)
@@ -39,6 +40,21 @@ export const validationRespectPack = {
39
40
  return;
40
41
  state.validationFailed = resultFailed(result) ? context.toolName : undefined;
41
42
  },
43
+ // Plan 104 Task 2: one tool name (or nothing) survives a resume, so a mutation the suspended
44
+ // run denied stays denied. The codec is the pack's own bound: 128 chars, never arguments.
45
+ state: {
46
+ snapshot: (state) => (typeof state.validationFailed === "string" ? { validationFailed: state.validationFailed } : undefined),
47
+ parse: (json) => {
48
+ const record = typeof json === "object" && json !== null ? json : {};
49
+ const failed = record.validationFailed;
50
+ if (failed === undefined)
51
+ return {};
52
+ if (typeof failed !== "string" || !failed.trim() || failed.length > MAX_VALIDATION_TOOL_CHARS) {
53
+ throw new GuardrailPackError(`validation-respect persisted state.validationFailed must be a non-empty string of at most ${MAX_VALIDATION_TOOL_CHARS} chars`);
54
+ }
55
+ return { validationFailed: failed };
56
+ },
57
+ },
42
58
  rules: [
43
59
  {
44
60
  id: "no-mutation-after-failed-validation",
@@ -1,10 +1,20 @@
1
- import type { AgentEvent, GuardrailContext, GuardrailPackRef, GuardrailRecord, GuardrailStage, Guardrails, GuardrailValue } from "./contracts.js";
1
+ import type { AgentEvent, GuardrailContext, GuardrailPackRef, GuardrailRecord, GuardrailRule, GuardrailStage, Guardrails, GuardrailValue } from "./contracts.js";
2
2
  export { GuardrailPackError } from "./guardrail-packs/errors.js";
3
3
  import type { SecretRedactor } from "./redaction.js";
4
4
  export declare const MAX_GUARDRAIL_CONCURRENCY = 16;
5
5
  /** Guardrail pack compile bounds (plan 092 Task 2). All ceilings are config-shape limits, not runtime budgets. */
6
6
  export declare const MAX_GUARDRAIL_PACKS = 8;
7
7
  export declare const MAX_GUARDRAIL_PACK_RULES = 64;
8
+ /**
9
+ * Plan 104 T4/T6: the bounded, redacted refusal line for a terminal record that came from a compiled
10
+ * pack rule — `<prefix> by guardrail rule pack:<pack>/<rule>`, plus the pack's own reason when it set
11
+ * one — or `undefined` for any other guardrail, so the caller keeps its own neutral text. Only the
12
+ * compiler writes the `pack`/`rule` metadata, so a host-written guardrail named `pack:…` is never
13
+ * presented as a pack rule. Reasons are redacted where the record is built, pack names are
14
+ * compiler-bounded to 128 bytes (the identity always survives the cap), and a long reason is
15
+ * truncated, so the same derivation serves the tool refusal and decision-time revalidation.
16
+ */
17
+ export declare function guardrailRefusalText(record: GuardrailRecord, prefix?: string): string | undefined;
8
18
  export declare class GuardrailError extends Error {
9
19
  readonly code: string;
10
20
  readonly record: GuardrailRecord;
@@ -33,6 +43,30 @@ export interface GuardrailPackRow {
33
43
  readonly stage: "tool_input" | "tool_output";
34
44
  readonly revision: string | null;
35
45
  }
46
+ /** Plan 104 Task 2: one replayable pack row of a durable checkpoint (`id`, resolved version, host options). */
47
+ interface GuardrailPackRefRow {
48
+ readonly id: string;
49
+ readonly version: number;
50
+ /** Host options the pack was compiled with, replayed verbatim on resume so enforcement is identical. */
51
+ readonly options?: Readonly<Record<string, unknown>>;
52
+ /**
53
+ * Plan 104 T3: the host's own rule list for an inline pack. Patterns are data and ride the
54
+ * checkpoint; a `deny` predicate or a `RegExp` pattern cannot round-trip and is refused at save.
55
+ */
56
+ readonly rules?: readonly GuardrailRule[];
57
+ }
58
+ /** Plan 104 Task 2: a compile result that can round-trip through a durable checkpoint. */
59
+ interface CompiledGuardrailPacks {
60
+ readonly guardrails: Guardrails | undefined;
61
+ /** Rows a durable checkpoint replays; empty when no packs are configured. */
62
+ readonly packs: readonly GuardrailPackRefRow[];
63
+ /** Plan 104 T3: `ask` rules as the charge-time durable gate — a match records `interrupt`. */
64
+ readonly askGate?: Guardrails;
65
+ /** Plan 104 T3: the same rules as plain blocks, merged into a run that cannot suspend. */
66
+ readonly askBlocks?: Guardrails;
67
+ /** Pack-owned state snapshot (`{ <packId>: <pack state> }`); `undefined` when nothing needs persisting. */
68
+ readonly snapshotState: () => Record<string, Readonly<Record<string, unknown>>> | undefined;
69
+ }
36
70
  /**
37
71
  * Compiles `guardrailPacks` config onto the existing tool interception seams: one `tool_input`
38
72
  * guardrail per rule (`name = pack:<pack>/<rule>`), plus one `tool_output` recorder for packs that
@@ -40,5 +74,12 @@ export interface GuardrailPackRow {
40
74
  * Throws `GuardrailPackError` on malformed config (fail closed); returns `undefined` when unset.
41
75
  */
42
76
  export declare function compileGuardrailPacks(refs: readonly GuardrailPackRef[] | undefined, registry?: ReadonlyMap<string, import("./guardrail-packs/types.js").GuardrailPackDefinition>): Guardrails | undefined;
77
+ /**
78
+ * Plan 104 Task 2: the internal compile entry behind `compileGuardrailPacks`. Passing `initial`
79
+ * marks a durable restore — rows must then come from the installed registry, match its version, and
80
+ * parse through the pack's own state codec, so a mismatch fails closed instead of restoring a
81
+ * weaker policy. `snapshotState` is the checkpoint-side counterpart.
82
+ */
83
+ export declare function compileGuardrailPacksWithState(refs: readonly GuardrailPackRef[] | undefined, registry?: ReadonlyMap<string, import("./guardrail-packs/types.js").GuardrailPackDefinition>, initial?: Readonly<Record<string, unknown>>): CompiledGuardrailPacks;
43
84
  /** Stable identity rows for the same config `compileGuardrailPacks` accepts (no state, no guardrails built). */
44
85
  export declare function describeGuardrailPacks(refs: readonly GuardrailPackRef[] | undefined, registry?: ReadonlyMap<string, import("./guardrail-packs/types.js").GuardrailPackDefinition>): readonly GuardrailPackRow[];