@arnilo/prism 0.7.0 → 0.8.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 (99) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +12 -11
  3. package/dist/agent-approval.d.ts +4 -0
  4. package/dist/agent-approval.js +5 -1
  5. package/dist/agent-run-lifecycle.js +29 -5
  6. package/dist/agent-run-state.d.ts +13 -1
  7. package/dist/agent-run-state.js +12 -2
  8. package/dist/agent-session/helpers.js +6 -1
  9. package/dist/agent-session/session/assemble.js +126 -5
  10. package/dist/agent-session/session/persist.d.ts +16 -0
  11. package/dist/agent-session/session/persist.js +58 -2
  12. package/dist/agent-session/session/provider-round.d.ts +3 -3
  13. package/dist/agent-session/session/provider-round.js +12 -6
  14. package/dist/agent-session/session/tool-round.js +4 -1
  15. package/dist/agent-session/session/types.d.ts +12 -1
  16. package/dist/agent-session/session.d.ts +2 -0
  17. package/dist/agent-session/session.js +2 -0
  18. package/dist/checkpoints.js +7 -11
  19. package/dist/contracts-core/content.d.ts +5 -0
  20. package/dist/contracts-core/loop.d.ts +42 -0
  21. package/dist/contracts-core/run-limits.d.ts +2 -0
  22. package/dist/contracts-protocol.d.ts +15 -3
  23. package/dist/contracts-run-state.d.ts +26 -5
  24. package/dist/evidence-grounding.d.ts +29 -0
  25. package/dist/evidence-grounding.js +162 -0
  26. package/dist/host-composition.d.ts +13 -0
  27. package/dist/host-composition.js +33 -2
  28. package/dist/index.d.ts +6 -2
  29. package/dist/index.js +3 -1
  30. package/dist/provider-events.d.ts +3 -1
  31. package/dist/provider-events.js +2 -2
  32. package/dist/providers/transport.d.ts +3 -1
  33. package/dist/providers/transport.js +36 -0
  34. package/dist/redaction.js +18 -2
  35. package/dist/run-bundle.d.ts +89 -0
  36. package/dist/run-bundle.js +149 -0
  37. package/dist/testing/state-concurrency-conformance.js +5 -12
  38. package/docs/ag-ui.md +5 -0
  39. package/docs/agent-loops.md +33 -0
  40. package/docs/agent-session-runtime.md +4 -3
  41. package/docs/coding-security.md +1 -0
  42. package/docs/coding-tools.md +0 -1
  43. package/docs/compaction-observational-memory.md +1 -1
  44. package/docs/connected-apps.md +116 -0
  45. package/docs/context-and-skills.md +13 -0
  46. package/docs/core.md +1 -1
  47. package/docs/diagrams.md +6 -6
  48. package/docs/document-reader.md +9 -9
  49. package/docs/documents.md +32 -11
  50. package/docs/durable-runs.md +87 -0
  51. package/docs/enterprise-postgres-state.md +4 -0
  52. package/docs/execution-timeline.md +6 -0
  53. package/docs/guardrails.md +33 -0
  54. package/docs/history/079-messaging-primitive-review.md +391 -0
  55. package/docs/history/080-messaging-followon-primitive-review.md +234 -0
  56. package/docs/history/081-connected-apps-primitive-review.md +74 -0
  57. package/docs/history/083-prism-work-primitive-review.md +84 -0
  58. package/docs/history/084-primitive-review.md +96 -0
  59. package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
  60. package/docs/history/README.md +5 -0
  61. package/docs/history/release-handoffs.md +38 -0
  62. package/docs/host-compositions.md +8 -6
  63. package/docs/host-security.md +2 -2
  64. package/docs/index.md +47 -29
  65. package/docs/live-testing.md +5 -3
  66. package/docs/mcp-tools.md +1 -0
  67. package/docs/messaging-channel-operations.md +166 -0
  68. package/docs/messaging-channels.md +150 -0
  69. package/docs/migrate-to-0.8.md +124 -0
  70. package/docs/migration.md +30 -0
  71. package/docs/model-registry.md +12 -2
  72. package/docs/openapi-tools.md +1 -1
  73. package/docs/operations.md +1 -3
  74. package/docs/options-index.md +34 -2
  75. package/docs/peer-dependencies.md +6 -6
  76. package/docs/postgres-persistence.md +1 -1
  77. package/docs/provider-layer.md +2 -2
  78. package/docs/provider-packages.md +20 -20
  79. package/docs/providers/neuralwatt.md +5 -1
  80. package/docs/rag.md +1 -1
  81. package/docs/release-and-install.md +52 -46
  82. package/docs/run-bundle.md +92 -0
  83. package/docs/runs-and-usage.md +14 -0
  84. package/docs/server.md +2 -0
  85. package/docs/sheets.md +9 -9
  86. package/docs/signal-channel.md +112 -0
  87. package/docs/speech.md +5 -1
  88. package/docs/sqlite-persistence.md +1 -1
  89. package/docs/telegram-channel.md +157 -0
  90. package/docs/testing.md +2 -2
  91. package/docs/wiki.md +1 -1
  92. package/docs/work-artifacts-and-review.md +1 -1
  93. package/docs/work-connectors.md +9 -9
  94. package/docs/work-sandbox.md +115 -0
  95. package/docs/work-tools.md +38 -16
  96. package/package.json +5 -3
  97. package/templates/business-worker/manifest.json +2 -1
  98. package/templates/business-worker/src/agent.ts.tmpl +1 -1
  99. package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
@@ -56,11 +56,37 @@ function sanitizeCredentialRefs(refs = [], redactor) {
56
56
  }
57
57
  return Object.freeze(sanitized);
58
58
  }
