@arnilo/prism 0.3.2 → 0.5.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 (208) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/README.md +42 -62
  3. package/dist/agent-run-lifecycle.js +4 -0
  4. package/dist/agent-run-state.d.ts +5 -2
  5. package/dist/agent-run-state.js +18 -8
  6. package/dist/agent-session/session/assemble.d.ts +6 -0
  7. package/dist/agent-session/session/assemble.js +391 -0
  8. package/dist/agent-session/session/persist.d.ts +28 -0
  9. package/dist/agent-session/session/persist.js +166 -0
  10. package/dist/agent-session/session/provider-round.d.ts +6 -0
  11. package/dist/agent-session/session/provider-round.js +231 -0
  12. package/dist/agent-session/session/tool-round.d.ts +31 -0
  13. package/dist/agent-session/session/tool-round.js +473 -0
  14. package/dist/agent-session/session/types.d.ts +115 -0
  15. package/dist/agent-session/session/types.js +5 -0
  16. package/dist/agent-session/session.d.ts +54 -41
  17. package/dist/agent-session/session.js +23 -1132
  18. package/dist/capture.d.ts +63 -0
  19. package/dist/capture.js +67 -0
  20. package/dist/cli-dev.d.ts +29 -0
  21. package/dist/cli-dev.js +52 -0
  22. package/dist/cli-init.d.ts +34 -3
  23. package/dist/cli-init.js +192 -24
  24. package/dist/cli-runner.d.ts +6 -2
  25. package/dist/cli-runner.js +57 -10
  26. package/dist/content.d.ts +3 -3
  27. package/dist/content.js +3 -1
  28. package/dist/contracts-core/agent.d.ts +8 -0
  29. package/dist/contracts-core/batch.d.ts +97 -0
  30. package/dist/contracts-core/batch.js +65 -0
  31. package/dist/contracts-core/content.d.ts +72 -1
  32. package/dist/contracts-core/embeddings.d.ts +30 -0
  33. package/dist/contracts-core/embeddings.js +17 -0
  34. package/dist/contracts-core/images.d.ts +60 -0
  35. package/dist/contracts-core/images.js +17 -0
  36. package/dist/contracts-core/moderation.d.ts +46 -0
  37. package/dist/contracts-core/moderation.js +34 -0
  38. package/dist/contracts-core/speech.d.ts +39 -0
  39. package/dist/contracts-core/speech.js +17 -0
  40. package/dist/contracts-core/transcription.d.ts +48 -0
  41. package/dist/contracts-core/transcription.js +17 -0
  42. package/dist/contracts-core/video.d.ts +61 -0
  43. package/dist/contracts-core/video.js +17 -0
  44. package/dist/contracts-core.d.ts +7 -0
  45. package/dist/contracts-core.js +7 -0
  46. package/dist/contracts-protocol.d.ts +18 -0
  47. package/dist/contracts-run-state.d.ts +1 -2
  48. package/dist/index.d.ts +7 -3
  49. package/dist/index.js +5 -3
  50. package/dist/input.d.ts +8 -0
  51. package/dist/input.js +4 -0
  52. package/dist/node/agent-definitions.d.ts +1 -8
  53. package/dist/node/agent-definitions.js +0 -34
  54. package/dist/node/settings.d.ts +0 -1
  55. package/dist/node/settings.js +0 -5
  56. package/dist/pinned-fetch.js +29 -3
  57. package/dist/provider-events.js +3 -4
  58. package/dist/providers/media.d.ts +1 -2
  59. package/dist/providers/media.js +1 -4
  60. package/dist/rpc.d.ts +1 -1
  61. package/dist/rpc.js +4 -4
  62. package/dist/testing/persistence-schema.d.ts +1 -1
  63. package/dist/testing/persistence-schema.js +32 -28
  64. package/dist/testing/provider-conformance.d.ts +114 -5
  65. package/dist/testing/provider-conformance.js +342 -0
  66. package/dist/testing/tool-conformance.d.ts +25 -0
  67. package/dist/testing/tool-conformance.js +128 -1
  68. package/dist/testing/tool-effect-store-conformance.d.ts +0 -1
  69. package/dist/testing/tool-effect-store-conformance.js +0 -3
  70. package/dist/thinking.d.ts +48 -9
  71. package/dist/thinking.js +134 -8
  72. package/dist/tool-search.d.ts +76 -0
  73. package/dist/tool-search.js +199 -0
  74. package/docs/0.1.0-readiness.md +3 -3
  75. package/docs/a2a.md +2 -2
  76. package/docs/acp-agent.md +1 -1
  77. package/docs/acp.md +3 -3
  78. package/docs/ag-ui-adoption.md +1 -1
  79. package/docs/ag-ui.md +1 -2
  80. package/docs/agent-definitions.md +1 -1
  81. package/docs/agent-events.md +5 -5
  82. package/docs/agent-identity.md +13 -2
  83. package/docs/audit-export.md +3 -3
  84. package/docs/batch-jobs.md +120 -0
  85. package/docs/browser-automation.md +5 -5
  86. package/docs/caveman.md +2 -2
  87. package/docs/cli-rpc.md +43 -9
  88. package/docs/coding-agent-tools.md +19 -19
  89. package/docs/coding-review-and-diagnostics.md +2 -2
  90. package/docs/coding-security.md +5 -5
  91. package/docs/coding-tools.md +82 -0
  92. package/docs/coding-workspaces.md +2 -2
  93. package/docs/compaction-and-retry.md +2 -2
  94. package/docs/compaction-llm.md +4 -4
  95. package/docs/compaction-observational-memory.md +3 -3
  96. package/docs/computer-use-linux.md +13 -2
  97. package/docs/context-and-skills.md +3 -1
  98. package/docs/conversations.md +4 -4
  99. package/docs/core.md +85 -0
  100. package/docs/credential-storage.md +12 -8
  101. package/docs/credentials-and-redaction.md +1 -1
  102. package/docs/data-classification.md +1 -1
  103. package/docs/database-persistence.md +7 -3
  104. package/docs/dev-inspector.md +103 -0
  105. package/docs/device-adapters.md +2 -2
  106. package/docs/diagrams.md +247 -0
  107. package/docs/document-reader.md +6 -6
  108. package/docs/documents.md +214 -0
  109. package/docs/embeddings.md +112 -0
  110. package/docs/enterprise-postgres-state.md +7 -7
  111. package/docs/evaluations.md +41 -7
  112. package/docs/extensions.md +3 -3
  113. package/docs/forge-integration.md +3 -3
  114. package/docs/graft.md +5 -5
  115. package/docs/guardrails.md +2 -2
  116. package/docs/host-security.md +16 -15
  117. package/docs/image-generation.md +129 -0
  118. package/docs/impeccable.md +7 -5
  119. package/docs/index.md +84 -46
  120. package/docs/indexed-code-search.md +2 -2
  121. package/docs/language-intelligence.md +4 -4
  122. package/docs/live-testing.md +126 -0
  123. package/docs/mcp-tools.md +44 -13
  124. package/docs/middleware-hooks.md +1 -1
  125. package/docs/migrate-to-0.4.md +312 -0
  126. package/docs/migrate-to-0.5.md +122 -0
  127. package/docs/migration.md +51 -1
  128. package/docs/model-registry.md +38 -0
  129. package/docs/model-routing.md +6 -6
  130. package/docs/moderation.md +117 -0
  131. package/docs/multi-agent-patterns.md +177 -0
  132. package/docs/multimodal-content.md +27 -3
  133. package/docs/obscura.md +12 -12
  134. package/docs/observability.md +32 -7
  135. package/docs/openapi-tools.md +14 -4
  136. package/docs/operations.md +11 -0
  137. package/docs/performance.md +30 -10
  138. package/docs/persistence-credentials-multimodality-primitives.md +7 -7
  139. package/docs/policy-and-audit.md +18 -8
  140. package/docs/ponytail.md +3 -3
  141. package/docs/postgres-persistence.md +5 -5
  142. package/docs/process-sessions.md +2 -2
  143. package/docs/prompt-registry.md +106 -0
  144. package/docs/provider-caching.md +36 -32
  145. package/docs/provider-conformance.md +24 -2
  146. package/docs/provider-packages.md +58 -22
  147. package/docs/provider-primitives.md +5 -5
  148. package/docs/provider-request-policies.md +1 -1
  149. package/docs/providers/ai-sdk.md +18 -6
  150. package/docs/providers/alibaba.md +10 -6
  151. package/docs/providers/anthropic.md +10 -6
  152. package/docs/providers/azure.md +20 -4
  153. package/docs/providers/bedrock.md +18 -3
  154. package/docs/providers/clinepass.md +7 -3
  155. package/docs/providers/commandcode.md +253 -0
  156. package/docs/providers/deepseek.md +7 -3
  157. package/docs/providers/google.md +8 -4
  158. package/docs/providers/hyper.md +284 -0
  159. package/docs/providers/kimi.md +7 -3
  160. package/docs/providers/neuralwatt.md +12 -8
  161. package/docs/providers/ollama.md +18 -3
  162. package/docs/providers/openai-compatible.md +5 -1
  163. package/docs/providers/openai.md +9 -5
  164. package/docs/providers/opencode-go.md +8 -4
  165. package/docs/providers/openrouter.md +8 -4
  166. package/docs/providers/vertex.md +21 -5
  167. package/docs/providers/xai.md +7 -3
  168. package/docs/providers/zai.md +7 -3
  169. package/docs/rag.md +31 -9
  170. package/docs/release-and-install.md +181 -76
  171. package/docs/resource-loading.md +1 -1
  172. package/docs/runs-and-usage.md +28 -3
  173. package/docs/server.md +94 -5
  174. package/docs/settings-auth-trust-security.md +7 -5
  175. package/docs/sheets.md +229 -0
  176. package/docs/speech.md +126 -0
  177. package/docs/sqlite-persistence.md +4 -4
  178. package/docs/supervisors.md +4 -3
  179. package/docs/thinking-and-reasoning.md +93 -60
  180. package/docs/tool-conformance.md +28 -3
  181. package/docs/tool-execution-primitives.md +8 -8
  182. package/docs/tools.md +32 -5
  183. package/docs/web-tools.md +3 -3
  184. package/docs/wiki.md +7 -7
  185. package/docs/work-artifacts-and-review.md +17 -6
  186. package/docs/work-connectors.md +4 -4
  187. package/docs/work-tools.md +5 -5
  188. package/docs/workflow-orchestration-primitives.md +35 -11
  189. package/docs/workflows.md +74 -13
  190. package/docs/working-and-semantic-memory.md +53 -5
  191. package/package.json +14 -31
  192. package/templates/README.md +23 -0
  193. package/templates/deep-research/README.md.tmpl +47 -0
  194. package/templates/deep-research/env.example.tmpl +12 -0
  195. package/templates/deep-research/gitignore.tmpl +7 -0
  196. package/templates/deep-research/manifest.json +12 -0
  197. package/templates/deep-research/package.json.tmpl +23 -0
  198. package/templates/deep-research/src/agent.ts.tmpl +81 -0
  199. package/templates/deep-research/src/index.ts.tmpl +53 -0
  200. package/templates/deep-research/src/tests/research.test.ts.tmpl +114 -0
  201. package/templates/deep-research/src/tools.ts.tmpl +86 -0
  202. package/templates/deep-research/src/types.ts.tmpl +45 -0
  203. package/templates/deep-research/src/workflow.ts.tmpl +156 -0
  204. package/templates/deep-research/tsconfig.json.tmpl +15 -0
  205. package/templates/init/manifest.json +5 -0
  206. package/templates/init/package.json.tmpl +2 -1
  207. package/templates/init/providers.json +40 -24
  208. package/docs/antigravity-agent.md +0 -207
