@arnilo/prism 0.4.0 → 0.5.1
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.
- package/CHANGELOG.md +41 -1
- package/README.md +23 -20
- package/dist/agent-run-state.d.ts +1 -2
- package/dist/agent-run-state.js +0 -3
- package/dist/agent-session/session/assemble.d.ts +6 -0
- package/dist/agent-session/session/assemble.js +391 -0
- package/dist/agent-session/session/persist.d.ts +28 -0
- package/dist/agent-session/session/persist.js +166 -0
- package/dist/agent-session/session/provider-round.d.ts +6 -0
- package/dist/agent-session/session/provider-round.js +231 -0
- package/dist/agent-session/session/tool-round.d.ts +31 -0
- package/dist/agent-session/session/tool-round.js +473 -0
- package/dist/agent-session/session/types.d.ts +115 -0
- package/dist/agent-session/session/types.js +5 -0
- package/dist/agent-session/session.d.ts +49 -43
- package/dist/agent-session/session.js +24 -1180
- package/dist/capture.d.ts +63 -0
- package/dist/capture.js +67 -0
- package/dist/cli-init.d.ts +18 -2
- package/dist/cli-init.js +2 -7
- package/dist/cli-runner.d.ts +2 -2
- package/dist/cli-runner.js +45 -9
- package/dist/content.d.ts +3 -3
- package/dist/content.js +3 -1
- package/dist/contracts-core/agent.d.ts +4 -0
- package/dist/contracts-core/batch.d.ts +97 -0
- package/dist/contracts-core/batch.js +65 -0
- package/dist/contracts-core/content.d.ts +72 -1
- package/dist/contracts-core/embeddings.d.ts +30 -0
- package/dist/contracts-core/embeddings.js +17 -0
- package/dist/contracts-core/images.d.ts +60 -0
- package/dist/contracts-core/images.js +17 -0
- package/dist/contracts-core/moderation.d.ts +46 -0
- package/dist/contracts-core/moderation.js +34 -0
- package/dist/contracts-core/speech.d.ts +39 -0
- package/dist/contracts-core/speech.js +17 -0
- package/dist/contracts-core/transcription.d.ts +48 -0
- package/dist/contracts-core/transcription.js +17 -0
- package/dist/contracts-core/video.d.ts +61 -0
- package/dist/contracts-core/video.js +17 -0
- package/dist/contracts-core.d.ts +7 -0
- package/dist/contracts-core.js +7 -0
- package/dist/contracts-protocol.d.ts +2 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +5 -4
- package/dist/input.js +3 -2
- package/dist/node/agent-definitions.d.ts +1 -8
- package/dist/node/agent-definitions.js +0 -34
- package/dist/node/settings.d.ts +0 -1
- package/dist/node/settings.js +0 -5
- package/dist/pinned-fetch.js +29 -3
- package/dist/provider-events.js +3 -4
- package/dist/provider-request-policy.d.ts +15 -0
- package/dist/provider-request-policy.js +52 -0
- package/dist/providers/media.d.ts +1 -2
- package/dist/providers/media.js +1 -4
- package/dist/rpc.d.ts +1 -1
- package/dist/rpc.js +4 -4
- package/dist/testing/provider-conformance.d.ts +114 -5
- package/dist/testing/provider-conformance.js +342 -0
- package/dist/testing/tool-effect-store-conformance.d.ts +0 -1
- package/dist/testing/tool-effect-store-conformance.js +0 -3
- package/dist/thinking.d.ts +48 -9
- package/dist/thinking.js +134 -8
- package/docs/0.1.0-readiness.md +3 -3
- package/docs/a2a.md +2 -2
- package/docs/acp.md +3 -3
- package/docs/ag-ui-adoption.md +1 -1
- package/docs/ag-ui.md +1 -2
- package/docs/agent-definitions.md +1 -1
- package/docs/agent-events.md +5 -5
- package/docs/agent-identity.md +13 -2
- package/docs/agent-session-runtime.md +2 -1
- package/docs/audit-export.md +3 -3
- package/docs/batch-jobs.md +120 -0
- package/docs/cli-rpc.md +20 -9
- package/docs/coding-agent-tools.md +19 -19
- package/docs/coding-review-and-diagnostics.md +2 -2
- package/docs/coding-security.md +4 -4
- package/docs/coding-workspaces.md +2 -2
- package/docs/compaction-llm.md +2 -0
- package/docs/compaction-observational-memory.md +3 -0
- package/docs/computer-use-linux.md +13 -2
- package/docs/context-and-skills.md +1 -1
- package/docs/conversations.md +4 -4
- package/docs/credential-storage.md +11 -7
- package/docs/credentials-and-redaction.md +1 -1
- package/docs/data-classification.md +1 -1
- package/docs/database-persistence.md +4 -4
- package/docs/dev-inspector.md +6 -6
- package/docs/device-adapters.md +2 -2
- package/docs/diagrams.md +1 -1
- package/docs/document-reader.md +6 -6
- package/docs/documents.md +5 -4
- package/docs/embeddings.md +112 -0
- package/docs/enterprise-postgres-state.md +7 -7
- package/docs/evaluations.md +8 -8
- package/docs/extensions.md +3 -3
- package/docs/forge-integration.md +3 -3
- package/docs/graft.md +2 -2
- package/docs/guardrails.md +1 -1
- package/docs/host-security.md +15 -15
- package/docs/image-generation.md +129 -0
- package/docs/impeccable.md +5 -3
- package/docs/index.md +64 -36
- package/docs/indexed-code-search.md +2 -2
- package/docs/input-and-prompt-assembly.md +1 -1
- package/docs/language-intelligence.md +4 -4
- package/docs/live-testing.md +126 -0
- package/docs/mcp-tools.md +43 -12
- package/docs/middleware-hooks.md +1 -1
- package/docs/migrate-to-0.4.md +3 -3
- package/docs/migrate-to-0.5.md +144 -0
- package/docs/migration.md +33 -1
- package/docs/model-registry.md +38 -0
- package/docs/model-routing.md +5 -5
- package/docs/moderation.md +117 -0
- package/docs/multi-agent-patterns.md +4 -4
- package/docs/multimodal-content.md +26 -2
- package/docs/obscura.md +2 -2
- package/docs/observability.md +32 -7
- package/docs/openapi-tools.md +13 -3
- package/docs/operations.md +11 -0
- package/docs/performance.md +7 -7
- package/docs/persistence-credentials-multimodality-primitives.md +6 -6
- package/docs/policy-and-audit.md +17 -7
- package/docs/ponytail.md +1 -1
- package/docs/postgres-persistence.md +5 -5
- package/docs/process-sessions.md +2 -2
- package/docs/prompt-registry.md +7 -7
- package/docs/provider-caching.md +8 -2
- package/docs/provider-conformance.md +23 -1
- package/docs/provider-packages.md +49 -17
- package/docs/provider-primitives.md +1 -1
- package/docs/provider-request-policies.md +19 -6
- package/docs/providers/ai-sdk.md +27 -3
- package/docs/providers/alibaba.md +17 -1
- package/docs/providers/anthropic.md +16 -0
- package/docs/providers/azure.md +29 -1
- package/docs/providers/bedrock.md +27 -0
- package/docs/providers/clinepass.md +16 -0
- package/docs/providers/commandcode.md +265 -0
- package/docs/providers/deepseek.md +16 -0
- package/docs/providers/google.md +16 -0
- package/docs/providers/hyper.md +296 -0
- package/docs/providers/kimi.md +16 -0
- package/docs/providers/neuralwatt.md +16 -0
- package/docs/providers/ollama.md +27 -0
- package/docs/providers/openai-compatible.md +16 -0
- package/docs/providers/openai.md +16 -0
- package/docs/providers/opencode-go.md +16 -0
- package/docs/providers/openrouter.md +17 -1
- package/docs/providers/vertex.md +28 -0
- package/docs/providers/xai.md +16 -0
- package/docs/providers/zai.md +16 -0
- package/docs/public-contracts.md +1 -1
- package/docs/rag.md +26 -4
- package/docs/release-and-install.md +103 -46
- package/docs/resource-loading.md +1 -1
- package/docs/runs-and-usage.md +14 -2
- package/docs/server.md +5 -5
- package/docs/settings-auth-trust-security.md +7 -5
- package/docs/sheets.md +2 -2
- package/docs/speech.md +126 -0
- package/docs/sqlite-persistence.md +4 -4
- package/docs/supervisors.md +3 -3
- package/docs/thinking-and-reasoning.md +99 -61
- package/docs/tool-conformance.md +1 -1
- package/docs/tool-execution-primitives.md +8 -8
- package/docs/tools.md +4 -4
- package/docs/use-case-model-selection.md +1 -1
- package/docs/web-tools.md +1 -1
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +17 -6
- package/docs/work-connectors.md +4 -4
- package/docs/work-tools.md +5 -5
- package/docs/workflow-orchestration-primitives.md +11 -11
- package/docs/workflows.md +5 -5
- package/package.json +11 -8
- package/templates/init/providers.json +24 -8
- package/docs/antigravity-agent.md +0 -207
|
@@ -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,
|
package/dist/thinking.d.ts
CHANGED
|
@@ -9,8 +9,16 @@ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
|
9
9
|
* Compat mapping families used by ≥2 packages, or explicit no-op for host-owned adapters.
|
|
10
10
|
* Provider packages keep unique escape hatches (budgets, keep/all, tool_stream) local.
|
|
11
11
|
*/
|
|
12
|
-
export type ThinkingCompatFamily = "openai_reasoning" | "reasoning_effort" | "thinking_type" | "noop";
|
|
12
|
+
export type ThinkingCompatFamily = "openai_reasoning" | "reasoning_effort" | "thinking_type" | "google" | "output_config_effort" | "noop";
|
|
13
13
|
export declare function isThinkingLevel(value: unknown): value is ThinkingLevel;
|
|
14
|
+
/**
|
|
15
|
+
* Parse a host thinking-level value without guessing: known levels canonicalize to
|
|
16
|
+
* `ThinkingLevel`, unknown non-empty strings pass through as opaque `{ opaque }`
|
|
17
|
+
* (forward-compat passthrough), invalid/empty/non-string input fails closed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseThinkingLevel(value: unknown): ThinkingLevel | {
|
|
20
|
+
readonly opaque: string;
|
|
21
|
+
} | undefined;
|
|
14
22
|
/**
|
|
15
23
|
* Normalize a host thinkingLevel string. Known levels are lowercased; other non-empty
|
|
16
24
|
* strings pass through as opaque effort values for forward-compatible provider fields.
|
|
@@ -26,17 +34,48 @@ export declare function thinkingCompatFor(family: ThinkingCompatFamily, level: T
|
|
|
26
34
|
* Per-turn patches win over prior compat via {@link mergeProviderRequestOptions}.
|
|
27
35
|
*/
|
|
28
36
|
export declare function applyThinkingLevel(options: ProviderRequestOptions | undefined, level: ThinkingLevel | string, family?: ThinkingCompatFamily): ProviderRequestOptions;
|
|
37
|
+
/**
|
|
38
|
+
* Declared portable thinking levels for a model, if any (ascending ladder order).
|
|
39
|
+
* `undefined` means the provider declares no subset — forward-compat passthrough.
|
|
40
|
+
*/
|
|
41
|
+
export declare function thinkingLevelsForModel(model: Pick<ModelConfig, "provider" | "compat" | "capabilities">): readonly string[] | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Strict declared-set membership (hosts fail closed on unknown levels).
|
|
44
|
+
* A model that declares no levels supports any value (forward-compat passthrough).
|
|
45
|
+
*/
|
|
46
|
+
export declare function isSupportedThinkingLevel(model: Pick<ModelConfig, "provider" | "compat" | "capabilities">, level: unknown): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Snap a portable level to a model's declared set (design record §2):
|
|
49
|
+
* in-set → unchanged; below the declared minimum → up to the minimum
|
|
50
|
+
* (never silently disable what cannot be disabled); otherwise nearest declared
|
|
51
|
+
* level by ladder distance with ties breaking up; undeclared levels and
|
|
52
|
+
* undeclared sets pass through. Provider-documented snap tables
|
|
53
|
+
* (deepseek, Z.AI GLM-5.2, clinepass slots) override this generic fallback
|
|
54
|
+
* inside their own resolvers.
|
|
55
|
+
*/
|
|
56
|
+
export declare function snapThinkingLevel(model: Pick<ModelConfig, "provider" | "compat" | "capabilities">, level: ThinkingLevel | string): ThinkingLevel | string;
|
|
57
|
+
/**
|
|
58
|
+
* Model-aware thinking-level application (design record §5). Resolves the family
|
|
59
|
+
* stamp-first (`compat.thinkingFamily` → inference → `capabilities.reasoning`),
|
|
60
|
+
* snaps the level to the model's declared set, and merges the compat patch
|
|
61
|
+
* per-turn-wins. Returns options unchanged for non-reasoning models — never
|
|
62
|
+
* invents a field where the model declares no thinking support.
|
|
63
|
+
*/
|
|
64
|
+
export declare function applyThinkingLevelForModel(options: ProviderRequestOptions | undefined, level: ThinkingLevel | string, model: Pick<ModelConfig, "provider" | "compat" | "capabilities">): ProviderRequestOptions;
|
|
29
65
|
/**
|
|
30
66
|
* Best-effort family inference from model metadata without a second options tree.
|
|
31
|
-
* Prefer an explicit
|
|
67
|
+
* Prefer an explicit `compat.thinkingFamily` stamp in host/use-case workers when
|
|
68
|
+
* the provider is known; inference is the fallback (stamp-first).
|
|
32
69
|
*
|
|
33
70
|
* Heuristics (ordered):
|
|
34
|
-
* 1.
|
|
35
|
-
* 2. Existing `compat.
|
|
36
|
-
* 3. Existing `compat.
|
|
37
|
-
* 4.
|
|
38
|
-
* 5.
|
|
39
|
-
* 6. `
|
|
40
|
-
* 7.
|
|
71
|
+
* 1. `compat.thinkingFamily` stamp → itself
|
|
72
|
+
* 2. Existing `compat.thinking` object → `thinking_type`
|
|
73
|
+
* 3. Existing `compat.thinkingConfig` object/boolean → `google`
|
|
74
|
+
* 4. Existing `compat.reasoning` → `openai_reasoning`
|
|
75
|
+
* 5. Existing `compat.reasoning_effort` → `reasoning_effort`
|
|
76
|
+
* 6. Provider id starting with `openai` → `openai_reasoning`
|
|
77
|
+
* 7. Provider id `neuralwatt` → `reasoning_effort`
|
|
78
|
+
* 8. `capabilities.reasoning` → `reasoning_effort` (portable string field)
|
|
79
|
+
* 9. Else `noop`
|
|
41
80
|
*/
|
|
42
81
|
export declare function thinkingFamilyForModel(model: Pick<ModelConfig, "provider" | "compat" | "capabilities">): ThinkingCompatFamily;
|
package/dist/thinking.js
CHANGED
|
@@ -7,6 +7,40 @@ export const THINKING_LEVELS = ["none", "minimal", "low", "medium", "high", "xhi
|
|
|
7
7
|
export function isThinkingLevel(value) {
|
|
8
8
|
return typeof value === "string" && THINKING_LEVELS.includes(value);
|
|
9
9
|
}
|
|
10
|
+
const LEVEL_RANK = {
|
|
11
|
+
none: 0,
|
|
12
|
+
minimal: 1,
|
|
13
|
+
low: 2,
|
|
14
|
+
medium: 3,
|
|
15
|
+
high: 4,
|
|
16
|
+
xhigh: 5,
|
|
17
|
+
max: 6,
|
|
18
|
+
};
|
|
19
|
+
function isThinkingFamily(value) {
|
|
20
|
+
return (typeof value === "string" &&
|
|
21
|
+
(value === "openai_reasoning" ||
|
|
22
|
+
value === "reasoning_effort" ||
|
|
23
|
+
value === "thinking_type" ||
|
|
24
|
+
value === "google" ||
|
|
25
|
+
value === "output_config_effort" ||
|
|
26
|
+
value === "noop"));
|
|
27
|
+
}
|
|
28
|
+
function thinkingLevelRank(level) {
|
|
29
|
+
return isThinkingLevel(level) ? LEVEL_RANK[level] : undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse a host thinking-level value without guessing: known levels canonicalize to
|
|
33
|
+
* `ThinkingLevel`, unknown non-empty strings pass through as opaque `{ opaque }`
|
|
34
|
+
* (forward-compat passthrough), invalid/empty/non-string input fails closed.
|
|
35
|
+
*/
|
|
36
|
+
export function parseThinkingLevel(value) {
|
|
37
|
+
if (typeof value !== "string")
|
|
38
|
+
return undefined;
|
|
39
|
+
const normalized = normalizeThinkingLevel(value);
|
|
40
|
+
if (!normalized)
|
|
41
|
+
return undefined;
|
|
42
|
+
return isThinkingLevel(normalized) ? normalized : { opaque: normalized };
|
|
43
|
+
}
|
|
10
44
|
/**
|
|
11
45
|
* Normalize a host thinkingLevel string. Known levels are lowercased; other non-empty
|
|
12
46
|
* strings pass through as opaque effort values for forward-compatible provider fields.
|
|
@@ -32,6 +66,10 @@ export function thinkingCompatFor(family, level) {
|
|
|
32
66
|
return { reasoning_effort: normalized };
|
|
33
67
|
case "thinking_type":
|
|
34
68
|
return { thinking: { type: normalized === "none" ? "disabled" : "enabled" } };
|
|
69
|
+
case "google":
|
|
70
|
+
return { thinkingLevel: normalized };
|
|
71
|
+
case "output_config_effort":
|
|
72
|
+
return { output_config: { effort: normalized } };
|
|
35
73
|
default: {
|
|
36
74
|
const _exhaustive = family;
|
|
37
75
|
return _exhaustive;
|
|
@@ -62,23 +100,111 @@ export function applyThinkingLevel(options, level, family = "reasoning_effort")
|
|
|
62
100
|
}
|
|
63
101
|
return mergeProviderRequestOptions(options, { compat: patch });
|
|
64
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Declared portable thinking levels for a model, if any (ascending ladder order).
|
|
105
|
+
* `undefined` means the provider declares no subset — forward-compat passthrough.
|
|
106
|
+
*/
|
|
107
|
+
export function thinkingLevelsForModel(model) {
|
|
108
|
+
return model.capabilities?.thinkingLevels;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Strict declared-set membership (hosts fail closed on unknown levels).
|
|
112
|
+
* A model that declares no levels supports any value (forward-compat passthrough).
|
|
113
|
+
*/
|
|
114
|
+
export function isSupportedThinkingLevel(model, level) {
|
|
115
|
+
const parsed = parseThinkingLevel(level);
|
|
116
|
+
if (!parsed)
|
|
117
|
+
return false;
|
|
118
|
+
const declared = thinkingLevelsForModel(model);
|
|
119
|
+
if (!declared || declared.length === 0)
|
|
120
|
+
return true;
|
|
121
|
+
const value = typeof parsed === "string" ? parsed : parsed.opaque;
|
|
122
|
+
return declared.includes(value);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Snap a portable level to a model's declared set (design record §2):
|
|
126
|
+
* in-set → unchanged; below the declared minimum → up to the minimum
|
|
127
|
+
* (never silently disable what cannot be disabled); otherwise nearest declared
|
|
128
|
+
* level by ladder distance with ties breaking up; undeclared levels and
|
|
129
|
+
* undeclared sets pass through. Provider-documented snap tables
|
|
130
|
+
* (deepseek, Z.AI GLM-5.2, clinepass slots) override this generic fallback
|
|
131
|
+
* inside their own resolvers.
|
|
132
|
+
*/
|
|
133
|
+
export function snapThinkingLevel(model, level) {
|
|
134
|
+
const normalized = normalizeThinkingLevel(String(level));
|
|
135
|
+
if (!normalized)
|
|
136
|
+
return String(level);
|
|
137
|
+
const declared = thinkingLevelsForModel(model);
|
|
138
|
+
if (!declared || declared.length === 0)
|
|
139
|
+
return normalized;
|
|
140
|
+
if (declared.includes(normalized))
|
|
141
|
+
return normalized;
|
|
142
|
+
const rank = thinkingLevelRank(normalized);
|
|
143
|
+
const ranked = declared
|
|
144
|
+
.map((entry) => ({ entry, rank: thinkingLevelRank(entry) }))
|
|
145
|
+
.filter((entry) => entry.rank != null);
|
|
146
|
+
if (rank == null || ranked.length === 0)
|
|
147
|
+
return normalized;
|
|
148
|
+
const minRank = Math.min(...ranked.map(({ rank: r }) => r));
|
|
149
|
+
if (rank < minRank)
|
|
150
|
+
return ranked.find(({ rank: r }) => r === minRank).entry;
|
|
151
|
+
let best = ranked[0].entry;
|
|
152
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
153
|
+
let bestRank = Number.NEGATIVE_INFINITY;
|
|
154
|
+
for (const { entry, rank: candidateRank } of ranked) {
|
|
155
|
+
const distance = Math.abs(candidateRank - rank);
|
|
156
|
+
if (distance < bestDistance || (distance === bestDistance && candidateRank > bestRank)) {
|
|
157
|
+
best = entry;
|
|
158
|
+
bestDistance = distance;
|
|
159
|
+
bestRank = candidateRank;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return best;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Model-aware thinking-level application (design record §5). Resolves the family
|
|
166
|
+
* stamp-first (`compat.thinkingFamily` → inference → `capabilities.reasoning`),
|
|
167
|
+
* snaps the level to the model's declared set, and merges the compat patch
|
|
168
|
+
* per-turn-wins. Returns options unchanged for non-reasoning models — never
|
|
169
|
+
* invents a field where the model declares no thinking support.
|
|
170
|
+
*/
|
|
171
|
+
export function applyThinkingLevelForModel(options, level, model) {
|
|
172
|
+
const normalized = normalizeThinkingLevel(String(level));
|
|
173
|
+
if (!normalized)
|
|
174
|
+
return options ?? {};
|
|
175
|
+
const family = model.compat?.thinkingFamily != null && isThinkingFamily(model.compat.thinkingFamily)
|
|
176
|
+
? model.compat.thinkingFamily
|
|
177
|
+
: thinkingFamilyForModel(model);
|
|
178
|
+
if (family === "noop")
|
|
179
|
+
return options ?? {};
|
|
180
|
+
return applyThinkingLevel(options, snapThinkingLevel(model, normalized), family);
|
|
181
|
+
}
|
|
65
182
|
/**
|
|
66
183
|
* Best-effort family inference from model metadata without a second options tree.
|
|
67
|
-
* Prefer an explicit
|
|
184
|
+
* Prefer an explicit `compat.thinkingFamily` stamp in host/use-case workers when
|
|
185
|
+
* the provider is known; inference is the fallback (stamp-first).
|
|
68
186
|
*
|
|
69
187
|
* Heuristics (ordered):
|
|
70
|
-
* 1.
|
|
71
|
-
* 2. Existing `compat.
|
|
72
|
-
* 3. Existing `compat.
|
|
73
|
-
* 4.
|
|
74
|
-
* 5.
|
|
75
|
-
* 6. `
|
|
76
|
-
* 7.
|
|
188
|
+
* 1. `compat.thinkingFamily` stamp → itself
|
|
189
|
+
* 2. Existing `compat.thinking` object → `thinking_type`
|
|
190
|
+
* 3. Existing `compat.thinkingConfig` object/boolean → `google`
|
|
191
|
+
* 4. Existing `compat.reasoning` → `openai_reasoning`
|
|
192
|
+
* 5. Existing `compat.reasoning_effort` → `reasoning_effort`
|
|
193
|
+
* 6. Provider id starting with `openai` → `openai_reasoning`
|
|
194
|
+
* 7. Provider id `neuralwatt` → `reasoning_effort`
|
|
195
|
+
* 8. `capabilities.reasoning` → `reasoning_effort` (portable string field)
|
|
196
|
+
* 9. Else `noop`
|
|
77
197
|
*/
|
|
78
198
|
export function thinkingFamilyForModel(model) {
|
|
199
|
+
const stamp = model.compat?.thinkingFamily;
|
|
200
|
+
if (isThinkingFamily(stamp))
|
|
201
|
+
return stamp;
|
|
79
202
|
const compat = model.compat ?? {};
|
|
80
203
|
if (compat.thinking != null && typeof compat.thinking === "object")
|
|
81
204
|
return "thinking_type";
|
|
205
|
+
if (compat.thinkingConfig != null && (typeof compat.thinkingConfig === "object" || typeof compat.thinkingConfig === "boolean")) {
|
|
206
|
+
return "google";
|
|
207
|
+
}
|
|
82
208
|
if (compat.reasoning != null)
|
|
83
209
|
return "openai_reasoning";
|
|
84
210
|
if (compat.reasoning_effort != null)
|
package/docs/0.1.0-readiness.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0.1.0 / 1.0 Readiness Gates
|
|
2
2
|
|
|
3
|
-
Status: **0.
|
|
3
|
+
Status: **0.5.1** is the current release line (provider request construction cut: 10 active packages, family subpaths, `^0.5.1` peers); **0.3.3** was the terminal 0.3.x cut; **0.1.7** was the terminal 0.1.x baseline; **1.0** readiness remains operator-gated, not automatic.
|
|
4
4
|
|
|
5
5
|
This page distills runnable readiness gates into one command-per-gate table.
|
|
6
6
|
The **Last evidence** column records the 0.1.0-tree snapshot (plan 012 Tasks
|
|
@@ -20,7 +20,7 @@ Historical release lines (0.0.16 floor → 0.0.27 Phase 10 ACP interop → 0.1.0
|
|
|
20
20
|
keep their per-phase evidence in the pages above; this page records the 0.2.6
|
|
21
21
|
snapshot (plan 026) with the 0.1.x tables below as the historical record.
|
|
22
22
|
|
|
23
|
-
## Current line (0.
|
|
23
|
+
## Current line (0.5.1)
|
|
24
24
|
|
|
25
25
|
| Item | Status |
|
|
26
26
|
|---|---|
|
|
@@ -66,7 +66,7 @@ snapshot (plan 026) with the 0.1.x tables below as the historical record.
|
|
|
66
66
|
| Publish order + tarball validation | `node scripts/release.mjs publish --version 0.1.0 --dry-run --allow-dirty --allow-untagged` | 49/49 packages `dry-run` twice with byte-identical reports, deterministic dependency order, no failures (Task 7) | Operator (dry-run), CI |
|
|
67
67
|
| Node 20 compatibility | CI `node20-compat` (build + public-import smoke) | all 21 root exports import cleanly on Node 20.20.2 | CI |
|
|
68
68
|
| PostgreSQL suite | `PRISM_TEST_POSTGRES_URL="$DATABASE_URL" npm run test:postgres` | 0.1.0: Phase 7 conformance + Phase 12 restart-recovery + 74 workspace checks green against PostgreSQL 16 (Task 4 recording, re-run green at 0.1.0 on 2026-08-09); operator-gated | Operator |
|
|
69
|
-
| Keychain suite | `PRISM_TEST_KEYCHAIN=1 npm test --workspace @arnilo/prism-credentials
|
|
69
|
+
| Keychain suite | `PRISM_TEST_KEYCHAIN=1 npm test --workspace @arnilo/prism-core/credentials/node` (protected) | 28/28 green incl. native keychain round-trip against the OS secret-service backend (gnome-keyring, 2026-08-09) | Operator host |
|
|
70
70
|
| Live-provider suites | `npm run test:live` (protected) | **operator-gated** (requires credentials; `live-canaries.yml` blocked gate, `canary-report.json` retained) | Operator |
|
|
71
71
|
| SAST | GitHub CodeQL | **operator-gated** (runs in CI workflow) | CI |
|
|
72
72
|
| Signed, provenance publication | `npm run release:publish` (clean tagged tree, OIDC) | **operator-gated** (see "Remaining for 1.0") | Operator |
|
package/docs/a2a.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What it does
|
|
4
4
|
|
|
5
|
-
`@arnilo/prism-supervisor` implements bounded A2A 1.0 over the JSON-RPC/HTTPS binding. Supported operations: `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, push-notification-config create/get/list/delete, and `GetExtendedAgentCard`. `client.streamMessage()` additionally exposes verified rich task/message events for frontend adapters while legacy `stream()` remains text-compatible. Agent Cards retain explicit ES256 verification. gRPC, HTTP+JSON, discovery registries, automatic JWK/OAuth fetching, and an internal task worker/store are absent.
|
|
5
|
+
`@arnilo/prism-core/runtime/supervisor` implements bounded A2A 1.0 over the JSON-RPC/HTTPS binding. Supported operations: `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, push-notification-config create/get/list/delete, and `GetExtendedAgentCard`. `client.streamMessage()` additionally exposes verified rich task/message events for frontend adapters while legacy `stream()` remains text-compatible. Agent Cards retain explicit ES256 verification. gRPC, HTTP+JSON, discovery registries, automatic JWK/OAuth fetching, and an internal task worker/store are absent.
|
|
6
6
|
|
|
7
7
|
## When to use it
|
|
8
8
|
|
|
@@ -41,7 +41,7 @@ Parts, messages, artifacts, histories, metadata, and aggregate responses are unt
|
|
|
41
41
|
|
|
42
42
|
## AG-UI server-side exposure (Task 13, 0.0.26)
|
|
43
43
|
|
|
44
|
-
`createAgUiA2AServer()` in `@arnilo/prism-ag-ui` fronts one host-selected **local AG-UI agent** as an A2A 1.0 server, the reverse direction of `createAgUiA2AAdapter()`: remote A2A clients start and stream local runs through the same AG-UI input allow-list and event mapper as the AG-UI SSE path (same projection, redaction, and byte caps). It reuses this package's `createA2AHandler` transport/lifecycle; it creates no second runtime, task store, or worker. Requires the optional `@arnilo/prism-supervisor` peer (imported lazily; plain `@arnilo/prism-ag-ui` imports keep working without it).
|
|
44
|
+
`createAgUiA2AServer()` in `@arnilo/prism-ag-ui` fronts one host-selected **local AG-UI agent** as an A2A 1.0 server, the reverse direction of `createAgUiA2AAdapter()`: remote A2A clients start and stream local runs through the same AG-UI input allow-list and event mapper as the AG-UI SSE path (same projection, redaction, and byte caps). It reuses this package's `createA2AHandler` transport/lifecycle; it creates no second runtime, task store, or worker. Requires the optional `@arnilo/prism-core/runtime/supervisor` peer (imported lazily; plain `@arnilo/prism-ag-ui` imports keep working without it).
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
import { createAgentEventSourceAgUiReplay, createAgUiA2AServer } from "@arnilo/prism-ag-ui";
|