@arnilo/prism 0.0.3 → 0.0.4

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 +22 -0
  2. package/README.md +32 -20
  3. package/dist/agent-loops.d.ts +8 -1
  4. package/dist/agent-loops.js +57 -11
  5. package/dist/agents.js +70 -17
  6. package/dist/checkpoints.d.ts +11 -0
  7. package/dist/checkpoints.js +144 -0
  8. package/dist/compaction.js +9 -1
  9. package/dist/content.d.ts +102 -0
  10. package/dist/content.js +410 -0
  11. package/dist/contracts.d.ts +142 -2
  12. package/dist/event-multiplexer.d.ts +23 -0
  13. package/dist/event-multiplexer.js +136 -0
  14. package/dist/execution-policy.d.ts +28 -0
  15. package/dist/execution-policy.js +24 -0
  16. package/dist/index.d.ts +17 -5
  17. package/dist/index.js +11 -4
  18. package/dist/input.js +11 -1
  19. package/dist/leases.d.ts +8 -0
  20. package/dist/leases.js +111 -0
  21. package/dist/node/agent-definitions.js +3 -5
  22. package/dist/node/config.d.ts +1 -0
  23. package/dist/node/config.js +5 -3
  24. package/dist/node/contribution-discovery.js +5 -8
  25. package/dist/node/session-store-jsonl.js +8 -5
  26. package/dist/node/settings.js +2 -2
  27. package/dist/node/trust.js +2 -4
  28. package/dist/observability.d.ts +3 -0
  29. package/dist/observability.js +18 -0
  30. package/dist/providers/media.d.ts +42 -0
  31. package/dist/providers/media.js +116 -0
  32. package/dist/providers/openai-compatible.js +18 -119
  33. package/dist/providers/openai-primitives.d.ts +9 -0
  34. package/dist/providers/openai-primitives.js +129 -0
  35. package/dist/providers/transport.d.ts +40 -0
  36. package/dist/providers/transport.js +221 -0
  37. package/dist/redaction.js +40 -13
  38. package/dist/resources.d.ts +5 -0
  39. package/dist/resources.js +4 -0
  40. package/dist/structured-output.d.ts +11 -0
  41. package/dist/structured-output.js +59 -0
  42. package/dist/testing/persistence-schema.d.ts +102 -0
  43. package/dist/testing/persistence-schema.js +457 -0
  44. package/dist/testing/provider-conformance.js +10 -1
  45. package/dist/testing/run-ledger-conformance.d.ts +33 -0
  46. package/dist/testing/run-ledger-conformance.js +172 -0
  47. package/dist/testing/session-store-conformance.d.ts +16 -0
  48. package/dist/testing/session-store-conformance.js +73 -0
  49. package/dist/tools.d.ts +17 -0
  50. package/dist/tools.js +29 -2
  51. package/docs/agent-events.md +13 -4
  52. package/docs/agent-loops.md +10 -4
  53. package/docs/agent-session-runtime.md +1 -0
  54. package/docs/cli-rpc.md +3 -0
  55. package/docs/coding-agent-tools.md +41 -7
  56. package/docs/coding-security.md +84 -0
  57. package/docs/credential-storage.md +177 -0
  58. package/docs/credentials-and-redaction.md +2 -1
  59. package/docs/database-persistence.md +44 -2
  60. package/docs/host-security.md +15 -1
  61. package/docs/index.md +28 -12
  62. package/docs/input-and-prompt-assembly.md +6 -5
  63. package/docs/mcp-tools.md +139 -0
  64. package/docs/middleware-hooks.md +2 -0
  65. package/docs/migration.md +21 -28
  66. package/docs/model-registry.md +5 -3
  67. package/docs/multimodal-content.md +148 -0
  68. package/docs/observability.md +163 -0
  69. package/docs/performance.md +40 -1
  70. package/docs/persistence-credentials-multimodality-primitives.md +303 -0
  71. package/docs/postgres-persistence.md +141 -0
  72. package/docs/provider-conformance.md +17 -0
  73. package/docs/provider-layer.md +1 -1
  74. package/docs/provider-primitives.md +281 -0
  75. package/docs/providers/kimi.md +1 -0
  76. package/docs/providers/neuralwatt.md +1 -0
  77. package/docs/providers/openai-compatible.md +2 -1
  78. package/docs/providers/openai.md +8 -1
  79. package/docs/providers/opencode-go.md +1 -0
  80. package/docs/providers/openrouter.md +1 -0
  81. package/docs/providers/zai.md +1 -0
  82. package/docs/public-contracts.md +9 -2
  83. package/docs/release-and-install.md +209 -25
  84. package/docs/resource-loading.md +14 -4
  85. package/docs/review-coverage-2026-07-14.md +260 -0
  86. package/docs/run-ledger-conformance.md +96 -0
  87. package/docs/runs-and-usage.md +2 -0
  88. package/docs/session-store-conformance.md +16 -0
  89. package/docs/session-stores-and-branching.md +1 -0
  90. package/docs/settings-auth-trust-security.md +2 -1
  91. package/docs/sqlite-persistence.md +122 -0
  92. package/docs/structured-output.md +9 -0
  93. package/docs/tool-conformance.md +1 -0
  94. package/docs/tool-execution-primitives.md +374 -0
  95. package/docs/tools.md +39 -1
  96. package/docs/workflow-orchestration-primitives.md +565 -0
  97. package/docs/workflow-tui-primitives.md +5 -0
  98. package/docs/workflows.md +219 -0
  99. package/package.json +33 -5