@@ -0,0 +1,122 @@
1
+ # Migrate Prism 0.4 to 0.5
2
+
3
+ > **Status: released 2026-09-06** (`v0.5.0` tag). Covers every host-visible change from plan 055 onward: new provider adapters (055), security hardening (056), the dead-export cut (058), dependency majors (062), the MCP 2026-07-28 adoption (063), the CLI real-provider contract (064), and model-aware thinking effort (065).
4
+
5
+ ## What changes
6
+
7
+ Prism 0.5 is a **lockstep cut**: all 10 publishable manifests move `0.4.x` → `0.5.0` and internal first-party ranges move `^0.4.0` → `^0.5.0`. Package names and import subpaths from 0.4 stay valid; the breaking surface is (1) 27 removed unused exports, (2) the MCP SDK module move, (3) two thinking-effort wire moves, and (4) two hardening behavior changes. Everything else is additive.
8
+
9
+ ## 1. New provider adapters — additive (plan 055, shipped as 0.4.1)
10
+
11
+ `@arnilo/prism-providers/hyper` and `@arnilo/prism-providers/commandcode` join the first-party catalog (Hyper with chat + `/v1/responses` passthrough + intelligent-routing metadata; Command Code with dual-route chat/Anthropic-Messages and GPT-5.6 explicit caching). No action needed for existing hosts; both adapters follow the standard provider factory contract (`factory({ apiKey })`).
12
+
13
+ ## 2. Security hardening — behavior changes (plan 056)
14
+
15
+ - **Tenant-aware store factories:** session/memory/enterprise store factory signatures now require an explicit tenant scope — constructing a durable store without a tenant fails closed. Pass the tenant in the factory options (see the store pages under `docs/`).
16
+ - **Child process env allow-list:** spawned processes (shell default, LSP client, process sessions, computer-use-linux MCP transport) no longer inherit the ambient `process.env`. A deterministic allow-list (`buildChildEnv` / `DEFAULT_CHILD_ENV_INHERIT`, `src/agent/env.ts`) governs inheritance. If you relied on custom env vars reaching child tools, pass them explicitly through the session/spawn options.
17
+
18
+ ## 3. Dead-export removal — 27 symbols (plan 058, breaking)
19
+
20
+ Every removed symbol was verified unused (zero in-repo references, no third-party import evidence) and carried `@deprecated` "no replacement" earlier in the same cycle. If your code compiled against 0.4 without importing any symbol below, this section does not affect you.
21
+
22
+ | package | removed export | kind |
23
+ |---|---|---|
24
+ | `@arnilo/prism` | `statusFromState` | function |
25
+ | `@arnilo/prism` | `isInitProvider` | type-guard function |
26
+ | `@arnilo/prism` | `isInitTemplate` | function |
27
+ | `@arnilo/prism` | `parseContextFile` | function |
28
+ | `@arnilo/prism` | `parseToolFile` | function |
29
+ | `@arnilo/prism` | `defaultUserSettingsPath` | function |
30
+ | `@arnilo/prism` | `resolveProviderMediaBlock` | async function |
31
+ | `@arnilo/prism` | `ProviderSecretLeakConformanceOptions` | interface |
32
+ | `@arnilo/prism` | `runToolEffectStoreConformance` | async function |
33
+ | `@arnilo/prism-core` | `OidcIdentityVerifierResult` | type alias |
34
+ | `@arnilo/prism-core` | `assertDiffLines` | function |
35
+ | `@arnilo/prism-core` | `ResolvedPromptLimits` | type alias |
36
+ | `@arnilo/prism-core` | `encodeMetadata` | function |
37
+ | `@arnilo/prism-core` | `parseListOffsetCursor` | function |
38
+ | `@arnilo/prism-core` | `nodeKindOf` | function |
39
+ | `@arnilo/prism-memory` | `DEFAULT_MAX_PROMPT_CHARS` | const |
40
+ | `@arnilo/prism-memory` | `GRAFT_RESOLVE_ERROR_CODE` | const |
41
+ | `@arnilo/prism-memory` | `LinterOptions` | interface |
42
+ | `@arnilo/prism-memory` | `ResolvedGraftExtension` | interface |
43
+ | `@arnilo/prism-memory` | `WikiCategory` | type alias |
44
+ | `@arnilo/prism-coding-tools` | `codingSha256Hex` | function |
45
+ | `@arnilo/prism-coding-tools` | `DEFAULT_MAX_REVIEW_DELTA_ENTRIES` | const |
46
+ | `@arnilo/prism-coding-tools` | `HARD_MAX_REVIEW_DELTA_ENTRIES` | const |
47
+ | `@arnilo/prism-coding-tools` | `indexErrorCode` | function |
48
+ | `@arnilo/prism-coding-tools` | `PONYTAIL_PEER_RANGE` | const |
49
+ | `@arnilo/prism-coding-tools` | `CavemanSkillName` | type alias |
50
+ | `@arnilo/prism-providers` | `withOpenRouterCacheMarker` | function |
51
+
52
+ Per-symbol local replacements for the two cases where 0.4 docs showed a usage pattern:
53
+
54
+ - `defaultUserSettingsPath(appName)` → build the path with stdlib:
55
+ `join(homedir(), ".config", appName, "settings.json")` (see
56
+ [`docs/settings-auth-trust-security.md`](settings-auth-trust-security.md)).
57
+ - `parseContextFile` / `parseToolFile` → colocated `CONTEXT.md` / tool-descriptor parsing is
58
+ host-owned; parse frontmatter with `parseAgentFile` (kept) plus your own file reading
59
+ (see [`docs/agent-definitions.md`](agent-definitions.md)).
60
+
61
+ ## 4. Dependency majors (plan 062)
62
+
63
+ - **`pdf-parse` 1.1 → 2.4** (`@arnilo/prism-coding-tools` optional peer): v2 parses in a worker thread and transfers the `data` TypedArray — do not reuse the buffer after `extract`; requires Node ≥ 20.16 (repo engines already `>=20`). Embedded-script execution is hard-disabled (`isEvalSupported: false`). Extracted text is byte-identical to v1 modulo trailing newline.
64
+ - **`better-sqlite3` 12 → 13** (`@arnilo/prism-core` optional peer): N-API rewrite with bundled prebuilt binaries (no per-Node rebuild); unsupported platforms compile from source during install. No API removals.
65
+ - **`@napi-rs/keyring` 1.x → 2.0** (`@arnilo/prism-core`): locked/inaccessible keychain reads and deletes now **reject** with typed errors (`CredentialStoreLockedError`, `CredentialStoreUnavailableError`) instead of silently resolving `undefined`/`false`. A missing credential still resolves `undefined`. If you treated a locked keychain as an empty vault, handle the typed errors.
66
+
67
+ ## 5. MCP: TypeScript SDK v2 modular adoption (plan 063, breaking for MCP hosts)
68
+
69
+ `@arnilo/prism-mcp` replaces the monolithic `@modelcontextprotocol/sdk` 1.30.0 with the modular v2 packages (`@modelcontextprotocol/client` + `@modelcontextprotocol/server` 2.0.0 exact pins). Prism's public surface (`createPrismMcpServer`, `createPrismMcpWebHandler`) keeps its shape; hosts that imported SDK types directly must move to the v2 module imports. Highlights: modern client negotiation, SDK-managed routing headers (SEP-2243), MRTR-compatible elicitation, dual-era HTTP/stdio serving, 2026-07-28-conformant OAuth (issuer-keyed storage, RFC 9207 `iss` validation). Draft-era task vocabulary fails closed. Full migration table + legacy-session timeline: [`docs/migration.md`](migration.md), canonical API in [`docs/mcp-tools.md`](mcp-tools.md).
70
+
71
+ ## 6. CLI real-provider contract (plan 064)
72
+
73
+ `prism --provider <id>` runs real providers: ids resolve through the init provider catalog (`templates/init/providers.json`), the factory is imported from the installed `@arnilo/prism-providers/*` package, and the credential comes from the catalog's env var. `--mode print|json|rpc` all honor it. **Contract change:** omitting `--provider` fails with a usage error (exit 2) — the mock provider runs only on explicit `--provider mock`. The RPC session factory is async-capable (`AgentSession | Promise<AgentSession>`).
74
+
75
+ ## 7. Model-aware thinking effort (plan 065, two wire moves)
76
+
77
+ Thinking/reasoning effort is now model-aware, declared, and snapped: every reasoning-capable model in a first-party catalog declares `capabilities.thinkingLevels` and a `compat.thinkingFamily` stamp, and one adapter resolves, snaps, and merges the level.
78
+
79
+ Breaking / behavior changes:
80
+
81
+ 1. **Anthropic Messages: `effort` → `output_config.effort`.** Prism emits `output_config: { effort }` (the `output_config_effort` family). Hosts hand-building `compat.effort` / `compat.reasoning_effort` keep working (resolver reads the aliases); hosts *reading* emitted bodies must look at `output_config.effort`. Thinking is generation-aware: 4.6+ models map bare `enabled` to `adaptive`; legacy 4.5 models get `enabled` + `budget_tokens` default 10000 (bare `enabled` without a budget is rejected upstream).
82
+ 2. **xAI now sends `reasoning_effort`** (previously dropped): grok-4.6 `low/medium/high/xhigh`, grok-4.5 `low/medium/high`, grok-4.3 `none/low/medium/high`; out-of-set values snap. `grok-build` and unknown models pass through verbatim. `reasoning_content` replay is unchanged.
83
+ 3. **Snapping replaces silent drop** on declared models: out-of-set portable levels snap to the nearest declared level (ladder distance, ties up; below-minimum snaps up). Provider-documented tables (DeepSeek, Z.AI GLM-5.2/5.3, Kimi K3, ClinePass slot maps) remain wire authority. Opaque strings on reasoning-capable models still pass through.
84
+ 4. **Azure / Vertex / Bedrock forward thinking compat** through a sanitized forwarder (`reasoning_effort` + aliases, or `reasoning` object with `summary` preserved). Unrecognized compat keys are dropped, not leaked. Hand-built `options.extra` workarounds can become `compat`.
85
+
86
+ New surface on `@arnilo/prism`:
87
+
88
+ | Export | Purpose |
89
+ |---|---|
90
+ | `applyThinkingLevelForModel(base, level, model)` | One-call adapter: family resolution + snap + merge — prefer over `applyThinkingLevel` |
91
+ | `parseThinkingLevel(value)` | Known level → canonical; other string → opaque passthrough; empty/non-string → `undefined` (fail closed) |
92
+ | `isSupportedThinkingLevel(model, level)` | Is the level in the model's declared set |
93
+ | `thinkingLevelsForModel(model)` | Declared set or `undefined` |
94
+ | `snapThinkingLevel(model, level)` | Snap to the declared set (nearest, ties up) |
95
+
96
+ New compat families: `google` (`{ thinkingLevel }`) and `output_config_effort` (`{ output_config: { effort } }`). Family inference is stamp-first via `compat.thinkingFamily`.
97
+
98
+ What to do:
99
+
100
+ 1. Replace `applyThinkingLevel(base, level, family)` call sites with `applyThinkingLevelForModel(base, level, model)`.
101
+ 2. Gate level pickers on `model.capabilities?.thinkingLevels` when present.
102
+ 3. Update any body assertion reading Anthropic top-level `effort` to `output_config.effort`.
103
+ 4. Remove xAI "never send reasoning_effort" workarounds.
104
+ 5. Nothing else: `mergeProviderRequestOptions`, `RunOptions`, and persisted shapes are unchanged.
105
+
106
+ Contract reference: [`docs/thinking-and-reasoning.md`](thinking-and-reasoning.md); per-provider declared levels + wire fields on each `docs/providers/*.md` page.
107
+
108
+ ## Upgrade steps
109
+
110
+ 1. Bump every `@arnilo/*` dependency/peer to `^0.5.0`.
111
+ 2. Build; if the compiler flags a removed symbol above, apply the replacement from the table.
112
+ 3. If you host MCP: move SDK imports to the v2 modular packages (section 5).
113
+ 4. If you spawn child processes with ambient env: pass env explicitly (section 2).
114
+ 5. If you construct durable stores: pass the tenant scope (section 2).
115
+ 6. If you set thinking levels: move to `applyThinkingLevelForModel` (section 7).
116
+ 7. Run your suite. No persisted-data migration exists or is needed.
117
+
118
+ ## Rollback
119
+
120
+ Pin the previous version: `@arnilo/prism@0.4.x` (exact pins per package). Nothing persisted
121
+ changes under 0.5, so a pin rollback is safe. The MCP module move (section 5) is the only
122
+ migration that touches host import code — keep a 0.4 pin if you need the monolithic SDK.
package/docs/migration.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Migration guide
2
2
 
