@arnilo/prism 0.4.0 → 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 (171) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +23 -20
  3. package/dist/agent-run-state.d.ts +1 -2
  4. package/dist/agent-run-state.js +0 -3
  5. package/dist/agent-session/session/assemble.d.ts +6 -0
  6. package/dist/agent-session/session/assemble.js +391 -0
  7. package/dist/agent-session/session/persist.d.ts +28 -0
  8. package/dist/agent-session/session/persist.js +166 -0
  9. package/dist/agent-session/session/provider-round.d.ts +6 -0
  10. package/dist/agent-session/session/provider-round.js +231 -0
  11. package/dist/agent-session/session/tool-round.d.ts +31 -0
  12. package/dist/agent-session/session/tool-round.js +473 -0
  13. package/dist/agent-session/session/types.d.ts +115 -0
  14. package/dist/agent-session/session/types.js +5 -0
  15. package/dist/agent-session/session.d.ts +49 -43
  16. package/dist/agent-session/session.js +11 -1177
  17. package/dist/capture.d.ts +63 -0
  18. package/dist/capture.js +67 -0
  19. package/dist/cli-init.d.ts +18 -2
  20. package/dist/cli-init.js +2 -7
  21. package/dist/cli-runner.d.ts +2 -2
  22. package/dist/cli-runner.js +45 -9
  23. package/dist/content.d.ts +3 -3
  24. package/dist/content.js +3 -1
  25. package/dist/contracts-core/agent.d.ts +2 -0
  26. package/dist/contracts-core/batch.d.ts +97 -0
  27. package/dist/contracts-core/batch.js +65 -0
  28. package/dist/contracts-core/content.d.ts +72 -1
  29. package/dist/contracts-core/embeddings.d.ts +30 -0
  30. package/dist/contracts-core/embeddings.js +17 -0
  31. package/dist/contracts-core/images.d.ts +60 -0
  32. package/dist/contracts-core/images.js +17 -0
  33. package/dist/contracts-core/moderation.d.ts +46 -0
  34. package/dist/contracts-core/moderation.js +34 -0
  35. package/dist/contracts-core/speech.d.ts +39 -0
  36. package/dist/contracts-core/speech.js +17 -0
  37. package/dist/contracts-core/transcription.d.ts +48 -0
  38. package/dist/contracts-core/transcription.js +17 -0
  39. package/dist/contracts-core/video.d.ts +61 -0
  40. package/dist/contracts-core/video.js +17 -0
  41. package/dist/contracts-core.d.ts +7 -0
  42. package/dist/contracts-core.js +7 -0
  43. package/dist/index.d.ts +5 -3
  44. package/dist/index.js +4 -3
  45. package/dist/node/agent-definitions.d.ts +1 -8
  46. package/dist/node/agent-definitions.js +0 -34
  47. package/dist/node/settings.d.ts +0 -1
  48. package/dist/node/settings.js +0 -5
  49. package/dist/pinned-fetch.js +29 -3
  50. package/dist/provider-events.js +3 -4
  51. package/dist/providers/media.d.ts +1 -2
  52. package/dist/providers/media.js +1 -4
  53. package/dist/rpc.d.ts +1 -1
  54. package/dist/rpc.js +4 -4
  55. package/dist/testing/provider-conformance.d.ts +114 -5
  56. package/dist/testing/provider-conformance.js +342 -0
  57. package/dist/testing/tool-effect-store-conformance.d.ts +0 -1
  58. package/dist/testing/tool-effect-store-conformance.js +0 -3
  59. package/dist/thinking.d.ts +48 -9
  60. package/dist/thinking.js +134 -8
  61. package/docs/0.1.0-readiness.md +3 -3
  62. package/docs/a2a.md +2 -2
  63. package/docs/acp.md +3 -3
  64. package/docs/ag-ui-adoption.md +1 -1
  65. package/docs/ag-ui.md +1 -2
  66. package/docs/agent-definitions.md +1 -1
  67. package/docs/agent-events.md +5 -5
  68. package/docs/agent-identity.md +13 -2
  69. package/docs/audit-export.md +3 -3
  70. package/docs/batch-jobs.md +120 -0
  71. package/docs/cli-rpc.md +20 -9
  72. package/docs/coding-agent-tools.md +19 -19
  73. package/docs/coding-review-and-diagnostics.md +2 -2
  74. package/docs/coding-security.md +4 -4
  75. package/docs/coding-workspaces.md +2 -2
  76. package/docs/computer-use-linux.md +13 -2
  77. package/docs/context-and-skills.md +1 -1
  78. package/docs/conversations.md +4 -4
  79. package/docs/credential-storage.md +11 -7
  80. package/docs/credentials-and-redaction.md +1 -1
  81. package/docs/data-classification.md +1 -1
  82. package/docs/database-persistence.md +4 -4
  83. package/docs/dev-inspector.md +6 -6
  84. package/docs/device-adapters.md +2 -2
  85. package/docs/diagrams.md +1 -1
  86. package/docs/document-reader.md +6 -6
  87. package/docs/documents.md +5 -4
  88. package/docs/embeddings.md +112 -0
  89. package/docs/enterprise-postgres-state.md +7 -7
  90. package/docs/evaluations.md +8 -8
  91. package/docs/extensions.md +3 -3
  92. package/docs/forge-integration.md +3 -3
  93. package/docs/graft.md +2 -2
  94. package/docs/guardrails.md +1 -1
  95. package/docs/host-security.md +15 -15
  96. package/docs/image-generation.md +129 -0
  97. package/docs/impeccable.md +5 -3
  98. package/docs/index.md +60 -33
  99. package/docs/indexed-code-search.md +2 -2
  100. package/docs/language-intelligence.md +4 -4
  101. package/docs/live-testing.md +126 -0
  102. package/docs/mcp-tools.md +43 -12
  103. package/docs/middleware-hooks.md +1 -1
  104. package/docs/migrate-to-0.4.md +3 -3
  105. package/docs/migrate-to-0.5.md +122 -0
  106. package/docs/migration.md +29 -1
  107. package/docs/model-registry.md +38 -0
  108. package/docs/model-routing.md +5 -5
  109. package/docs/moderation.md +117 -0
  110. package/docs/multi-agent-patterns.md +4 -4
  111. package/docs/multimodal-content.md +26 -2
  112. package/docs/obscura.md +2 -2
  113. package/docs/observability.md +32 -7
  114. package/docs/openapi-tools.md +13 -3
  115. package/docs/operations.md +11 -0
  116. package/docs/performance.md +7 -7
  117. package/docs/persistence-credentials-multimodality-primitives.md +6 -6
  118. package/docs/policy-and-audit.md +17 -7
  119. package/docs/ponytail.md +1 -1
  120. package/docs/postgres-persistence.md +5 -5
  121. package/docs/process-sessions.md +2 -2
  122. package/docs/prompt-registry.md +7 -7
  123. package/docs/provider-caching.md +4 -0
  124. package/docs/provider-conformance.md +23 -1
  125. package/docs/provider-packages.md +39 -3
  126. package/docs/provider-primitives.md +1 -1
  127. package/docs/provider-request-policies.md +1 -1
  128. package/docs/providers/ai-sdk.md +15 -3
  129. package/docs/providers/alibaba.md +5 -1
  130. package/docs/providers/anthropic.md +4 -0
  131. package/docs/providers/azure.md +17 -1
  132. package/docs/providers/bedrock.md +15 -0
  133. package/docs/providers/clinepass.md +4 -0
  134. package/docs/providers/commandcode.md +253 -0
  135. package/docs/providers/deepseek.md +4 -0
  136. package/docs/providers/google.md +4 -0
  137. package/docs/providers/hyper.md +284 -0
  138. package/docs/providers/kimi.md +4 -0
  139. package/docs/providers/neuralwatt.md +4 -0
  140. package/docs/providers/ollama.md +15 -0
  141. package/docs/providers/openai-compatible.md +4 -0
  142. package/docs/providers/openai.md +4 -0
  143. package/docs/providers/opencode-go.md +4 -0
  144. package/docs/providers/openrouter.md +5 -1
  145. package/docs/providers/vertex.md +16 -0
  146. package/docs/providers/xai.md +4 -0
  147. package/docs/providers/zai.md +4 -0
  148. package/docs/rag.md +26 -4
  149. package/docs/release-and-install.md +103 -46
  150. package/docs/resource-loading.md +1 -1
  151. package/docs/runs-and-usage.md +14 -2
  152. package/docs/server.md +5 -5
  153. package/docs/settings-auth-trust-security.md +7 -5
  154. package/docs/sheets.md +2 -2
  155. package/docs/speech.md +126 -0
  156. package/docs/sqlite-persistence.md +4 -4
  157. package/docs/supervisors.md +3 -3
  158. package/docs/thinking-and-reasoning.md +93 -60
  159. package/docs/tool-conformance.md +1 -1
  160. package/docs/tool-execution-primitives.md +8 -8
  161. package/docs/tools.md +4 -4
  162. package/docs/web-tools.md +1 -1
  163. package/docs/wiki.md +1 -1
  164. package/docs/work-artifacts-and-review.md +17 -6
  165. package/docs/work-connectors.md +4 -4
  166. package/docs/work-tools.md +5 -5
  167. package/docs/workflow-orchestration-primitives.md +11 -11
  168. package/docs/workflows.md +5 -5
  169. package/package.json +11 -8
  170. package/templates/init/providers.json +24 -8
  171. package/docs/antigravity-agent.md +0 -207