package/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.4] - 2026-07-14
11
+
12
+ ### Added
13
+
14
+ - Shared bounded provider transport, OpenAI serialization/media helpers, native structured-output contracts, provider/tool timing metadata, and audio/file/document content capability checks.
15
+ - Generic checkpoint, atomic lease, and bounded event-multiplexer contracts plus persistence/run-ledger conformance helpers.
16
+ - Optional packages for JSON Schema tool validation, MCP, coding approval/sandboxing, OpenTelemetry, encrypted/keychain credentials, SQLite/PostgreSQL persistence, and bounded workflow orchestration.
17
+ - Manifest-only `base`, `code`, and `sdk` profiles; `prism-all` now transitively installs every first-party package.
18
+ - Workflow, multimodal, persistence/resume, provider telemetry, cache, and external-adapter examples.
19
+
20
+ ### Changed
21
+
22
+ - Single-shot loops support ordered bounded parallel tools; `ToolDefinition.exclusive` serializes dangerous turns without reducing later concurrency.
23
+ - Provider requests, SSE/error bodies, media, schemas, event queues, checkpoints, and workflow fan-out/output use documented finite limits.
24
+ - Session/ledger writes preserve order and redact before persistence; revision-loop transcript ordering and OAuth abort polling are hardened.
25
+ - All first-party providers use shared bounded transport helpers and expose current structured-output, multimodal, caching, reasoning, telemetry, and retry behavior where supported.
26
+
27
+ ### Security
28
+
29
+ - Added fail-closed schema/prototype-pollution, SSRF/media, SQL/tenant, path/shell approval, MCP result, credential-envelope, OAuth, redaction, and stale-worker fencing coverage.
30
+ - Optional privileged capabilities remain inactive until hosts explicitly register transports/tools, configure roots/credentials/databases, and approve execution.
31
+
10
32
  ## [0.0.3] - 2026-07-08
11
33
 
12
34
  ### Added
package/README.md CHANGED
@@ -17,10 +17,9 @@ packages. Prism defines contracts, not apps.
17
17
  OpenAI/OpenRouter use best-effort explicit cache hints, NeuralWatt uses
18
18
  best-effort implicit prefix caching, and other providers have route/model-specific
19
19
  or no cache-control support; see [docs/provider-caching.md](docs/provider-caching.md).
20
- - **First-party packages**: `@arnilo/prism-provider-openai`, `@arnilo/prism-provider-opencode-go`,
21
- `@arnilo/prism-provider-openrouter`, `@arnilo/prism-provider-zai`, `@arnilo/prism-provider-kimi`,
22
- `@arnilo/prism-provider-neuralwatt`, `@arnilo/prism-compaction-llm`, and
23
- `@arnilo/prism-compaction-observational-memory`.
20
+ - **First-party packages**: six provider adapters, two compaction strategies,
21
+ coding tools/security, JSON Schema validation, MCP, workflows, OpenTelemetry,
22
+ encrypted credentials, SQLite/PostgreSQL persistence, and manifest-only install profiles.
24
23
  - **Tools, context, skills**: host-owned tool registry with allow/deny filtering
25
24
  and dispatch, context providers, and a skill registry with progressive
26
25
  disclosure.
@@ -41,14 +40,16 @@ packages. Prism defines contracts, not apps.
41
40
  npm install @arnilo/prism
42
41
  ```
43
42
 
44
- First-party provider/compaction packages are separate installs and require
45
- `@arnilo/prism` as a non-optional peer. Install a single package, or use an
46
- umbrella to grab a whole family in one line:
43
+ First-party code packages are separate imports and require `@arnilo/prism` as
44
+ a non-optional peer. Install atomic packages directly or choose a manifest-only
45
+ family/profile; profiles install packages but expose no alias exports and activate nothing:
47
46
 
48
47
  ```bash
