@arnilo/prism 0.0.5 → 0.0.7

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 +39 -1
  2. package/dist/agent-loops.d.ts +1 -0
  3. package/dist/agent-loops.js +27 -16
  4. package/dist/agent-run-lifecycle.d.ts +28 -0
  5. package/dist/agent-run-lifecycle.js +33 -0
  6. package/dist/agent-run-state.d.ts +53 -0
  7. package/dist/agent-run-state.js +127 -0
  8. package/dist/agents.d.ts +3 -1
  9. package/dist/agents.js +337 -46
  10. package/dist/contracts.d.ts +205 -3
  11. package/dist/contracts.js +4 -0
  12. package/dist/guardrails.d.ts +25 -0
  13. package/dist/guardrails.js +133 -0
  14. package/dist/ids.d.ts +2 -0
  15. package/dist/ids.js +6 -0
  16. package/dist/index.d.ts +17 -3
  17. package/dist/index.js +10 -3
  18. package/dist/input.js +2 -0
  19. package/dist/resources.js +2 -1
  20. package/dist/run-limits.d.ts +34 -0
  21. package/dist/run-limits.js +163 -0
  22. package/dist/secure-agent.d.ts +3 -0
  23. package/dist/secure-agent.js +63 -0
  24. package/dist/session-stores.js +2 -3
  25. package/dist/testing/persistence-schema.d.ts +45 -7
  26. package/dist/testing/persistence-schema.js +138 -24
  27. package/dist/thinking.d.ts +42 -0
  28. package/dist/thinking.js +92 -0
  29. package/dist/tools.d.ts +10 -2
  30. package/dist/tools.js +56 -7
  31. package/dist/use-case-model.d.ts +63 -0
  32. package/dist/use-case-model.js +52 -0
  33. package/docs/a2a.md +4 -2
  34. package/docs/agent-events.md +23 -16
  35. package/docs/agent-loops.md +19 -8
  36. package/docs/agent-session-runtime.md +33 -1
  37. package/docs/coding-agent-tools.md +33 -12
  38. package/docs/coding-security.md +2 -2
  39. package/docs/compaction-llm.md +17 -7
  40. package/docs/compaction-observational-memory.md +28 -4
  41. package/docs/credential-storage.md +58 -9
  42. package/docs/credentials-and-redaction.md +1 -1
  43. package/docs/database-persistence.md +8 -3
  44. package/docs/guardrails.md +75 -0
  45. package/docs/host-security.md +16 -8
  46. package/docs/index.md +26 -22
  47. package/docs/mcp-tools.md +32 -12
  48. package/docs/migration.md +164 -2
  49. package/docs/node-filesystem-config.md +1 -0
  50. package/docs/node-jsonl-session-store.md +5 -4
  51. package/docs/postgres-persistence.md +3 -3
  52. package/docs/provider-caching.md +16 -4
  53. package/docs/provider-conformance.md +39 -1
  54. package/docs/provider-packages.md +60 -3
  55. package/docs/providers/ai-sdk.md +36 -0
  56. package/docs/providers/kimi.md +124 -61
  57. package/docs/providers/neuralwatt.md +19 -13
  58. package/docs/providers/openai.md +56 -13
  59. package/docs/providers/opencode-go.md +118 -30
  60. package/docs/providers/openrouter.md +105 -35
  61. package/docs/providers/zai.md +94 -45
  62. package/docs/release-and-install.md +47 -49
  63. package/docs/review-coverage-2026-07-17-provider-validation.md +192 -0
  64. package/docs/runs-and-usage.md +30 -3
  65. package/docs/server.md +5 -2
  66. package/docs/sqlite-persistence.md +2 -2
  67. package/docs/structured-output.md +1 -1
  68. package/docs/thinking-and-reasoning.md +98 -0
  69. package/docs/tool-execution-primitives.md +3 -3
  70. package/docs/tools.md +21 -1
  71. package/docs/use-case-model-selection.md +109 -0
  72. package/docs/workflow-orchestration-primitives.md +1 -0
  73. package/docs/workflows.md +18 -10
  74. package/docs/working-and-semantic-memory.md +1 -0
  75. package/package.json +2 -2
