@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
@@ -0,0 +1,139 @@
1
+ # MCP client bridge
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-mcp` connects Prism hosts to remote [Model Context Protocol](https://modelcontextprotocol.io) servers and maps discovered tools to ordinary `ToolDefinition`s. Transports are stdio subprocesses and Streamable HTTP. The package wraps the official MCP TypeScript SDK (`@modelcontextprotocol/sdk` v1.29+) and does **not** add MCP-specific branches to core Prism.
6
+
7
+ Primary API:
8
+
9
+ ```ts
10
+ import { connectMcpTools } from "@arnilo/prism-mcp";
11
+
12
+ const bridge = await connectMcpTools({
13
+ serverId: "fs",
14
+ transport: { type: "stdio", command: "node", args: ["server.js"] },
15
+ });
16
+
17
+ // bridge.tools are ToolDefinition[] — register with createToolRegistry / createAgent
18
+ await bridge.refresh(); // re-list after notifications or TTL expiry
19
+ await bridge.close(); // close client + transport
20
+ ```
21
+
22
+ Advanced hosts that manage their own `Client` + `Transport` can call `attachMcpToolBridge(client, transport, options)` after `client.connect(transport)`.
23
+
24
+ ## When to use it
25
+
26
+ - **Integrate external MCP tool servers** (filesystem, databases, SaaS adapters) without reimplementing JSON-RPC transports in your app.
27
+ - **Keep core dispatch gates** — register returned tools and let `dispatchToolCall` enforce permission, JSON Schema validation (`ToolValidator`), middleware, abort, and parallel execution (Plan 055 Tasks 1–2).
28
+ - **Explicit lifecycle** — connect, refresh on `notifications/tools/list_changed`, and `close()` when the session ends.
29
+
30
+ Do **not** use this package as a sandbox, permission engine, or auto-discovery loader. Hosts must trust configured commands/URLs and gate registration.
31
+
32
+ ## Inputs / request
33
+
34
+ `connectMcpTools()` requires a stable `serverId` plus an explicit stdio or Streamable HTTP transport. Optional bounds control list caching, call timeout, result bytes, name prefix, and abort behavior; defaults are listed below.
35
+
36
+ ## Outputs / response / events
37
+
38
+ The resolved `McpToolBridge` exposes `tools`, `refresh()`, and `close()`. Each discovered MCP tool becomes a normal Prism `ToolDefinition`; calls return `ToolResult`, with remote `isError` mapped to `ToolResult.error`. List-change notifications invalidate the cache but register nothing automatically.
39
+
40
+ ## Request/response example
41
+
42
+ ```json
43
+ {
44
+ "request": { "serverId": "docs", "transport": { "type": "stdio", "command": "node", "args": ["server.js"] } },
45
+ "mappedTool": { "name": "mcp:docs:search", "parameters": { "type": "object" } }
46
+ }
47
+ ```
48
+
49
+ ## Implementation example
50
+
51
+ ```ts
52
+ import { createToolRegistry } from "@arnilo/prism";
53
+ import { connectMcpTools } from "@arnilo/prism-mcp";
54
+
55
+ const bridge = await connectMcpTools({
56
+ serverId: "docs",
57
+ transport: { type: "stdio", command: "node", args: ["server.js"] },
58
+ callTimeoutMs: 30_000,
59
+ });
60
+ const registry = createToolRegistry({ duplicate: "error" });
61
+ for (const tool of bridge.tools) registry.register(tool);
62
+ ```
63
+
64
+ ## Tool naming and mapping
65
+
66
+ | MCP | Prism |
67
+ | --- | --- |
68
+ | `tools/list` `inputSchema` | `ToolDefinition.parameters` |
69
+ | `tools/call` arguments | Parsed `ToolCallContent.arguments` |
70
+ | `tools/call` content blocks | `ToolResult.content` (`text`, `image`; resource/audio/link → descriptive `text`) |
71
+ | Tool `name` | Prefixed `mcp:<serverId>:<name>` (override with `namePrefix`) |
72
+ | `isError` results | `ToolResult.error` with summarized text |
73
+ | `structuredContent` | `ToolResult.value` / metadata |
74
+
75
+ Duplicate prefixed names throw `McpToolNameCollisionError` at refresh time.
76
+
77
+ ## Extension and configuration notes
78
+
79
+ | Option | Default | Purpose |
80
+ | --- | --- | --- |
81
+ | `serverId` | required | Stable identifier used in default name prefix |
82
+ | `transport` | required | `stdio` or `streamable-http` config |
83
+ | `namePrefix` | `mcp:<serverId>:` | Registry namespace for remote tools |
84
+ | `listCacheTtlMs` | `30000` | Skip re-listing until TTL expires (invalidated on list-changed) |
85
+ | `callTimeoutMs` | `60000` | Per-call MCP request timeout |
86
+ | `maxResultBytes` | `10000000` | Bound mapped result content |
87
+ | `signal` | none | Abort connect and trigger close on abort |
88
+
89
+ ### Stdio transport
90
+
91
+ ```ts
92
+ {
93
+ type: "stdio",
94
+ command: "node",
95
+ args: ["path/to/server.js"],
96
+ env?: Record<string, string>,
97
+ cwd?: string,
98
+ stderr?: "inherit" | "pipe" | "ignore" | "overlapped",
99
+ }
100
+ ```
101
+
102
+ The host explicitly chooses the executable, arguments, environment, and working directory. Prism does not search `PATH` for unknown servers or inject credentials.
103
+
104
+ ### Streamable HTTP transport
105
+
106
+ ```ts
107
+ {
108
+ type: "streamable-http",
109
+ url: "https://mcp.example.com/mcp",
110
+ requestInit?: RequestInit,
111
+ sessionId?: string,
112
+ }
113
+ ```
114
+
115
+ Only `http:` and `https:` URLs are accepted. Authentication (Bearer tokens, cookies) is supplied through `requestInit.headers` by the host.
116
+
117
+ ## Security and performance notes
118
+
119
+ | Risk | Mitigation |
120
+ | --- | --- |
121
+ | Untrusted subprocess (stdio) | Explicit `command` / `args` / `env` / `cwd`; review before deploy |
122
+ | SSRF / open redirects (HTTP) | Host URL allow-lists, network policy, no implicit discovery |
123
+ | Tool-name shadowing | Prefixed names + `createToolRegistry({ duplicate: "error" })` |
124
+ | Oversized server output | `maxResultBytes` on content mapping |
125
+ | Unvalidated arguments | Register tools with `createJsonSchemaToolArgumentValidator()` at dispatch |
126
+ | Missing permission gate | `PermissionPolicy` on `tool:mcp:<serverId>:<name>:execute` (or broader deny rules) |
127
+
128
+ MCP output is untrusted. Apply `SecretRedactor` and host logging policy to `ToolResult` before persisting or displaying.
129
+
130
+ ## Related APIs
131
+
132
+ - [Tools](tools.md): registry, dispatch, validation
133
+ - [Tool execution primitives](tool-execution-primitives.md): Plan 055 design and conformance matrix
134
+ - [Host security guide](host-security.md): permission, trust, validation checklist
135
+ - Package README: [`@arnilo/prism-mcp`](../packages/mcp/README.md)
136
+
137
+ ## Testing
138
+
139
+ Package tests use in-memory MCP transports (no network). Hosts should integration-test their configured stdio commands and HTTP endpoints in staging before production registration.
@@ -101,6 +101,7 @@ export const extension: Extension = {
101
101
  - `retry` middleware may stop retrying or adjust delay, but runtime still owns retry event emission, abort-aware waiting, and provider-turn boundaries.
102
102
  - The registry does not discover packages, read manifests, load config, call providers, execute tools, read resources, or start sessions.
103
103
  - Hosts may pass a middleware registry into `createExtensionKernel({ middleware })` to share it with direct host code.
104
+ - For OpenTelemetry export, prefer `session.subscribe()` + `@arnilo/prism-observability-opentelemetry` (see [Observability](observability.md)) rather than adding a parallel event bus. Middleware hooks remain for transforming payloads at named boundaries.
104
105
 
105
106
  ## Security and performance notes
106
107
 
@@ -118,6 +119,7 @@ export const extension: Extension = {
118
119
  - [Input and prompt assembly](input-and-prompt-assembly.md): `input_assembly` and `prompt_build` helper call sites.
119
120
  - [Compaction and retry policies](compaction-and-retry.md): compaction/retry middleware payloads and runtime timing.
120
121
  - [Context and skills](context-and-skills.md): `context` helper call site.
122
+ - [Observability](observability.md): optional OpenTelemetry adapter over `AgentEvent` streams.
121
123
  - [Public contracts](public-contracts.md): provider, tool, context, session, and extension contracts that runtimes can pass through hooks.
122
124
 
123
125
  Permission checks for tools, extensions, and resources are hard guards; middleware can transform payloads but cannot bypass a denied `PermissionPolicy`.
package/docs/migration.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- This page is the single navigation entry for the two cross-cutting migrations external apps hit when moving from Prism's development defaults to its production persistence and explicit-capability surfaces:
5
+ Prism 0.0.4 is source-compatible with documented 0.0.3 agent construction; no mandatory code migration is required. New capabilities are additive and inactive until configured. This page covers two optional adoption paths:
6
6
 
7
- 1. **In-memory / JSONL → database-backed persistence** — swap the single-process development `SessionStore` for a host-implemented `ProductionPersistenceStore` / `SessionStore` adapter, and optionally attach a durable `RunLedger`.
8
- 2. **Permissive capability defaults → explicit capability activation** — move from "omitted tool/skill lists activate everything in scope" (pre-Phase 38 behavior) to named, fail-closed tool/skill activation.
7
+ 1. **In-memory / JSONL → database-backed persistence** — replace the single-process development `SessionStore` with `@arnilo/prism-session-store-sqlite`, `@arnilo/prism-session-store-postgres`, or a host implementation, and optionally attach its durable `RunLedger`.
8
+ 2. **Legacy permissive capability configuration → explicit activation** — name tools/skills and keep omitted capabilities fail-closed.
9
9
 
10
- It is a thin, link-first guide: it states before/after shapes and points at the detailed pages for schema, indexes, redaction, branch handles, capability semantics, and security.
10
+ It states before/after shapes and links detailed schema, redaction, branch, capability, and security guidance.
11
11
 
12
12
  ## When to use it
13
13
 
@@ -15,7 +15,7 @@ Read this page when:
15
15
 
16
16
  - you are taking an app from the `createMemorySessionStore()` / `createJsonlSessionStore()` path to a multi-process, multi-tenant, or durable database backend;
17
17
  - you are hardening an agent that previously relied on "every scoped tool/skill is active" and need to name capabilities explicitly;
18
- - you are adopting the Phase 34–40 production surfaces (atomic append, branch handles, run/event/tool/usage ledger, security boundary hardening) for the first time.
18
+ - you are adopting 0.0.4 persistence, checkpoints/leases, workflows, structured output, multimodality, or explicit tool safety for the first time.
19
19
 
20
20
  If you are new to Prism, start at [Session stores](session-stores.md) and [Agent/session runtime](agent-session-runtime.md) instead.
21
21
 
@@ -26,6 +26,8 @@ There is no runtime import for this page. The migrations below use these surface
26
26
  | Surface | Where | Migration role |
27
27
  | --- | --- | --- |
28
28
  | `SessionStore` | `@arnilo/prism` | Runtime seam swapped from memory/JSONL to DB. |
29
+ | `createSqlitePersistence` | `@arnilo/prism-session-store-sqlite` | Local durable session, ledger, query, checkpoint, and lease adapter. |
30
+ | `createPostgresPersistence` | `@arnilo/prism-session-store-postgres` | Multi-process pooled persistence with advisory-lock migrations. |
29
31
  | `ProductionPersistenceStore` | `@arnilo/prism` | Adapter-facing contract for paginated, multi-tenant reads (`query*`, optional `readBranchPath`). |
30
32
  | `RunLedger` / `RunLedgerRecord` | `@arnilo/prism` | Durable run/event/tool-call/usage ledger attached via `AgentConfig.runLedger` / `RunOptions.runLedger`. |
31
33
  | `SessionAppendOptions` / `SessionAppendConflictError` / `SessionBranchHandle` | `@arnilo/prism` | Atomic append, retry dedup, durable branch handles. |
@@ -77,34 +79,23 @@ Capability migration (before/after):
77
79
 
78
80
  ### Migration 1 — in-memory / JSONL → database-backed persistence
79
81
 
80
- A complete, network-free reference adapter that implements these contracts against in-memory tables (and wires a `RunLedger`, branch-handle checkout, fork, and prior-run timeline resume) lives at [`examples/external-app-db-backed.ts`](../examples/external-app-db-backed.ts). The steps below mirror its structure.
82
+ Runnable references: [`examples/workflow-sqlite-resume.ts`](../examples/workflow-sqlite-resume.ts), credential-gated [`examples/workflow-postgres-resume.ts`](../examples/workflow-postgres-resume.ts), and the network-free custom-adapter example [`examples/external-app-db-backed.ts`](../examples/external-app-db-backed.ts).
81
83
 
82
- Step 1: implement a `SessionStore` (or the richer `ProductionPersistenceStore`) against your database. The runtime only requires `append(entry, options?)`, `list(sessionId)`, and optional `get(id)` / `readBranchPath(query)`.
84
+ Step 1: replace the development store with a first-party adapter. Use PostgreSQL instead when multiple processes or sustained concurrent writers matter.
83
85
 
84
86
  ```ts
