@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
package/dist/rpc.d.ts CHANGED
@@ -8,7 +8,7 @@ export interface RpcRequest {
8
8
  readonly params?: Record<string, unknown>;
9
9
  }
10
10
  export interface RpcSessionFactory {
11
- createSession(id?: string): AgentSession;
11
+ createSession(id?: string): AgentSession | Promise<AgentSession>;
12
12
  readonly commands?: readonly CommandDefinition[];
13
13
  /** Host-opt-in driver capabilities forwarded to contributed commands on the
14
14
  * `command` execution context. Absent ⇒ context shape unchanged. */
package/dist/rpc.js CHANGED
@@ -2,7 +2,7 @@ import { createInterface } from "node:readline";
2
2
  import { resolveInstructionInjectors } from "./instruction-injection.js";
3
3
  import { errorToErrorInfo } from "./redaction.js";
4
4
  export async function runRpcServer(options) {
5
- const first = options.createSession();
5
+ const first = await options.createSession();
6
6
  const state = {
7
7
  current: first,
8
8
  currentHandleId: first.id,
@@ -140,7 +140,7 @@ async function handleRequest(request, state, stdout, activeRuns) {
140
140
  const handleId = stringParam(request.params, "handleId") ?? stringParam(request.params, "sessionId") ?? stringParam(request.params, "id");
141
141
  if (!handleId)
142
142
  throw new Error("switchSession requires params.handleId (or sessionId)");
143
- const session = state.sessions.get(handleId) ?? makeSession(state, handleId);
143
+ const session = state.sessions.get(handleId) ?? (await makeSession(state, handleId));
144
144
  state.current = session;
145
145
  state.currentHandleId = handleId;
146
146
  write(stdout, { id: request.id, ok: true, result: { sessionId: session.id, leafId: session.leafId, handleId } });
@@ -220,8 +220,8 @@ function registerSession(state, session, preferredHandleId) {
220
220
  state.sessions.set(handleId, session);
221
221
  return handleId;
222
222
  }
223
- function makeSession(state, id) {
224
- const session = state.createSession(id);
223
+ async function makeSession(state, id) {
224
+ const session = await state.createSession(id);
225
225
  state.sessions.set(session.id, session);
226
226
  return session;
227
227
  }
@@ -1,6 +1,6 @@
1
1
  import type { PersistencePage, SessionEntry, SessionEntryQuery } from "../contracts.js";
2
2
  /** Current shared persistence schema version for production database adapters. */
3
- export declare const PERSISTENCE_SCHEMA_VERSION = 8;
3
+ export declare const PERSISTENCE_SCHEMA_VERSION = 9;
4
4
  export type PersistenceTableName = "prism_tenants" | "prism_accounts" | "prism_users" | "prism_agent_definitions" | "prism_sessions" | "prism_branches" | "prism_session_entries" | "prism_session_append_idempotency" | "prism_runs" | "prism_agent_events" | "prism_agent_event_streams" | "prism_tool_calls" | "prism_usage" | "prism_run_feedback" | "prism_retention_policies" | "prism_legal_holds" | "prism_tenant_quotas" | "prism_migrations";
5
5
  export type PersistenceColumnType = "text" | "integer" | "number" | "boolean" | "json" | "timestamp";
6
6
  export interface PersistenceColumnDefinition {
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
4
4
  // this module defines the shared table/index/pagination/migration expectations
5
5
  // adapter authors implement and test against before shipping dialect-specific DDL.
6
6
  /** Current shared persistence schema version for production database adapters. */
7
- export const PERSISTENCE_SCHEMA_VERSION = 8;
7
+ export const PERSISTENCE_SCHEMA_VERSION = 9;
8
8
  /** Guidance adapters must follow: values are bound parameters, never interpolated. */
9
9
  export const PARAMETERIZED_QUERY_GUIDANCE = "Bind every user-supplied value (session ids, idempotency keys, tenant ids, timestamps, JSON payloads) as a query parameter. Quote/validate schema and table identifiers only; never interpolate untrusted strings into SQL text.";
10
10
  const TENANT_COLUMNS = [
@@ -157,6 +157,7 @@ export function createPersistenceSchemaModel() {
157
157
  { name: "error", type: "json", nullable: true },
158
158
  ...TENANT_COLUMNS,
159
159
  { name: "metadata", type: "json", nullable: true },
160
+ { name: "prompt_version", type: "json", nullable: true },
160
161
  ],
161
162
  foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
162
163
  },
@@ -502,33 +503,35 @@ function migrationStep(version, name, description) {
502
503
  ? { indexes: ["prism_agent_events_owner_timestamp_sequence_idx"] }
503
504
  : version === 8
504
505
  ? { table: "prism_sessions", columns: ["version"] }
505
- : version === 6
506
- ? {
507
- tables: ["prism_agent_event_streams"],
508
- indexes: ["prism_agent_events_run_sequence_idx"],
509
- }
510
- : version === 2
511
- ? { table: "prism_usage", columns: ["scope", "turn", "attempt"], indexes: ["prism_usage_session_scope_recorded_idx"] }
512
- : version === 3
513
- ? {
514
- tables: ["prism_run_feedback"],
515
- indexes: model.indexes.filter((index) => index.name.startsWith("prism_run_feedback_")).map((index) => index.name),
516
- }
517
- : version === 4
518
- ? // Adapter-local FTS objects (SQLite FTS5 / Postgres tsvector) map to this canonical name.
519
- { search: ["prism_session_search"], indexes: ["prism_sessions_updated_id_idx"] }
520
- : version === 5
521
- ? {
522
- tables: ["prism_legal_holds", "prism_tenant_quotas"],
523
- indexes: [
524
- "prism_legal_holds_owner_resource_idx",
525
- "prism_legal_holds_created_id_idx",
526
- "prism_tenant_quotas_owner_kind_idx",
527
- ],
528
- }
529
- : (() => {
530
- throw new Error(`Unknown migration version ${version}`);
531
- })();
506
+ : version === 9
507
+ ? { table: "prism_runs", columns: ["prompt_version"] }
508
+ : version === 6
509
+ ? {
510
+ tables: ["prism_agent_event_streams"],
511
+ indexes: ["prism_agent_events_run_sequence_idx"],
512
+ }
513
+ : version === 2
514
+ ? { table: "prism_usage", columns: ["scope", "turn", "attempt"], indexes: ["prism_usage_session_scope_recorded_idx"] }
515
+ : version === 3
516
+ ? {
517
+ tables: ["prism_run_feedback"],
518
+ indexes: model.indexes.filter((index) => index.name.startsWith("prism_run_feedback_")).map((index) => index.name),
519
+ }
520
+ : version === 4
521
+ ? // Adapter-local FTS objects (SQLite FTS5 / Postgres tsvector) map to this canonical name.
522
+ { search: ["prism_session_search"], indexes: ["prism_sessions_updated_id_idx"] }
523
+ : version === 5
524
+ ? {
525
+ tables: ["prism_legal_holds", "prism_tenant_quotas"],
526
+ indexes: [
527
+ "prism_legal_holds_owner_resource_idx",
528
+ "prism_legal_holds_created_id_idx",
529
+ "prism_tenant_quotas_owner_kind_idx",
530
+ ],
531
+ }
532
+ : (() => {
533
+ throw new Error(`Unknown migration version ${version}`);
534
+ })();
532
535
  return {
533
536
  version,
534
537
  name,
@@ -550,6 +553,7 @@ export function createPersistenceMigrationContract() {
550
553
  migrationStep(6, "006_agent_event_source", "Add transactional per-run event counters and unique durable event sequencing."),
551
554
  migrationStep(7, "007_agent_event_retention_index", "Add an exact-owner durable-event retention cleanup index."),
552
555
  migrationStep(8, "008_session_version", "Add a NOT NULL DEFAULT 0 version column to prism_sessions for appendSession metadata CAS."),
556
+ migrationStep(9, "009_run_prompt_version", "Add a nullable prompt_version JSON provenance ref column to prism_runs."),
553
557
  ],
554
558
  lockGuidance: "Acquire a dialect-specific migration lock before applying steps (PostgreSQL advisory lock; SQLite exclusive transaction). Only one process should migrate at a time.",
555
559
  leastPrivilegeGuidance: "Run migrations with a DDL-capable role; use a separate least-privilege runtime role limited to INSERT/SELECT/UPDATE on adapter tables. Never grant migration credentials to the agent runtime.",
@@ -1,4 +1,4 @@
1
- import type { AIProvider, ContentBlock, JsonObject, ProviderEvent, ProviderRequest, ToolCallContent, Usage } from "../contracts.js";
1
+ import type { AIProvider, BatchJobsProvider, BatchRequestItem, ContentBlock, EmbeddingsProvider, EmbeddingsResult, ImageGenerationProvider, ImageGenerationResult, JsonObject, ModerationProvider, ModerationResult, ProviderEvent, ProviderRequest, SpeechProvider, SpeechResult, ToolCallContent, TranscriptionProvider, TranscriptionResult, Usage, VideoGenerationJob, VideoGenerationProvider } from "../contracts.js";
2
2
  export interface ProviderStreamConformanceOptions {
3
3
  readonly provider: AIProvider;
4
4
  readonly request: ProviderRequest;
@@ -29,10 +29,6 @@ export interface ProviderHeaderOwnershipConformanceOptions {
29
29
  /** Caller-supplied headers, including attempts to override owned names and non-owned additions. */
30
30
  readonly caller: Readonly<Record<string, string>>;
31
31
  }
32
- export interface ProviderSecretLeakConformanceOptions {
33
- readonly events: readonly ProviderEvent[];
34
- readonly secrets: readonly string[];
35
- }
36
32
  export declare function collectProviderEvents(provider: AIProvider, request: ProviderRequest): Promise<readonly ProviderEvent[]>;
37
33
  export declare function assertProviderStreamConforms(options: ProviderStreamConformanceOptions): Promise<readonly ProviderEvent[]>;
38
34
  export declare function assertAbortIsObserved(options: ProviderAbortConformanceOptions): Promise<void>;
@@ -51,3 +47,116 @@ export declare function assertNoForeignCacheFields(body: unknown, allowed?: read
51
47
  /** Provider construction and setup must perform zero network calls; discovery and streams are caller-gated. */
52
48
  export declare function assertNoFetches(calls: readonly unknown[]): void;
53
49
  export declare function assertUsageAccounting(events: readonly ProviderEvent[], expected: Usage): Usage;
50
+ export interface EmbeddingsConformanceOptions {
51
+ readonly provider: EmbeddingsProvider;
52
+ readonly model: string;
53
+ /** When set, an oversized batch must fail with `EmbeddingsError("batch_too_large")`. */
54
+ readonly maxBatchSize?: number;
55
+ /** Happy-path mapping probe: inputs to embed and the expected vector count/dimensions. */
56
+ readonly sample?: {
57
+ readonly inputs: readonly string[];
58
+ readonly dimensions?: number;
59
+ };
60
+ }
61
+ /** Offline conformance for any `EmbeddingsProvider` (plan 061): typed empty-input
62
+ * error, typed oversized-batch error, and input-order vector mapping with finite
63
+ * coordinates. No network — the caller supplies the provider (real adapter with a
64
+ * fake transport, or a fake provider). */
65
+ export declare function runEmbeddingsConformance(options: EmbeddingsConformanceOptions): Promise<EmbeddingsResult | undefined>;
66
+ export interface SpeechConformanceOptions {
67
+ readonly provider: SpeechProvider;
68
+ readonly model: string;
69
+ /** When set, an oversized input must fail with `SpeechError("input_too_large")`. */
70
+ readonly maxInputChars?: number;
71
+ /** Happy-path probe: text to synthesize, optional voice/format. */
72
+ readonly sample?: {
73
+ readonly input?: string;
74
+ readonly voice?: string;
75
+ readonly format?: string;
76
+ };
77
+ }
78
+ /** Offline conformance for any `SpeechProvider` (plan 061): typed empty-input and
79
+ * oversized-input errors, byte results, and stream ordering — the stream must emit
80
+ * at least one chunk before closing. Asserts event order, never wall clock. */
81
+ export declare function runSpeechConformance(options: SpeechConformanceOptions): Promise<SpeechResult | undefined>;
82
+ export interface TranscriptionConformanceOptions {
83
+ readonly provider: TranscriptionProvider;
84
+ readonly model: string;
85
+ /** When set, oversized audio must fail with `TranscriptionError("audio_too_large")`. */
86
+ readonly maxAudioBytes?: number;
87
+ /** Happy-path probe: audio bytes and the expected transcript prefix. */
88
+ readonly sample?: {
89
+ readonly audio?: Uint8Array;
90
+ readonly format?: string;
91
+ readonly textIncludes?: string;
92
+ };
93
+ }
94
+ /** Offline conformance for any `TranscriptionProvider` (plan 061): typed empty-audio
95
+ * and oversized-audio errors, one-shot text, and stream ordering — at least one
96
+ * `transcript_delta` partial, then exactly one `done` terminal event. */
97
+ export declare function runTranscriptionConformance(options: TranscriptionConformanceOptions): Promise<TranscriptionResult | undefined>;
98
+ export interface ImageGenerationConformanceOptions {
99
+ readonly provider: ImageGenerationProvider;
100
+ readonly model: string;
101
+ /** When set, an oversized prompt must fail with `ImageGenerationError("input_too_large")`. */
102
+ readonly maxPromptChars?: number;
103
+ /** Happy-path probe: prompt, requested image count, expected provenance. */
104
+ readonly sample?: {
105
+ readonly prompt?: string;
106
+ readonly size?: string;
107
+ readonly count?: number;
108
+ };
109
+ }
110
+ /** Offline conformance for any `ImageGenerationProvider` (plan 061): typed empty-input
111
+ * and oversized-prompt errors, plus image shape — non-empty bytes, an `image/*` mime
112
+ * type, and preserved provenance (`provider`/`model`) on every image. */
113
+ export declare function runImageGenerationConformance(options: ImageGenerationConformanceOptions): Promise<ImageGenerationResult | undefined>;
114
+ export interface VideoGenerationConformanceOptions {
115
+ readonly provider: VideoGenerationProvider;
116
+ readonly model: string;
117
+ /** When set, an oversized prompt must fail with `VideoGenerationError("input_too_large")`. */
118
+ readonly maxPromptChars?: number;
119
+ /** Happy-path lifecycle probe: submit, poll to terminal state, assert result shape. */
120
+ readonly sample?: {
121
+ readonly prompt?: string;
122
+ readonly maxPolls?: number;
123
+ };
124
+ }
125
+ /** Offline conformance for any `VideoGenerationProvider` (plan 061): typed
126
+ * empty-input and oversized-prompt errors, plus the submit→status lifecycle —
127
+ * a job id is returned, polling reaches a terminal state, and succeeded jobs
128
+ * carry a video with provenance (`provider`/`model`) and at least one source
129
+ * (`bytes` or `url`). */
130
+ export declare function runVideoGenerationConformance(options: VideoGenerationConformanceOptions): Promise<VideoGenerationJob | undefined>;
131
+ export interface ModerationConformanceOptions {
132
+ readonly provider: ModerationProvider;
133
+ readonly model: string;
134
+ /** When set, an oversized input must fail with `ModerationError("input_too_large")`. */
135
+ readonly maxInputChars?: number;
136
+ /** Happy-path probe: a benign string classification. */
137
+ readonly sample?: {
138
+ readonly input?: string;
139
+ };
140
+ }
141
+ /** Offline conformance for any `ModerationProvider` (plan 061): typed empty-input
142
+ * and oversized-input errors, plus a classification probe — every category
143
+ * verdict carries a numeric score in [0,1], a boolean `flagged`, and the
144
+ * top-level `flagged` boolean is present. Scores are provider output; conformance
145
+ * asserts no local policy decisions are baked in. */
146
+ export declare function runModerationConformance(options: ModerationConformanceOptions): Promise<ModerationResult | undefined>;
147
+ export interface BatchJobsConformanceOptions {
148
+ readonly provider: BatchJobsProvider;
149
+ /** Sample lifecycle driver: submitted via `submit` and expected to reach `completed`. */
150
+ readonly sample?: {
151
+ readonly model: string;
152
+ readonly requests: readonly BatchRequestItem[];
153
+ };
154
+ /** When set, a submit over this count must fail with `BatchJobsError("too_many_requests")`. */
155
+ readonly maxRequests?: number;
156
+ }
157
+ /** Offline conformance for any `BatchJobsProvider` (plan 061): typed empty/oversized
158
+ * submit errors, opaque job ids, status returning members of the neutral state
159
+ * union, terminal resolution via `pollBatch` (plain utility), and paged results
160
+ * that walk to exhaustion with cursor continuity. Failure/cancel terminal
161
+ * transitions are covered by provider fakes in adapter test suites. */
162
+ export declare function runBatchJobsConformance(options: BatchJobsConformanceOptions): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { BatchJobsError, EmbeddingsError, ImageGenerationError, ModerationError, pollBatch, SpeechError, TranscriptionError, VideoGenerationError, } from "../contracts.js";
1
2
  import { reconstructToolCallDeltas } from "../provider-events.js";
2
3
  import { canonicalizeJsonSchema } from "../providers/schema.js";
3
4
  export async function collectProviderEvents(provider, request) {
@@ -194,4 +195,345 @@ function jsonPrimitives(value) {
194
195
  function textFrom(events) {
195
196
  return events.map((event) => (event.type === "content_delta" && event.content.type === "text" ? event.content.text : "")).join("");
196
197
  }
198
+ /** Offline conformance for any `EmbeddingsProvider` (plan 061): typed empty-input
199
+ * error, typed oversized-batch error, and input-order vector mapping with finite
200
+ * coordinates. No network — the caller supplies the provider (real adapter with a
201
+ * fake transport, or a fake provider). */
202
+ export async function runEmbeddingsConformance(options) {
203
+ await assertEmbeddingsErrorCode(() => options.provider.embedMany({ model: options.model, inputs: [] }), "empty_input", "empty inputs must reject with EmbeddingsError(empty_input)");
204
+ if (options.maxBatchSize !== undefined) {
205
+ const inputs = Array.from({ length: options.maxBatchSize + 1 }, (_, i) => `input-${i}`);
206
+ await assertEmbeddingsErrorCode(() => options.provider.embedMany({ model: options.model, inputs }), "batch_too_large", `batches over ${options.maxBatchSize} must reject with EmbeddingsError(batch_too_large)`);
207
+ }
208
+ if (!options.sample)
209
+ return undefined;
210
+ const result = await options.provider.embedMany({ model: options.model, inputs: options.sample.inputs });
211
+ if (result.vectors.length !== options.sample.inputs.length)
212
+ throw new Error(`vector count ${result.vectors.length} must match input count ${options.sample.inputs.length}`);
213
+ for (const vector of result.vectors) {
214
+ if (!Array.isArray(vector) || vector.length === 0 || !vector.every(Number.isFinite))
215
+ throw new Error("vectors must be non-empty arrays of finite numbers");
216
+ }
217
+ if (!(result.dimensions > 0))
218
+ throw new Error("result.dimensions must be positive");
219
+ if (options.sample.dimensions !== undefined && result.dimensions !== options.sample.dimensions)
220
+ throw new Error(`result.dimensions ${result.dimensions} must match expected ${options.sample.dimensions}`);
221
+ return result;
222
+ }
223
+ async function assertEmbeddingsErrorCode(run, code, label) {
224
+ let error;
225
+ try {
226
+ await run();
227
+ }
228
+ catch (caught) {
229
+ error = caught;
230
+ }
231
+ if (!(error instanceof EmbeddingsError))
232
+ throw new Error(`${label}; got ${error === undefined ? "a successful result" : String(error)}`);
233
+ if (error.code !== code)
234
+ throw new Error(`${label}; got code ${error.code} (${error.message})`);
235
+ }
236
+ /** Offline conformance for any `SpeechProvider` (plan 061): typed empty-input and
237
+ * oversized-input errors, byte results, and stream ordering — the stream must emit
238
+ * at least one chunk before closing. Asserts event order, never wall clock. */
239
+ export async function runSpeechConformance(options) {
240
+ const input = options.sample?.input ?? "conformance";
241
+ await assertSpeechErrorCode(() => options.provider.synthesize({ model: options.model, input: "" }), "empty_input", "empty input must reject with SpeechError(empty_input)");
242
+ if (options.maxInputChars !== undefined) {
243
+ await assertSpeechErrorCode(() => options.provider.synthesize({ model: options.model, input: "x".repeat(options.maxInputChars + 1) }), "input_too_large", `inputs over ${options.maxInputChars} chars must reject with SpeechError(input_too_large)`);
244
+ }
245
+ if (!options.sample)
246
+ return undefined;
247
+ const request = {
248
+ model: options.model,
249
+ input,
250
+ ...(options.sample.voice ? { voice: options.sample.voice } : {}),
251
+ ...(options.sample.format ? { format: options.sample.format } : {}),
252
+ };
253
+ const result = await options.provider.synthesize(request);
254
+ if (!(result.audio instanceof Uint8Array) || result.audio.byteLength === 0)
255
+ throw new Error("synthesize must return non-empty audio bytes");
256
+ if (typeof result.format !== "string" || result.format.length === 0)
257
+ throw new Error("result.format must be a non-empty string");
258
+ const streamed = await options.provider.synthesizeStream(request);
259
+ const reader = streamed.audio.getReader();
260
+ let chunks = 0;
261
+ let firstChunkBeforeClose = false;
262
+ while (true) {
263
+ const { done, value } = await reader.read();
264
+ if (done)
265
+ break;
266
+ if (chunks === 0 && value.byteLength > 0)
267
+ firstChunkBeforeClose = true;
268
+ chunks += 1;
269
+ }
270
+ if (chunks === 0 || !firstChunkBeforeClose)
271
+ throw new Error("synthesizeStream must emit at least one non-empty chunk before closing");
272
+ return result;
273
+ }
274
+ async function assertSpeechErrorCode(run, code, label) {
275
+ let error;
276
+ try {
277
+ await run();
278
+ }
279
+ catch (caught) {
280
+ error = caught;
281
+ }
282
+ if (!(error instanceof SpeechError))
283
+ throw new Error(`${label}; got ${error === undefined ? "a successful result" : String(error)}`);
284
+ if (error.code !== code)
285
+ throw new Error(`${label}; got code ${error.code} (${error.message})`);
286
+ }
287
+ /** Offline conformance for any `TranscriptionProvider` (plan 061): typed empty-audio
288
+ * and oversized-audio errors, one-shot text, and stream ordering — at least one
289
+ * `transcript_delta` partial, then exactly one `done` terminal event. */
290
+ export async function runTranscriptionConformance(options) {
291
+ const audio = options.sample?.audio ?? new Uint8Array([1, 2, 3]);
292
+ await assertTranscriptionErrorCode(() => options.provider.transcribe({ model: options.model, audio: new Uint8Array(0) }), "empty_input", "empty audio must reject with TranscriptionError(empty_input)");
293
+ if (options.maxAudioBytes !== undefined) {
294
+ await assertTranscriptionErrorCode(() => options.provider.transcribe({ model: options.model, audio: new Uint8Array(options.maxAudioBytes + 1) }), "audio_too_large", `audio over ${options.maxAudioBytes} bytes must reject with TranscriptionError(audio_too_large)`);
295
+ }
296
+ if (!options.sample)
297
+ return undefined;
298
+ const request = {
299
+ model: options.model,
300
+ audio,
301
+ ...(options.sample.format ? { format: options.sample.format } : {}),
302
+ };
303
+ const result = await options.provider.transcribe(request);
304
+ if (typeof result.text !== "string")
305
+ throw new Error("transcribe must return string text");
306
+ if (options.sample.textIncludes !== undefined && !result.text.includes(options.sample.textIncludes))
307
+ throw new Error(`transcript ${JSON.stringify(result.text)} must include ${JSON.stringify(options.sample.textIncludes)}`);
308
+ let deltas = 0;
309
+ let doneEvents = 0;
310
+ for await (const event of options.provider.transcribeStream(request)) {
311
+ if (event.type === "transcript_delta") {
312
+ if (typeof event.text !== "string")
313
+ throw new Error("transcript_delta must carry string text");
314
+ deltas += 1;
315
+ }
316
+ else if (event.type === "done") {
317
+ doneEvents += 1;
318
+ if (typeof event.text !== "string")
319
+ throw new Error("done must carry string text");
320
+ }
321
+ else {
322
+ throw new Error(`unexpected transcript event ${event.type}`);
323
+ }
324
+ }
325
+ if (deltas === 0)
326
+ throw new Error("transcribeStream must yield at least one transcript_delta before done");
327
+ if (doneEvents !== 1)
328
+ throw new Error(`transcribeStream must yield exactly one done event; got ${doneEvents}`);
329
+ return result;
330
+ }
331
+ async function assertTranscriptionErrorCode(run, code, label) {
332
+ let error;
333
+ try {
334
+ await run();
335
+ }
336
+ catch (caught) {
337
+ error = caught;
338
+ }
339
+ if (!(error instanceof TranscriptionError))
340
+ throw new Error(`${label}; got ${error === undefined ? "a successful result" : String(error)}`);
341
+ if (error.code !== code)
342
+ throw new Error(`${label}; got code ${error.code} (${error.message})`);
343
+ }
344
+ /** Offline conformance for any `ImageGenerationProvider` (plan 061): typed empty-input
345
+ * and oversized-prompt errors, plus image shape — non-empty bytes, an `image/*` mime
346
+ * type, and preserved provenance (`provider`/`model`) on every image. */
347
+ export async function runImageGenerationConformance(options) {
348
+ await assertImageGenerationErrorCode(() => options.provider.generate({ model: options.model, prompt: "" }), "empty_input", "empty prompts must reject with ImageGenerationError(empty_input)");
349
+ if (options.maxPromptChars !== undefined) {
350
+ await assertImageGenerationErrorCode(() => options.provider.generate({ model: options.model, prompt: "x".repeat(options.maxPromptChars + 1) }), "input_too_large", `prompts over ${options.maxPromptChars} chars must reject with ImageGenerationError(input_too_large)`);
351
+ }
352
+ if (!options.sample)
353
+ return undefined;
354
+ const result = await options.provider.generate({
355
+ model: options.model,
356
+ prompt: options.sample.prompt ?? "conformance cube",
357
+ ...(options.sample.size ? { size: options.sample.size } : {}),
358
+ ...(options.sample.count ? { count: options.sample.count } : {}),
359
+ });
360
+ if (result.images.length === 0)
361
+ throw new Error("generate must return at least one image");
362
+ if (options.sample.count !== undefined && result.images.length !== options.sample.count)
363
+ throw new Error(`image count ${result.images.length} must match requested ${options.sample.count}`);
364
+ for (const image of result.images) {
365
+ if (!(image.bytes instanceof Uint8Array) || image.bytes.byteLength === 0)
366
+ throw new Error("generated images must carry non-empty bytes");
367
+ if (typeof image.mimeType !== "string" || !image.mimeType.startsWith("image/"))
368
+ throw new Error(`image mime type ${image.mimeType} must be image/*`);
369
+ if (image.provider !== options.provider.id)
370
+ throw new Error(`image provenance provider ${image.provider} must be preserved (${options.provider.id})`);
371
+ if (image.model !== options.model)
372
+ throw new Error(`image provenance model ${image.model} must be preserved (${options.model})`);
373
+ }
374
+ return result;
375
+ }
376
+ async function assertImageGenerationErrorCode(run, code, label) {
377
+ let error;
378
+ try {
379
+ await run();
380
+ }
381
+ catch (caught) {
382
+ error = caught;
383
+ }
384
+ if (!(error instanceof ImageGenerationError))
385
+ throw new Error(`${label}; got ${error === undefined ? "a successful result" : String(error)}`);
386
+ if (error.code !== code)
387
+ throw new Error(`${label}; got code ${error.code} (${error.message})`);
388
+ }
389
+ /** Offline conformance for any `VideoGenerationProvider` (plan 061): typed
390
+ * empty-input and oversized-prompt errors, plus the submit→status lifecycle —
391
+ * a job id is returned, polling reaches a terminal state, and succeeded jobs
392
+ * carry a video with provenance (`provider`/`model`) and at least one source
393
+ * (`bytes` or `url`). */
394
+ export async function runVideoGenerationConformance(options) {
395
+ await assertVideoGenerationErrorCode(() => options.provider.submit({ model: options.model, prompt: "" }), "empty_input", "empty prompts must reject with VideoGenerationError(empty_input)");
396
+ if (options.maxPromptChars !== undefined) {
397
+ await assertVideoGenerationErrorCode(() => options.provider.submit({ model: options.model, prompt: "x".repeat(options.maxPromptChars + 1) }), "input_too_large", `prompts over ${options.maxPromptChars} chars must reject with VideoGenerationError(input_too_large)`);
398
+ }
399
+ if (!options.sample)
400
+ return undefined;
401
+ const { jobId } = await options.provider.submit({ model: options.model, prompt: options.sample.prompt ?? "conformance clip" });
402
+ if (typeof jobId !== "string" || jobId.length === 0)
403
+ throw new Error("submit must return a non-empty job id");
404
+ const maxPolls = options.sample.maxPolls ?? 10;
405
+ let job;
406
+ for (let poll = 0; poll < maxPolls; poll += 1) {
407
+ job = await options.provider.status(jobId);
408
+ if (!job)
409
+ throw new Error("status must return a job");
410
+ if (job.state === "succeeded" || job.state === "failed")
411
+ break;
412
+ }
413
+ if (!job || (job.state !== "succeeded" && job.state !== "failed"))
414
+ throw new Error(`status did not reach a terminal state within ${maxPolls} polls`);
415
+ if (job.state === "failed")
416
+ return job;
417
+ const video = job.video;
418
+ if (!video)
419
+ throw new Error("succeeded jobs must carry a video");
420
+ if (!video.bytes && !video.url)
421
+ throw new Error("generated videos must carry bytes or a url");
422
+ if (video.provider !== options.provider.id)
423
+ throw new Error(`video provenance provider ${video.provider} must be preserved (${options.provider.id})`);
424
+ if (video.model !== options.model)
425
+ throw new Error(`video provenance model ${video.model} must be preserved (${options.model})`);
426
+ return job;
427
+ }
428
+ async function assertVideoGenerationErrorCode(run, code, label) {
429
+ let error;
430
+ try {
431
+ await run();
432
+ }
433
+ catch (caught) {
434
+ error = caught;
435
+ }
436
+ if (!(error instanceof VideoGenerationError))
437
+ throw new Error(`${label}; got ${error === undefined ? "a successful result" : String(error)}`);
438
+ if (error.code !== code)
439
+ throw new Error(`${label}; got code ${error.code} (${error.message})`);
440
+ }
441
+ /** Offline conformance for any `ModerationProvider` (plan 061): typed empty-input
442
+ * and oversized-input errors, plus a classification probe — every category
443
+ * verdict carries a numeric score in [0,1], a boolean `flagged`, and the
444
+ * top-level `flagged` boolean is present. Scores are provider output; conformance
445
+ * asserts no local policy decisions are baked in. */
446
+ export async function runModerationConformance(options) {
447
+ await assertModerationErrorCode(() => options.provider.moderate({ input: "", model: options.model }), "empty_input", "empty inputs must reject with ModerationError(empty_input)");
448
+ if (options.maxInputChars !== undefined) {
449
+ await assertModerationErrorCode(() => options.provider.moderate({ input: "x".repeat(options.maxInputChars + 1), model: options.model }), "input_too_large", `inputs over ${options.maxInputChars} chars must reject with ModerationError(input_too_large)`);
450
+ }
451
+ if (!options.sample)
452
+ return undefined;
453
+ const classified = await options.provider.moderate({ input: options.sample.input ?? "conformance probe", model: options.model });
454
+ if (Array.isArray(classified))
455
+ throw new Error("single-string input must classify to one ModerationResult, not a batch");
456
+ const result = classified;
457
+ if (typeof result.flagged !== "boolean")
458
+ throw new Error("moderation results must carry a top-level flagged boolean");
459
+ const entries = Object.entries(result.categories);
460
+ if (entries.length === 0)
461
+ throw new Error("moderation results must expose at least one category verdict");
462
+ for (const [name, verdict] of entries) {
463
+ if (typeof verdict.score !== "number" || !(verdict.score >= 0 && verdict.score <= 1))
464
+ throw new Error(`category ${name} score ${verdict.score} must be a number in [0,1]`);
465
+ if (typeof verdict.flagged !== "boolean")
466
+ throw new Error(`category ${name} verdict must carry a flagged boolean`);
467
+ }
468
+ return result;
469
+ }
470
+ async function assertModerationErrorCode(run, code, label) {
471
+ let error;
472
+ try {
473
+ await run();
474
+ }
475
+ catch (caught) {
476
+ error = caught;
477
+ }
478
+ if (!(error instanceof ModerationError))
479
+ throw new Error(`${label}; got ${error === undefined ? "a successful result" : String(error)}`);
480
+ if (error.code !== code)
481
+ throw new Error(`${label}; got code ${error.code} (${error.message})`);
482
+ }
483
+ /** Offline conformance for any `BatchJobsProvider` (plan 061): typed empty/oversized
484
+ * submit errors, opaque job ids, status returning members of the neutral state
485
+ * union, terminal resolution via `pollBatch` (plain utility), and paged results
486
+ * that walk to exhaustion with cursor continuity. Failure/cancel terminal
487
+ * transitions are covered by provider fakes in adapter test suites. */
488
+ export async function runBatchJobsConformance(options) {
489
+ await assertBatchJobsErrorCode(() => options.provider.submit({ model: "batch-model", requests: [] }), "empty_requests", "empty submits must reject with BatchJobsError(empty_requests)");
490
+ if (options.maxRequests !== undefined) {
491
+ await assertBatchJobsErrorCode(() => options.provider.submit({ model: "batch-model", requests: Array.from({ length: options.maxRequests + 1 }, () => ({ body: {} })) }), "too_many_requests", `submits over ${options.maxRequests} requests must reject with BatchJobsError(too_many_requests)`);
492
+ }
493
+ if (!options.sample)
494
+ return;
495
+ const submitted = await options.provider.submit({ model: options.sample.model, requests: options.sample.requests });
496
+ if (typeof submitted.id !== "string" || submitted.id.length === 0)
497
+ throw new Error("submit must return an opaque non-empty job id");
498
+ const status = await options.provider.status(submitted.id);
499
+ if (typeof status.state !== "string")
500
+ throw new Error("status must return a typed job state");
501
+ const terminal = await pollBatch(options.provider, submitted.id, { intervalMs: 1, maxAttempts: 10 });
502
+ if (terminal.state !== "completed")
503
+ throw new Error(`sample job must reach completed; got ${terminal.state}`);
504
+ const seen = [];
505
+ let cursor = null;
506
+ let pages = 0;
507
+ while (pages < 10) {
508
+ const page = await options.provider.results(submitted.id, { cursor: cursor ?? null });
509
+ for (const item of page.items) {
510
+ if (typeof item.customId !== "string" || item.customId.length === 0)
511
+ throw new Error("result items must carry non-empty custom ids");
512
+ seen.push(item.customId);
513
+ }
514
+ if (!page.nextCursor)
515
+ break;
516
+ if (page.nextCursor === cursor)
517
+ throw new Error("results cursor must advance between pages");
518
+ cursor = page.nextCursor;
519
+ pages += 1;
520
+ }
521
+ if (seen.length === 0)
522
+ throw new Error("completed job must expose at least one result item");
523
+ if (new Set(seen).size !== seen.length)
524
+ throw new Error("result paging must not duplicate items across pages");
525
+ }
526
+ async function assertBatchJobsErrorCode(run, code, label) {
527
+ let error;
528
+ try {
529
+ await run();
530
+ }
531
+ catch (caught) {
532
+ error = caught;
533
+ }
534
+ if (!(error instanceof BatchJobsError))
535
+ throw new Error(`${label}; got ${error === undefined ? "a successful result" : String(error)}`);
536
+ if (error.code !== code)
537
+ throw new Error(`${label}; got code ${error.code} (${error.message})`);
538
+ }
197
539
  //# sourceMappingURL=provider-conformance.js.map
@@ -37,3 +37,28 @@ export declare function dispatchAndCollect(probe: ToolDispatchProbeOptions): Pro
37
37
  result: ToolResult;
38
38
  events: AgentEvent[];
39
39
  }>;
40
+ export interface ToolDisclosureConformanceOptions {
41
+ /** Host-active tool definitions; may include schema-bearing and oversized-description tools. */
42
+ readonly tools: readonly ToolDefinition[];
43
+ /** Host allow/deny bounds applied before disclosure (same input the runtime narrows). */
44
+ readonly filter?: ToolFilterInput;
45
+ /** Search options under test (topK). */
46
+ readonly search?: {
47
+ readonly topK?: number;
48
+ };
49
+ /** Turn text used for the relevance query. Defaults to a zero-match probe. */
50
+ readonly input?: string;
51
+ /** Secret values that must never appear in model-facing search output. */
52
+ readonly secrets?: readonly (string | undefined)[];
53
+ }
54
+ /**
55
+ * Assert the tool-disclosure contract (plan 041) against the same narrowing the
56
+ * runtime applies: search mode only narrows (disclosed set is a subset of the
57
+ * allow/deny-filtered input, never wider, never zero, deterministic order); a
58
+ * denied tool is never described; the generated `search_tools` tool is always
59
+ * kept and its output is inert — names plus byte-truncated descriptions only,
60
+ * no JSON structure, no secret values — and activation stays disclosed beside
61
+ * the turn top-k next turn. Fails closed: an index over the hard cap discloses
62
+ * the full eligible list. Throws on the first violation.
63
+ */
64
+ export declare function assertToolDisclosureConforms(options: ToolDisclosureConformanceOptions): void;