@@ -0,0 +1,92 @@
1
+ import { mergeProviderRequestOptions } from "./provider-request-policy.js";
2
+ /**
3
+ * Portable thinking / reasoning effort levels shared across first-party providers.
4
+ * Model-dependent legality (which values a given model accepts) stays provider-owned.
5
+ */
6
+ export const THINKING_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
7
+ export function isThinkingLevel(value) {
8
+ return typeof value === "string" && THINKING_LEVELS.includes(value);
9
+ }
10
+ /**
11
+ * Normalize a host thinkingLevel string. Known levels are lowercased; other non-empty
12
+ * strings pass through as opaque effort values for forward-compatible provider fields.
13
+ */
14
+ export function normalizeThinkingLevel(level) {
15
+ const normalized = level.trim().toLowerCase();
16
+ if (!normalized)
17
+ return undefined;
18
+ return isThinkingLevel(normalized) ? normalized : normalized;
19
+ }
20
+ /**
21
+ * Build the `ProviderRequestOptions.compat` patch for a shared thinking level.
22
+ * Does not invent a second options tree — providers keep reading official fields from `compat`.
23
+ */
24
+ export function thinkingCompatFor(family, level) {
25
+ const normalized = typeof level === "string" ? normalizeThinkingLevel(level) : level;
26
+ if (!normalized || family === "noop")
27
+ return {};
28
+ switch (family) {
29
+ case "openai_reasoning":
30
+ return { reasoning: { effort: normalized } };
31
+ case "reasoning_effort":
32
+ return { reasoning_effort: normalized };
33
+ case "thinking_type":
34
+ return { thinking: { type: normalized === "none" ? "disabled" : "enabled" } };
35
+ default: {
36
+ const _exhaustive = family;
37
+ return _exhaustive;
38
+ }
39
+ }
40
+ }
41
+ /**
42
+ * Merge a shared thinking level into `providerOptions.compat` for the given family.
43
+ * Per-turn patches win over prior compat via {@link mergeProviderRequestOptions}.
44
+ */
45
+ export function applyThinkingLevel(options, level, family = "reasoning_effort") {
46
+ const normalized = normalizeThinkingLevel(String(level));
47
+ if (!normalized || family === "noop")
48
+ return options ?? {};
49
+ const patch = thinkingCompatFor(family, normalized);
50
+ if (family === "openai_reasoning" && options?.compat?.reasoning && typeof options.compat.reasoning === "object" && !Array.isArray(options.compat.reasoning)) {
51
+ return mergeProviderRequestOptions(options, {
52
+ compat: {
53
+ reasoning: {
54
+ ...options.compat.reasoning,
55
+ ...patch.reasoning,
56
+ },
57
+ },
58
+ });
59
+ }
60
+ return mergeProviderRequestOptions(options, { compat: patch });
61
+ }
62
+ /**
63
+ * Best-effort family inference from model metadata without a second options tree.
64
+ * Prefer an explicit family in hosts/use-case workers when the provider is known.
65
+ *
66
+ * Heuristics (ordered):
67
+ * 1. Existing `compat.thinking` object → `thinking_type`
68
+ * 2. Existing `compat.reasoning` → `openai_reasoning`
69
+ * 3. Existing `compat.reasoning_effort` → `reasoning_effort`
70
+ * 4. Provider id starting with `openai` → `openai_reasoning`
71
+ * 5. Provider id `neuralwatt` → `reasoning_effort`
72
+ * 6. `capabilities.reasoning` → `reasoning_effort` (portable string field)
73
+ * 7. Else `noop`
74
+ */
75
+ export function thinkingFamilyForModel(model) {
76
+ const compat = model.compat ?? {};
77
+ if (compat.thinking != null && typeof compat.thinking === "object")
78
+ return "thinking_type";
79
+ if (compat.reasoning != null)
80
+ return "openai_reasoning";
81
+ if (compat.reasoning_effort != null)
82
+ return "reasoning_effort";
83
+ const provider = model.provider.trim().toLowerCase();
84
+ if (provider === "openai" || provider.startsWith("openai"))
85
+ return "openai_reasoning";
86
+ if (provider === "neuralwatt")
87
+ return "reasoning_effort";
88
+ if (model.capabilities?.reasoning)
89
+ return "reasoning_effort";
90
+ return "noop";
91
+ }
92
+ //# sourceMappingURL=thinking.js.map
package/dist/tools.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { AgentEvent, ErrorInfo, JsonObject, OwnershipScope, RunLedger, ToolCallContent, ToolDefinition, ToolExecutionContext, ToolRegistry, ToolResult } from "./contracts.js";
1
+ import type { AgentEvent, ErrorInfo, Guardrails, JsonObject, OwnershipScope, RunLedger, ToolCallContent, ToolDefinition, ToolExecutionContext, ToolRegistry, ToolResult } from "./contracts.js";
2
+ import type { RunLimitTracker } from "./run-limits.js";
2
3
  import type { MiddlewareRegistry } from "./middleware.js";
3
4
  import { type SecretRedactor } from "./redaction.js";
4
5
  import { type DuplicateRegistrationOptions } from "./registry-options.js";