85
87
  // Before: development store, single process.
86
88
  import { createJsonlSessionStore } from "@arnilo/prism/node/session-store-jsonl";
87
- const store = createJsonlSessionStore("./sessions.jsonl");
88
-
89
- // After: host-implemented database adapter implementing the documented contract, no real DB needed to satisfy the contract.
90
- import type { SessionStore, SessionEntry, SessionAppendOptions, PersistencePage, SessionBranchRead } from "@arnilo/prism";
91
-
92
- const store: SessionStore = {
93
- async append(entry: SessionEntry, options?: SessionAppendOptions) {
94
- // 1. idempotency dedup: insert (session_id, expected_parent_id, idempotency_key, entry_id)
95
- // into prism_session_append_idempotency; unique hit => SessionAppendConflictError { idempotencyDuplicate: true }
96
- // 2. expectedParentId existence check => SessionAppendConflictError { expectedParentId } if missing
97
- // 3. insert prism_session_entries row; duplicate id fails the transaction
98
- // 4. optionally compare-and-swap prism_branches.leaf_entry_id
99
- },
100
- async list(sessionId: string) { /* O(n) development fallback only */ return []; },
101
- async readBranchPath(query: SessionBranchRead): Promise<PersistencePage<SessionEntry>> {
102
- // one recursive CTE / ancestor query — do NOT list(sessionId)+in-memory walk for long sessions
103
- return { items: [] };
104
- },
105
- };
89
+ const oldStore = createJsonlSessionStore("./sessions.jsonl");
90
+
91
+ // After: local durable adapter. The same object implements SessionStore,
92
+ // RunLedger, ProductionPersistenceStore, checkpoints, and leases.
93
+ import { createSqlitePersistence } from "@arnilo/prism-session-store-sqlite";
94
+ const store = createSqlitePersistence({ filename: "./prism.db" });
106
95
  ```
107
96
 
97
+ Custom adapters remain supported through `SessionStore` / `ProductionPersistenceStore`; implement indexed `readBranchPath()` rather than full-session scans.
98
+
108
99
  Step 2: optionally attach a durable run/event/tool/usage ledger and ownership scope so a process exit leaves enough to resume and bill:
109
100
 
110
101
  ```ts