49
- npm install @arnilo/prism @arnilo/prism-provider-openai # core + one provider
50
- npm install @arnilo/prism @arnilo/prism-providers # core + all providers
51
- npm install @arnilo/prism-all # everything
48
+ npm install @arnilo/prism @arnilo/prism-provider-openai # core + one provider
49
+ npm install @arnilo/prism-base # core + compaction + validation
50
+ npm install @arnilo/prism-code @arnilo/prism-provider-openai # coding-agent profile
51
+ npm install @arnilo/prism-sdk @arnilo/prism-provider-openai # application profile
52
+ npm install @arnilo/prism-all # every first-party package
52
53
  ```
53
54
 
54
55
  See [docs/release-and-install.md](docs/release-and-install.md) for install
@@ -111,8 +112,8 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
111
112
 
112
113
  - [docs/index.md](docs/index.md) — navigational map of every public surface.
113
114
  - The `examples/` directory holds compile-checked typed examples and runnable
114
- mock demos (provider registration, auth, tools, stores/branching, compaction,
115
- observational-memory recall, CLI, RPC).
115
+ offline demos covering providers, auth, tools, stores, compaction, structured
116
+ output, multimodality, workflows, CLI, and RPC.
116
117
 
117
118
  ## Packages
118
119
 
@@ -127,9 +128,21 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
127
128
  | `@arnilo/prism-provider-neuralwatt` | NeuralWatt provider with implicit vLLM prefix caching |
128
129
  | `@arnilo/prism-compaction-llm` | provider-backed compaction strategy |
129
130
  | `@arnilo/prism-compaction-observational-memory` | source-backed memory + recall tool |
130
- | `@arnilo/prism-providers` | umbrella: all 6 provider adapters |
131
- | `@arnilo/prism-compaction` | umbrella: both compaction strategies |
132
- | `@arnilo/prism-all` | umbrella: core + providers + compaction |
131
+ | `@arnilo/prism-coding-agent` | bounded shell/read/write/edit tools |
132
+ | `@arnilo/prism-coding-security` | coding approval, containment, and sandbox adapters |
133
+ | `@arnilo/prism-tool-validator-json-schema` | bounded JSON Schema tool validation |
134
+ | `@arnilo/prism-mcp` | MCP client/tool bridge |
135
+ | `@arnilo/prism-workflows` | bounded DAG workflows and durable coordination |
136
+ | `@arnilo/prism-observability-opentelemetry` | optional OpenTelemetry adapter |
137
+ | `@arnilo/prism-credentials-node` | encrypted-file and keychain credentials |
138
+ | `@arnilo/prism-session-store-sqlite` | SQLite persistence/checkpoints/leases |
139
+ | `@arnilo/prism-session-store-postgres` | PostgreSQL persistence/checkpoints/leases |
140
+ | `@arnilo/prism-providers` | family: all 6 provider adapters |
141
+ | `@arnilo/prism-compaction` | family: both compaction strategies |
142
+ | `@arnilo/prism-base` | profile: core + compaction + JSON Schema validation |
143
+ | `@arnilo/prism-code` | profile: base + coding tools/security + MCP |
144
+ | `@arnilo/prism-sdk` | profile: base + workflows + MCP + credentials + OpenTelemetry |
145
+ | `@arnilo/prism-all` | every first-party package, including both persistence adapters |
133
146
 
134
147
  ## Scripts
135
148
 
@@ -142,8 +155,7 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
142
155
 
143
156
  ## Non-goals (v1)
144
157
 
145
- - Built-in shell/filesystem/browser tools — ship as separate packages only.
146
- - MCP bridge — external extension package after extension APIs settle.
147
- - TUI or interactive terminal — CLI/RPC only in core.
148
- - Workflow/graph orchestration — bounded agent loops first.
149
- - Encrypted/keychain credential storage — host-owned until a real app needs it.
158
+ - Privileged tools, MCP servers, telemetry, credentials, or databases activated by install — hosts explicitly configure and register every capability.
159
+ - Browser automation or interactive terminal UI — CLI/RPC and workflow control APIs only.
160
+ - Provider, credential, extension, or package auto-discovery.
161
+ - Core-owned database drivers, secret persistence, sandbox, or application policy — optional packages implement adapters over host-owned boundaries.
@@ -1,4 +1,4 @@
1
- import type { AgentLoopOptions, AgentLoopStrategy, ArtifactParser, ArtifactRepairer, ArtifactValidator } from "./contracts.js";
1
+ import type { AgentLoopOptions, AgentLoopStrategy, ArtifactParser, ArtifactRepairer, ArtifactValidator, LoopContext, ToolCallContent } from "./contracts.js";
2
2
  export declare const singleShotLoop: AgentLoopStrategy;
3
3
  export declare function isAgentLoopOptions(value: AgentLoopStrategy | AgentLoopOptions | undefined): value is AgentLoopOptions;
4
4
  export declare function generateValidateReviseLoop(opts: {
@@ -7,6 +7,13 @@ export declare function generateValidateReviseLoop(opts: {
7
7
  readonly repairer?: ArtifactRepairer<unknown>;
8
8
  readonly maxRevisions?: number;
9
9
  }): AgentLoopStrategy;
10
+ export declare function resolveToolConcurrency(options: {
11
+ loop?: AgentLoopStrategy | AgentLoopOptions;
12
+ }, config: {
13
+ loop?: AgentLoopStrategy | AgentLoopOptions;
14
+ }): number;
15
+ /** Dispatch tool calls with bounded concurrency; append transcript rows in call order. */
16
+ export declare function dispatchToolCallsInOrder(calls: readonly ToolCallContent[], ctx: LoopContext): Promise<void>;
10
17
  export declare function resolveLoop(options: {
11
18
  loop?: AgentLoopStrategy | AgentLoopOptions;
12
19
  }, config: {
@@ -18,13 +18,12 @@ export const singleShotLoop = {
18
18
  name: "single-shot",
19
19
  async run(ctx) {
20
20
  let usage;
21
- const toolResults = [];
22
21
  let toolRounds = 0;
23
22
  let nextInput = ctx.input;
24
23
  for (let turn = 1;; turn += 1) {
25
24
  throwIfAborted(ctx.signal);
26
25
  ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
27
- const request = await ctx.assemble(nextInput, toolResults, turn);
26
+ const request = await ctx.assemble(nextInput, undefined, turn);
28
27
  throwIfAborted(ctx.signal);
29
28
  const { content, calls, messageId, started, usage: turnUsage } = await ctx.generate(request);
30
29
  usage = turnUsage ?? usage;
@@ -40,12 +39,7 @@ export const singleShotLoop = {
40
39
  if (calls.length === 0 || toolRounds >= ctx.maxToolRounds)
41
40
  break;
42
41
  toolRounds += 1;
43
- for (const call of calls) {
44
- const result = await ctx.dispatchToolCall(call);
45
- toolResults.push(result);
46
- await ctx.appendMessage(toolResultMessage(result));
47
- throwIfAborted(ctx.signal);
48
- }
42
+ await dispatchToolCallsInOrder(calls, ctx);
49
43
  nextInput = [];
50
44
  }
51
45
  return usage;
@@ -73,6 +67,7 @@ export function generateValidateReviseLoop(opts) {
73
67
  async run(ctx) {
74
68
  let usage;
75
69
  let nextInput = ctx.input;
70
+ let pendingHistory = [];
76
71
  for (let turn = 1; turn <= max + 1; turn += 1) {
77
72
  throwIfAborted(ctx.signal);
78
73
  ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
@@ -80,6 +75,10 @@ export function generateValidateReviseLoop(opts) {
80
75
  throwIfAborted(ctx.signal);
81
76
  const { content, messageId, started, usage: turnUsage } = await ctx.generate(request);
82
77
  usage = turnUsage ?? usage;
78
+ if (pendingHistory.length > 0) {
79
+ ctx.history.push(...pendingHistory);
80
+ pendingHistory = [];
81
+ }
83
82
  if (turn === 1)
84
83
  ctx.history.push(...ctx.inputMessages);
85
84
  if (started) {
@@ -121,10 +120,9 @@ export function generateValidateReviseLoop(opts) {
121
120
  ctx.emit({ type: "artifact_revision_started", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt, failure: result });
122
121
  const repair = await repairer(parsed.value, result, artifactCtx);
123
122
  const repairMessages = inputMessages(repair).map((m) => ({ ...m, id: randomId("msg") }));
124
- for (const message of repairMessages) {
125
- ctx.history.push(message);
123
+ for (const message of repairMessages)
126
124
  await ctx.appendMessage(message);
127
- }
125
+ pendingHistory = repairMessages;
128
126
  nextInput = repairMessages;
129
127
  continue;
130
128
  }
@@ -135,6 +133,54 @@ export function generateValidateReviseLoop(opts) {
135
133
  // ponytail: resolveLoop returns the loop to run. RunOptions.loop wins over
136
134
  // AgentConfig.loop; default is singleShotLoop. generate-validate-revise maps
137
135
  // its options to the factory; unknown strategy throws.
136
+ export function resolveToolConcurrency(options, config) {
137
+ const loop = options.loop ?? config.loop;
138
+ if (!loop || !isAgentLoopOptions(loop) || loop.strategy !== "single-shot")
139
+ return 1;
140
+ const value = loop.toolConcurrency;
141
+ if (value === undefined)
142
+ return 1;
143
+ if (!Number.isFinite(value) || value < 1)
144
+ return 1;
145
+ return Math.floor(value);
146
+ }
147
+ /** Dispatch tool calls with bounded concurrency; append transcript rows in call order. */
148
+ export async function dispatchToolCallsInOrder(calls, ctx) {
149
+ if (calls.length === 0)
150
+ return;
151
+ const concurrency = calls.some((call) => ctx.isToolCallExclusive?.(call))
152
+ ? 1
153
+ : Math.max(1, Math.min(ctx.toolConcurrency, calls.length));
154
+ if (concurrency === 1) {
155
+ for (const call of calls) {
156
+ const result = await ctx.dispatchToolCall(call);
157
+ await appendToolResultMessage(result, ctx);
158
+ }
159
+ return;
160
+ }
161
+ const results = new Array(calls.length);
162
+ let nextIndex = 0;
163
+ const workers = Array.from({ length: concurrency }, async () => {
164
+ for (;;) {
165
+ throwIfAborted(ctx.signal);
166
+ const index = nextIndex;
167
+ nextIndex += 1;
168
+ if (index >= calls.length)
169
+ return;
170
+ results[index] = await ctx.dispatchToolCall(calls[index]);
171
+ }
172
+ });
173
+ await Promise.all(workers);
174
+ for (const result of results) {
175
+ throwIfAborted(ctx.signal);
176
+ await appendToolResultMessage(result, ctx);
177
+ }
178
+ }
179
+ async function appendToolResultMessage(result, ctx) {
180
+ const message = toolResultMessage(result);
181
+ ctx.history.push(message);
182
+ await ctx.appendMessage(message);
183
+ }
138
184
  export function resolveLoop(options, config) {
139
185
  const loop = options.loop ?? config.loop;
140
186
  if (!loop)
package/dist/agents.js CHANGED
@@ -1,8 +1,10 @@
1
- import { resolveLoop } from "./agent-loops.js";
1
+ import { resolveLoop, resolveToolConcurrency } from "./agent-loops.js";
2
+ import { createProviderTurnMetadata, readProviderHttpStatus } from "./observability.js";
2
3
  import { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
3
4
  import { assembleProviderInput } from "./input.js";
4
5
  import { providerToolCallDeltaContent, reconstructToolCallDeltas } from "./provider-events.js";
5
- import { createProviderRequestPolicyChain, mergeProviderRequestOptions, normalizeProviderRequestPolicyResult } from "./provider-request-policy.js";
6
+ import { createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult } from "./provider-request-policy.js";
7
+ import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } from "./structured-output.js";
6
8
  import { errorToErrorInfo, redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "./redaction.js";
7
9
  import { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
8
10
  import { createDefaultRetryPolicy, waitForRetry } from "./retry.js";
@@ -34,7 +36,9 @@ class RuntimeAgentSession {
34
36
  activeLedger;
35
37
  activeOwnership;
36
38
  activeIdempotencyKey;
37
- ledgerPromises = [];
39
+ activeLoopTurn = 1;
40
+ ledgerChain = Promise.resolve();
41
+ ledgerFailure;
38
42
  constructor(config) {
39
43
  this.id = config.id ?? randomId("session");
40
44
  this.agent = config.agent;
@@ -101,12 +105,15 @@ class RuntimeAgentSession {
101
105
  // ponytail: skill context after host context; no per-skill token budget yet.
102
106
  ...activeSkills.flatMap((skill) => skill.context ?? []),
103
107
  ];
104
- const providerOptions = mergeProviderRequestOptions(this.agent.config.providerOptions, options.providerOptions);
108
+ const providerOptions = resolveRunProviderOptions(options, this.agent.config);
109
+ assertStructuredOutputRequestSupported(options.model ?? this.agent.config.model, providerOptions);
105
110
  const validate = options.validate ?? this.agent.config.validator;
106
111
  // ponytail: RunOptions.instructionInjectors overrides AgentConfig.instructionInjectors (mirrors validate/loop).
107
112
  const instructionInjectors = options.instructionInjectors ?? this.agent.config.instructionInjectors ?? [];
108
113
  const inputLayout = options.inputLayout ?? this.agent.config.inputLayout;
109
114
  const loop = resolveLoop(options, this.agent.config);
115
+ const toolConcurrency = resolveToolConcurrency(options, this.agent.config);
116
+ this.activeLoopTurn = 1;
110
117
  // ponytail: LoopContext binds existing private helpers; loop orchestrates only.
111
118
  const ctx = {
112
119
  sessionId: this.id,
@@ -117,6 +124,7 @@ class RuntimeAgentSession {
117
124
  input,
118
125
  inputMessages,
119
126
  maxToolRounds,
127
+ toolConcurrency,
120
128
  assemble: async (nextInput, toolResults, turn) => assembleProviderInput({
121
129
  model: options.model ?? this.agent.config.model,
122
130
  input: nextInput,
@@ -144,8 +152,9 @@ class RuntimeAgentSession {
144
152
  generate: async (request) => {
145
153
  const policyResult = await this.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
146
154
  const middlewareRequest = await this.agent.config.middleware?.run("provider_request", policyResult.request) ?? policyResult.request;
147
- return this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets);
155
+ return this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn);
148
156
  },
157
+ isToolCallExclusive: (call) => registry.get(call.name)?.exclusive === true,
149
158
  dispatchToolCall: (call) => dispatchToolCall({
150
159
  call,
151
160
  registry,
@@ -160,7 +169,11 @@ class RuntimeAgentSession {
160
169
  validate,
161
170
  }),
162
171
  appendMessage: (message) => this.appendMessage(message, runId),
163
- emit: (event) => this.emit(event),
172
+ emit: (event) => {
173
+ if (event.type === "turn_started")
174
+ this.activeLoopTurn = event.turn;
175
+ this.emit(event);
176
+ },
164
177
  };
165
178
  const usage = await loop.run(ctx);
166
179
  if (usage && this.activeLedger) {
@@ -301,7 +314,17 @@ class RuntimeAgentSession {
301
314
  redacted: Boolean(this.activeRedactor),
302
315
  ...this.activeOwnership,
303
316
  };
304
- this.ledgerPromises.push(this.activeLedger.appendEvent(redactRunLedgerRecord(record, this.activeRedactor)));
317
+ const ledger = this.activeLedger;
318
+ this.ledgerChain = this.ledgerChain.then(async () => {
319
+ if (this.ledgerFailure)
320
+ return;
321
+ try {
322
+ await ledger.appendEvent(redactRunLedgerRecord(record, this.activeRedactor));
323
+ }
324
+ catch (error) {
325
+ this.ledgerFailure = error;
326
+ }
327
+ });
305
328
  }
306
329
  }
307
330
  closeSubscribers() {
@@ -310,19 +333,20 @@ class RuntimeAgentSession {
310
333
  this.subscribers.clear();
311
334
  }
312
335
  async drainLedger() {
313
- if (this.ledgerPromises.length === 0)
314
- return;
315
- const pending = this.ledgerPromises;
316
- this.ledgerPromises = [];
317
- await Promise.all(pending);
318
- }
319
- async generateWithRetry(request, runId, options, signal, requestSecrets = []) {
336
+ await this.ledgerChain;
337
+ const failure = this.ledgerFailure;
338
+ this.ledgerChain = Promise.resolve();
339
+ this.ledgerFailure = undefined;
340
+ if (failure)
341
+ throw failure;
342
+ }
343
+ async generateWithRetry(request, runId, options, signal, requestSecrets = [], turn = 1) {
320
344
  const retry = mergeRetry(this.agent.config.retry, options.retry);
321
345
  const secrets = [...requestSecrets, ...(retry?.secrets ?? [])];
322
346
  const policy = retry?.policy ?? (retry ? createDefaultRetryPolicy(retry) : undefined);
323
347
  for (let attempt = 1;; attempt += 1) {
324
348
  try {
325
- return await this.generateProviderTurn(request, runId, signal, secrets);
349
+ return await this.generateProviderTurn(request, runId, signal, secrets, turn, attempt);
326
350
  }
327
351
  catch (error) {
328
352
  const failure = error instanceof ProviderTurnFailure ? error : undefined;
@@ -341,7 +365,17 @@ class RuntimeAgentSession {
341
365
  }
342
366
  }
343
367
  }
344
- async generateProviderTurn(request, runId, signal, secrets = []) {
368
+ async generateProviderTurn(request, runId, signal, secrets = [], turn = 1, attempt = 1) {
369
+ const startedAt = performance.now();
370
+ const providerId = this.activeProvider?.id ?? request.model.provider;
371
+ const buildMetadata = (extra = {}) => createProviderTurnMetadata(request, providerId, { attempt, ...extra });
372
+ this.emit({
373
+ type: "provider_turn_started",
374
+ sessionId: this.id,
375
+ runId,
376
+ turn,
377
+ metadata: buildMetadata(),
378
+ });
345
379
  const content = [];
346
380
  const calls = [];
347
381
  const toolDeltas = [];
@@ -399,12 +433,31 @@ class RuntimeAgentSession {
399
433
  calls.push(call);
400
434
  this.emit({ type: "message_delta", sessionId: this.id, runId, content: call });
401
435
  }
436
+ const latencyMs = Math.round(performance.now() - startedAt);
437
+ this.emit({
438
+ type: "provider_turn_finished",
439
+ sessionId: this.id,
440
+ runId,
441
+ turn,
442
+ metadata: buildMetadata({ latencyMs }),
443
+ usage,
444
+ });
402
445
  return { content, calls, messageId, started, usage };
403
446
  }
404
447
  catch (error) {
448
+ const latencyMs = Math.round(performance.now() - startedAt);
449
+ const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : errorToErrorInfo(error, secrets);
450
+ this.emit({
451
+ type: "provider_turn_finished",
452
+ sessionId: this.id,
453
+ runId,
454
+ turn,
455
+ metadata: buildMetadata({ latencyMs, httpStatus: readProviderHttpStatus(info) }),
456
+ error: info,
457
+ });
405
458
  if (error instanceof ProviderTurnFailure)
406
459
  throw error;
407
- throw new ProviderTurnFailure(errorToErrorInfo(error, secrets), started);
460
+ throw new ProviderTurnFailure(info, started);
408
461
  }
409
462
  }
410
463
  async applyProviderRequestPolicies(request, runId, options, metadata, signal) {
@@ -0,0 +1,11 @@
1
+ import type { CheckpointStore } from "./contracts.js";
2
+ export declare const CHECKPOINT_CONFLICT_CODE = "ERR_PRISM_CHECKPOINT_CONFLICT";
3
+ export declare class CheckpointConflictError extends Error {
4
+ readonly code = "ERR_PRISM_CHECKPOINT_CONFLICT";
5
+ constructor(message: string);
6
+ }
7
+ export interface MemoryCheckpointStoreOptions {
8
+ readonly maxPageSize?: number;
9
+ }
10
+ /** In-process reference implementation of the generic checkpoint contract. */
11
+ export declare function createMemoryCheckpointStore(options?: MemoryCheckpointStoreOptions): CheckpointStore;
@@ -0,0 +1,144 @@
1
+ export const CHECKPOINT_CONFLICT_CODE = "ERR_PRISM_CHECKPOINT_CONFLICT";
2
+ export class CheckpointConflictError extends Error {
3
+ code = CHECKPOINT_CONFLICT_CODE;
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "CheckpointConflictError";
7
+ }
8
+ }
9
+ /** In-process reference implementation of the generic checkpoint contract. */
10
+ export function createMemoryCheckpointStore(options = {}) {
11
+ const records = new Map();
12
+ const maxPageSize = Math.max(1, options.maxPageSize ?? 500);
13
+ return {
14
+ async saveCheckpoint(input) {
15
+ throwIfAborted(input.signal);
16
+ assertKey(input);
17
+ if (!Number.isSafeInteger(input.version) || input.version < 1) {
18
+ throw new CheckpointConflictError("Checkpoint version must be a positive safe integer");
19
+ }
20
+ const id = recordKey(input);
21
+ const existing = records.get(id);
22
+ if (existing)
23
+ assertOwnership(input, existing);
24
+ if (input.expectedVersion !== undefined && input.expectedVersion !== (existing?.version ?? 0)) {
25
+ throw new CheckpointConflictError(`Checkpoint compare-and-swap failed (expected ${input.expectedVersion}, current ${existing?.version ?? 0})`);
26
+ }
27
+ if (existing && input.version <= existing.version) {
28
+ throw new CheckpointConflictError(`Stale checkpoint version ${input.version} (current ${existing.version})`);
29
+ }
30
+ if (existing?.fencingToken !== undefined && (input.fencingToken === undefined || input.fencingToken < existing.fencingToken)) {
31
+ throw new CheckpointConflictError(`Stale checkpoint fencing token ${input.fencingToken ?? "missing"} (current ${existing.fencingToken})`);
32
+ }
33
+ const now = new Date().toISOString();
34
+ const value = cloneJson(input.value, "Checkpoint value");
35
+ const metadata = input.metadata === undefined ? undefined : cloneJson(input.metadata, "Checkpoint metadata");
36
+ const record = {
37
+ namespace: input.namespace,
38
+ key: input.key,
39
+ version: input.version,
40
+ ...(input.fencingToken === undefined ? {} : { fencingToken: input.fencingToken }),
41
+ value,
42
+ ...(input.category === undefined ? {} : { category: input.category }),
43
+ ...ownership(input),
44
+ createdAt: existing?.createdAt ?? now,
45
+ updatedAt: now,
46
+ ...(metadata === undefined ? {} : { metadata }),
47
+ };
48
+ records.set(id, record);
49
+ return record;
50
+ },
51
+ async loadCheckpoint(input) {
52
+ throwIfAborted(input.signal);
53
+ assertKey(input);
54
+ const record = records.get(recordKey(input));
55
+ if (!record)
56
+ return null;
57
+ assertOwnership(input, record);
58
+ return record;
59
+ },
60
+ async listCheckpoints(query = {}) {
61
+ throwIfAborted(query.signal);
62
+ const offset = decodeCursor(query.cursor);
63
+ const limit = Math.min(Math.max(1, query.limit ?? 100), maxPageSize);
64
+ const categories = query.category === undefined
65
+ ? undefined
66
+ : new Set(Array.isArray(query.category) ? query.category : [query.category]);
67
+ const items = [...records.values()]
68
+ .filter((record) => query.namespace === undefined || record.namespace === query.namespace)
69
+ .filter((record) => query.keyPrefix === undefined || record.key.startsWith(query.keyPrefix))
70
+ .filter((record) => categories === undefined || (record.category !== undefined && categories.has(record.category)))
71
+ .filter((record) => ownershipFilterMatches(query, record))
72
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.key.localeCompare(b.key));
73
+ const page = items.slice(offset, offset + limit);
74
+ const next = offset + page.length;
75
+ return {
76
+ items: page,
77
+ ...(next < items.length ? { nextCursor: String(next) } : {}),
78
+ };
79
+ },
80
+ async deleteCheckpoint(input) {
81
+ throwIfAborted(input.signal);
82
+ assertKey(input);
83
+ const id = recordKey(input);
84
+ const record = records.get(id);
85
+ if (!record)
86
+ return false;
87
+ assertOwnership(input, record);
88
+ return records.delete(id);
89
+ },
90
+ };
91
+ }
92
+ function recordKey(input) {
93
+ return `${input.namespace}\0${input.key}`;
94
+ }
95
+ function assertKey(input) {
96
+ if (!input.namespace || !input.key)
97
+ throw new CheckpointConflictError("Checkpoint namespace and key are required");
98
+ }
99
+ function ownership(input) {
100
+ return {
101
+ ...(input.tenantId === undefined ? {} : { tenantId: input.tenantId }),
102
+ ...(input.accountId === undefined ? {} : { accountId: input.accountId }),
103
+ ...(input.userId === undefined ? {} : { userId: input.userId }),
104
+ };
105
+ }
106
+ function ownershipMatches(expected, actual) {
107
+ return expected.tenantId === actual.tenantId
108
+ && expected.accountId === actual.accountId
109
+ && expected.userId === actual.userId;
110
+ }
111
+ function ownershipFilterMatches(expected, actual) {
112
+ return (expected.tenantId === undefined || expected.tenantId === actual.tenantId)
113
+ && (expected.accountId === undefined || expected.accountId === actual.accountId)
114
+ && (expected.userId === undefined || expected.userId === actual.userId);
115
+ }
116
+ function assertOwnership(expected, actual) {
117
+ if (!ownershipMatches(expected, actual)) {
118
+ throw new CheckpointConflictError("Checkpoint ownership mismatch");
119
+ }
120
+ }
121
+ function cloneJson(value, label) {
122
+ try {
123
+ const encoded = JSON.stringify(value);
124
+ if (encoded === undefined)
125
+ throw new Error("not JSON serializable");
126
+ return JSON.parse(encoded);
127
+ }
128
+ catch (error) {
129
+ throw new CheckpointConflictError(`${label} must be JSON serializable: ${error instanceof Error ? error.message : String(error)}`);
130
+ }
131
+ }
132
+ function decodeCursor(cursor) {
133
+ if (cursor === undefined)
134
+ return 0;
135
+ const value = Number(cursor);
136
+ if (!Number.isSafeInteger(value) || value < 0)
137
+ throw new CheckpointConflictError("Invalid checkpoint cursor");
138
+ return value;
139
+ }
140
+ function throwIfAborted(signal) {
141
+ if (signal?.aborted)
142
+ throw signal.reason ?? new DOMException("Aborted", "AbortError");
143
+ }
144
+ //# sourceMappingURL=checkpoints.js.map
@@ -59,7 +59,15 @@ function blockText(block) {
59
59
  return `[tool_call_delta ${block.name ?? block.index}]`;
60
60
  if (block.type === "tool_result")
61
61
  return `[tool_result ${block.name}]`;
62
- return "[image]";
62
+ if (block.type === "image")
63
+ return "[image]";
64
+ if (block.type === "audio")
65
+ return "[audio]";
66
+ if (block.type === "file")
67
+ return "[file]";
68
+ if (block.type === "document")
69
+ return "[document]";
70
+ return "[content]";
63
71
  }
64
72
  function truncate(text, max) {
65
73
  if (max === 0 || text.length <= max)