@@ -99,7 +99,11 @@ export async function requestPinned(url, address, init, errorPrefix = "Request")
99
99
  const nodeRequest = request(url, {
100
100
  method,
101
101
  headers: Object.fromEntries(headers.entries()),
102
- signal: init?.signal ?? undefined,
102
+ // NOTE: the caller signal is NOT passed as the http.request `signal` option:
103
+ // Node's internal abort wiring destroys the (possibly keep-alive, already
104
+ // completed) socket with the abort reason, surfacing it as an unhandled
105
+ // socket 'error' when the response resolved and nothing listens anymore.
106
+ // Abort is handled manually below with a reason-less destroy.
103
107
  lookup: ((_hostname, options, callback) => {
104
108
  if (options.all)
105
109
  callback(null, [{ address: address.address, family: address.family }]);
@@ -132,8 +136,13 @@ export async function requestPinned(url, address, init, errorPrefix = "Request")
132
136
  controller.error(error);
133
137
  }
134
138
  },
135
- cancel(reason) {
136
- incoming.destroy(reason instanceof Error ? reason : undefined);
139
+ cancel() {
140
+ // Destroy WITHOUT the abort reason: once the response resolved,
141
+ // nothing listens on the socket, and propagate-destroy(reason)
142
+ // surfaces the DOMException as an unhandled socket 'error'
143
+ // (SDK transports abort their controller on close()). Readers
144
+ // that still pull observe EOF via the iterator.
145
+ incoming.destroy();
137
146
  },
138
147
  });
139
148
  resolve(new Response(stream, {
@@ -143,12 +152,29 @@ export async function requestPinned(url, address, init, errorPrefix = "Request")
143
152
  }));
144
153
  });