@@ -174,7 +165,7 @@ Runtime skill activation remains explicit: `RunOptions.activeSkills` narrows per
174
165
 
175
166
  ## Extension and configuration notes
176
167
 
177
- - **Persistence is host-owned.** Prism ships no database adapter, no DDL, no migration runner. Hosts own connection pools, transactions, cursor encoding, retention jobs, and tenant isolation. The runtime only talks to `SessionStore` (+ optional `readBranchPath`) and `RunLedger`.
168
+ - **Persistence remains host-configured.** Optional SQLite/PostgreSQL packages ship adapters and versioned setup, but hosts choose connection paths/pools, TLS, credentials, retention, tenant policy, and lifecycle. Core only consumes `SessionStore`, `RunLedger`, checkpoint, and lease contracts.
178
169
  - **`RunLedger` is not a `SessionStore` replacement.** Messages, branches, and session entries still flow through `SessionStore.append()`; the ledger records run/event/tool/usage facts. See [Runs and usage ledger](runs-and-usage.md).
179
170
  - **Capability activation is config over code.** Every seam lives on `AgentDefinition` / `AgentDefinitionResolutionContext` / `RunOptions`; no auto-activation, no privilege grant. A declaration cannot grant permissions or bypass `toolNames`.
180
171
  - **Migration order is decoupled.** You can adopt database persistence without changing capability activation, and vice versa. Both migrations are independent config swaps.