5
- import { type PermissionPolicy } from "./security.js";
6
+ import { type PermissionPolicy, type TrustPolicy } from "./security.js";
6
7
  export interface ToolFilter {
7
8
  readonly allow?: readonly string[];
8
9
  readonly deny?: readonly string[];
@@ -33,12 +34,19 @@ export interface DispatchToolCallOptions {
33
34
  readonly filter?: ToolFilterInput;
34
35
  readonly middleware?: MiddlewareRegistry;
35
36
  readonly validate?: ToolValidator;
37
+ /** Adapter-specific policy check immediately before the tool side effect. */
38
+ readonly beforeExecute?: (call: ToolCallContent, tool: ToolDefinition, context: ToolExecutionContext) => void | Promise<void>;
36
39
  readonly emit?: (event: AgentEvent) => void | Promise<void>;
37
40
  readonly secrets?: readonly (string | undefined)[];
38
41
  readonly permission?: PermissionPolicy;
42
+ readonly trust?: TrustPolicy;
39
43
  readonly redactor?: SecretRedactor;
40
44
  readonly ledger?: RunLedger;
41
45
  readonly ownership?: OwnershipScope;
46
+ /** Tool stages run after middleware normalization and before side effects/exposure. */
47
+ readonly guardrails?: Guardrails;
48
+ /** Shared run tracker; direct hosts may supply one for their call scope. */
49
+ readonly limitTracker?: RunLimitTracker;
42
50
  }
43
51
  export interface ToolRegistryOptions extends DuplicateRegistrationOptions {
44
52
  }
package/dist/tools.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { isJsonObject } from "./config.js";
2
+ import { createId } from "./ids.js";
3
+ import { GuardrailError, runGuardrails } from "./guardrails.js";
2
4
  import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "./redaction.js";
3
5
  import { assertCanRegister } from "./registry-options.js";
4
- import { assertPermission } from "./security.js";
6
+ import { assertPermission, assertTrusted } from "./security.js";
5
7
  /** Wrap a schema adapter as the existing `ToolValidator` seam used by dispatch and the agent runtime. */
6
8
  export function createToolParameterValidator(validator, options = {}) {
7
9
  const missingSchema = options.missingSchema ?? "allow";
@@ -58,10 +60,28 @@ function toolExecutionMetadata(startedAt, status) {
58
60
  export async function dispatchToolCall(options) {
59
61
  const secrets = options.secrets ?? [];
60
62
  const startedAt = new Date().toISOString();
61
- const precheck = await checkCall(options.call, options, startedAt);
62
- if (precheck)
63
- return precheck;
63
+ options.limitTracker?.charge("maxToolCalls");
64
64
  const mediatedCall = await (options.middleware?.run("tool_call", options.call) ?? options.call);
65
+ const inputGuards = await runGuardrails({
66
+ stage: "tool_input",
67
+ guardrails: options.guardrails,
68
+ value: mediatedCall,
69
+ context: {
70
+ sessionId: options.context.sessionId,
71
+ runId: options.context.runId,
72
+ toolCallId: mediatedCall.id,
73
+ toolName: mediatedCall.name,
74
+ metadata: options.context.metadata ?? {},
75
+ signal: options.context.signal,
76
+ },
77
+ redactor: options.redactor,
78
+ emit: options.emit,
79
+ });
80
+ if (inputGuards.terminal) {
81
+ if (inputGuards.terminal.action !== "block")
82
+ throw new GuardrailError(inputGuards.terminal);
83
+ return blocked(mediatedCall, options.context, "guardrail_blocked", { message: "Tool call blocked by guardrail" }, options, startedAt);
84
+ }
65
85
  const tool = options.registry.get(mediatedCall.name);
66
86
  const postcheck = await checkCall(mediatedCall, options, startedAt);
67
87
  if (postcheck)
@@ -88,6 +108,7 @@ export async function dispatchToolCall(options) {
88
108
  },
89
109
  };
90
110
  try {
111
+ await assertTrusted(options.trust, { kind: "tool", target: mediatedCall.name, capability: "execute", metadata: options.context.metadata });
91
112
  await assertPermission(options.permission, { kind: "tool", action: "execute", target: mediatedCall.name, metadata: options.context.metadata });
92
113
  }
93
114
  catch (error) {
@@ -96,11 +117,39 @@ export async function dispatchToolCall(options) {
96
117
  const validation = await options.validate?.(tool, mediatedCall.arguments, context);
97
118
  if (validation)
98
119
  return blocked(mediatedCall, context, "validation_failed", toErrorInfo(validation, secrets), options, startedAt);
120
+ try {
121
+ await options.beforeExecute?.(mediatedCall, tool, context);
122
+ }
123
+ catch (error) {
124
+ if (error?.code === "ERR_PRISM_AGENT_RUN_SUSPENDED")
125
+ throw error;
126
+ return blocked(mediatedCall, context, "execution_denied", errorToErrorInfo(error, secrets), options, startedAt);
127
+ }
99
128
  await options.emit?.({ type: "tool_execution_started", sessionId: context.sessionId, runId: context.runId, call: mediatedCall });
100
129
  await appendToolCallRecord(options, "started", mediatedCall, startedAt, {});
101
130
  try {
102
131
  const raw = await tool.execute(mediatedCall.arguments, context);
103
132
  const mediatedResult = await (options.middleware?.run("tool_result", raw) ?? raw);
133
+ const outputGuards = await runGuardrails({
134
+ stage: "tool_output",
135
+ guardrails: options.guardrails,
136
+ value: mediatedResult,
137
+ context: {
138
+ sessionId: context.sessionId,
139
+ runId: context.runId,
140
+ toolCallId: mediatedCall.id,
141
+ toolName: mediatedCall.name,
142
+ metadata: context.metadata ?? {},
143
+ signal: context.signal,
144
+ },
145
+ redactor: options.redactor,
146
+ emit: options.emit,
147
+ });
148
+ if (outputGuards.terminal) {
149
+ if (outputGuards.terminal.action !== "block")
150
+ throw new GuardrailError(outputGuards.terminal);
151
+ return blocked(mediatedCall, context, "guardrail_blocked", { message: "Tool result blocked by guardrail" }, options, startedAt);
152
+ }
104
153
  const result = options.redactor?.redact(mediatedResult) ?? mediatedResult;
105
154
  const finishedAt = new Date().toISOString();
106
155
  const metadata = toolExecutionMetadata(startedAt, "finished");
@@ -109,6 +158,8 @@ export async function dispatchToolCall(options) {
109
158
  return result;
110
159
  }
111
160
  catch (error) {
161
+ if (error instanceof GuardrailError)
162
+ throw error;
112
163
  const info = errorToErrorInfo(error, secrets);
113
164
  const result = { toolCallId: mediatedCall.id, name: mediatedCall.name, error: info };
114
165
  const finishedAt = new Date().toISOString();
@@ -149,9 +200,7 @@ async function blocked(call, context, reason, error, options, startedAt) {
149
200
  function toErrorInfo(value, secrets) {
150
201
  return typeof value === "string" ? errorToErrorInfo(value, secrets) : redactSecrets(value, secrets);
151
202
  }
152
- function randomId(prefix) {
153
- return `${prefix}_${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
154
- }
203
+ const randomId = createId;
155
204
  function appendToolCallRecord(options, status, call, startedAt, fields) {
156
205
  if (!options.ledger)
157
206
  return undefined;
@@ -0,0 +1,63 @@
1
+ import type { ModelConfig, ProviderRequestOptions } from "./contracts.js";
2
+ /**
3
+ * Host binding for a non-session LLM job (observational memory, LLM compaction,
4
+ * declarative agents, evals, etc.). Omitting `model` means "use the session model"
5
+ * when a session fallback is supplied to {@link resolveUseCaseModel}.
6
+ *
7
+ * Workers must not write `model_change` session entries; they resolve a model for
8
+ * their own provider calls only.
9
+ */
10
+ export interface UseCaseModelBinding {
11
+ /** Explicit use-case model. When omitted, {@link resolveUseCaseModel} falls back to `sessionModel`. */
12
+ readonly model?: ModelConfig;
13
+ /**
14
+ * Optional provider id hint for docs / credential routing.
15
+ * When `model` is set, `model.provider` is authoritative.
16
+ */
17
+ readonly provider?: string;
18
+ readonly providerOptions?: ProviderRequestOptions;
19
+ /** Portable thinking level; packages map via `applyThinkingLevel` into `compat`. */
20
+ readonly thinkingLevel?: string;
21
+ /**
22
+ * When true, do not fall back to `sessionModel` — leave resolution empty if
23
+ * `model` is omitted (preserves historical explicit-worker `missing_model` behavior).
24
+ */
25
+ readonly requireExplicitModel?: boolean;
26
+ }
27
+ export interface ResolveUseCaseModelInput {
28
+ /** Explicit use-case model (or `binding.model`). */
29
+ readonly configured?: ModelConfig;
30
+ /** Active session / agent model used when `configured` is omitted. */
31
+ readonly sessionModel?: ModelConfig;
32
+ /** When true, skip session fallback (OM `missing_model` escape hatch). */
33
+ readonly requireExplicitModel?: boolean;
34
+ readonly providerOptions?: ProviderRequestOptions;
35
+ readonly thinkingLevel?: string;
36
+ }
37
+ export interface ResolvedUseCaseModel {
38
+ readonly model: ModelConfig;
39
+ /** Whether the model came from the use-case binding or session fallback. */
40
+ readonly source: "configured" | "session";
41
+ readonly providerOptions?: ProviderRequestOptions;
42
+ readonly thinkingLevel?: string;
43
+ }
44
+ /**
45
+ * Resolve the model for a non-session LLM job.
46
+ *
47
+ * Precedence:
48
+ * 1. `configured` → `source: "configured"`
49
+ * 2. Else `sessionModel` when `requireExplicitModel` is not set → `source: "session"`
50
+ * 3. Else `undefined` (caller skips / throws per package policy)
51
+ *
52
+ * O(1); no network. Does not mutate session history.
53
+ */
54
+ export declare function resolveUseCaseModel(input: ResolveUseCaseModelInput): ResolvedUseCaseModel | undefined;
55
+ /**
56
+ * Resolve from a {@link UseCaseModelBinding} plus optional session fallback.
57
+ */
58
+ export declare function resolveUseCaseModelBinding(binding: UseCaseModelBinding | undefined, sessionModel?: ModelConfig): ResolvedUseCaseModel | undefined;
59
+ /**
60
+ * Provider id for credential requests: always the **resolved** model's provider.
61
+ * Optional `binding.provider` is only a hint when no model resolved yet.
62
+ */
63
+ export declare function useCaseCredentialProviderId(resolved: ResolvedUseCaseModel | undefined, binding?: Pick<UseCaseModelBinding, "provider">): string | undefined;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Resolve the model for a non-session LLM job.
3
+ *
4
+ * Precedence:
5
+ * 1. `configured` → `source: "configured"`
6
+ * 2. Else `sessionModel` when `requireExplicitModel` is not set → `source: "session"`
7
+ * 3. Else `undefined` (caller skips / throws per package policy)
8
+ *
9
+ * O(1); no network. Does not mutate session history.
10
+ */
11
+ export function resolveUseCaseModel(input) {
12
+ const { providerOptions, thinkingLevel } = input;
13
+ if (input.configured) {
14
+ return {
15
+ model: input.configured,
16
+ source: "configured",
17
+ providerOptions,
18
+ thinkingLevel,
19
+ };
20
+ }
21
+ if (input.requireExplicitModel)
22
+ return undefined;
23
+ if (input.sessionModel) {
24
+ return {
25
+ model: input.sessionModel,
26
+ source: "session",
27
+ providerOptions,
28
+ thinkingLevel,
29
+ };
30
+ }
31
+ return undefined;
32
+ }
33
+ /**
34
+ * Resolve from a {@link UseCaseModelBinding} plus optional session fallback.
35
+ */
36
+ export function resolveUseCaseModelBinding(binding, sessionModel) {
37
+ return resolveUseCaseModel({
38
+ configured: binding?.model,
39
+ sessionModel,
40
+ requireExplicitModel: binding?.requireExplicitModel,
41
+ providerOptions: binding?.providerOptions,
42
+ thinkingLevel: binding?.thinkingLevel,
43
+ });
44
+ }
45
+ /**
46
+ * Provider id for credential requests: always the **resolved** model's provider.
47
+ * Optional `binding.provider` is only a hint when no model resolved yet.
48
+ */
49
+ export function useCaseCredentialProviderId(resolved, binding) {
50
+ return resolved?.model.provider ?? binding?.provider;
51
+ }
52
+ //# sourceMappingURL=use-case-model.js.map
package/docs/a2a.md CHANGED
@@ -21,7 +21,7 @@ Use it to expose one explicitly selected Prism agent at an A2A endpoint or call
21
21
 
22
22
  ## Outputs / response / events
23
23
 
24
- The handler serves `GET /.well-known/agent-card.json` and its configured POST endpoint. JSON-RPC returns `{ result: { task } }` or a bounded error. Streaming returns backpressure-driven SSE task envelopes. Client `send()` maps a terminal remote task to `AgentRunResult`; `stream()` yields validated/redacted text artifacts.
24
+ The handler serves `GET /.well-known/agent-card.json` and its configured POST endpoint. JSON-RPC returns `{ result: { task } }` or a bounded error. Streaming returns backpressure-driven SSE task envelopes. Client `send()` maps a terminal remote task to `AgentRunResult`; `stream()` incrementally yields validated/redacted text artifacts. Client SSE accepts LF, CRLF, mixed blank-line separators, comments/unknown fields, and multiline `data:` joined with LF.
25
25
 
26
26
  ## Request/response example
27
27
 
@@ -59,7 +59,9 @@ Only `text` parts are accepted. File/data parts, push notifications, task persis
59
59
  ## Security and performance notes
60
60
 
61
61
  - Endpoints and card URLs must be HTTPS and exactly origin-allow-listed before fetch; `redirect: "error"` prevents redirect SSRF.
62
- - Treat every remote card, error, task, status, artifact, and SSE frame as untrusted. Shape/count/byte/time limits apply before mapping.
62
+ - Treat every remote card, error, task, status, artifact, and SSE frame as untrusted. Shape/count/byte/time limits apply before mapping. Streaming keeps raw stream bytes, current frame bytes, and event count as separate existing limits.
63
+ - One fatal streaming UTF-8 decoder is reused across every body chunk and flushed once at EOF. Split multibyte code points are preserved; malformed/truncated UTF-8 fails rather than inserting `U+FFFD` into JSON. A small coalesced line buffer keeps one-byte chunk handling incremental.
64
+ - SSE frames require a terminating blank line. A non-whitespace final partial frame, malformed JSON, missing terminal task, failed/canceled task, or any event after a completed task fails with bounded package-owned text. Existing request/response/event/stream/count/timeout hard caps are unchanged.
63
65
  - Card verification pins `alg=ES256`, optional key ID, issue/expiry, optional maximum age, and canonical unsigned-card payload. Hosts provision trusted public keys; remote `jku` is never fetched automatically.
64
66
  - Card discovery is public; extended-card and invoke methods call host authorization. Use TLS, rate limits, and replay controls at the host edge.
65
67
  - Credentials remain in the client auth callback or server authorizer and never enter cards, messages, events, or metrics.
@@ -38,11 +38,12 @@ The `AgentEvent` union (grouped by concern):
38
38
 
39
39
  | Group | Variants |
40
40
  | --- | --- |
41
- | Agent lifecycle | `agent_started`, `agent_finished` |
41
+ | Agent lifecycle | `agent_started`, `agent_suspended`, `agent_resumed`, `agent_denied`, `agent_finished` |
42
42
  | Turns | `turn_started`, `turn_finished` |
43
43
  | Provider turns | `provider_turn_started`, `provider_turn_finished` |
44
44
  | Assistant messages | `message_started`, `message_delta`, `message_finished` |
45
45
  | Tool execution | `tool_execution_started`, `tool_execution_progress`, `tool_execution_finished`, `tool_execution_error`, `tool_execution_blocked` |
46
+ | Guardrails | `guardrail_decision` |
46
47
  | Queue/subscribers | `queue_updated`, `event_subscriber_overflow` |
47
48
  | Compaction | `compaction_started`, `compaction_finished` |
48
49
  | Retry | `retry_scheduled` |
@@ -59,6 +60,9 @@ Agent / turn / message events:
59
60
  | --- | --- |
60
61
  | `agent_started` | `sessionId`, `runId` |
61
62
  | `agent_finished` | `sessionId`, `runId`, `usage?: Usage` (aggregate of all usage-bearing provider turns) |
63
+ | `agent_suspended` | `sessionId`, `runId`, redacted `interruption`, checkpoint `version`; no tool side effect has started. |
64
+ | `agent_resumed` | `sessionId`, `runId`, checkpoint `version`. |
65
+ | `agent_denied` | `sessionId`, `runId`, redacted `interruption`, checkpoint `version`; no tool side effect runs. |
62
66
  | `turn_started` / `turn_finished` | `sessionId`, `runId`, `turn: number` |
63
67
  | `message_started` / `message_finished` | `sessionId`, `runId`, `message: Message` |
64
68
  | `message_delta` | `sessionId`, `runId`, `content: ContentBlock` (`tool_call_delta` fragments may appear here for live UI streaming; stored messages use final `tool_call` blocks) |
@@ -75,6 +79,14 @@ Tool execution events:
75
79
  | `tool_execution_error` | `sessionId`, `runId`, `call: ToolCallContent`, `error: ErrorInfo`, `metadata: ToolExecutionMetadata` |
76
80
  | `tool_execution_blocked` | `sessionId`, `runId`, `toolCallId`, `name`, `reason: string`, `error: ErrorInfo`, `metadata: ToolExecutionMetadata` |
77
81
 
82
+ Guardrail events:
83
+
84
+ | Variant | Fields |
85
+ | --- | --- |
86
+ | `guardrail_decision` | `sessionId`, `runId`, optional `toolCallId`/`toolName`, and redacted bounded `record: GuardrailRecord` (`guardrail`, stage, action, reason, metadata). |
87
+
88
+ Guardrails emit their decision before a terminal run error or blocked tool result. Provider-output checks buffer assistant content, and tool-output checks discard blocked raw results before event/ledger/transcript exposure; see [Guardrails](guardrails.md).
89
+
78
90
  Queue / subscriber / compaction / retry / provider events:
79
91
 
80
92
  | Variant | Fields |
@@ -100,28 +112,23 @@ Artifact validation/refinement events (emitted only by `generateValidateReviseLo
100
112
  | `artifact_validation_finished` | `sessionId`, `runId`, `turn`, `attempt`, `result: ArtifactValidation` |
101
113
  | `artifact_revision_started` | `sessionId`, `runId`, `turn`, `attempt`, `failure: ArtifactValidation` |
102
114
  | `artifact_finished` | `sessionId`, `runId`, `turn`, `attempt`, `result: ArtifactValidation` (loop ended successfully) |
103
- | `artifact_failed` | `sessionId`, `runId`, `turn`, `attempt`, `result: ArtifactValidation` (budget exhausted) |
115
+ | `artifact_failed` | `sessionId`, `runId`, `turn`, `attempt`, `result: ArtifactValidation` (candidate budget exhausted, or `result.metadata.reason === "tool_round_limit"`) |
104
116
 
105
117
  ### Artifact event ordering
106
118
 
107
- A `generateValidateReviseLoop` run emits normal turn/message events for every provider turn, then a strictly ordered artifact sequence, correlated by `runId` / `turn` / `attempt`:
119
+ A call-free candidate in `generateValidateReviseLoop` emits normal turn/message events then a strictly ordered artifact sequence, correlated by `runId` / `turn` / `attempt`:
108
120
 
109
121
  ```
110
- turn_started
111
- → message_started
112
- → message_delta*
113
- → message_finished
114
- → turn_finished
115
- → artifact_validation_started
116
- → artifact_validation_finished
117
- → artifact_revision_started # when a revision will run next
118
- | artifact_finished # loop ended successfully
119
- | artifact_failed # budget exhausted (maxRevisions+1 attempts)
122
+ turn_started → message_started → message_delta* → message_finished → turn_finished
123
+ → artifact_validation_started → artifact_validation_finished
124
+ → artifact_revision_started | artifact_finished | artifact_failed
120
125
  ```
121
126
 
122
- - `attempt` is 1-indexed per validation attempt and equals the provider `turn` within `generateValidateReviseLoop`; it mirrors `retry_scheduled.attempt` and the `tool_execution_*` block/finish pairing.
127
+ With opt-in `toolCalls: "bounded"`, a provider turn containing calls emits its normal assistant envelope followed by existing `tool_execution_*` events and matching persisted tool results; it emits no validation event and the next provider turn consumes that transcript. A post-`maxToolRounds` call emits terminal `artifact_failed` directly after `turn_finished` and has no tool execution event.
128
+
129
+ - `attempt` is 1-indexed per call-free validation candidate. It can differ from provider `turn` when bounded tool calls occur.
123
130
  - Single-shot runs emit zero artifact events.
124
- - **Validation failure triggering a revision is recoverable and never an `error`.** Only terminal budget exhaustion emits `artifact_failed`. The `error` channel is reserved for real failures (provider failures not caught by retry, aborts, etc.), matching the existing convention used by `tool_execution_blocked`.
131
+ - **Validation failure triggering a revision is recoverable and never an `error`.** Terminal candidate-budget or `tool_round_limit` exhaustion emits `artifact_failed`; real failures remain on the `error` channel.
125
132
 
126
133
  ## Request/response example
127
134
 
@@ -193,7 +200,7 @@ for await (const event of session.stream("draft", { loop: { strategy: "generate-
193
200
  - Slow consumers are bounded by `SubscribeOptions`. Use `RunLedger` or host storage for durable replay; do not rely on a live subscriber as a queue.
194
201
  - Redaction is exact-string-match only and opt-in via `createSecretRedactor`; values not passed as known secrets are not redacted.
195
202
  - `ArtifactValidation.errors[].message` and `metadata` may echo model text; `redactAgentEvent` walks arbitrary nesting and replaces cyclic references with `"[Circular]"` (WeakSet cycle guard), so secret values in `result`/`failure` are redacted without crashing.
196
- - `artifact_*` events are bounded by `maxRevisions + 1` validation attempts; an always-failing validator cannot loop forever and emits exactly one terminal `artifact_failed`.
203
+ - `artifact_*` validation events are bounded by `maxRevisions + 1` call-free candidates. With opt-in bounded artifact tools, provider turns are additionally bounded by run-global `maxToolRounds` (maximum `1 + maxRevisions + maxToolRounds`); a post-cap call emits exactly one terminal `artifact_failed` with `result.metadata.reason === "tool_round_limit"` and has no tool lifecycle event because it never dispatches.
197
204
  - Runtime events contain messages/content only; do not put secrets in prompts, metadata, provider events, session entries, tool results, or artifact validation payloads.
198
205
 
199
206
  ## Related APIs
@@ -16,7 +16,7 @@ The `Artifact*` contracts (`ArtifactValidation`, `ArtifactContext`, `ArtifactPar
16
16
 
17
17
  Use the default `singleShotLoop` implicitly whenever you call `session.run()` — no configuration needed. Opt into `generateValidateReviseLoop` when a run should produce an artifact that must satisfy a host-supplied schema before it is considered complete (e.g. structured output, a validated JSON document, a generated file passing lint) and the host wants Prism to drive the revision turns.
18
18
 
19
- Do not use a loop to re-implement provider calls, retry, abort, store, or event emission — those stay runtime-owned and are exposed to the loop only through `LoopContext`. A loop that needs tools in revision turns is out of scope for `generateValidateReviseLoop`; use `singleShotLoop` or supply a custom `AgentLoopStrategy`.
19
+ Do not use a loop to re-implement provider calls, retry, abort, store, or event emission — those stay runtime-owned and are exposed to the loop only through `LoopContext`. Artifact-loop tools stay disabled by default; opt into bounded calls only for host-registered, least-privilege lookup tools that must inform an artifact candidate.
20
20
 
21
21
  ## Inputs / request
22
22
 
@@ -57,6 +57,7 @@ await session.run(input, {
57
57
  parser: hostParser, // optional; default treats assistant text as the value
58
58
  repairer: hostRepairer, // optional; default stringifies validation.errors[].message
59
59
  maxRevisions: 3, // optional; default 3
60
+ toolCalls: "bounded", // optional; default "disabled"; uses limits.maxToolRounds
60
61
  },
61
62
  });
62
63
 
@@ -79,6 +80,8 @@ type AgentLoopOptions =
79
80
  readonly parser?: ArtifactParser<unknown>;
80
81
  readonly repairer?: ArtifactRepairer<unknown>;
81
82
  readonly maxRevisions?: number;
83
+ /** Default "disabled". "bounded" dispatches sequentially up to limits.maxToolRounds. */
84
+ readonly toolCalls?: "disabled" | "bounded";
82
85
  };
83
86
  ```
84
87
 
@@ -106,13 +109,17 @@ Host callback contracts (all generic over host `T`):
106
109
  | `appendMessage(message)` | Appends to the store under the run (redacted). |
107
110
  | `emit(event)` | Emits a redacted `AgentEvent`. |
108
111
 
112
+ ## Durable runs
113
+
114
+ `RunOptions.runState` supports only built-in loop options (`single-shot` and `generate-validate-revise`). A custom `AgentLoopStrategy` has arbitrary in-memory cursor state, so durable configuration rejects it before provider work. Built-in suspension occurs only before an input provider call or immediately before a tool side effect; completed provider turns remain in `SessionStore` history and are not repeated after `resumeAgentRun()`.
115
+
109
116
  ## Outputs / response / events
110
117
 
111
118
  `AgentLoopStrategy.run(ctx)` returns `Promise<Usage | undefined>` as a fallback for custom loops. Core runtime independently accumulates every usage-bearing provider turn in O(turns), persists scoped turn/run rows, and emits `agent_finished` with the aggregate.
112
119
 
113
120
  Events during a loop run are the existing `AgentEvent`s (`turn_started`, `message_started`, `message_delta`, `message_finished`, `turn_finished`, tool-execution events when the loop dispatches tools, `error` on real failures). Both built-in loops emit `turn_started` before each provider turn, `message_finished` for every assistant draft, and `turn_finished` after the assistant draft is appended. First-turn input is appended to live history once, matching the already-persisted user message.
114
121
 
115
- Validation-failure-triggering-a-revision is **not** an `error` event — it is recoverable, like `tool_execution_blocked`. `generateValidateReviseLoop` emits normal turn/message events around each provider turn, then the artifact event sequence `artifact_validation_started` → `artifact_validation_finished` → (`artifact_revision_started`)* → `artifact_finished` (success) | `artifact_failed` (budget exhausted), correlated by `runId`/`turn`/`attempt`; see [Agent events § Artifact event ordering](agent-events.md#artifact-event-ordering). `singleShotLoop` emits zero artifact events. Real failures stay on the `error` channel.
122
+ Validation-failure-triggering-a-revision is **not** an `error` event — it is recoverable, like `tool_execution_blocked`. In bounded artifact mode, a tool-calling provider response emits normal assistant/tool lifecycle events, skips artifact parsing/validation, then the next turn sees its persisted result. `generateValidateReviseLoop` emits artifact events only for call-free candidates: `artifact_validation_started` → `artifact_validation_finished` → (`artifact_revision_started`)* → `artifact_finished` | `artifact_failed`. A request beyond `maxToolRounds` executes nothing and emits terminal `artifact_failed` with `result.metadata.reason === "tool_round_limit"`; see [Agent events § Artifact event ordering](agent-events.md#artifact-event-ordering). `singleShotLoop` emits zero artifact events. Real failures stay on the `error` channel.
116
123
 
117
124
  A loop has no path to credentials, provider objects, or unredacted secrets. `LoopContext.generate` receives the already-policy-applied, middleware-run, redacted request; `LoopContext.emit` runs through `redactAgentEvent` with the active `SecretRedactor`.
118
125
 
@@ -198,20 +205,24 @@ await session.run(input, { loop: twoShotLoop });
198
205
  - `RunOptions.loop` wins over `AgentConfig.loop`; when neither is set the runtime uses `singleShotLoop`. This mirrors the other `RunOptions` overrides (`redactor`, `validate`, `activeSkills`).
199
206
  - `{ strategy: "single-shot" }` resolves to the exported `singleShotLoop`; `{ strategy: "generate-validate-revise", ... }` is mapped by `resolveLoop()` to `generateValidateReviseLoop(opts)`. An unknown `strategy` throws before the first turn. Passing an `AgentLoopStrategy` instance bypasses the options form entirely (custom-loop escape hatch).
200
207
  - The loop is resolved once per run inside `RuntimeAgentSession.run()`, after the usual setup (provider/skills/tools resolution, history rebuild, model-change entry, input append, auto-compaction). The runtime's outer try/catch/finally, run-exclusivity, abort bridging, and subscriber close remain in place around `loop.run(ctx)`.
201
- - `LoopContext.assemble(nextInput, toolResults?)` accepts an optional tool-result accumulator so `singleShotLoop` can pass its loop-local `toolResults`; `generateValidateReviseLoop` omits it (no tools in revision turns).
202
- - `maxToolRounds` bounds `singleShotLoop` tool rounds; `toolConcurrency` (default `1`) bounds how many independent tool calls from one provider turn may execute concurrently. Results and transcript rows are still appended in original call order. If any resolved `ToolDefinition` in a turn has `exclusive: true`, that turn uses concurrency `1`; later non-exclusive turns restore configured concurrency.
203
- - `maxRevisions` (default 3) bounds `generateValidateReviseLoop` revision turns. Budget exhaustion ends the loop and returns the last usage; it does not throw.
204
- - A revision cycle appends one assistant draft and one repair user message per revision to the session store, so store entries reflect every attempted draft. The original user input is stored once by the runtime and pushed into loop history once on the first turn.
208
+ - `LoopContext.assemble(nextInput, toolResults?)` accepts an optional tool-result accumulator so `singleShotLoop` can pass its loop-local results. Bounded artifact tools append results directly to shared history, then assemble the next turn with empty new input; no second transcript path exists.
209
+ - `limits.maxToolRounds` bounds both `singleShotLoop` and opt-in bounded artifact tool rounds across the whole run. Artifact mode always dispatches sequentially, regardless of `toolConcurrency`; all dispatches still use existing registry/filter/permission/validator/middleware/redactor/ledger guards. Deprecated `maxToolRounds` only narrows this limit.
210
+ - `maxRevisions` (default 3) counts only failed call-free artifact candidates. Bounded artifact runs make at most `1 + maxRevisions + maxToolRounds` provider turns. A tool-round limit is terminal and returns last usage after `artifact_failed`; it does not throw.
211
+ - A revision cycle appends one assistant draft and one repair user message per revision to the session store, so store entries reflect every attempted draft. The original user input is stored once by the runtime and pushed into loop history once on the first turn. Repair messages are assembled as the next provider `nextInput` and only pushed into live history after that revision request has been generated, so the model never receives a duplicated repair instruction.
205
212
 
206
213
  ## Security and performance notes
207
214
 
208
215
  - Loops have no path to credentials, provider objects, or unredacted secrets. `LoopContext.generate` consumes an already-redacted request; `LoopContext.emit` runs through `redactAgentEvent` with the active `SecretRedactor`; `LoopContext.appendMessage` appends a redacted entry.
209
216
  - `ArtifactValidation.errors[].message` may echo model text — `artifact_*` event payloads flow through the same `redactAgentEvent` path as other `AgentEvent`s (see [Agent events](agent-events.md)).
210
- - `generateValidateReviseLoop` makes at most `maxRevisions + 1` provider turns; it cannot loop forever on an always-failing validator. Each revision costs one provider turn plus one store append.
211
- - Parallel tool dispatch uses a bounded worker pool over the calls in one turn; queue depth is `calls.length`, not unbounded. Exclusive turns use the same sequential path. Each call still runs through `dispatchToolCall` (permission + validation + execute). Tool lifecycle events may complete out of order; history/store appends stay in call order.
217
+ - `generateValidateReviseLoop` makes at most `1 + maxRevisions + maxToolRounds` provider turns when bounded tools are enabled (otherwise `maxRevisions + 1`); it cannot loop forever. Each revision costs one provider turn plus one store append.
218
+ - Bounded artifact tool calls run sequentially through `dispatchToolCall` (permission + validation + execute); their assistant call and result are persisted before the next provider request. `singleShotLoop` retains its bounded parallel worker pool and original call-order transcript behavior.
212
219
  - The loop is a plain object/factory; no class hierarchy, no background work, no extra dependencies. `LoopContext` is a single object literal of bound arrows built once per run.
213
220
  - The Synapta-free boundary is guarded by tests: `src/` imports no `synapta*` package, and the `Artifact*`/`AgentLoop*`/`LoopContext` contracts contain no `workflow`/`node`/`step` field names. Hosts supply their own schema; no host domain type is imported by `src/`.
214
221
 
222
+ ## Guardrails
223
+
224
+ Built-in loops and custom loops that use `LoopContext.generate()` / `LoopContext.dispatchToolCall()` inherit runtime guardrails. Provider output is checked before a loop appends assistant content; tool stages remain in shared dispatch. Do not call providers or `ToolDefinition.execute()` directly if guardrail enforcement is required; see [Guardrails](guardrails.md).
225
+
215
226
  ## Related APIs
216
227
  - [Agent/session runtime](agent-session-runtime.md): `RuntimeAgentSession.run()` builds the `LoopContext` and delegates to the resolved loop.
217
228
  - [Agent events](agent-events.md): the `artifact_*` event variants and ordering emitted by `generateValidateReviseLoop`.
@@ -5,6 +5,7 @@
5
5
  The agent/session runtime adds the minimal shared SDK surface for running provider turns, dispatching complete host-owned tool calls, and subscribing to session events:
6
6
 
7
7
  - `createAgent(config)`
8
+ - `createSecureAgent(options)` for opt-in fail-closed composition
8
9
  - `createAgentSession(config)`
9
10
  - `agent.createSession(config)`
10
11
  - `session.run(input, options)` → `AgentRunResult`
@@ -17,6 +18,8 @@ The agent/session runtime adds the minimal shared SDK surface for running provid
17
18
  - `session.checkout(leafId?)`
18
19
  - `session.fork(options?)`
19
20
  - `session.clone(options?)`
21
+ - `resumeAgentRun(agent, ref, decision, options)`
22
+ - `createAgentRunLifecycle({ checkpoints, resolveAgent })` for host-selected remote status/resume adapters
20
23
 
21
24
  The runtime streams provider text/tool-call content into `AgentEvent` values. Complete `tool_call` events are dispatched through the active host `ToolRegistry`, then returned as tool-result messages on the next provider turn. When a store is supplied, user, assistant, tool-result, and model-change entries are appended under the current branch leaf. Abort propagation and run exclusivity use native `AbortController`.
22
25
 
@@ -43,7 +46,9 @@ string | Message | readonly Message[]
43
46
 
44
47
  `AgentSessionConfig.store` overrides `AgentConfig.store`; otherwise the session gets a private memory store. `AgentSessionConfig.leafId` selects the branch leaf to resume from.
45
48
 
46
- `RunOptions.model` can override the request model for a run. Model overrides append a `model_change` entry. `AgentConfig.inputLayout` selects the default input assembly layout (`"legacy"` by default, or opt-in `"cache_aware"`); `RunOptions.inputLayout` wins for one run. `AgentConfig.providerOptions`/`RunOptions.providerOptions` supply generic provider request options; `timeoutMs`, `maxRetries`, and `maxRetryDelayMs` are deprecated inert provider-level hints in first-party providers. Use `RunOptions.signal`/host abort controllers for timeouts and `AgentConfig.retry`/`RunOptions.retry` for retry. `AgentConfig.providerRequestPolicies`/`RunOptions.providerRequestPolicies` run before `AIProvider.generate()` and before `provider_request` middleware. `AgentConfig.systemPrompt` and `RunOptions.systemPrompt` add explicit layered system prompt contributions; `RunOptions.systemPrompt: false` disables configured prompt layers for that run while keeping `AgentConfig.instructions` as the base path. `RunOptions.compaction` can enable auto-compaction for that run or use `false` to disable configured auto-compaction. `RunOptions.retry` can enable provider-turn retry for that run or use `false` to disable configured retry. `RunOptions.metadata` is merged with agent/session metadata for assembly, provider requests, and tool contexts. `RunOptions.maxToolRounds` bounds repeated tool turns and defaults to `1`. `RunOptions.signal` is bridged into the per-run abort signal passed to assembly, providers, tools, auto-compaction, and retry backoff.
49
+ `AgentConfig.limits` sets run ceilings; `RunOptions.limits` may only narrow configured agent values. Limits cover turns, provider attempts, tool rounds/calls, wall time, request/response bytes, tokens, and optional single-currency cost. A breach emits one `run_limit_exceeded` event and throws `AgentRunError` with `result.limit`; see [Runs and usage ledger](runs-and-usage.md#run-limits).
50
+
51
+ `RunOptions.model` can override the request model for a run. Model overrides append a `model_change` entry. `AgentConfig.inputLayout` selects the default input assembly layout (`"legacy"` by default, or opt-in `"cache_aware"`); `RunOptions.inputLayout` wins for one run. `AgentConfig.providerOptions`/`RunOptions.providerOptions` supply generic provider request options; `timeoutMs`, `maxRetries`, and `maxRetryDelayMs` are deprecated inert provider-level hints in first-party providers. Use `RunOptions.signal`/host abort controllers for timeouts and `AgentConfig.retry`/`RunOptions.retry` for retry. `AgentConfig.providerRequestPolicies`/`RunOptions.providerRequestPolicies` run before `AIProvider.generate()` and before `provider_request` middleware. `AgentConfig.systemPrompt` and `RunOptions.systemPrompt` add explicit layered system prompt contributions; `RunOptions.systemPrompt: false` disables configured prompt layers for that run while keeping `AgentConfig.instructions` as the base path. `RunOptions.compaction` can enable auto-compaction for that run or use `false` to disable configured auto-compaction. `RunOptions.retry` can enable provider-turn retry for that run or use `false` to disable configured retry. `RunOptions.metadata` is merged with agent/session metadata for assembly, provider requests, and tool contexts. Deprecated `RunOptions.maxToolRounds` narrows `limits.maxToolRounds`. `RunOptions.signal` is bridged into the per-run abort signal passed to assembly, providers, tools, auto-compaction, and retry backoff.
47
52
 
48
53
  ## Outputs / response / events
49
54
 
@@ -160,6 +165,33 @@ await agent.createSession().run("Hi", { model: overrideModel });
160
165
  - Runtime events contain messages/content only; do not put secrets in prompts, metadata, provider events, session entries, or docs examples.
161
166
  - The event broadcaster is in-memory, live-only, and bounded per subscriber by `SubscribeOptions`. It adds no dependency, timer, filesystem/network discovery, worker, or durable queue.
162
167
 
168
+ ## Durable interruption
169
+
170
+ Set `runState` with a host-owned `CheckpointStore`, stable `definitionRevision`, and `interruptBeforeTool: true` to suspend at a persisted pre-side-effect boundary. A suspended result has `status: "suspended"`, a redacted `interruption`, and `runState.version`; it releases session resources before returning.
171
+
172
+ ```ts
173
+ const result = await session.run("Publish draft", {
174
+ runState: { checkpoints, definitionRevision: "2026-07-20.1", interruptBeforeTool: true },
175
+ });
176
+ if (result.status === "suspended") {
177
+ await resumeAgentRun(agent, { runId: result.runId, sessionId: result.sessionId }, {
178
+ decision: "approve", expectedVersion: result.runState!.version!,
179
+ }, { checkpoints, definitionRevision: "2026-07-20.1" });
180
+ }
181
+ ```
182
+
183
+ Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. Only built-in loop options are durable; custom `AgentLoopStrategy` rejects before provider work.
184
+
185
+ ## Secure composition
186
+
187
+ `createSecureAgent()` is optional; `createAgent()` remains explicit and backward-compatible. Secure composition requires an ID, non-empty definition revision, exact non-empty ownership, redactor, permission and trust policies, finite explicit limits, a host `ToolArgumentValidator`, non-empty schema for every tool, and checkpoints. It builds a duplicate-error registry, rejects missing schemas, always enables durable pre-tool interruption, and reuses normal provider/request policies without discovery or background work.
188
+
189
+ Per-run options may narrow `limits` and append `guardrails`; they cannot replace secure ownership, redaction, validator, or durable checkpoint policy. Every active tool is trust-checked then permission-checked before validation and its side effect. See [`examples/secure-agent.ts`](../examples/secure-agent.ts).
190
+
191
+ ## Guardrails
192
+
193
+ `AgentConfig.guardrails` applies typed input, output, tool-input, and tool-output checks to every run. `RunOptions.guardrails` appends checks for one run. Input checks run before session append; configured output checks buffer provider content until allowed, so blocked content is never emitted or stored. See [Guardrails](guardrails.md).
194
+
163
195
  ## Related APIs
164
196
 
165
197
  - [Public contracts](public-contracts.md): `Agent`, `AgentSession`, `RunOptions`, and `AgentEvent` contracts.