145
154
  nodeRequest.on("error", reject);
155
+ if (init?.signal) {
156
+ const signal = init.signal;
157
+ if (signal.aborted) {
158
+ reject(abortReason(signal));
159
+ nodeRequest.destroy();
160
+ }
161
+ else {
162
+ signal.addEventListener("abort", () => {
163
+ nodeRequest.destroy();
164
+ reject(abortReason(signal));
165
+ }, { once: true });
166
+ }
167
+ }
146
168
  if (body)
147
169
  nodeRequest.end(body);
148
170
  else
149
171
  nodeRequest.end();
150
172
  });
151
173
  }
174
+ function abortReason(signal) {
175
+ const reason = signal.reason;
176
+ return reason instanceof Error ? reason : new Error(String(reason));
177
+ }
152
178
  async function requestBody(body, errorPrefix) {
153
179
  if (body === undefined || body === null)
154
180
  return undefined;
@@ -27,10 +27,9 @@ export function reconstructToolCallDeltas(events) {
27
27
  if (event.type !== "tool_call_delta")
28
28
  continue;
29
29
  const partial = partials.get(event.index) ?? { argumentsText: "" };
30
- if (event.id !== undefined)
31
- partial.id = event.id;
32
- if (event.name !== undefined)
33
- partial.name = event.name;
30
+ // Conformant OpenAI-compatible providers repeat identity as null on continuation chunks; coalesce like openai-compatible.js instead of clobbering.
31
+ partial.id = event.id ?? partial.id;
32
+ partial.name = event.name ?? partial.name;
34
33
  if (event.argumentsText !== undefined)
35
34
  partial.argumentsText += event.argumentsText;
36
35
  partials.set(event.index, partial);
@@ -1,5 +1,5 @@
1
1
  import { type MediaContentBlock, type ModelInputCapability, type ResolvedMediaContent, type ResolveMediaContentOptions } from "../content.js";
2
- import type { AudioContent, ContentBlock, DocumentContent, FileContent, JsonObject, Message, ModelCapabilities, ModelConfig } from "../contracts.js";
2
+ import type { ContentBlock, JsonObject, Message, ModelCapabilities, ModelConfig } from "../contracts.js";
3
3
  /** Default upload-cache entry cap per provider media session. */
4
4
  export declare const DEFAULT_PROVIDER_UPLOAD_CACHE_ENTRIES = 32;
5
5
  /** Inline OpenAI file_data ceiling before preferring Files API upload. */
@@ -37,7 +37,6 @@ export declare function serializePdfDocumentWireBlock(options: {
37
37
  readonly data: string;
38
38
  readonly title?: string;
39
39
  }): JsonObject;
40
- export declare function resolveProviderMediaBlock(block: AudioContent | FileContent | DocumentContent, options?: ResolveMediaContentOptions): Promise<ResolvedMediaContent>;
41
40
  /** Resolve every media block once and enforce aggregate request bounds before provider I/O. */
42
41
  export declare function resolveProviderMediaMessages(messages: readonly Message[], model: ModelConfig, options?: ResolveMediaContentOptions): Promise<ReadonlyMap<MediaContentBlock, ResolvedMediaContent>>;
43
42
  export declare function defaultProviderFilename(block: MediaContentBlock, fallback: string): string;
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { assertMessagesSupportModelCapabilities, contentBlockInputModality, DEFAULT_MAX_MEDIA_ITEM_BYTES, resolveMediaContentBlock, resolveMediaContentBlocks, UnsupportedModalityError, } from "../content.js";
2
+ import { assertMessagesSupportModelCapabilities, contentBlockInputModality, DEFAULT_MAX_MEDIA_ITEM_BYTES, resolveMediaContentBlocks, UnsupportedModalityError, } from "../content.js";
3
3
  /** Default upload-cache entry cap per provider media session. */
4
4
  export const DEFAULT_PROVIDER_UPLOAD_CACHE_ENTRIES = 32;
5
5
  /** Inline OpenAI file_data ceiling before preferring Files API upload. */
@@ -93,9 +93,6 @@ export function serializePdfDocumentWireBlock(options) {
93
93
  ...(options.title ? { title: options.title } : {}),
94
94
  };
95
95
  }
96
- export async function resolveProviderMediaBlock(block, options = {}) {
97
- return resolveMediaContentBlock(block, options);
98
- }
99
96
  /** Resolve every media block once and enforce aggregate request bounds before provider I/O. */
100
97
  export async function resolveProviderMediaMessages(messages, model, options = {}) {
101
98
  assertMessagesSupportModelCapabilities(model, messages);
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,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
@@ -6,4 +6,3 @@ export interface ToolEffectStoreConformanceOptions {
6
6
  }
7
7
  /** Assert core claim/CAS, duplicate, reconciliation, and cleanup semantics without a test framework. */
8
8
  export declare function assertToolEffectStoreConforms(factory: () => ToolEffectStore | Promise<ToolEffectStore>, options?: ToolEffectStoreConformanceOptions): Promise<void>;
9
- export declare function runToolEffectStoreConformance(factory: () => ToolEffectStore | Promise<ToolEffectStore>, options?: ToolEffectStoreConformanceOptions): Promise<void>;
@@ -44,9 +44,6 @@ export async function assertToolEffectStoreConforms(factory, options = {}) {
44
44
  if (cleanup.deleted < 1)
45
45
  throw new Error("store cleanup must remove terminal effects");
46
46
  }
47
- export async function runToolEffectStoreConformance(factory, options = {}) {
48
- await assertToolEffectStoreConforms(factory, options);
49
- }
50
47
  function key(identity, ownership, value, toolCallId = "call") {
51
48
  return {
52
49
  identity,