@@ -190,7 +181,9 @@ Runtime skill activation remains explicit: `RunOptions.activeSkills` narrows per
190
181
 
191
182
  ## Related APIs
192
183
 
193
- - [Database persistence](database-persistence.md): production persistence contracts, reference schema, indexes, conditional append, retention, migrations, NoSQL mapping.
184
+ - [Database persistence](database-persistence.md): production contracts, reference schema, indexes, conditional append, retention, migrations, and custom adapters.
185
+ - [SQLite persistence](sqlite-persistence.md): local durable first-party adapter and writer ceiling.
186
+ - [PostgreSQL persistence](postgres-persistence.md): pooled multi-process adapter, TLS/pool ownership, and live gate.
194
187
  - [Session stores](session-stores.md): `SessionStore` contract, `SessionAppendOptions`, `SessionAppendConflictError`, branch handles, `readBranchPath`.
195
188
  - [Session stores and branching](session-stores-and-branching.md): detailed branch semantics and helper reference.
196
189
  - [Runs and usage ledger](runs-and-usage.md): `RunLedger` record shapes, redaction, and event/usage ordering.
@@ -36,7 +36,7 @@ import { createModelRegistry, type ModelConfig } from "@arnilo/prism";
36
36
  | --- | --- |
37
37
  | `provider` / `model` | Required registry key. |
38
38
  | `displayName` | Human-readable label. |
39
- | `capabilities` | Input/output modes plus reasoning/tools/streaming booleans. |
39
+ | `capabilities` | Input/output modes (`text`, `image`, `audio`, `file`, `document`) plus reasoning/tools/streaming booleans and optional `structuredOutput` (`true` or `"json_schema"`) for native JSON-schema requests. |
40
40
  | `limits` | Context and output-token limits. |
41
41
  | `cost` | Input/output/cache read/cache write pricing. |
42
42
  | `cache` | Generic `ModelCacheCapabilities`. |
