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