3
+ ## 0.4.x → 0.5.0 lockstep cut (breaking)
4
+
5
+ Prism 0.5 (plans 055–065) ships four breaking surfaces: the 27 removed unused exports from the plan 058 sweep (symbol-surface only), the MCP TypeScript SDK v2 module move (plan 063 — hosts importing `@modelcontextprotocol/sdk` directly must move to the modular `client`/`server` 2.0.0 packages), the thinking-effort wire moves (plan 065 — Anthropic `effort` → `output_config.effort`, xAI `reasoning_effort` now sent, snap-on-declared semantics), and the plan 056 hardening behavior changes (tenant-scoped store factories, child env allow-list). Dependency majors (plan 062: pdf-parse 2.4 with Node ≥ 20.16, better-sqlite3 13, keyring 2 with typed locked-store errors) and the CLI real-provider contract (plan 064: explicit `--provider` required, exit 2 when omitted) are behavior changes without import impact. See the complete guide with per-symbol replacements and upgrade steps in [migrate-to-0.5.md](migrate-to-0.5.md). All 10 publishable manifests bump to `0.5.0` lockstep; internal first-party ranges move `^0.4.0` → `^0.5.0`. Security keeper surface (ownership/checkpoint guards, `secureCompare`, `zeroBuffer`, sandbox path-escape guard, RAG scope guard, MCP content-bounds guard, secret-leak conformance assert) is unchanged.
6
+
7
+ ## 0.3.3 → 0.4.0 package reorganization (breaking)
8
+
9
+ Prism 0.4 consolidates package names into explicit family subpaths. It is a dependency and import-specifier migration, not a persisted-data migration. See the complete [legacy 0.3 → 0.4 guide](migrate-to-0.4.md) for all 54 retired package mappings, profile replacements, optional peers/host binaries, security checks, rollback, and npm legacy-warning behavior.
10
+
11
+ ## 0.3.1 → 0.3.2: bounded workflow loop durability (additive, no migration)
12
+
13
+ `@arnilo/prism-workflows@0.3.2` adds the bounded `loopNode` durable extension. It adds optional `WorkflowNodeCheckpoint.iterations` records, each carrying `schemaVersion: 1`, a zero-based `iteration`, stable `iterationId`, and bounded/redacted output. Existing `WorkflowCheckpointValue.schemaVersion` remains `1`; older checkpoints without `iterations` remain readable through the legacy `iteration`/`lastOutput` cursor, and older hosts ignore the additive field. No SQL or generic checkpoint-store migration is required. Replay creates a new run and never mutates source iteration evidence. Hosts using saga compensation keep one saga step/aggregate and register per-iteration compensation by `iterationId` in reverse order.
14
+
15
+ This independent package patch freezes budget accounting: `maxNodes` counts declared DAG nodes once, while loop body executions consume only the required hard-capped `maxIterations` budget. Rollback is package-version rollback; no persisted migration is needed.
16
+
17
+ ## 0.3.2 → 0.3.3: run-ledger prompt provenance (additive, schema version 9)
18
+
19
+ Plan 042 adds an optional typed `promptVersion` ref (`{ name, version, hash }`) to `RunOptions` and `RunRecord`. Hosts resolve a prompt from `@arnilo/prism-prompts` and stamp the run: the ref is copied onto the start/finish ledger records and persisted by the first-party SQLite/PostgreSQL stores as a nullable `prompt_version` JSON column (shared schema migration `009_run_prompt_version`, schema version 8 → 9, forward-only and applied automatically by the adapters' checksummed `prism_migrations`). Strictly additive: unset `promptVersion` produces byte-identical rows and records, legacy rows read back without the field, and no exported declaration was removed. The ref carries identity only (`sha256:` body hash) — prompt bodies stay in the separate `@arnilo/prism-prompts` tables and out of run rows, metadata, and telemetry.
20
+
21
+ ## 0.3.2 → 0.3.3: tool progressive disclosure (additive, no migration)
22
+
23
+ Plan 041 adds opt-in progressive tool loading to `@arnilo/prism`: `toolsDisclosure` (default `"all"`, byte-identical to previous releases) and `toolsSearch.topK` on `AgentConfig` / `RunOptions`, plus the generated `search_tools` tool in search mode. Strictly additive — no exported declaration removed, no persisted shape repurposed. Durable run state gains an optional `sessionState.activatedToolNames` (names only, capped at 128); stores that ignore it resume exactly as before. Set nothing and behavior is unchanged; see [Tools](tools.md#tool-disclosure-progressive-tool-loading).
24
+
25
+ ## 0.3.1 → 0.3.2 memory package: composite recall scoring (additive, no migration)
26
+
27
+ `@arnilo/prism-memory@0.3.2` adds opt-in `RecallOptions.scoring`: sum-normalized similarity/recency/importance blending, with a positive `halfLifeMs` required only when `recencyWeight > 0`. Default recall (no `scoring`) keeps its existing ordering and query count. `MemoryVectorRecord.importance?` persists through an additive nullable `importance REAL` column (`ADD COLUMN IF NOT EXISTS`); legacy NULL rows score neutral `1.0`, so no re-index or data migration is required. At write, hosts may pass a clamped `[0,1]` `entry.importance` or an `importanceFrom` hook over a redacted reflection; it runs once at write, never at recall. Rollback is package-version rollback only: old readers ignore the nullable column, and new readers treat absent values neutrally.
28
+
3
29
  ## 0.3.0 → 0.3.1 production RAG engine (independent patch)
4
30
 
5
31
  Only `@arnilo/prism-rag`, `@arnilo/prism-memory`, and `@arnilo/prism-observability-opentelemetry` move to `0.3.1`. Keep every other first-party package on `^0.3.0` — those ranges already satisfy `0.3.1`.
@@ -796,7 +822,31 @@ Existing text `createA2AHandler({ exposure })`, `client.send()`, and `client.str
796
822
 
797
823
  ## 0.0.7 → 0.0.8 MCP capabilities and sessions
798
824
 
799
- `@arnilo/prism-mcp` now pins official SDK 1.29.0. Existing `connectMcpTools()` and stateless web handlers remain compatible. Use `connectMcpCapabilities()` for bounded resources/prompts and explicit roots/sampling/elicitation callbacks. Server resources/prompts must be selected explicitly and authorize every operation. Stateful Streamable HTTP additionally requires `sessionIdGenerator`, exact `allowedOrigins`, and host `resolveIdentity`; omission preserves stateless mode. `Last-Event-ID` replay is not enabled. Missing capability calls fail with `ERR_PRISM_MCP_UNSUPPORTED_CAPABILITY`.
825
+ `@arnilo/prism-mcp` now pins the modular TypeScript SDK v2 packages (`@modelcontextprotocol/client` + `@modelcontextprotocol/server` 2.0.0). Existing `connectMcpTools()` and stateless web handlers remain compatible. Use `connectMcpCapabilities()` for bounded resources/prompts and explicit callbacks. **Roots (`roots`) and sampling (`sampling`) callbacks are deprecated with protocol revision 2026-07-28 (SEP-2577)** and kept for existing legacy callers only — migrate server-hosted state to explicit tool arguments and host-side model calls; elicitation is the active capability and works across eras (legacy direct `elicitation/create` dispatch, or SDK MRTR `input_required` auto-fulfilment on the modern era, capped by `maxMrtrRounds` with the call timeout as the outer ceiling). Server resources/prompts must be selected explicitly and authorize every operation.
826
+
827
+ HTTP/stdio serving is dual-era: `createPrismMcpWebHandler(factory)` now serves modern 2026-07-28 traffic through SDK `createMcpHandler` (one fresh `McpServer` per request, no `Mcp-Session-Id`, no sticky routing) with the SDK stateless fallback for 2025 traffic, and stays callable while gaining `fetch`/`close`/`notify`/`bus`. Host/origin allowlists are enforced by Prism before body parsing and auth (the SDK entry provides no validation); `maxRequestBytes`, response bounding, concurrency, and request timeouts are unchanged. Configuring `sessionIdGenerator` keeps legacy sessionful serving (identity-bound POST/GET/DELETE/SSE) beside a strict modern handler; a bare `McpServer` instance with sessions is legacy-only — pass a factory for dual-era serving. `servePrismMcpStdio(factory, options)` replaces hand-wired stdio serving with SDK dual-era `serveStdio` (era pinned by the opening exchange, stdout protocol-only). Stateful Streamable HTTP still requires `sessionIdGenerator`, exact `allowedOrigins`, and host `resolveIdentity`. `Last-Event-ID` replay is not enabled. Missing capability calls fail with `ERR_PRISM_MCP_UNSUPPORTED_CAPABILITY`.
828
+
829
+ OAuth client behavior is 2026-07-28 conformant: `finishAuth` now takes the full callback `URLSearchParams` (persisted `state` validated fail-closed, RFC 9207 `iss` checked before the code is redeemed; the bare-code string form remains as the unvalidated legacy path — switch to the params form). Persisted token/client records are SDK issuer-stamped; `McpClientAuthState` credential methods take the validated `issuer` (existing implementations ignoring the parameter keep working for single-server hosts, but **un-stamped pre-upgrade records are refused on issuer-keyed reads** rather than guessed — hosts see one interactive re-authorization, after which every record is stamped). CIMD (SEP-991) is the preferred registration strategy, `dcr` is `@deprecated` with `application_type` defaulting to `"native"`, and `onInsufficientScope: "reauthorize" | "throw"` makes the 403 step-up policy explicit. Server 401 challenges now include the configured `scope`; host token verifiers must validate the token audience.
830
+
831
+ ### Monolithic SDK 1.x → modular SDK v2 migration table
832
+
833
+ | v1 (`@modelcontextprotocol/sdk` 1.30.0) | v2 / current Synapta API | Notes |
834
+ | --- | --- | --- |
835
+ | `new Client(...)` from `sdk/client/index.js` | same name from `@modelcontextprotocol/client` | `ClientOptions` gains `versionNegotiation`, `listChanged`, `inputRequired`, `cachePartition`, `listMaxPages` |
836
+ | `InMemoryTransport` / `StdioClientTransport` / `StreamableHTTPClientTransport` | `InMemoryTransport`/`StreamableHTTPClientTransport` from `@modelcontextprotocol/client`; `StdioClientTransport` from `@modelcontextprotocol/client/stdio` | subpath moves only |
837
+ | `McpServer`, `WebStandardStreamableHTTPServerTransport` from `sdk/server/*` | `@modelcontextprotocol/server` root | `createMcpHandler` (dual-era HTTP) + `serveStdio` (dual-era stdio) replace hand-wired serving |
838
+ | `client.request({method}, Schema, options)` | method-keyed `client.request(...)` or high-level `listTools`/`listResources`/`readResource`/`callTool` | per-page list walks must send an explicit cursor (`{cursor: ""}` on page 0) — the no-cursor form is the SDK's uncapped aggregate |
839
+ | `CompatibilityCallToolResultSchema` (`toolResult` member) | gone from the client codec; `callTool` decodes `CallToolResult` only | draft-era `task` members fail closed at the bridge |
840
+ | `server.setRequestHandler(Schema, handler)` | `setRequestHandler("method/string", handler)` with `ctx.mcpReq.{id,signal}` | `ctx.authInfo` moved to `ctx.http?.authInfo`; unknown tools now error (`-32602`) instead of `isError` results |
841
+ | `sdk/server/auth/types.js` `AuthInfo` | `AuthInfo` from `@modelcontextprotocol/server` | type-only move |
842
+ | `OAuthClientProvider` (string-keyed persistence) | issuer-keyed `StoredOAuthTokens`/`StoredOAuthClientInformation` + optional `ctx: {issuer}` params, `clientMetadataUrl` (CIMD) | see the OAuth paragraph above |
843
+ | `LATEST_PROTOCOL_VERSION` mock fixtures (initialize results) | legacy-era negotiation unchanged; modern (2026-07-28) results need top-level `resultType: "complete"`, `ttlMs`, `cacheScope` | raw mock fixtures only |
844
+
845
+ ### Legacy-session timeline
846
+
847
+ - **Now (default):** `createPrismMcpWebHandler(factory)` without `sessionIdGenerator` is stateless dual-era — modern 2026-07-28 serving plus the SDK stateless fallback for 2025 clients. No `Mcp-Session-Id`, no sticky routing, `Last-Event-ID` replay not enabled.
848
+ - **Now (opt-in):** configuring `sessionIdGenerator` keeps identity-bound legacy sessions (POST/GET/DELETE/SSE beside the strict modern handler) for hosts that still need them; the pairing requires exact `allowedOrigins` and host `resolveIdentity`.
849
+ - **Planned removal:** the legacy session leg is deprecated once Synapta's clients and documented hosts are modern-era; removal lands as a breaking 0.x cut with a migration note here (the `sessionIdGenerator` option disappears and legacy traffic gets the SDK stateless fallback, which 2025 clients already work against). No date is committed in this release.
800
850
 
801
851
  ## 0.0.7 → 0.0.8 OpenTelemetry adapter
802
852
 
@@ -107,14 +107,51 @@ Provider packages register models through `ProviderPackageAPI.registerModel(mode
107
107
 
108
108
  `ModelConfig.compat` remains for provider-owned inert JSON. Prefer typed fields (`capabilities`, `limits`, `cost`, `cache`) for generic behavior shared across providers.
109
109
 
110
+ ### Model-list/capability discovery with provenance
111
+
112
+ `ModelDiscovery` (plan 062) is the normalized `listModels()` seam: it returns the existing `ModelConfig` contract verbatim (id = `model`, context window = `limits`, pricing hint = `cost`) plus `provenance` (`provider`, `fetchedAt` ISO timestamp, `source: "api" | "catalog"`, and `ttlMs` cache guidance). Discovery execution stays provider/host code — Prism core ships only the result types; adapters live in `@arnilo/prism-providers/model-discovery`:
113
+
114
+ ```ts
115
+ import { createOpenAiCompatibleModelDiscovery, createGoogleModelDiscovery, createFakeModelDiscovery, runModelDiscoveryConformance } from "@arnilo/prism-providers/model-discovery";
116
+
117
+ const discovery = createOpenAiCompatibleModelDiscovery({
118
+ baseUrl: "https://gw.internal/v1", // GET <baseUrl>/models
119
+ apiKey, // CredentialValueSource — sent as Bearer, resolved via the existing credential seam
120
+ catalog: registry.list(), // host overrides merged by model id; catalog fields win
121
+ });
122
+ const { models, provenance } = await discovery.listModels({ ttlMs: 3_600_000 });
123
+
124
+ // Independent provider: Google Gemini `GET <baseUrl>/v1beta/models` (x-goog-api-key), paginated:
125
+ const google = createGoogleModelDiscovery({ apiKey: gcpKey });
126
+
127
+ // Network-free fake + conformance for any ModelDiscovery implementation:
128
+ await runModelDiscoveryConformance(() => createFakeModelDiscovery());
129
+ ```
130
+
131
+ - Passthrough normalization: entries the provider does not describe stay bare (`{provider, model}`); there is no hard-coded catalog in core or adapters. Hosts merge catalog overrides (`capabilities`/`limits`/`cost`/`displayName`) over normalized entries by model id via the `catalog` option.
132
+ - Results cache per discovery instance within the configured TTL (default 3,600,000 ms). `listModels()` in loops performs no network until the TTL expires; `ttlMs: 0` forces a refresh. The provenance `fetchedAt`/`ttlMs` fields let hosts layer their own caching on top.
133
+ - `ModelDiscoveryError` is the typed failure (provider label + HTTP status); credentials are resolved through the existing `CredentialValueSource` seam, redacted from every error message, and responses are byte-bounded through the shared provider transport.
134
+
110
135
  ## Security and performance notes
111
136
 
112
137
  - Model metadata must not contain credentials or secrets.
113
138
  - Declare truthful `capabilities.input` tags. Prism core rejects undeclared modalities in `assembleProviderInput()` when the list is present.
114
139
  - Registration is in-memory and O(1) by provider/model key.
115
140
  - `ModelConfig.cache` is declarative capability info only; it does not grant permissions, select tools, or bypass auth.
141
+ - Model-discovery listings are untrusted metadata: normalized entries carry no tool authority, cached results stay per discovery instance (no cross-provider cache bleed), and discovery requests reuse the bounded transport with credential redaction. Catalog overrides come from host-owned registries only — provider responses never write into the host's `ModelRegistry` without host code in between.
116
142
  - Provider-specific behavior belongs in provider packages, not Prism core.
117
143
 
144
+ ## Live probe (discovery adapters)
145
+
146
+ The `@arnilo/prism-providers/model-discovery` adapters have an opt-in live leg that runs `runModelDiscoveryConformance` against a real listing endpoint:
147
+
148
+ ```bash
149
+ PRISM_LIVE_PROVIDER_TESTS=1 OPENAI_API_KEY=... # or GEMINI_API_KEY for the Google route
150
+ node --test packages/prism-providers/dist/model-discovery/__tests__/live.test.js
151
+ ```
152
+
153
+ Chooses the OpenAI-compatible or Google route based on which key is present; neither → skip.
154
+
118
155
  ## Related APIs
119
156
 
120
157
  - [Multimodal content](multimodal-content.md): `audio`/`file`/`document` blocks and `MODEL_INPUT_CAPABILITIES`.
@@ -122,3 +159,4 @@ Provider packages register models through `ProviderPackageAPI.registerModel(mode
122
159
  - [Provider caching](provider-caching.md): `ModelCacheCapabilities` and cache helpers.
123
160
  - [Provider packages](provider-packages.md): package registration of model metadata.
124
161
  - [Public contracts](public-contracts.md): `ModelConfig`, `ModelCost`, and cache type contracts.
162
+ - [Provider layer](provider-layer.md): `ModelDiscovery` adapters, provenance, and TTL semantics.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What it does
4
4
 
5
- `@arnilo/prism-model-router` is an optional governance facade over an existing `ProviderResolver`. It enforces allow-lists, residency, token/cost budgets, rate limits, circuit breaking, and bounded fallbacks before provider selection, and emits redacted selection diagnostics. It does not implement a second provider runtime.
5
+ `@arnilo/prism-core/governance/model-router` is an optional governance facade over an existing `ProviderResolver`. It enforces allow-lists, residency, token/cost budgets, rate limits, circuit breaking, and bounded fallbacks before provider selection, and emits redacted selection diagnostics. It does not implement a second provider runtime.
6
6
 
7
7
  ## When to use it
8
8
 
@@ -57,8 +57,8 @@ Frozen caps (default / hard): attempts `3 / 8`, circuit keys `1,024 / 16,384`, d
57
57
 
58
58
  ```ts
59
59
  import { createAgent, createProviderResolver } from "@arnilo/prism";
60
- import { createModelRouter } from "@arnilo/prism-model-router";
61
- import { createPostgresEnterpriseState } from "@arnilo/prism-enterprise-postgres";
60
+ import { createModelRouter } from "@arnilo/prism-core/governance/model-router";
61
+ import { createPostgresEnterpriseState } from "@arnilo/prism-core/enterprise/postgres";
62
62
 
63
63
  const enterprise = await createPostgresEnterpriseState({ pool, schema: "prism" });
64
64
  const router = createModelRouter({
@@ -91,7 +91,7 @@ await enterprise.close();
91
91
 
92
92
  ## Extension and configuration notes
93
93
 
94
- Router is optional. Chain returned `providerRequestPolicy` with other `ProviderRequestPolicy` values. Wire `onDiagnostics` to `@arnilo/prism-policy` when audit export is required. OpenRouter package behavior is unchanged; routing metadata participates only when this gate allows it.
94
+ Router is optional. Chain returned `providerRequestPolicy` with other `ProviderRequestPolicy` values. Wire `onDiagnostics` to `@arnilo/prism-core/governance/policy` when audit export is required. OpenRouter package behavior is unchanged; routing metadata participates only when this gate allows it.
95
95
 
96
96
  ### Selection policies (0.1.7)
97
97
 
@@ -100,7 +100,7 @@ By default the router tries candidates in input order: the primary model, then
100
100
  `createModelRouter` to rank the candidates before the governance checks run:
101
101
 
102
102
  ```ts
103
- import { createCostLatencySelection, createModelRouter } from "@arnilo/prism-model-router";
103
+ import { createCostLatencySelection, createModelRouter } from "@arnilo/prism-core/governance/model-router";
104
104
 
105
105
  const router = createModelRouter({
106
106
  resolver,
@@ -156,4 +156,4 @@ await router.recordOutcome({ identity, provider, model, success: true, latencyMs
156
156
  - [Policy and audit](policy-and-audit.md)
157
157
  - [Agent identity](agent-identity.md)
158
158
  - [Enterprise PostgreSQL state](enterprise-postgres-state.md): durable router state, migration, cleanup, and ownership requirements.
159
- - Package README: [`@arnilo/prism-model-router`](../packages/model-router/README.md)
159
+ - Package README: [`@arnilo/prism-core`](../packages/prism-core/README.md)
@@ -0,0 +1,117 @@
1
+ # Moderation
2
+
3
+ ## What it does
4
+
5
+ `ModerationProvider` is the provider-neutral content-classification contract:
6
+ `moderate({ input })` sends text to a provider classifier and returns per-category
7
+ `{ score, flagged }` verdicts keyed by a provider-neutral vocabulary (plan 061
8
+ Task 6). Raw provider category/score fields ride along unmodified as `raw` for
9
+ host-side audits. Scores and flagged booleans are **provider output** — core
10
+ bakes in no policy: thresholds, blocking, and routing stay host-owned
11
+ ([host security](host-security.md)). The first-party adapter is
12
+ [`createOpenAIModerationProvider`](providers/openai.md) (`POST /v1/moderations`,
13
+ `omni-moderation-latest`); offline conformance runs via
14
+ `runModerationConformance` from `@arnilo/prism/testing/provider-conformance`.
15
+
16
+ ## When to use it
17
+
18
+ Use it when a host or guardrail seam wants pre-flight or post-hoc text
19
+ classification from a vendor classifier with portable category names. Do not use
20
+ it for local policy enforcement — Prism core never decides what is blocked;
21
+ hosts read `flagged`/`score` and apply their own thresholds.
22
+
23
+ ## Inputs / request
24
+
25
+ | Field | Type | Meaning |
26
+ | --- | --- | --- |
27
+ | `input` | `string \| readonly string[]` | One input per call, or a batch (results match input arity and order). |
28
+ | `model` | `string?` | Classification model; adapter default `omni-moderation-latest`. |
29
+ | `signal` | `AbortSignal?` | Cancellation. |
30
+
31
+ Adapter options: `apiKey` (`CredentialValueSource` — resolved per call, redacted
32
+ from errors), `baseUrl`, `fetch` (fake transport for offline tests), `headers`,
33
+ `model`. Input caps reject typed before network I/O
34
+ (`OPENAI_MODERATION_INPUT_MAX_CHARS`, 100,000 chars — no provider-documented limit,
35
+ conservative ceiling).
36
+
37
+ ## Outputs / response / events
38
+
39
+ | Field | Type | Meaning |
40
+ | --- | --- | --- |
41
+ | `flagged` | `boolean` | Provider's own top-level decision, verbatim. |
42
+ | `categories` | `Record<string, { score, flagged }>` | Verdicts keyed by the neutral vocabulary (see below); unknown raw categories pass through untouched. |
43
+ | `categories[k].score` | `number` | Provider-reported score in [0,1] — never locally recomputed. |
44
+ | `raw` | `JsonObject?` | Provider-native response fields for audits. |
45
+
46
+ Neutral category vocabulary (`MODERATION_CATEGORIES`): `harassment`,
47
+ `harassment/threatening`, `hate`, `hate/threatening`, `illicit`,
48
+ `illicit/violent`, `self-harm`, `self-harm/instructions`, `self-harm/intent`,
49
+ `sexual`, `sexual/minors`, `violence`, `violence/graphic`. The OpenAI adapter
50
+ maps via a data-driven table; vendor categories missing from the table surface
51
+ under their raw names so no provider signal is dropped.
52
+
53
+ Failures throw `ModerationError` with a stable `code`: `empty_input`,
54
+ `input_too_large`, `unsupported_model` (via `assertModerationSupported` when the
55
+ host checks `ModelCapabilities.moderation`), `request_failed` (non-2xx,
56
+ secret-redacted), `response_malformed` (missing `results`, non-numeric scores
57
+ downstream).
58
+
59
+ ## Request/response example
60
+
61
+ ```json
62
+ { "model": "omni-moderation-latest", "input": "text to classify" }
63
+ ```
64
+
65
+ ## Implementation example
66
+
67
+ ```ts
68
+ import { createOpenAIModerationProvider } from "@arnilo/prism-providers/openai";
69
+ import { runModerationConformance } from "@arnilo/prism/testing/provider-conformance";
70
+
71
+ const moderation = createOpenAIModerationProvider({ apiKey: process.env.OPENAI_API_KEY });
72
+ const result = await moderation.moderate({ input: text });
73
+ // Host-owned policy — Prism applies no thresholds:
74
+ if (result.categories.violence?.score ?? 0 > myPolicy.violenceCutoff) { ... }
75
+
76
+ // Batch where the provider allows it (arity- and order-preserving):
77
+ const batch = await moderation.moderate({ input: ["first", "second"] });
78
+
79
+ // Offline conformance (fake transport, no network):
80
+ await runModerationConformance({
81
+ provider: createOpenAIModerationProvider({ apiKey: "sk-test", fetch: fakeFetch }),
82
+ model: "omni-moderation-latest",
83
+ maxInputChars: 100_000,
84
+ sample: { input: "conformance probe" },
85
+ });
86
+ ```
87
+
88
+ ## Extension and configuration notes
89
+
90
+ - Implement `ModerationProvider` for other vendors; the contract is structural —
91
+ no base class, no registry. Keep the data-driven category table per provider;
92
+ never hard-code category logic in call paths.
93
+ - Models declare support with `capabilities.moderation`; hosts gate with
94
+ `modelSupportsModeration` / `assertModerationSupported`, mirroring the
95
+ embeddings/speech/image/video guard pattern.
96
+ - Guardrail seams consume the contract at the host layer: wire `moderate()`
97
+ into request/output inspection stages; core stays policy-free by design.
98
+
99
+ ## Security and performance notes
100
+
101
+ - API keys resolve through the existing `CredentialValueSource` seam and are
102
+ redacted from every thrown error; no new secret paths.
103
+ - Classification responses are read through the bounded JSON reader
104
+ (`OPENAI_MODERATION_MAX_RESPONSE_BYTES`, 8 MiB) — oversized payloads reject
105
+ instead of buffering.
106
+ - One provider request per input; batch inputs loop the same bounded path.
107
+ - Inputs, verdicts, and raw payloads are never logged by core; error messages
108
+ carry status and a redacted body only.
109
+
110
+ ## Related APIs
111
+
112
+ - [Host security](host-security.md): policy ownership — thresholds and blocking
113
+ stay host-side.
114
+ - [Provider conformance](provider-conformance.md): `runModerationConformance`
115
+ and the offline conformance matrix.
116
+ - [Provider packages](provider-packages.md): subpath import rules for
117
+ `@arnilo/prism-providers/openai`.
@@ -0,0 +1,177 @@
1
+ # Multi-agent patterns: handoff, hierarchical crew, supervisor delegation, A2A
2
+
3
+ ## What it does
4
+
5
+ Maps the four Prism answers for "more than one agent" onto one decision table. All four compose existing seams — none introduces a new runtime:
6
+
7
+ - **In-session handoff (swarm)** — agent A transfers control of the ongoing conversation to agent B by calling a host-built `handoff` tool; the host resolves the target `AgentDefinition` with `resolveAgentDefinition` and opens the specialist against the same session (same store + session id, previous run's `leafId`). One transcript, no new session. No helper primitive ships; the tool factory lives in [`examples/handoff-swarm.ts`](../examples/handoff-swarm.ts).
8
+ - **Hierarchical crew** — a manager agent decomposes a goal into typed tasks (`{ tasks: [{ role, instruction }] }`) via structured output ([`Artifact*`](structured-output.md)), fans out to parallel role specialists with bounded `maxFanOut` ([`fanOutNode`](workflows.md)), aggregates deliverables with host reduce ([`joinNode`](workflows.md)), and validates outputs with conditional routing to completion or revision ([`conditionalNode`](workflows.md)). The entire process is a deterministic DAG workflow with zero new runtime primitives. Live demo in [`examples/crew-hierarchy.ts`](../examples/crew-hierarchy.ts).
9
+ - **Supervisor delegation** — `@arnilo/prism-core/runtime/supervisor` `delegate()` invokes allow-listed child agents as bounded runs and returns their result to the parent. Separate child transcripts, hooks, budgets, narrowing.
10
+ - **A2A 1.0** — cross-service interop over the JSON-RPC/HTTPS binding; the remote peer's lifecycle is host-owned behind `A2ATaskLifecycle`.
11
+
12
+ ## When to use it
13
+
14
+ | Pattern | Use when | Conversation boundary | Ownership / identity | Telemetry |
15
+ | --- | --- | --- | --- | --- |
16
+ | In-session handoff | One host, one ongoing conversation; the model decides **when** to transfer; specialists are alternate definitions of the same app | One continuous transcript chain (same store, session id, `leafId`) | Same session scope; give the specialist its own identity via its definition (`AgentConfig.identity` / `RunOptions.identity`) | Attribution is per-run: each `session.run()`'s events/result belong to the active definition — record the swap in host bookkeeping; no `delegated_agent_step` event exists for in-process swaps |
17
+ | Hierarchical crew | A goal requires dynamic decomposition by a manager LLM, parallel execution by role specialists, host aggregation, and conditional validation/revision loop | Workflow DAG execution — each specialist executes a bounded child task session; final deliverable returns to host | Workflow tenant/ownership scopes propagate; specialists activate only their own narrowed `tools` | Workflow node events (`node_started`/`node_finished`/`agent_event`); task attribution per role in the aggregated deliverable |
18
+ | Supervisor delegation | Parent agent needs a child as a *tool call*: bounded budget, hooks that redact/narrow, nested delegation, durable child approvals | Separate runs; child result returns to the parent transcript | Parent identity/effectStore propagate; child factories receive derived resource/thread ids and AND-composed permission | Dedicated `delegation_started/finished/rejected/error` events, projectable through observability `handleDelegation()`; opt-in `delegation_child_event` passthrough |
19
+ | A2A 1.0 | The other agent is owned by a **different service/deployment**; cross-org or cross-cluster; needs durable task lifecycle, push configs, streaming | Protocol boundary (JSON-RPC/HTTPS agent card); replay/reconnect via host-owned task adapter | Exact-origin verified client, `A2AAuthorization` per operation, principal-scoped push configs | Host-owned task adapter records the remote lifecycle; Prism creates no worker/store |
20
+
21
+ Rule of thumb: same conversation → handoff; dynamic task decomposition + parallel execution → hierarchical crew; same process but a subtask → supervisor delegation; different deployment/trust boundary → A2A.
22
+
23
+ ## How in-session handoff works
24
+
25
+ The pattern is a definition swap over existing seams — triage keeps calling `handoff(target)`; the host authorizes, swaps, and continues the same session:
26
+
27
+ ```ts
28
+ // Host-built allow-list tool; untrusted target -> fail-closed tool error result.
29
+ const handoffTool: ToolDefinition = {
30
+ name: "handoff",
31
+ description: "Transfer this conversation to a named specialist agent.",
32
+ parameters: { type: "object", required: ["target"], properties: { target: { type: "string" } } },
33
+ execute(args, ctx): ToolResult {
34
+ const target = String((args as { target: string }).target);
35
+ if (!(target in handoffTargets)) {
36
+ return { toolCallId: ctx.toolCallId, name: "handoff", error: { message: `Unknown handoff target: ${target}` } };
37
+ }
38
+ return { toolCallId: ctx.toolCallId, name: "handoff", value: { transferredTo: target } };
39
+ },
40
+ };
41
+
42
+ // Host authorizes the transfer mid-run: resolve the target definition and
43
+ // continue the SAME session (same store + sessionId). The previous run's
44
+ // leafId carries the transcript pointer; without it the next append forks
45
+ // a sibling branch and the specialist loses the carried context.
46
+ const specialist = await resolveAgentDefinition(handoffTargets[target], {
47
+ tools: [refundTool], // narrowed: no handoff tool unless the host allows it
48
+ overrides: { provider: specialistProvider },
49
+ });
50
+ const specialistSession = createAgentSession({ agent: specialist, store, id: "handoff-demo", leafId: triageRun.leafId });
51
+ ```
52
+
53
+ Live demo: [`examples/handoff-swarm.ts`](../examples/handoff-swarm.ts) — triage → billing transfer with fail-closed unknown target, a specialist whose re-handoff attempt is blocked (`unknown_tool`), and zero provider calls for the swap (it is a registry-level operation).
54
+
55
+ Two non-obvious details the example encodes:
56
+
57
+ 1. **`leafId` carries the transcript pointer.** The specialist session must pass the triage run's `leafId`; creating the session without it appends to a sibling branch and the specialist loses the carried context.
58
+ 2. **Carried context is the transcript chain itself.** Handoff is not delegation: there is no input-payload boundary to sanitize; whatever was said to triage is what the specialist reads.
59
+
60
+ ## How hierarchical crew orchestration works
61
+
62
+ Hierarchical multi-agent orchestration (the CrewAI "Hierarchical Process" pattern) decomposes a high-level goal into structured tasks, assigns each task to a role specialist agent in parallel, aggregates deliverables, and validates the outcome in a deterministic workflow:
63
+
64
+ ```ts
65
+ // 1. Manager produces a typed task plan via structured output.
66
+ // Untrusted model output is validated against the schema before updating state.
67
+ const manager = agentNode({
68
+ agent: "manager",
69
+ input: (ctx) => ({ goal: ctx.workflowInput }),
70
+ output: async (ctx) => {
71
+ const plan = parseTaskPlan(await getSessionOutput(ctx.session));
72
+ if (!plan.ok) throw new Error(`Invalid task plan: ${plan.error}`);
73
+ await ctx.updateState({ plan: plan.value });
74
+ return plan.value;
75
+ },
76
+ });
77
+
78
+ // 2. fan_out maps each task item to its corresponding role specialist.
79
+ const fan = fanOutNode({
80
+ items: (ctx) => (ctx.state.plan as TaskPlan).tasks,
81
+ map: async (task, _index, _ctx) => {
82
+ const agent = await resolveAgentDefinition(definitions[task.role], { tools });
83
+ const session = createAgentSession({ agent });
84
+ const result = await session.run(task.instruction);
85
+ return { role: task.role, result: result.text, attribution: { agent: task.role } };
86
+ },
87
+ maxFanOut: 8,
88
+ });
89
+
90
+ // 3. join aggregates all specialist deliverables and computes per-role attribution.
91
+ const aggregate = joinNode({
92
+ from: "fan",
93
+ reduce: async (items, ctx) => ({
94
+ deliverables: items,
95
+ summary: items.map((d) => `[${d.role}]: ${d.result}`).join("\n"),
96
+ validationPassed: evaluateQuality(items),
97
+ }),
98
+ });
99
+
100
+ // 4. conditional validation routes to completion or revision.
101
+ const validate = conditionalNode({
102
+ when: async (ctx) => Boolean((ctx.upstream.aggregate as AggregatedDeliverable).validationPassed),
103
+ then: ["complete"],
104
+ else: ["revise"],
105
+ });
106
+
107
+ const complete = functionNode({ execute: async (ctx) => formatDeliverable(ctx.upstream.aggregate) });
108
+ const revise = functionNode({ execute: async (ctx) => formatRevision(ctx.upstream.aggregate) });
109
+
110
+ // 5. Entire flow is a single defineWorkflow DAG with fixed revision ID.
111
+ const crewWorkflow = defineWorkflow({
112
+ revision: "crew-demo-1",
113
+ id: "hierarchical-crew",
114
+ nodes: { manager, fan, aggregate, validate, complete, revise },
115
+ edges: [
116
+ ["manager", "fan"],
117
+ ["fan", "aggregate"],
118
+ ["aggregate", "validate"],
119
+ ["validate", "complete"],
120
+ ["validate", "revise"],
121
+ ],
122
+ limits: { maxFanOut: 8, maxConcurrency: 4, maxNodes: 32 },
123
+ });
124
+ ```
125
+
126
+ Live demo: [`examples/crew-hierarchy.ts`](../examples/crew-hierarchy.ts) — manager structured task decomposition, parallel role specialists (`researcher`, `writer`), host reduce aggregation with per-role attribution, and conditional validation/revision routing.
127
+
128
+ ## CrewAI to Prism mapping table
129
+
130
+ | CrewAI Concept | Prism Primitive | Notes & Documentation |
131
+ | --- | --- | --- |
132
+ | **Crew** | Workflow ([`defineWorkflow`](workflows.md)) | A deterministic DAG with explicit revision id, node concurrency, and checkpoint persistence. |
133
+ | **Manager Agent** | Agent Node ([`agentNode`](workflows.md)) + Structured Output ([`generateValidateReviseLoop`](structured-output.md)) | Manager emits a typed `{ tasks: [{ role, instruction }] }` schema via `ArtifactValidator`/`ArtifactParser`. |
134
+ | **Task** | Fan-out item ([`fanOutNode`](workflows.md)) | Bounded dynamic fan-out (`maxFanOut`), mapping each decomposed task to a role specialist session. |
135
+ | **Role Agent (Specialist)** | Agent Definition ([`resolveAgentDefinition`](agent-definitions.md)) | Declarative agent with fail-closed tool narrowing; activated per role during `fan_out.map`. |
136
+ | **Process (Sequential / Hierarchical)** | Workflow DAG ([`defineWorkflow`](workflows.md) / Edges) | Edges define data and execution dependencies; no unconstrained agent-to-agent loops. |
137
+ | **Task Output Aggregation** | Join Node ([`joinNode`](workflows.md) + `reduce`) | Host-controlled reduction aggregating specialist outputs and computing per-role attribution. |
138
+ | **Validation & Quality Review** | Conditional Node ([`conditionalNode`](workflows.md)) | Deterministic branch routing to `complete` or `revise` based on validation criteria. |
139
+ | **Process Revision Loop** | Node Retries / DAG Branching / Loop Node ([`loopNode`](workflows.md)) | Bounded retry/revision path or bounded in-graph loop iteration. |
140
+
141
+ ## Where Prism is stronger
142
+
143
+ - **Durable Human-in-the-Loop (HITL)**: Prism workflows support durable pause and resume via [`suspend()`](workflows.md#durable-suspension-and-resumption) and [`resumeWorkflow()`](workflows.md) across worker restarts or approval gates ([Agent durable approval](agent-session-runtime.md)).
144
+ - **Strict Budget & Concurrency Caps**: Workflows enforce hard limits on `maxNodes`, `maxFanOut`, `maxConcurrency`, and timeout bounds ([Workflow limits](workflows.md)).
145
+ - **Fail-Closed Capability Narrowing**: Specialists receive only their explicitly authorized `tools` via [`resolveAgentDefinition`](agent-definitions.md); managers cannot invoke specialist tools directly, preventing accidental tool leakage.
146
+ - **Untrusted Model Output Validation**: Manager task plans are treated as untrusted LLM output and validated against a typed schema before triggering fan-out ([Structured output](structured-output.md)).
147
+ - **Durable Audit & Telemetry**: Every node start/finish and agent event is emitted with deterministic sequence numbers and can be persisted to signed audit ledgers ([Policy and audit](policy-and-audit.md), [Observability](observability.md)).
148
+
149
+ ## Security and performance notes
150
+
151
+ - **Transfers are explicit model-initiated, host-authorized.** The `handoff` tool exists on the triage agent's allow-list only; the target name is validated against the host-authored targets map before any definition resolves. Unknown names fail closed as a standard tool error (`Unknown handoff target: <name>`).
152
+ - **No permission escalation through handoff or delegation.** The specialist's capabilities come solely from its own `AgentDefinition` as resolved by `resolveAgentDefinition` (fail-closed for omitted capabilities). Handoff or fan-out grants nothing: tools/identity are what the host put on that definition. The specialist cannot invoke manager tools unless its definition explicitly includes them — the standard `unknown_tool` block applies otherwise.
153
+ - **Narrowing on transfer, never widening.** If the specialist needs the caller's verified identity, project it through `narrowIdentity` / `assertIdentityPropagation` ([Agent identity](agent-identity.md)) so scopes and tenant cannot widen across the swap. For delegation the same discipline is built in (`narrowIdentity`, AND-composed policies); for A2A the exact-origin client plus per-operation authorization is the boundary.
154
+ - **Manager-generated task plans are untrusted model output.** Manager plan outputs are validated against the typed schema via `ArtifactValidator` before being persisted to workflow state or dispatched to `fan_out`. Malformed or invalid plans trigger the artifact repair loop or fail closed before any specialist is invoked.
155
+ - **Redaction of carried context.** Handoff carries the raw transcript by design — same rows a human replay would read. Apply the session egress seams on the way out: `redactSessionEntry` / `redactMessage` with a host field policy (see [Data classification](data-classification.md)) and `AgentConfig.redactor`; for durable replay across tenants reuse the redacted transcript seam discipline used by ACP `sessions.transcript` ([ACP interop](acp.md)).
156
+ - **Telemetry attribution.** Which agent produced which turn is not stored on message entries; the host knows (it performed the swap or aggregated fan-out results) and should pin it per run via `RunOptions.identity` (principal kind `agent`) so `identityTelemetryAttributes` (`prism.identity.*`) carries redacted attribution on telemetry, or via observability metadata. Supervisor runs emit dedicated `delegation_*` events; an in-process definition swap has no session seam to emit one, so the host records attribution.
157
+ - **Performance.** The swap performs zero provider calls; it costs one registry resolution plus one session open (~sub-millisecond in the example fixture). The transferred turn costs what any tool round costs.
158
+
159
+ ## Extension and configuration notes
160
+
161
+ - Handoff targets may be code-defined `AgentDefinition` objects or `<configRoot>/agents/<name>/AGENT.md` bundles resolved via `resolveAgentBundle` — the allow-list maps names to either.
162
+ - Hosts wanting the pattern behind a UI timeline can emit their own step events from the swap.
163
+ - A reusable in-session handoff helper was evaluated and **not** shipped in 0.3.x: the unavoidable boilerplate is a ~20-line allow-list tool plus one `createAgentSession` call. Revisit only if multiple hosts show materially different swap semantics.
164
+ - Hierarchical crew patterns compose entirely on existing `@arnilo/prism-core/runtime/workflows` and `@arnilo/prism` primitives (`agentNode`, `fanOutNode`, `joinNode`, `conditionalNode`, `ArtifactValidator`, `resolveAgentDefinition`); no separate helper package is needed.
165
+
166
+ ## Related APIs
167
+
168
+ - [Workflows](workflows.md): `defineWorkflow`, `fanOutNode`, `joinNode`, `conditionalNode`, `runWorkflow`.
169
+ - [Structured output](structured-output.md): `ArtifactParser`, `ArtifactValidator`, `generateValidateReviseLoop`.
170
+ - [Agent definitions](agent-definitions.md): `resolveAgentDefinition` and fail-closed capability activation — the swap seam itself.
171
+ - [Supervisor delegation](supervisors.md): same-process subtasks with budgets, hooks, and durable nested approvals.
172
+ - [A2A interoperability](a2a.md): the cross-service protocol boundary.
173
+ - [Agent identity](agent-identity.md): verified identity propagation and narrowing (`narrowIdentity`, `assertIdentityPropagation`).
174
+ - [Agent events](agent-events.md): `delegated_agent_step` and delegation event surfaces for timelines.
175
+ - [Policy and audit](policy-and-audit.md): decision ledger, approval gates, and signed audit export.
176
+ - [Observability](observability.md): OpenTelemetry agent/provider/tool hierarchy and metrics.
177
+ - [Middleware hooks](middleware-hooks.md): context bridging and redaction without permission grants.