@@ -74,7 +74,7 @@ The registry emits no events and performs no I/O.
74
74
  "model": {
75
75
  "provider": "demo",
76
76
  "model": "demo-large",
77
- "capabilities": { "input": ["text"], "tools": true, "streaming": true },
77
+ "capabilities": { "input": ["text", "image", "audio", "file", "document"], "tools": true, "streaming": true },
78
78
  "limits": { "contextWindow": 128000, "maxOutputTokens": 8192 },
79
79
  "cost": { "input": 10, "output": 30, "cacheRead": 2, "currency": "USD", "unit": "1M tokens" },
80
80
  "cache": { "kind": "cache_control", "maxBreakpoints": 4, "longRetention": true }
@@ -91,7 +91,7 @@ const model: ModelConfig = {
91
91
  provider: "demo",
92
92
  model: "demo-large",
93
93
  displayName: "Demo Large",
94
- capabilities: { input: ["text"], output: ["text"], tools: true, streaming: true },
94
+ capabilities: { input: ["text", "document"], output: ["text"], tools: true, streaming: true },
95
95
  limits: { contextWindow: 128_000, maxOutputTokens: 8_192 },
96
96
  cost: { input: 10, output: 30, cacheRead: 2, cacheWrite: 12, currency: "USD", unit: "1M tokens" },
97
97
  cache: { kind: "cache_control", maxBreakpoints: 4, minCacheableTokens: 1024, longRetention: true },
@@ -110,12 +110,14 @@ Provider packages register models through `ProviderPackageAPI.registerModel(mode
110
110
  ## Security and performance notes
111
111
 
112
112
  - Model metadata must not contain credentials or secrets.
113
+ - Declare truthful `capabilities.input` tags. Prism core rejects undeclared modalities in `assembleProviderInput()` when the list is present.
113
114
  - Registration is in-memory and O(1) by provider/model key.
114
115
  - `ModelConfig.cache` is declarative capability info only; it does not grant permissions, select tools, or bypass auth.
115
116
  - Provider-specific behavior belongs in provider packages, not Prism core.
116
117
 
117
118
  ## Related APIs
118
119
 
120
+ - [Multimodal content](multimodal-content.md): `audio`/`file`/`document` blocks and `MODEL_INPUT_CAPABILITIES`.
119
121
  - [Provider layer](provider-layer.md): provider/model registry overview.
120
122
  - [Provider caching](provider-caching.md): `ModelCacheCapabilities` and cache helpers.
121
123
  - [Provider packages](provider-packages.md): package registration of model metadata.
@@ -0,0 +1,148 @@
1
+ # Multimodal content
2
+
3
+ ## What it does
4
+
5
+ Prism core ships generic `audio`, `file`, and `document` `ContentBlock` types plus bounded media resolution helpers. Blocks carry MIME type, optional name, and exactly one source: inline base64 `data`, remote `url`, or host `resourceUri`. Optional `transcript` metadata can accompany audio/document blocks.
6
+
7
+ `assembleProviderInput()` calls `assertMessagesSupportModelCapabilities()` so declared `ModelCapabilities.input` tags are enforced before provider calls. First-party provider packages map supported blocks locally; unsupported combinations fail closed with `UnsupportedModalityError` or an explicit provider error.
8
+
9
+ ## When to use it
10
+
11
+ - **Host apps** attaching PDFs, audio clips, or generic files to user messages before a provider turn.
12
+ - **Resource loaders** returning binary payloads for `resourceUri` references under trust/permission policy.
13
+ - **Provider authors** reading truthful `ModelCapabilities.input` tags (`text`, `image`, `audio`, `file`, `document`) before mapping wire formats.
14
+
15
+ Do not embed provider upload IDs, tenant-scoped remote file IDs, or API-specific handles in core content blocks.
16
+
17
+ ## Inputs / request
18
+
19
+ ```ts
20
+ import {
21
+ resolveMediaContentBlock,
22
+ assertSsrfAllowedUrl,
23
+ type AudioContent,
24
+ type FileContent,
25
+ type DocumentContent,
26
+ } from "@arnilo/prism";
27
+
28
+ const file: FileContent = {
29
+ type: "file",
30
+ mediaType: "application/pdf",
31
+ name: "report.pdf",
32
+ data: base64Pdf,
33
+ };
34
+
35
+ const audio: AudioContent = {
36
+ type: "audio",
37
+ mediaType: "audio/wav",
38
+ resourceUri: "package://demo/sample.wav",
39
+ durationMs: 12_000,
40
+ };
41
+
42
+ await resolveMediaContentBlock(file, { bounds: { maxItemBytes: 10_000_000 } });
43
+ ```
44
+
45
+ Known `ModelCapabilities.input` tags are exported as `MODEL_INPUT_CAPABILITIES`:
46
+
47
+ | Tag | Block type | First-party mapping (declared capability required) |
48
+ | --- | --- | --- |
49
+ | `text` | `text` (default) | All providers |
50
+ | `image` | `image` | OpenAI Responses, OpenRouter, OpenCode Go Anthropic route, Kimi, NeuralWatt |
51
+ | `audio` | `audio` | OpenAI Responses (`input_audio`) |
52
+ | `file` | `file` | OpenAI Responses (`input_file`); Anthropic routes map PDF only |
53
+ | `document` | `document` | OpenAI Responses (`input_file`); OpenCode Go Anthropic route; Kimi |
54
+
55
+ ## Outputs / response / events
56
+
57
+ - `resolveMediaContentBlock()` returns `{ mediaType, bytes, name?, durationMs?, transcript?, metadata? }`.
58
+ - `assertModelSupportsContentBlocks()` / `assertMessagesSupportModelCapabilities()` throw `UnsupportedModalityError` when a declared capability list omits the block modality.
59
+ - `assertMediaBlocksWithinBounds()` enforces per-item bytes, total request bytes, item count, and audio duration ceilings.
60
+ - `assertSsrfAllowedUrl()` rejects private/link-local/metadata hosts unless explicitly allow-listed.
61
+ - `sniffMediaMimeType()` / `assertDeclaredMediaTypeMatches()` compare declared MIME types to magic bytes.
62
+ - No events are emitted and no provider calls occur in these helpers.
63
+
64
+ Default ceilings:
65
+
66
+ | Constant | Value |
67
+ | --- | --- |
68
+ | `DEFAULT_MAX_MEDIA_ITEM_BYTES` | 10 MB |
69
+ | `DEFAULT_MAX_MEDIA_REQUEST_BYTES` | 32 MiB |
70
+ | `DEFAULT_MAX_AUDIO_DURATION_MS` | 5 minutes |
71
+ | `DEFAULT_MEDIA_FETCH_TIMEOUT_MS` | 30 seconds |
72
+ | `DEFAULT_MAX_MEDIA_ITEMS_PER_REQUEST` | 32 items |
73
+
74
+ ## Request/response example
75
+
76
+ ```json
77
+ {
78
+ "block": {
79
+ "type": "file",
80
+ "mediaType": "application/pdf",
81
+ "name": "report.pdf",
82
+ "resourceUri": "package://demo/report.pdf"
83
+ },
84
+ "resolved": {
85
+ "mediaType": "application/pdf",
86
+ "bytes": "<Uint8Array>",
87
+ "name": "report.pdf"
88
+ }
89
+ }
90
+ ```
91
+
92
+ ## Implementation example
93
+
94
+ ```ts
95
+ import {
96
+ assembleProviderInput,
97
+ loadBinaryResource,
98
+ resolveMediaContentBlock,
99
+ UnsupportedModalityError,
100
+ } from "@arnilo/prism";
101
+
102
+ const loader = {
103
+ async load(uri) {
104
+ return { uri, mediaType: "application/pdf", data: pdfBytes };
105
+ },
106
+ };
107
+
108
+ const bytes = await loadBinaryResource(loader, "package://demo/report.pdf");
109
+ const resolved = await resolveMediaContentBlock(
110
+ { type: "document", mediaType: "application/pdf", resourceUri: "package://demo/report.pdf" },
111
+ { loader },
112
+ );
113
+
114
+ try {
115
+ await assembleProviderInput({
116
+ model: { provider: "demo", model: "text-only", capabilities: { input: ["text"] } },
117
+ input: [{ role: "user", content: [{ type: "file", mediaType: "application/pdf", data: "..." }] }],
118
+ });
119
+ } catch (error) {
120
+ if (error instanceof UnsupportedModalityError) {
121
+ // Host-visible reject before provider HTTP.
122
+ }
123
+ }
124
+ ```
125
+
126
+ ## Extension and configuration notes
127
+
128
+ - URL fetches use injectable `fetch` for tests and custom transports; production hosts should supply TLS, auth, and logging policy outside Prism core.
129
+ - `resourceUri` resolution requires a caller-provided `ResourceLoader` and optional `ResourceLoadContext.permission` check.
130
+ - Local filesystem paths should use trust policies such as `createPathTrustPolicy()` before exposing URIs to loaders.
131
+ - Provider upload/create/delete lifecycles are provider-package-local. `@arnilo/prism-provider-openai` inlines files under 4 MiB as `data:<mediaType>;base64,...` `file_data`, otherwise uses a bounded per-run upload cache and best-effort `DELETE /v1/files` cleanup after each stream.
132
+ - Shared wire helpers live in `@arnilo/prism/providers/media` (`serializeOpenAIResponsesInputFile`, `serializePdfDocumentWireBlock`, `createBoundedUploadCache`).
133
+
134
+ ## Security and performance notes
135
+
136
+ - SSRF deny-by-default blocks loopback, RFC1918, link-local, and cloud metadata hostnames unless `SsrfPolicy.allowedHostnames` is set.
137
+ - MIME validation rejects common magic-byte spoofing; extensions alone are never trusted.
138
+ - Byte budgets use base64 size estimates before decode and re-check decoded `buffer.length` after read/fetch.
139
+ - Media errors omit raw bytes/base64 payloads from messages.
140
+ - Fetch readers are cancelled promptly after bound violations or abort signals.
141
+
142
+ ## Related APIs
143
+
144
+ - [Input and prompt assembly](input-and-prompt-assembly.md): attachments and `assembleProviderInput()` capability checks.
145
+ - [Resource loading](resource-loading.md): `loadBinaryResource()` and text/JSON helpers.
146
+ - [Model registry](model-registry.md): `ModelCapabilities.input` metadata.
147
+ - [Provider conformance](provider-conformance.md): serialized request coverage for content blocks.
148
+ - [Persistence, credentials, and multimodality primitives](persistence-credentials-multimodality-primitives.md): Plan 056 inventory and threat model.
@@ -0,0 +1,163 @@
1
+ # Observability
2
+
3
+ ## What it does
4
+
5
+ Prism exposes provider and tool timing through stable, metadata-only `AgentEvent` variants. Hosts subscribe via `session.subscribe()` or persist events through `RunLedger`. Core helpers build `ProviderTurnMetadata` and classify HTTP failures without echoing prompts, tool arguments, or credentials.
6
+
7
+ Optional package `@arnilo/prism-observability-opentelemetry` maps those events to OpenTelemetry spans and low-cardinality metrics. OpenTelemetry is **not** a dependency of `@arnilo/prism`.
8
+
9
+ APIs:
10
+
11
+ - `ProviderTurnMetadata`, `ToolExecutionMetadata` on `AgentEvent`
12
+ - `createProviderTurnMetadata()`, `readProviderHttpStatus()` in `@arnilo/prism`
13
+ - `createOpenTelemetryInstrumentation()`, `wrapOpenTelemetryApi()`, `createInMemoryTelemetry()` in `@arnilo/prism-observability-opentelemetry`
14
+
15
+ ## When to use it
16
+
17
+ Use agent events when you need run-scoped latency, retry attempt numbers, token/cache usage, tool duration, or error classification in-process or through your own exporter.
18
+
19
+ Use the OpenTelemetry adapter when you already run the OpenTelemetry SDK and want spans/metrics without forking the runtime.
20
+
21
+ Do not parse raw provider SSE for timing — provider packages normalize stream events; the session emits `provider_turn_*` once per `generate()` attempt.
22
+
23
+ ## Inputs / request
24
+
25
+ Core metadata helpers:
26
+
27
+ ```ts
28
+ import { createProviderTurnMetadata, readProviderHttpStatus } from "@arnilo/prism";
29
+
30
+ const metadata = createProviderTurnMetadata(request, providerId, { attempt: 2, latencyMs: 120 });
31
+ const httpStatus = readProviderHttpStatus(errorInfo);
32
+ ```
33
+
34
+ New agent event variants (metadata only):
35
+
36
+ | Variant | When | Key fields |
37
+ | --- | --- | --- |
38
+ | `provider_turn_started` | Before each provider `generate()` attempt | `turn`, `metadata: ProviderTurnMetadata` |
39
+ | `provider_turn_finished` | After success or failure of that attempt | `metadata` (includes `latencyMs`, optional `httpStatus`), `usage?`, `error?` |
40
+
41
+ `ToolExecutionMetadata` on terminal tool events:
42
+
43
+ | Field | Meaning |
44
+ | --- | --- |
45
+ | `durationMs` | Wall time from dispatch start to finish/block/error |
46
+ | `status` | `finished` \| `error` \| `blocked` |
47
+
48
+ OpenTelemetry adapter:
49
+
50
+ ```ts
51
+ import { trace, metrics } from "@opentelemetry/api";
52
+ import { createOpenTelemetryInstrumentation, wrapOpenTelemetryApi } from "@arnilo/prism-observability-opentelemetry";
53
+
54
+ const { tracer, meter } = wrapOpenTelemetryApi(trace.getTracer("app"), metrics.getMeter("app"));
55
+ const telemetry = createOpenTelemetryInstrumentation({ tracer, meter, onExporterError: console.error });
56
+
57
+ const detach = telemetry.attachSession(session);
58
+ // or: for await (const event of session.subscribe()) telemetry.handleAgentEvent(event);
59
+ ```
60
+
61
+ Set `enabled: false` or omit `tracer`/`meter` for a no-op adapter.
62
+
63
+ ## Outputs / response / events
64
+
65
+ Provider turn metadata fields:
66
+
67
+ | Field | Source |
68
+ | --- | --- |
69
+ | `providerId` | Active provider id |
70
+ | `model` | `ProviderRequest.model` |
71
+ | `requestId` | `request.metadata.requestId` or `request.options.sessionId` |
72
+ | `attempt` | Retry attempt (1-based) |
73
+ | `latencyMs` | Set on `provider_turn_finished` |
74
+ | `httpStatus` | Numeric `ErrorInfo.code` when present |
75
+ | `rateLimitRemaining` / `rateLimitResetMs` | Reserved for provider adapters (optional) |
76
+
77
+ OpenTelemetry mapping (when enabled):
78
+
79
+ | Agent event | Span | Metric labels |
80
+ | --- | --- | --- |
81
+ | `agent_started` / `agent_finished` | `prism.agent.run` | — |
82
+ | `provider_turn_*` | `prism.provider.turn` | `provider_id`, `outcome` on duration histogram |
83
+ | `tool_execution_*` (terminal) | `prism.tool.execute` when started | `status` on duration histogram |
84
+ | `provider_turn_finished` / `agent_finished` usage | span attributes | `prism.provider.tokens` counter (`kind`: input/output/cache_*) |
85
+
86
+ High-cardinality identifiers (`sessionId`, `runId`, `requestId`, `toolCallId`) are **span attributes only**, never metric labels.
87
+
88
+ ## Request/response example
89
+
90
+ ```json
91
+ {
92
+ "type": "provider_turn_finished",
93
+ "sessionId": "sess_01J...",
94
+ "runId": "run_01J...",
95
+ "turn": 1,
96
+ "metadata": {
97
+ "providerId": "openai",
98
+ "model": { "provider": "openai", "model": "gpt-4.1" },
99
+ "requestId": "sess_01J...",
100
+ "attempt": 2,
101
+ "latencyMs": 842,
102
+ "httpStatus": 503
103
+ },
104
+ "error": { "message": "upstream unavailable", "code": 503 }
105
+ }
106
+ ```
107
+
108
+ ```json
109
+ {
110
+ "type": "tool_execution_finished",
111
+ "sessionId": "sess_01J...",
112
+ "runId": "run_01J...",
113
+ "result": { "toolCallId": "call_1", "name": "echo" },
114
+ "metadata": { "durationMs": 12, "status": "finished" }
115
+ }
116
+ ```
117
+
118
+ ## Implementation example
119
+
120
+ ```ts
121
+ import { createAgent, createMockProvider, providerDone, providerTextDelta } from "@arnilo/prism";
122
+ import { createInMemoryTelemetry, createOpenTelemetryInstrumentation } from "@arnilo/prism-observability-opentelemetry";
123
+
124
+ const memory = createInMemoryTelemetry();
125
+ const telemetry = createOpenTelemetryInstrumentation({ tracer: memory.tracer, meter: memory.meter });
126
+
127
+ const session = createAgent({
128
+ model: { provider: "mock", model: "demo" },
129
+ provider: createMockProvider([providerTextDelta("hi"), providerDone()]),
130
+ }).createSession();
131
+
132
+ const detach = telemetry.attachSession(session);
133
+ await session.run("hello");
134
+ detach();
135
+
136
+ console.log(memory.spans.map((span) => span.name));
137
+ ```
138
+
139
+ ## Extension and configuration notes
140
+
141
+ - Events flow through `redactAgentEvent` before subscribers and ledger writes — configure `createSecretRedactor` on the agent/run.
142
+ - `retry_scheduled` still signals backoff; each retry attempt emits its own `provider_turn_*` pair with `metadata.attempt`.
143
+ - NeuralWatt `neuralwatt:telemetry` provider events remain package-local; hosts may forward numeric cost/energy into custom metrics.
144
+ - `@arnilo/prism-observability-opentelemetry` is optional and included through `@arnilo/prism-sdk` and `@arnilo/prism-all`; instrumentation remains disabled until a host configures it.
145
+ - Exporter failures are isolated: instrumentation catches tracer/meter errors and invokes `onExporterError` without affecting the run.
146
+ - Disabled instrumentation performs no per-delta span work (`enabled: false` or missing tracer/meter).
147
+
148
+ ## Security and performance notes
149
+
150
+ - Default events are metadata-only — no prompts, streamed deltas, tool arguments, or credentials.
151
+ - Opt-in content in other event types (`message_delta`, tool `result`) is still subject to `redactAgentEvent`.
152
+ - Metric labels stay low-cardinality (`provider_id`, `outcome`, `status`, token `kind`); never use `sessionId`/`runId` as labels.
153
+ - Target overhead when enabled is under 5% excluding exporter I/O; disabled hooks allocate no spans.
154
+ - Provider transport limits and redaction order are documented in [Provider primitives](provider-primitives.md).
155
+
156
+ ## Related APIs
157
+
158
+ - [Agent events](agent-events.md): full `AgentEvent` union and subscriber semantics.
159
+ - [Runs and usage ledger](runs-and-usage.md): durable `AgentEventRecord` persistence.
160
+ - [Middleware hooks](middleware-hooks.md): transform boundaries alongside event subscribers.
161
+ - [Provider primitives](provider-primitives.md): frozen observability contract for Plan 054.
162
+ - [Credentials and redaction](credentials-and-redaction.md): secret redaction before events and ledger rows.
163
+ - [Workflows](workflows.md): package-local `WorkflowEvent` stream that can wrap redacted `AgentEvent`s from agent nodes.