59
+ const HARD_MAX_CONNECTED_APPS = 32;
60
+ const MAX_CONNECTED_APP_IDENTIFIER_BYTES = 128;
61
+ function copyConnectedApps(value) {
62
+ if (value === undefined)
63
+ return undefined;
64
+ if (!value || typeof value !== "object")
65
+ throw new HostCompositionError("Connected apps must provide identifier arrays");
66
+ return Object.freeze({
67
+ appIds: copyConnectedAppIdentifiers(value.appIds, "appIds"),
68
+ serverIds: copyConnectedAppIdentifiers(value.serverIds, "serverIds"),
69
+ });
70
+ }
71
+ function copyConnectedAppIdentifiers(value, name) {
72
+ if (!Array.isArray(value) || value.length > HARD_MAX_CONNECTED_APPS) {
73
+ throw new HostCompositionError(`Connected app ${name} must contain at most ${HARD_MAX_CONNECTED_APPS} identifiers`);
74
+ }
75
+ return Object.freeze(value.map((identifier) => {
76
+ if (typeof identifier !== "string" ||
77
+ !identifier.trim() ||
78
+ Buffer.byteLength(identifier, "utf8") > MAX_CONNECTED_APP_IDENTIFIER_BYTES) {
79
+ throw new HostCompositionError(`Connected app ${name} must contain 1..${MAX_CONNECTED_APP_IDENTIFIER_BYTES} byte identifiers`);
80
+ }
81
+ return identifier;
82
+ }));
83
+ }
59
84
  function isContainedPath(parent, child) {
60
85
  const rel = relative(parent, child);
61
86
  return !rel.startsWith("..") && !isAbsolute(rel);
62
87
  }
63
- function resolveStorage(store, checkpoints) {
88
+ /** Classifies a store by declared kind/durability and constructor name — never reads contents or connection strings (plan 084 Task 4 reuses it for run-bundle snapshots). */
89
+ export function describeStorage(store, checkpoints) {
64
90
  const target = store ?? checkpoints;
65
91
  if (!target || typeof target !== "object") {
66
92
  return { kind: "none", durable: false };
@@ -111,6 +137,7 @@ export function inspectHostComposition(options) {
111
137
  }));
112
138
  // 2. Credential references (identifier references only, raw values redacted)
113
139
  const credRefs = sanitizeCredentialRefs(options.credentialRefs, redactor);
140
+ const connectedApps = copyConnectedApps(options.connectedApps);
114
141
  // 3. Ownership
115
142
  const rawOwnership = config?.ownership;
116
143
  const ownership = {
@@ -119,7 +146,7 @@ export function inspectHostComposition(options) {
119
146
  userId: typeof rawOwnership?.userId === "string" ? rawOwnership.userId.trim() : undefined,
120
147
  };
121
148
  // 4. Storage durability
122
- const storage = resolveStorage(options.store, options.checkpoints ?? config?.runState?.checkpoints);
149
+ const storage = describeStorage(options.store, options.checkpoints ?? config?.runState?.checkpoints);
123
150
  // 5. Sandbox capabilities
124
151
  const workspaceRoot = options.workspaceRoot;
125
152
  const sandboxRoots = options.sandboxRoots ?? [];
@@ -180,6 +207,9 @@ export function inspectHostComposition(options) {
180
207
  }
181
208
  // Verified identity verification
182
209
  const identity = config?.identity;
210
+ if (connectedApps !== undefined && identity?.verified !== true) {
211
+ errors.push("Business connected apps require a verified identity");
212
+ }
183
213
  if (identity) {
184
214
  if (!identity.principal?.id || !identity.tenantId) {
185
215
  errors.push("Business worker identity cannot be empty or fabricated");
@@ -219,6 +249,7 @@ export function inspectHostComposition(options) {
219
249
  profile: normalizedProfile,
220
250
  effectiveTools: Object.freeze(effectiveTools),
221
251
  credentialReferences: credRefs,
252
+ ...(connectedApps === undefined ? {} : { connectedApps }),
222
253
  ownership: Object.freeze(ownership),
223
254
  storage: Object.freeze(storage),
224
255
  sandbox: Object.freeze({
package/dist/index.d.ts CHANGED
@@ -9,8 +9,8 @@ export { AGENT_RUN_STATE_NAMESPACE, AGENT_RUN_STATE_SCHEMA_VERSION, agentFingerp
9
9
  export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
10
10
  export type { ArtifactApproval, ArtifactApprovalState, ArtifactBodyErrorCode, ArtifactBodyPresignOptions, ArtifactBodyRef, ArtifactBodyStore, ArtifactBodyTransferOptions, ArtifactCitation, ArtifactDecisionState, ArtifactDeliveryToken, ArtifactRecord, ArtifactRevision, CitationIntegrityReason, CitationIntegrityResult, CitationLiveSource, CitationSupport, } from "./artifacts.js";
11
11
  export { ARTIFACT_BODY_ERROR_CODES, ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactBodyStoreError, ArtifactError, approvalEvidenceIntact, artifactApprovalState, artifactCheckpointKey, checkCitationIntegrity, citationBindingDigest, HARD_CITATION_EXCERPT_BYTES, } from "./artifacts.js";
12
- export { ATTENTION_BUDGET_ERROR_CODE, AttentionBudgetError, createAttentionCompiler, createAttentionTruncationTrigger, DEFAULT_ATTENTION_COMPACT_RATIO, DEFAULT_ATTENTION_KEEP_LAST, DEFAULT_ATTENTION_RESERVE_TOKENS, DEFAULT_ATTENTION_THINKING_KEEP_TURNS, DEFAULT_ATTENTION_TRIGGER_RATIO, DEFAULT_ATTENTION_TRUNCATION_THRESHOLD, isAttentionBudgetError, resolveAttentionReserveTokens, resolveInputCap, resolveRunAttentionCompiler, } from "./attention-compiler.js";
13
12
  export type { AttentionTruncationTrigger, AttentionTruncationTriggerOptions, PersistedAttentionStickyFrontier, } from "./attention-compiler.js";
13
+ export { ATTENTION_BUDGET_ERROR_CODE, AttentionBudgetError, createAttentionCompiler, createAttentionTruncationTrigger, DEFAULT_ATTENTION_COMPACT_RATIO, DEFAULT_ATTENTION_KEEP_LAST, DEFAULT_ATTENTION_RESERVE_TOKENS, DEFAULT_ATTENTION_THINKING_KEEP_TURNS, DEFAULT_ATTENTION_TRIGGER_RATIO, DEFAULT_ATTENTION_TRUNCATION_THRESHOLD, isAttentionBudgetError, resolveAttentionReserveTokens, resolveInputCap, resolveRunAttentionCompiler, } from "./attention-compiler.js";
14
14
  export type { ApplyCacheControlOptions, CacheControlledContentBlock, CacheControlledMessage, CacheControlValue, CacheUsageReport, } from "./cache-helpers.js";
15
15
  export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, resolveBreakpoint, sanitizeCacheKey, systemCacheControlField, } from "./cache-helpers.js";
16
16
  export type { CacheTelemetry, CacheTelemetryOptions, CacheTelemetryReport, CacheTelemetrySample, } from "./cache-telemetry.js";
@@ -43,6 +43,8 @@ export type { DeviceAdapter, DeviceAdmitRequest, DeviceChunkResult, DeviceConfor
43
43
  export { acceptDeviceChunk, assertDeviceAdmit, DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
44
44
  export type { EventMultiplexer, EventMultiplexerOptions, EventOverflowInfo, EventOverflowPolicy } from "./event-multiplexer.js";
45
45
  export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMultiplexerError } from "./event-multiplexer.js";
46
+ export type { ClaimGroundingEvidence, ClaimGroundingEvidenceExtractor, ClaimGroundingEvidenceExtractorContext, ClaimGroundingGuardrailOptions, } from "./evidence-grounding.js";
47
+ export { createClaimGroundingGuardrail } from "./evidence-grounding.js";
46
48
  export type { ExecutionAction, ExecutionDecision, ExecutionPolicy, ExecutionRisk } from "./execution-policy.js";
47
49
  export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
48
50
  export type { ActivatedKernelConfig, ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy, LoadedExtension, } from "./extensions.js";
@@ -87,6 +89,8 @@ export type { LoadBinaryResourceOptions } from "./resources.js";
87
89
  export { loadBinaryResource, loadJsonResource, loadManifestResource, loadTextResource } from "./resources.js";
88
90
  export type { DefaultRetryPolicyOptions } from "./retry.js";
89
91
  export { createDefaultRetryPolicy, isTransientErrorInfo, waitForRetry } from "./retry.js";
92
+ export type { RunBundleSnapshot, RunBundleSnapshotInput } from "./run-bundle.js";
93
+ export { RUN_BUNDLE_SCHEMA_VERSION, snapshotRunBundle } from "./run-bundle.js";
90
94
  export type { BatchedRunLedgerOptions } from "./run-ledger.js";
91
95
  export { createBatchedRunLedger, DEFAULT_LEDGER_BATCH_BYTES, DEFAULT_LEDGER_BATCH_DELAY_MS, DEFAULT_LEDGER_BATCH_ENTRIES, HARD_LEDGER_BATCH_BYTES, HARD_LEDGER_BATCH_DELAY_MS, HARD_LEDGER_BATCH_ENTRIES, isFlushableRunLedger, } from "./run-ledger.js";
92
96
  export type { RunLimitTrackerOptions } from "./run-limits.js";
@@ -122,5 +126,5 @@ export { trimTrailingSlashes } from "./trim-trailing-slashes.js";
122
126
  export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
123
127
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
124
128
  export declare const name = "prism";
125
- export declare const version = "0.7.0";
129
+ export declare const version = "0.8.0";
126
130
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ export { createChainedCredentialResolver, createEnvCredentialResolver, createExp
22
22
  export { createDelegatedAgentStep, DelegatedAgentStepError, MAX_DELEGATED_AGENT_DURATION_MS, MAX_DELEGATED_AGENT_EVENT_BYTES, MAX_DELEGATED_AGENT_ID_BYTES, MAX_DELEGATED_AGENT_NAME_BYTES, MAX_DELEGATED_AGENT_STEP_INDEX, MAX_DELEGATED_AGENT_TOKEN_COUNT, } from "./delegated-agent-step.js";
23
23
  export { acceptDeviceChunk, assertDeviceAdmit, DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
24
24
  export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMultiplexerError } from "./event-multiplexer.js";
25
+ export { createClaimGroundingGuardrail } from "./evidence-grounding.js";
25
26
  export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
26
27
  export { activateKernel, createExtensionEventBus, createExtensionKernel } from "./extensions.js";
27
28
  export { createMemoryRunFeedbackStore, prepareRunFeedback, RunFeedbackError, requireRunFeedbackOwnership, runFeedbackPageLimit, } from "./feedback.js";
@@ -47,6 +48,7 @@ export { createProviderRegistry, createProviderResolver } from "./providers.js";
47
48
  export { createSecretRedactor, errorToErrorInfo, redactAgentEvent, redactMessage, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry, resolveRedactor, } from "./redaction.js";
48
49
  export { loadBinaryResource, loadJsonResource, loadManifestResource, loadTextResource } from "./resources.js";
49
50
  export { createDefaultRetryPolicy, isTransientErrorInfo, waitForRetry } from "./retry.js";
51
+ export { RUN_BUNDLE_SCHEMA_VERSION, snapshotRunBundle } from "./run-bundle.js";
50
52
  export { createBatchedRunLedger, DEFAULT_LEDGER_BATCH_BYTES, DEFAULT_LEDGER_BATCH_DELAY_MS, DEFAULT_LEDGER_BATCH_ENTRIES, HARD_LEDGER_BATCH_BYTES, HARD_LEDGER_BATCH_DELAY_MS, HARD_LEDGER_BATCH_ENTRIES, isFlushableRunLedger, } from "./run-ledger.js";
51
53
  export { createRunLimitTracker, DEFAULT_RUN_LIMITS, HARD_RUN_LIMITS, RunLimitError, RunLimitTracker, resolveRunLimits, } from "./run-limits.js";
52
54
  export { assertHostCompositionReadiness, createSecureAgent, HostCompositionError, inspectHostComposition, } from "./secure-agent.js";
@@ -67,6 +69,6 @@ export { createToolParameterValidator, createToolRegistry, dispatchToolCall, fil
67
69
  export { trimTrailingSlashes } from "./trim-trailing-slashes.js";
68
70
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
69
71
  export const name = "prism";
70
- export const version = "0.7.0";
72
+ export const version = "0.8.0";
71
73
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
72
74
  //# sourceMappingURL=index.js.map
@@ -14,7 +14,9 @@ export declare function providerContinuationRequired(cursor: string, reason?: st
14
14
  export declare function reconstructToolCallDeltas(events: readonly ProviderEvent[]): readonly ToolCallContent[];
15
15
  export declare function providerUsage(usage: Usage): ProviderEvent;
16
16
  export declare function providerDone(usage?: Usage): ProviderEvent;
17
- export declare function providerError(error: unknown, secrets?: readonly (string | undefined)[]): ProviderEvent;
17
+ export declare function providerError(error: unknown, secrets?: readonly (string | undefined)[]): Extract<ProviderEvent, {
18
+ type: "error";
19
+ }>;
18
20
  export declare function toolCallContent(id: string, name: string, args?: JsonObject): ToolCallContent;
19
21
  /** Build a tool call from streamed arguments text; malformed JSON becomes a blocked call (no throw). */
20
22
  export declare function toolCallFromArgumentsText(id: string, name: string, argumentsText: string): ToolCallContent;
@@ -1,4 +1,4 @@
1
- import { ProviderTransportError, tryParseJsonObjectArguments } from "./providers/transport.js";
1
+ import { classifyProviderFailure, ProviderTransportError, tryParseJsonObjectArguments } from "./providers/transport.js";
2
2
  import { errorToErrorInfo } from "./redaction.js";
3
3
  export function providerTextDelta(text) {
4
4
  return { type: "content_delta", content: { type: "text", text } };
@@ -51,7 +51,7 @@ export function providerDone(usage) {
51
51
  }
52
52
  export function providerError(error, secrets = []) {
53
53
  const info = errorToErrorInfo(error, secrets);
54
- return { type: "error", error: info };
54
+ return { type: "error", error: info.failureClass ? info : { ...info, failureClass: classifyProviderFailure(error) } };
55
55
  }
56
56
  export function toolCallContent(id, name, args = {}) {
57
57
  return { type: "tool_call", id, name, arguments: args };
@@ -1,4 +1,4 @@
1
- import type { JsonObject } from "../contracts.js";
1
+ import type { JsonObject, ProviderFailureClass } from "../contracts.js";
2
2
  export declare const DEFAULT_MAX_EVENT_BYTES = 262144;
3
3
  export declare const DEFAULT_MAX_BUFFER_BYTES = 524288;
4
4
  export declare const DEFAULT_MAX_RESPONSE_BODY_BYTES = 65536;
@@ -15,6 +15,8 @@ export interface SseEvent {
15
15
  readonly comments?: readonly string[];
16
16
  }
17
17
  export type ProviderTransportErrorCode = "sse_buffer_overflow" | "sse_event_overflow" | "response_body_overflow" | "aborted" | "invalid_json_arguments" | "incomplete_delta" | "response_body_shape";
18
+ /** Maps already-captured provider transport evidence to advisory outcome metadata. */
19
+ export declare function classifyProviderFailure(error: unknown): ProviderFailureClass;
18
20
  export declare class ProviderTransportError extends Error {
19
21
  readonly code: ProviderTransportErrorCode;
20
22
  readonly limitBytes?: number;
@@ -3,6 +3,22 @@ export const DEFAULT_MAX_EVENT_BYTES = 262_144;
3
3
  export const DEFAULT_MAX_BUFFER_BYTES = 524_288;
4
4
  export const DEFAULT_MAX_RESPONSE_BODY_BYTES = 65_536;
5
5
  export const DEFAULT_MAX_ARGUMENT_BYTES = 262_144;
6
+ const QUOTA_BODY = /quota|usage[\s_-]*limit|insufficient[\s_-]*(?:quota|credit|balance)|billing[\s_-]*(?:limit|quota)/i;
7
+ const TRANSIENT_NETWORK_CODES = new Set(["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "ENETUNREACH", "ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT"]);
8
+ /** Maps already-captured provider transport evidence to advisory outcome metadata. */
9
+ export function classifyProviderFailure(error) {
10
+ const status = readHttpStatus(error);
11
+ if (status === 401 || status === 403)
12
+ return "auth";
13
+ if (status === 429)
14
+ return QUOTA_BODY.test(errorMessage(error)) ? "quota" : "rate_limited";
15
+ if (status !== undefined)
16
+ return status >= 500 ? "transient" : status >= 400 ? "permanent" : "unknown";
17
+ const cause = readField(error, "cause");
18
+ if (isTransientNetworkCode(readField(error, "code")) || isTransientNetworkCode(readField(cause, "code")))
19
+ return "transient";
20
+ return "unknown";
21
+ }
6
22
  export class ProviderTransportError extends Error {
7
23
  code;
8
24
  limitBytes;
@@ -22,8 +38,28 @@ export function httpStatusError(prefix, response, bodyText) {
22
38
  const hint = parseRetryAfterMs(response.headers.get("retry-after"));
23
39
  if (hint !== undefined)
24
40
  error.retryAfterMs = hint;
41
+ Object.defineProperty(error, "failureClass", { value: classifyProviderFailure(error), enumerable: true });
25
42
  return error;
26
43
  }
44
+ function readHttpStatus(error) {
45
+ for (const key of ["code", "status", "statusCode"]) {
46
+ const value = readField(error, key);
47
+ const status = typeof value === "number" ? value : typeof value === "string" && /^\d{3}$/.test(value) ? Number(value) : undefined;
48
+ if (status !== undefined && status >= 100 && status <= 599)
49
+ return status;
50
+ }
51
+ return undefined;
52
+ }
53
+ function readField(error, key) {
54
+ return error && typeof error === "object" && key in error ? error[key] : undefined;
55
+ }
56
+ function isTransientNetworkCode(value) {
57
+ return typeof value === "string" && TRANSIENT_NETWORK_CODES.has(value.toUpperCase());
58
+ }
59
+ function errorMessage(error) {
60
+ const cause = readField(error, "cause");
61
+ return [readField(error, "message"), readField(cause, "message")].filter((value) => typeof value === "string").join(" ");
62
+ }
27
63
  /** Parse a `Retry-After` header (delay-seconds or HTTP-date) into milliseconds. */
28
64
  export function parseRetryAfterMs(value, now = Date.now()) {
29
65
  if (!value)
package/dist/redaction.js CHANGED
@@ -167,19 +167,22 @@ export function errorToErrorInfo(error, secrets = []) {
167
167
  const code = readErrorCode(error);
168
168
  const retry = readRetryAfterMs(error);
169
169
  const retryAfter = retry !== undefined ? { retryAfterMs: retry } : {};
170
+ const failureClass = readProviderFailureClass(error);
171
+ const failure = failureClass === undefined ? {} : { failureClass };
170
172
  if (error instanceof Error) {
171
173
  return {
172
174
  name: error.name,
173
175
  message: redactSecrets(error.message, secrets),
174
176
  code,
177
+ ...failure,
175
178
  ...retryAfter,
176
179
  cause: error.cause ? redactSecrets(String(error.cause), secrets) : undefined,
177
180
  };
178
181
  }
179
182
  if (error && typeof error === "object" && "message" in error) {
180
- return { message: redactSecrets(String(error.message), secrets), code, ...retryAfter };
183
+ return { message: redactSecrets(String(error.message), secrets), code, ...failure, ...retryAfter };
181
184
  }
182
- return { message: redactSecrets(String(error), secrets), code, ...retryAfter };
185
+ return { message: redactSecrets(String(error), secrets), code, ...failure, ...retryAfter };
183
186
  }
184
187
  function readErrorCode(error) {
185
188
  if (!error || typeof error !== "object" || !("code" in error))
@@ -193,4 +196,17 @@ function readRetryAfterMs(error) {
193
196
  const value = error.retryAfterMs;
194
197
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
195
198
  }
199
+ function readProviderFailureClass(error) {
200
+ if (!error || typeof error !== "object" || !("failureClass" in error))
201
+ return undefined;
202
+ const value = error.failureClass;
203
+ return value === "quota" ||
204
+ value === "auth" ||
205
+ value === "rate_limited" ||
206
+ value === "transient" ||
207
+ value === "permanent" ||
208
+ value === "unknown"
209
+ ? value
210
+ : undefined;
211
+ }
196
212
  //# sourceMappingURL=redaction.js.map
@@ -0,0 +1,89 @@
1
+ import type { Agent, AgentSessionConfig, GuardrailStage, RunOptions } from "./contracts.js";
2
+ /** Report format revision. Any shape change bumps this so pinned digests cannot compare across formats. */
3
+ export declare const RUN_BUNDLE_SCHEMA_VERSION = 1;
4
+ /** Inspectable projection of the inputs a run actually resolves to. Frozen JSON, safe to persist and diff. */
5
+ export interface RunBundleSnapshot {
6
+ readonly schemaVersion: number;
7
+ /** `sha256:<64 hex>` over the canonicalized, redacted snapshot (this field excluded). */
8
+ readonly digest: string;
9
+ /** `agentFingerprint()` of the same agent/revision: the durable-resume identity behind this snapshot. */
10
+ readonly fingerprint: string;
11
+ readonly agent: {
12
+ readonly id: string;
13
+ readonly definitionRevision: string | null;
14
+ };
15
+ readonly systemPrompt: {
16
+ readonly disabled: boolean;
17
+ /** Digest of `AgentConfig.instructions`; the body never leaves the process. */
18
+ readonly instructionsDigest: string | null;
19
+ readonly contributions: readonly {
20
+ readonly id: string;
21
+ readonly mode: string | null;
22
+ readonly source: string | null;
23
+ readonly digest: string;
24
+ }[];
25
+ };
26
+ readonly skills: readonly {
27
+ readonly name: string;
28
+ readonly instructionsDigest: string | null;
29
+ readonly toolNames: readonly string[];
30
+ }[];
31
+ /** Effective tool set: `run.toolNames` narrowing already applied, schemas reduced to digests. */
32
+ readonly tools: readonly {
33
+ readonly name: string;
34
+ readonly schemaDigest: string;
35
+ readonly exclusive: boolean;
36
+ readonly effect: string | null;
37
+ }[];
38
+ readonly activeSkills: readonly string[] | null;
39
+ readonly guardrails: readonly {
40
+ readonly name: string;
41
+ readonly stage: GuardrailStage;
42
+ readonly revision: string | null;
43
+ }[];
44
+ readonly loop: {
45
+ readonly strategy: string;
46
+ readonly revision: string | null;
47
+ };
48
+ readonly thinkingLevel: string | null;
49
+ readonly limits: Readonly<import("./contracts.js").ResolvedRunLimits>;
50
+ /** Host-shaped JSON as configured (`true`/`false`/options); `null` when unset. */
51
+ readonly attentionCompiler: unknown;
52
+ readonly model: {
53
+ readonly provider: string | null;
54
+ readonly model: string | null;
55
+ };
56
+ readonly requestPolicies: readonly string[];
57
+ /** Kinds only — never a connection string, path, or credential. */
58
+ readonly storage: {
59
+ readonly sessionStore: {
60
+ readonly kind: string;
61
+ readonly durable: boolean;
62
+ };
63
+ readonly checkpoints: {
64
+ readonly kind: string;
65
+ readonly durable: boolean;
66
+ };
67
+ readonly effectStore: {
68
+ readonly kind: string;
69
+ readonly durable: boolean;
70
+ };
71
+ readonly memory: {
72
+ readonly kind: string;
73
+ readonly durable: boolean;
74
+ };
75
+ };
76
+ }
77
+ export interface RunBundleSnapshotInput {
78
+ readonly agent: Agent;
79
+ /** Session-level inputs (store, leaf, cache TTL) that change what the run reads and writes. */
80
+ readonly config?: AgentSessionConfig;
81
+ readonly run?: RunOptions;
82
+ /** Optional memory store instance; only its kind/durability label is read, never its contents. */
83
+ readonly memory?: unknown;
84
+ }
85
+ /**
86
+ * Snapshots the effective run bundle: synchronous, in-memory, zero network and zero store reads.
87
+ * The counterpart of `agentFingerprint` for humans — same inputs, named fields, one stable digest to pin.
88
+ */
89
+ export declare function snapshotRunBundle(input: RunBundleSnapshotInput): RunBundleSnapshot;
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ import { agentFingerprint, BUILT_IN_LOOP_REVISIONS } from "./agent-run-state.js";
3
+ import { describeStorage } from "./host-composition.js";
4
+ import { canonicalizeJsonSchema } from "./providers/schema.js";
5
+ import { resolveRunLimits } from "./run-limits.js";
6
+ import { selectRunTools } from "./tools.js";
7
+ /** Report format revision. Any shape change bumps this so pinned digests cannot compare across formats. */
8
+ export const RUN_BUNDLE_SCHEMA_VERSION = 1;
9
+ /** Bounded by construction; a bundle larger than this is a host bug, not a snapshot to retain. */
10
+ const MAX_RUN_BUNDLE_BYTES = 512 * 1024;
11
+ /**
12
+ * Snapshots the effective run bundle: synchronous, in-memory, zero network and zero store reads.
13
+ * The counterpart of `agentFingerprint` for humans — same inputs, named fields, one stable digest to pin.
14
+ */
15
+ export function snapshotRunBundle(input) {
16
+ const config = input.agent.config;
17
+ const run = input.run;
18
+ const redactor = run?.redactor ?? config.redactor;
19
+ const definitionRevision = run?.runState?.definitionRevision ?? config.runState?.definitionRevision ?? null;
20
+ const tools = selectRunTools(listTools(config.tools), run?.toolNames).tools;
21
+ const skills = listSkills(config.skills, run?.skills);
22
+ const model = run?.model ?? config.model;
23
+ const effectiveLoop = run?.loop ?? config.loop;
24
+ const systemPrompt = run?.systemPrompt ?? config.systemPrompt;
25
+ const policies = run?.providerRequestPolicies ?? config.providerRequestPolicies;
26
+ const snapshot = {
27
+ schemaVersion: RUN_BUNDLE_SCHEMA_VERSION,
28
+ fingerprint: agentFingerprint(input.agent, definitionRevision ?? ""),
29
+ agent: { id: agentId(input.agent), definitionRevision },
30
+ systemPrompt: {
31
+ disabled: systemPrompt === false,
32
+ instructionsDigest: hashText(config.instructions),
33
+ contributions: systemPrompt === false || systemPrompt === undefined
34
+ ? []
35
+ : (Array.isArray(systemPrompt) ? systemPrompt : [systemPrompt]).map((contribution) => ({
36
+ id: contribution.id,
37
+ mode: contribution.mode ?? null,
38
+ source: contribution.source ?? null,
39
+ digest: hashText(contribution.text) ?? "",
40
+ })),
41
+ },
42
+ skills: skills.map((skill) => ({
43
+ name: skill.name,
44
+ instructionsDigest: hashText(skill.instructions),
45
+ toolNames: skill.toolNames ?? [],
46
+ })),
47
+ tools: tools.map((tool) => ({
48
+ name: tool.name,
49
+ schemaDigest: hashJson(canonicalizeJsonSchema(tool.parameters ?? { type: "object" })),
50
+ exclusive: tool.exclusive === true,
51
+ effect: tool.effect === undefined ? null : typeof tool.effect === "function" ? "classifier" : tool.effect.kind,
52
+ })),
53
+ activeSkills: run?.activeSkills ?? null,
54
+ guardrails: guardrailRows(config.guardrails, run?.guardrails),
55
+ loop: loopIdentity(effectiveLoop),
56
+ thinkingLevel: run?.thinkingLevel ?? config.thinkingLevel ?? null,
57
+ limits: resolveRunLimits(config.limits, run?.limits),
58
+ attentionCompiler: run?.attentionCompiler ?? config.attentionCompiler ?? null,
59
+ model: {
60
+ provider: typeof model === "string" ? (config.provider?.id ?? null) : (model?.provider ?? null),
61
+ model: typeof model === "string" ? model : (model?.model ?? null),
62
+ },
63
+ requestPolicies: policies === undefined ? [] : (Array.isArray(policies) ? policies : [policies]).map((policy) => policy.name),
64
+ storage: {
65
+ sessionStore: kindOf(describeStorage(input.config?.store ?? config.store, undefined)),
66
+ checkpoints: kindOf(describeStorage(undefined, (run?.runState ?? config.runState)?.checkpoints)),
67
+ effectStore: kindOf(describeStorage(run?.effectStore ?? config.effectStore, undefined)),
68
+ memory: kindOf(describeStorage(input.memory, undefined)),
69
+ },
70
+ };
71
+ const redacted = redactStrings(snapshot, redactor);
72
+ const bundle = deepFreeze({ ...redacted, digest: hashJson(canonicalizeJsonSchema(redacted)) });
73
+ const bytes = Buffer.byteLength(JSON.stringify(bundle), "utf8");
74
+ if (bytes > MAX_RUN_BUNDLE_BYTES) {
75
+ throw new TypeError(`Run bundle snapshot exceeds ${MAX_RUN_BUNDLE_BYTES} bytes`);
76
+ }
77
+ return bundle;
78
+ }
79
+ /** Keeps a store label a label: a declared `kind` that is really a connection string or path becomes `custom`. */
80
+ function kindOf(described) {
81
+ const raw = described.kind.toLowerCase();
82
+ return { kind: /^[a-z0-9_.-]{1,64}$/.test(raw) ? raw : "custom", durable: described.durable };
83
+ }
84
+ function agentId(agent) {
85
+ return agent.config.id ?? agent.config.name ?? "agent";
86
+ }
87
+ function listTools(tools) {
88
+ if (!tools)
89
+ return [];
90
+ return "list" in tools ? tools.list() : tools;
91
+ }
92
+ function listSkills(skills, runSkills) {
93
+ const listed = !skills ? [] : "list" in skills ? skills.list() : skills;
94
+ const byName = new Map(listed.map((skill) => [skill.name, skill]));
95
+ for (const skill of runSkills ?? [])
96
+ byName.set(skill.name, skill);
97
+ return [...byName.values()];
98
+ }
99
+ function guardrailRows(configGuardrails, runGuardrails) {
100
+ const rows = [];
101
+ for (const guardrails of [configGuardrails, runGuardrails]) {
102
+ if (!guardrails)
103
+ continue;
104
+ for (const stage of ["input", "output", "tool_input", "tool_output"]) {
105
+ const key = stage === "tool_input" ? "toolInput" : stage === "tool_output" ? "toolOutput" : stage;
106
+ for (const guardrail of guardrails[key] ?? []) {
107
+ rows.push({ name: guardrail.name, stage, revision: guardrail.revision ?? null });
108
+ }
109
+ }
110
+ }
111
+ return rows;
112
+ }
113
+ function loopIdentity(loop) {
114
+ if (typeof loop === "object" && loop && "strategy" in loop) {
115
+ return { strategy: loop.strategy, revision: BUILT_IN_LOOP_REVISIONS[loop.strategy] ?? null };
116
+ }
117
+ return {
118
+ strategy: loop?.name ?? "single-shot",
119
+ revision: loop?.revision ?? BUILT_IN_LOOP_REVISIONS["single-shot"] ?? null,
120
+ };
121
+ }
122
+ /** `sha256:<64 hex>` over UTF-8 text — the same convention as `hashPromptBody` in `@arnilo/prism-core`. */
123
+ function hashText(text) {
124
+ if (text === undefined)
125
+ return null;
126
+ return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
127
+ }
128
+ function hashJson(value) {
129
+ return `sha256:${createHash("sha256").update(JSON.stringify(value), "utf8").digest("hex")}`;
130
+ }
131
+ /** Redacts every string field so a pinned snapshot can never carry a secret. */
132
+ function redactStrings(value, redactor) {
133
+ if (typeof value === "string")
134
+ return redactor ? redactor.redact(value) : value;
135
+ if (!value || typeof value !== "object")
136
+ return value;
137
+ if (Array.isArray(value))
138
+ return value.map((item) => redactStrings(item, redactor));
139
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactStrings(item, redactor)]));
140
+ }
141
+ function deepFreeze(value) {
142
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
143
+ Object.freeze(value);
144
+ for (const item of Object.values(value))
145
+ deepFreeze(item);
146
+ }
147
+ return value;
148
+ }
149
+ //# sourceMappingURL=run-bundle.js.map
@@ -68,7 +68,11 @@ async function checkpointCasProbe(checkpoints) {
68
68
  const loaded = await checkpoints.loadCheckpoint(key);
69
69
  assert.equal(loaded?.version, 4, "winning version must persist");
70
70
  assert.equal(loaded?.fencingToken, 6, "winning fence must persist");
71
- await assertRejects(() => checkpoints.loadCheckpoint({ ...key, tenantId: "tenant-b" }), /ownership|tenant/i, "foreign checkpoint access must fail closed");
71
+ const foreign = await checkpoints.loadCheckpoint({ ...key, tenantId: "tenant-b" });
72
+ assert.equal(foreign, null, "a foreign checkpoint lands as a miss, not an ownership-shaped existence oracle");
73
+ await assertRejectsCode(() => checkpoints.saveCheckpoint({ ...key, tenantId: "tenant-b", version: 99, expectedVersion: 0, value: { evil: true } }), "ERR_PRISM_CHECKPOINT_CONFLICT", "a foreign checkpoint save must fail closed as a CAS conflict");
74
+ const intact = await checkpoints.loadCheckpoint(key);
75
+ assert.equal(intact?.version, 4, "a foreign writer must not disturb the owner's record");
72
76
  }
73
77
  /**
74
78
  * Approval determinism: concurrent approve/deny of the same pending decision
@@ -334,15 +338,4 @@ async function assertRejectsCode(action, code, message) {
334
338
  }
335
339
  throw new Error(`${message}; expected a rejection with code ${code}`);
336
340
  }
337
- async function assertRejects(action, pattern, message) {
338
- try {
339
- await action();
340
- }
341
- catch (error) {
342
- if (pattern.test(String(error)))
343
- return;
344
- throw new Error(`${message}; rejection did not match ${pattern}: ${String(error)}`);
345
- }
346
- throw new Error(`${message}; expected a rejection matching ${pattern}`);
347
- }
348
341
  //# sourceMappingURL=state-concurrency-conformance.js.map
package/docs/ag-ui.md CHANGED
@@ -31,6 +31,7 @@ npm install @arnilo/prism @arnilo/prism-ag-ui
31
31
  | `authorize` | Rebinds untrusted AG-UI thread/run selectors to host ownership on every request. `false` returns 403. |
32
32
  | `sessionFactory` | Returns an authorized Prism `AgentSession`; it receives only host-approved `AgUiPreparedInput`, never raw client tools/state. |
33
33
  | `input.project` | Opts into full `RunAgentInput`; turns bounded, still-untrusted history, state, context, forwarded props, media, and lineage into host-selected Prism `Message` values. Omit for legacy final-text mode. |
34
+ | `inputPolicy.clientState` (`AgUiInputPolicyOptions`) | `"honor"` (default) passes validated client `state`/`tools` to `input.project`. `"ignore"` validates the same envelope for shape and bounds, then discards both fields, so input comes only from the server session and projector; an unknown value fails at construction with `ERR_PRISM_AG_UI_INPUT` instead of silently honoring the client. |
34
35
  | `input.frontendTools` | Explicitly selects client-side handoffs. Returned names must be request-tool subset; adapter never turns JSON tool declarations into Prism `ToolDefinition`s. |
35
36
  | `mcp` | Optional `createAgUiMcpAdapter({ bridge, select })`; host selects reviewed `bridge.tools`, then `sessionFactory` receives them as `input.serverTools`. Normal Prism dispatch/loop remains sole executor. |
36
37
  | `a2a` | Optional `createAgUiA2AAdapter({ client, select, correlate })`; verified remote A2A task stream replaces this handler's local session only. Host selection/correlation binds each remote task to ownership. |
@@ -44,6 +45,10 @@ npm install @arnilo/prism @arnilo/prism-ag-ui
44
45
 
45
46
  The handler accepts only `POST` JSON validated with official AG-UI `RunAgentInputSchema`. Every aggregate is bounded before a callback runs. With no `input.project`, it preserves compatibility: final text user message only; non-empty state or frontend tools fail before authorization/session lookup. With a projector, all current roles/history, context, state, forwarded props, multimodal parts, parent lineage, and tool-result continuations are available as untrusted input. The projector must apply Prism media URL/SSRF/MIME policy before forwarding media. Start a run with no `resume` and no `?cursor=`; replay supplies `?cursor=`.
46
47
 
48
+ ### Server-authoritative input (`inputPolicy.clientState: "ignore"`)
49
+
50
+ An official browser client posts its own projection: `initialState` becomes the request `state` and `runAgent({ tools })` supplies a tool list. Hosts that keep projection and tools on the server had two options — relay/rewrite every request, or reject state posts and break every browser run. `inputPolicy: { clientState: "ignore" }` is the third: the posted envelope is still schema-validated and bounded (so malformed, oversized, and poisonous payloads fail exactly as before, `400`/`413`), and then client `state` and `tools` are dropped before authorization, `coWorkContext`, `mcp.prepare`, `input.project`, and `defaultAgUiInput` see the input. Input is derived solely from the server session and the host projector, so a browser posting full state gets a normal run whose projection and tool list are the server's own. `AgUiPreparedInput.clientState` reports which policy produced the payload, `frontendTools` stays empty, and `capabilities.tools.clientProvided` is refused under `"ignore"` because the handler never hands client tools to a session. `state`, `context`, and `forwardedProps` reaching the projector under `"honor"` remain untrusted: the projector is still the only authority.
51
+
47
52
  ## Outputs / response / events
48
53
 
49
54
  The handler returns `text/event-stream`, one `data: <AG-UI event>\n\n` frame per output. Mapper lifecycle is ordered: `RUN_*`, `STEP_*`, `TEXT_MESSAGE_*`, and `TOOL_CALL_*` are deterministic Prism mappings. Host projectors may additionally prove and emit `STATE_SNAPSHOT`/`STATE_DELTA`, `MESSAGES_SNAPSHOT`, `ACTIVITY_*`, current `REASONING_*`, `RAW`, and named `CUSTOM` values.
@@ -130,6 +130,39 @@ The snapshot is stored as `loopState: { name, revision, snapshot }` on the durab
130
130
 
131
131
  A strategy returned by `generateValidateReviseLoop()` is safe to reuse across sequential runs. Its built-in state is scoped to `(sessionId, runId)`; a new non-restored run resets attempts, artifact phase, saved schema, and pending repair messages, while a restored run keeps the checkpointed state. Arbitrary custom strategies are not cloned or reset automatically.
132
132
 
133
+ ## Turn policy
134
+
135
+ `RunOptions.turnPolicy` (`TurnPolicyOptions`) lets a host end a run **cleanly** at a provider-turn boundary — after the previous turn's tool results are persisted, before the next provider request (the same point `checkpointPolicy: "every-turn"` checkpoints at). This is the "stop when the agent has done enough" seam: an investigation that should stop at the first plan paint, a desk that stops after N turns, a policy that stops once a tool budget is spent.
136
+
137
+ ```ts
138
+ await session.run("Investigate the churn spike", {
139
+ turnPolicy: {
140
+ // Clean turn cap: reaching it stops the run instead of failing it.
141
+ maxTurns: 4,
142
+ // Consulted once per boundary; a stop ends the run as `succeeded`.
143
+ stop: (ctx) =>
144
+ ctx.turns >= 1 && ctx.toolCalls >= 1
145
+ ? { action: "stop", reason: "l1-first-plan-paint" }
146
+ : { action: "continue" },
147
+ },
148
+ });
149
+ ```
150
+
151
+ | `TurnBoundaryContext` field | Meaning |
152
+ | --- | --- |
153
+ | `sessionId`, `runId` | Run correlation. |
154
+ | `turn` | 1-based index of the provider turn this boundary precedes. |
155
+ | `turns` | Provider turns already completed (`turn - 1`; `0` at the first boundary). |
156
+ | `toolCalls` | Host tool calls dispatched so far in this run. |
157
+ | `usage` | Run-total usage so far, when the provider reported any. |
158
+ | `metadata` | Run metadata (never prompt text, tool arguments, or results). |
159
+
160
+ A `stop` decision is a **clean terminal outcome**, not an error or a limit breach: the run returns `status: "succeeded"` with `stopReason: "host_policy"` and `stopDetail` (the host's `reason`, ≤256 UTF-8 bytes, redacted). The same pair rides `agent_finished.finishReason`/`stopDetail`, the finish `RunRecord`, and the projected `ExecutionTimeline`. `turnPolicy.maxTurns` is a *clean* cap: it reports `stopReason: "turn_limit"` and, unlike a `limits.maxTurns` breach, never throws `AgentRunLimitError`. A run overlay may only narrow `limits.maxTurns` — widening throws before the first provider turn.
161
+
162
+ A policy stop stays **resumable**: with `runState: { checkpointPolicy: "every-turn" }` the terminal state keeps the run frontier, so `resumeAgentRun(..., { decision: "continue" })` continues from the boundary. Steers queued before the stop are already in the session history and reach the resumed leg exactly once. A `turnPolicy.maxTurns` stop is the exception — resuming it would re-stop on the first boundary. Resumed runs carry no `turnPolicy` (resume options are not run options), so a continued leg runs to its natural end unless the host stops it again.
163
+
164
+ The callback is synchronous and bounded, and is never called when `turnPolicy` is omitted: a run without a policy keeps its exact request stream. A callback that throws or returns a malformed decision fails the run closed with `ERR_PRISM_TURN_POLICY` (the boundary makes no provider call and the checkpoint stays fail-closed); a stopped run is never recorded as failed.
165
+
133
166
  ## Outputs / response / events
134
167
 
135
168
  `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.