@arnilo/prism 0.0.4 → 0.0.6
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 +46 -1
- package/README.md +34 -10
- package/dist/agent-loops.d.ts +1 -0
- package/dist/agent-loops.js +26 -16
- package/dist/agents.js +147 -21
- package/dist/cli-init.d.ts +41 -0
- package/dist/cli-init.js +390 -0
- package/dist/cli-runner.d.ts +7 -1
- package/dist/cli-runner.js +13 -1
- package/dist/content.d.ts +19 -0
- package/dist/content.js +197 -69
- package/dist/contracts.d.ts +96 -9
- package/dist/contracts.js +8 -0
- package/dist/feedback.d.ts +48 -0
- package/dist/feedback.js +230 -0
- package/dist/ids.d.ts +2 -0
- package/dist/ids.js +6 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +6 -3
- package/dist/providers/media.d.ts +3 -1
- package/dist/providers/media.js +11 -1
- package/dist/session-stores.js +2 -3
- package/dist/testing/feedback.d.ts +6 -0
- package/dist/testing/feedback.js +37 -0
- package/dist/testing/persistence-schema.d.ts +48 -10
- package/dist/testing/persistence-schema.js +166 -22
- package/dist/testing/run-ledger-conformance.js +7 -1
- package/dist/thinking.d.ts +42 -0
- package/dist/thinking.js +92 -0
- package/dist/tools.js +2 -3
- package/dist/use-case-model.d.ts +63 -0
- package/dist/use-case-model.js +52 -0
- package/docs/a2a.md +75 -0
- package/docs/agent-events.md +14 -21
- package/docs/agent-loops.md +12 -9
- package/docs/agent-session-runtime.md +14 -16
- package/docs/cli-rpc.md +35 -7
- package/docs/coding-agent-tools.md +35 -14
- package/docs/coding-security.md +7 -3
- package/docs/compaction-llm.md +17 -7
- package/docs/compaction-observational-memory.md +30 -4
- package/docs/context-and-skills.md +1 -0
- package/docs/credential-storage.md +58 -9
- package/docs/credentials-and-redaction.md +3 -3
- package/docs/database-persistence.md +17 -9
- package/docs/evaluations.md +122 -0
- package/docs/extensions.md +2 -2
- package/docs/host-security.md +26 -5
- package/docs/index.md +43 -28
- package/docs/mcp-tools.md +74 -13
- package/docs/migration.md +177 -3
- package/docs/multimodal-content.md +14 -6
- package/docs/node-filesystem-config.md +1 -0
- package/docs/node-jsonl-session-store.md +5 -4
- package/docs/observability.md +14 -6
- package/docs/performance.md +209 -0
- package/docs/postgres-persistence.md +8 -6
- package/docs/provider-caching.md +16 -4
- package/docs/provider-conformance.md +40 -1
- package/docs/provider-packages.md +62 -3
- package/docs/providers/ai-sdk.md +149 -0
- package/docs/providers/kimi.md +124 -61
- package/docs/providers/neuralwatt.md +19 -13
- package/docs/providers/openai.md +56 -13
- package/docs/providers/opencode-go.md +118 -30
- package/docs/providers/openrouter.md +105 -35
- package/docs/providers/zai.md +94 -45
- package/docs/public-contracts.md +6 -5
- package/docs/rag.md +113 -0
- package/docs/release-and-install.md +100 -79
- package/docs/review-coverage-2026-07-15.md +193 -0
- package/docs/review-coverage-2026-07-17-provider-validation.md +192 -0
- package/docs/runs-and-usage.md +42 -5
- package/docs/server.md +139 -0
- package/docs/settings-auth-trust-security.md +5 -5
- package/docs/sqlite-persistence.md +6 -5
- package/docs/structured-output.md +1 -1
- package/docs/supervisors.md +71 -0
- package/docs/thinking-and-reasoning.md +98 -0
- package/docs/tool-execution-primitives.md +3 -3
- package/docs/tools.md +15 -0
- package/docs/use-case-model-selection.md +109 -0
- package/docs/workflow-orchestration-primitives.md +20 -3
- package/docs/workflows.md +114 -33
- package/docs/working-and-semantic-memory.md +170 -0
- package/package.json +13 -3
- package/templates/init/README.md.tmpl +28 -0
- package/templates/init/env.example.tmpl +1 -0
- package/templates/init/gitignore.tmpl +11 -0
- package/templates/init/optional/evals-example.ts.tmpl +17 -0
- package/templates/init/optional/workflows-example.ts.tmpl +27 -0
- package/templates/init/package.json.tmpl +22 -0
- package/templates/init/providers.json +76 -0
- package/templates/init/src/agent.ts.tmpl +10 -0
- package/templates/init/src/index.ts.tmpl +12 -0
- package/templates/init/src/tests/agent.test.ts.tmpl +24 -0
- package/templates/init/tsconfig.json.tmpl +15 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
// ponytail: dialect-neutral persistence schema model and migration contracts for
|
|
2
3
|
// SQLite/PostgreSQL adapter packages (Plan 056 Task 1). SQL stays package-local;
|
|
3
4
|
// this module defines the shared table/index/pagination/migration expectations
|
|
4
5
|
// adapter authors implement and test against before shipping dialect-specific DDL.
|
|
5
6
|
/** Current shared persistence schema version for production database adapters. */
|
|
6
|
-
export const PERSISTENCE_SCHEMA_VERSION =
|
|
7
|
+
export const PERSISTENCE_SCHEMA_VERSION = 3;
|
|
7
8
|
/** Guidance adapters must follow: values are bound parameters, never interpolated. */
|
|
8
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.";
|
|
9
10
|
const TENANT_COLUMNS = [
|
|
@@ -107,7 +108,7 @@ export function createPersistenceSchemaModel() {
|
|
|
107
108
|
{ name: "run_id", type: "text", nullable: true },
|
|
108
109
|
{ name: "timestamp", type: "timestamp" },
|
|
109
110
|
{ name: "kind", type: "text" },
|
|
110
|
-
{ name: "schema_version", type: "integer" },
|
|
111
|
+
{ name: "schema_version", type: "integer", nullable: true },
|
|
111
112
|
{ name: "message", type: "json", nullable: true },
|
|
112
113
|
{ name: "event", type: "json", nullable: true },
|
|
113
114
|
{ name: "model", type: "json", nullable: true },
|
|
@@ -156,7 +157,6 @@ export function createPersistenceSchemaModel() {
|
|
|
156
157
|
...TENANT_COLUMNS,
|
|
157
158
|
{ name: "metadata", type: "json", nullable: true },
|
|
158
159
|
],
|
|
159
|
-
uniqueKeys: [["tenant_id", "idempotency_key"]],
|
|
160
160
|
foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
|
|
161
161
|
},
|
|
162
162
|
{
|
|
@@ -210,6 +210,9 @@ export function createPersistenceSchemaModel() {
|
|
|
210
210
|
{ name: "session_id", type: "text" },
|
|
211
211
|
{ name: "run_id", type: "text", nullable: true },
|
|
212
212
|
{ name: "entry_id", type: "text", nullable: true },
|
|
213
|
+
{ name: "scope", type: "text", defaultValue: "'run_total'" },
|
|
214
|
+
{ name: "turn", type: "integer", nullable: true },
|
|
215
|
+
{ name: "attempt", type: "integer", nullable: true },
|
|
213
216
|
{ name: "usage", type: "json" },
|
|
214
217
|
{ name: "recorded_at", type: "timestamp" },
|
|
215
218
|
...TENANT_COLUMNS,
|
|
@@ -217,6 +220,28 @@ export function createPersistenceSchemaModel() {
|
|
|
217
220
|
],
|
|
218
221
|
foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
|
|
219
222
|
},
|
|
223
|
+
{
|
|
224
|
+
name: "prism_run_feedback",
|
|
225
|
+
primaryKey: ["id"],
|
|
226
|
+
columns: [
|
|
227
|
+
{ name: "id", type: "text" },
|
|
228
|
+
{ name: "run_id", type: "text" },
|
|
229
|
+
{ name: "session_id", type: "text" },
|
|
230
|
+
{ name: "trace_id", type: "text", nullable: true },
|
|
231
|
+
{ name: "rating", type: "number", nullable: true },
|
|
232
|
+
{ name: "comment", type: "text", nullable: true },
|
|
233
|
+
{ name: "tags", type: "json" },
|
|
234
|
+
{ name: "scorer_ids", type: "json" },
|
|
235
|
+
{ name: "evaluation_ids", type: "json" },
|
|
236
|
+
{ name: "created_at", type: "timestamp" },
|
|
237
|
+
{ name: "created_by", type: "text", nullable: true },
|
|
238
|
+
{ name: "tenant_id", type: "text", tenantScoped: true },
|
|
239
|
+
{ name: "account_id", type: "text", nullable: true, tenantScoped: true },
|
|
240
|
+
{ name: "user_id", type: "text", nullable: true, tenantScoped: true },
|
|
241
|
+
{ name: "metadata", type: "json", nullable: true },
|
|
242
|
+
],
|
|
243
|
+
foreignKeys: [{ columns: ["run_id"], referencesTable: "prism_runs", referencesColumns: ["id"] }],
|
|
244
|
+
},
|
|
220
245
|
{
|
|
221
246
|
name: "prism_retention_policies",
|
|
222
247
|
primaryKey: ["id"],
|
|
@@ -258,7 +283,7 @@ export function createPersistenceSchemaModel() {
|
|
|
258
283
|
{ name: "prism_session_entries_session_run_ts_idx", table: "prism_session_entries", columns: ["session_id", "run_id", "timestamp"], purpose: "run-scoped entry listing" },
|
|
259
284
|
{ name: "prism_session_entries_session_ts_id_idx", table: "prism_session_entries", columns: ["session_id", "timestamp", "id"], purpose: "cursor pagination without full scans" },
|
|
260
285
|
{ name: "prism_session_entries_session_id_idx", table: "prism_session_entries", columns: ["session_id", "id"], purpose: "append parent validation and recursive branch reads" },
|
|
261
|
-
{ name: "prism_session_append_idempotency_unique", table: "prism_session_append_idempotency", columns: ["session_id", "expected_parent_id", "idempotency_key"],
|
|
286
|
+
{ name: "prism_session_append_idempotency_unique", table: "prism_session_append_idempotency", columns: ["session_id", "expected_parent_id", "idempotency_key"], purpose: "append retry deduplication (primary key enforces uniqueness)" },
|
|
262
287
|
{ name: "prism_runs_session_started_idx", table: "prism_runs", columns: ["session_id", "started_at", "id"], purpose: "run history pagination" },
|
|
263
288
|
{ name: "prism_runs_branch_started_idx", table: "prism_runs", columns: ["branch_id", "started_at", "id"], purpose: "branch-scoped runs" },
|
|
264
289
|
{ name: "prism_runs_tenant_idempotency_unique", table: "prism_runs", columns: ["tenant_id", "idempotency_key"], unique: true, purpose: "run-level idempotency deduplication per tenant" },
|
|
@@ -268,18 +293,45 @@ export function createPersistenceSchemaModel() {
|
|
|
268
293
|
{ name: "prism_tool_calls_run_started_idx", table: "prism_tool_calls", columns: ["run_id", "started_at"], purpose: "run tool-call listing" },
|
|
269
294
|
{ name: "prism_usage_run_recorded_idx", table: "prism_usage", columns: ["run_id", "recorded_at", "id"], purpose: "run usage pagination" },
|
|
270
295
|
{ name: "prism_usage_session_recorded_idx", table: "prism_usage", columns: ["session_id", "recorded_at"], purpose: "usage aggregation" },
|
|
296
|
+
{ name: "prism_usage_session_scope_recorded_idx", table: "prism_usage", columns: ["session_id", "scope", "recorded_at"], purpose: "scope-safe usage aggregation" },
|
|
297
|
+
{ name: "prism_run_feedback_owner_created_idx", table: "prism_run_feedback", columns: ["tenant_id", "account_id", "user_id", "created_at", "id"], purpose: "ownership-scoped feedback pagination" },
|
|
298
|
+
{ name: "prism_run_feedback_run_created_idx", table: "prism_run_feedback", columns: ["run_id", "created_at", "id"], purpose: "run feedback lookup" },
|
|
299
|
+
{ name: "prism_run_feedback_trace_created_idx", table: "prism_run_feedback", columns: ["trace_id", "created_at", "id"], purpose: "trace feedback lookup" },
|
|
271
300
|
{ name: "prism_agent_definitions_name_version_idx", table: "prism_agent_definitions", columns: ["name", "version"], purpose: "definition lookup" },
|
|
272
|
-
{ name: "prism_migrations_name_version_idx", table: "prism_migrations", columns: ["name", "version"],
|
|
301
|
+
{ name: "prism_migrations_name_version_idx", table: "prism_migrations", columns: ["name", "version"], purpose: "applied-migration lookup (table constraint enforces uniqueness)" },
|
|
273
302
|
],
|
|
274
303
|
};
|
|
275
304
|
}
|
|
305
|
+
function migrationStep(version, name, description) {
|
|
306
|
+
const model = createPersistenceSchemaModel();
|
|
307
|
+
const content = version === 1
|
|
308
|
+
? {
|
|
309
|
+
tables: model.tables
|
|
310
|
+
.filter((table) => table.name !== "prism_run_feedback")
|
|
311
|
+
.map((table) => table.name === "prism_usage"
|
|
312
|
+
? { ...table, columns: table.columns.filter((column) => !["scope", "turn", "attempt"].includes(column.name)) }
|
|
313
|
+
: table),
|
|
314
|
+
indexes: model.indexes.filter((index) => !index.name.startsWith("prism_usage_session_scope_") && !index.name.startsWith("prism_run_feedback_")),
|
|
315
|
+
}
|
|
316
|
+
: version === 2
|
|
317
|
+
? { table: "prism_usage", columns: ["scope", "turn", "attempt"], indexes: ["prism_usage_session_scope_recorded_idx"] }
|
|
318
|
+
: { tables: ["prism_run_feedback"], indexes: model.indexes.filter((index) => index.name.startsWith("prism_run_feedback_")).map((index) => index.name) };
|
|
319
|
+
return {
|
|
320
|
+
version,
|
|
321
|
+
name,
|
|
322
|
+
description,
|
|
323
|
+
checksum: createHash("sha256").update(JSON.stringify({ version, name, content })).digest("hex"),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
276
326
|
/** Canonical migration contract for production adapters. */
|
|
277
327
|
export function createPersistenceMigrationContract() {
|
|
278
328
|
return {
|
|
279
329
|
targetSchemaVersion: PERSISTENCE_SCHEMA_VERSION,
|
|
280
330
|
appliedMigrationsTable: "prism_migrations",
|
|
281
331
|
steps: [
|
|
282
|
-
|
|
332
|
+
migrationStep(1, "001_init", "Create core session, branch, entry, idempotency, run, ledger, and migration tables."),
|
|
333
|
+
migrationStep(2, "002_usage_scope", "Distinguish provider-turn usage from aggregate run totals."),
|
|
334
|
+
migrationStep(3, "003_run_feedback", "Add immutable ownership-scoped run/trace feedback and evaluation links."),
|
|
283
335
|
],
|
|
284
336
|
lockGuidance: "Acquire a dialect-specific migration lock before applying steps (PostgreSQL advisory lock; SQLite exclusive transaction). Only one process should migrate at a time.",
|
|
285
337
|
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.",
|
|
@@ -294,6 +346,7 @@ export function getPersistencePaginationCursors() {
|
|
|
294
346
|
{ table: "prism_agent_events", columns: ["session_id", "timestamp", "id"], supportsOrder: ["asc", "desc"], purpose: "session event stream" },
|
|
295
347
|
{ table: "prism_usage", columns: ["run_id", "recorded_at", "id"], supportsOrder: ["asc", "desc"], purpose: "run usage totals" },
|
|
296
348
|
{ table: "prism_tool_calls", columns: ["run_id", "started_at"], supportsOrder: ["asc", "desc"], purpose: "run tool-call listing" },
|
|
349
|
+
{ table: "prism_run_feedback", columns: ["tenant_id", "account_id", "user_id", "created_at", "id"], supportsOrder: ["asc", "desc"], purpose: "owned feedback listing" },
|
|
297
350
|
];
|
|
298
351
|
}
|
|
299
352
|
/** Build a tenant-scoped unique key column list for adapter DDL. */
|
|
@@ -326,7 +379,7 @@ export function assertPersistenceSchemaModel(model) {
|
|
|
326
379
|
throw new Error("Runs table must include tenant-scoped tenant_id");
|
|
327
380
|
}
|
|
328
381
|
const indexTables = new Set(model.indexes.map((index) => index.table));
|
|
329
|
-
for (const requiredIndex of ["prism_session_append_idempotency", "prism_session_entries", "prism_agent_events", "prism_runs"]) {
|
|
382
|
+
for (const requiredIndex of ["prism_session_append_idempotency", "prism_session_entries", "prism_agent_events", "prism_runs", "prism_run_feedback"]) {
|
|
330
383
|
if (!indexTables.has(requiredIndex)) {
|
|
331
384
|
throw new Error(`Persistence schema missing indexes for ${requiredIndex}`);
|
|
332
385
|
}
|
|
@@ -357,6 +410,8 @@ export function assertPersistenceMigrationContract(contract) {
|
|
|
357
410
|
throw new Error(`Migration steps must be strictly increasing; ${step.name} is out of order`);
|
|
358
411
|
if (names.has(step.name))
|
|
359
412
|
throw new Error(`Duplicate migration step name: ${step.name}`);
|
|
413
|
+
if (!/^[a-f0-9]{64}$/.test(step.checksum))
|
|
414
|
+
throw new Error(`Migration step ${step.name} must have a SHA-256 checksum`);
|
|
360
415
|
names.add(step.name);
|
|
361
416
|
previous = step.version;
|
|
362
417
|
}
|
|
@@ -364,6 +419,101 @@ export function assertPersistenceMigrationContract(contract) {
|
|
|
364
419
|
throw new Error(`Last migration step version ${previous} must equal targetSchemaVersion ${contract.targetSchemaVersion}`);
|
|
365
420
|
}
|
|
366
421
|
}
|
|
422
|
+
function schemaKey(columns) {
|
|
423
|
+
return columns.join("\u0000");
|
|
424
|
+
}
|
|
425
|
+
function normalizedDefault(value) {
|
|
426
|
+
return value?.trim().toLowerCase().replace(/::[a-z_ ]+$/, "");
|
|
427
|
+
}
|
|
428
|
+
function compatibleColumnType(dialect, actual, expected) {
|
|
429
|
+
const type = actual.trim().toUpperCase();
|
|
430
|
+
if (dialect === "sqlite") {
|
|
431
|
+
if (expected === "integer" || expected === "boolean")
|
|
432
|
+
return type === "INTEGER";
|
|
433
|
+
if (expected === "number")
|
|
434
|
+
return type === "REAL";
|
|
435
|
+
return type === "TEXT";
|
|
436
|
+
}
|
|
437
|
+
if (expected === "integer")
|
|
438
|
+
return type === "INTEGER";
|
|
439
|
+
if (expected === "boolean")
|
|
440
|
+
return type === "BOOLEAN";
|
|
441
|
+
if (expected === "number")
|
|
442
|
+
return type === "DOUBLE PRECISION";
|
|
443
|
+
return type === "TEXT";
|
|
444
|
+
}
|
|
445
|
+
/** Compare bounded dialect catalog output against every required schema-v3 detail. */
|
|
446
|
+
export function assertPersistenceSchemaShape(shape, dialect, model = createPersistenceSchemaModel()) {
|
|
447
|
+
assertPersistenceSchemaModel(model);
|
|
448
|
+
const actualTables = new Map(shape.tables.map((table) => [table.name, table]));
|
|
449
|
+
for (const expected of model.tables) {
|
|
450
|
+
const actual = actualTables.get(expected.name);
|
|
451
|
+
if (!actual)
|
|
452
|
+
throw new Error(`Persistence schema missing table ${expected.name}`);
|
|
453
|
+
if (actual.columns.length !== expected.columns.length)
|
|
454
|
+
throw new Error(`Persistence schema table ${expected.name} has unexpected columns`);
|
|
455
|
+
const actualColumns = new Map(actual.columns.map((column) => [column.name, column]));
|
|
456
|
+
for (const column of expected.columns) {
|
|
457
|
+
const found = actualColumns.get(column.name);
|
|
458
|
+
if (!found)
|
|
459
|
+
throw new Error(`Persistence schema table ${expected.name} missing column ${column.name}`);
|
|
460
|
+
if (!compatibleColumnType(dialect, found.type, column.type)) {
|
|
461
|
+
throw new Error(`Persistence schema column ${expected.name}.${column.name} has incompatible type`);
|
|
462
|
+
}
|
|
463
|
+
if (found.nullable !== (column.nullable === true)) {
|
|
464
|
+
throw new Error(`Persistence schema column ${expected.name}.${column.name} has incompatible nullability`);
|
|
465
|
+
}
|
|
466
|
+
if (normalizedDefault(found.defaultValue) !== normalizedDefault(column.defaultValue)) {
|
|
467
|
+
throw new Error(`Persistence schema column ${expected.name}.${column.name} has incompatible default`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (schemaKey(actual.primaryKey) !== schemaKey(expected.primaryKey)) {
|
|
471
|
+
throw new Error(`Persistence schema table ${expected.name} has incompatible primary key`);
|
|
472
|
+
}
|
|
473
|
+
const unique = new Set(actual.uniqueKeys.map(schemaKey));
|
|
474
|
+
for (const key of expected.uniqueKeys ?? []) {
|
|
475
|
+
if (!unique.has(schemaKey(key)) && schemaKey(actual.primaryKey) !== schemaKey(key)) {
|
|
476
|
+
throw new Error(`Persistence schema table ${expected.name} missing unique key (${key.join(", ")})`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const foreign = new Set(actual.foreignKeys.map((key) => `${schemaKey(key.columns)}>${key.referencesTable}:${schemaKey(key.referencesColumns)}`));
|
|
480
|
+
for (const key of expected.foreignKeys ?? []) {
|
|
481
|
+
if (!foreign.has(`${schemaKey(key.columns)}>${key.referencesTable}:${schemaKey(key.referencesColumns)}`)) {
|
|
482
|
+
throw new Error(`Persistence schema table ${expected.name} missing foreign key (${key.columns.join(", ")})`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const actualIndexes = new Map(shape.indexes.map((index) => [index.name, index]));
|
|
487
|
+
for (const expected of model.indexes) {
|
|
488
|
+
const actual = actualIndexes.get(expected.name);
|
|
489
|
+
if (!actual)
|
|
490
|
+
throw new Error(`Persistence schema missing required index ${expected.name}`);
|
|
491
|
+
if (actual.table !== expected.table || schemaKey(actual.columns) !== schemaKey(expected.columns) || actual.unique !== (expected.unique === true)) {
|
|
492
|
+
throw new Error(`Persistence schema index ${expected.name} has incompatible definition`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** Reject altered migration history before any new DDL or runtime write. */
|
|
497
|
+
export function assertAppliedPersistenceMigrations(contract, applied) {
|
|
498
|
+
assertPersistenceMigrationContract(contract);
|
|
499
|
+
if (applied.length > contract.steps.length)
|
|
500
|
+
throw new Error("Migration history has unknown rows");
|
|
501
|
+
const legacyChecksums = applied.some((row) => row.checksum === null);
|
|
502
|
+
if (legacyChecksums && (applied.length !== contract.steps.length || !applied.every((row) => row.checksum === null))) {
|
|
503
|
+
throw new Error("Migration history has incomplete legacy checksums");
|
|
504
|
+
}
|
|
505
|
+
for (let index = 0; index < applied.length; index++) {
|
|
506
|
+
const row = applied[index];
|
|
507
|
+
const expected = contract.steps[index];
|
|
508
|
+
if (row.name !== expected.name || row.version !== String(expected.version)) {
|
|
509
|
+
throw new Error(`Migration history row ${index} does not match ${expected.name}`);
|
|
510
|
+
}
|
|
511
|
+
if (!legacyChecksums && row.checksum !== expected.checksum) {
|
|
512
|
+
throw new Error(`Migration history checksum mismatch for ${expected.name}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return { legacyChecksums };
|
|
516
|
+
}
|
|
367
517
|
/**
|
|
368
518
|
* Assert a dialect-local adapter exposes the canonical table and index names.
|
|
369
519
|
* Adapters pass the table/index names their migration runner created.
|
|
@@ -393,23 +543,17 @@ export function assertParameterizedQuery(sql, boundValues) {
|
|
|
393
543
|
}
|
|
394
544
|
/** Simulate migration up + reopen: applied steps must match the contract in order. */
|
|
395
545
|
export function assertMigrationUpAndReopen(contract, appliedAfterUp, appliedAfterReopen) {
|
|
396
|
-
|
|
397
|
-
if (appliedAfterUp.length !== contract.steps.length) {
|
|
398
|
-
throw new Error("Migration up did not apply every contract step");
|
|
399
|
-
}
|
|
400
|
-
for (let i = 0; i < contract.steps.length; i++) {
|
|
401
|
-
const step = contract.steps[i];
|
|
402
|
-
const row = appliedAfterUp[i];
|
|
403
|
-
if (row.name !== step.name || row.version !== String(step.version)) {
|
|
404
|
-
throw new Error(`Applied migration row ${i} does not match contract step ${step.name}`);
|
|
405
|
-
}
|
|
546
|
+
const first = assertAppliedPersistenceMigrations(contract, appliedAfterUp);
|
|
547
|
+
if (appliedAfterUp.length !== contract.steps.length || first.legacyChecksums) {
|
|
548
|
+
throw new Error("Migration up did not apply every checksummed contract step");
|
|
406
549
|
}
|
|
407
|
-
|
|
408
|
-
|
|
550
|
+
const second = assertAppliedPersistenceMigrations(contract, appliedAfterReopen);
|
|
551
|
+
if (second.legacyChecksums || appliedAfterReopen.length !== appliedAfterUp.length) {
|
|
552
|
+
throw new Error("Reopened adapter migration history diverged");
|
|
409
553
|
}
|
|
410
|
-
for (let
|
|
411
|
-
if (appliedAfterReopen[
|
|
412
|
-
throw new Error("Reopened adapter migration
|
|
554
|
+
for (let index = 0; index < appliedAfterUp.length; index++) {
|
|
555
|
+
if (appliedAfterReopen[index].checksum !== appliedAfterUp[index].checksum) {
|
|
556
|
+
throw new Error("Reopened adapter migration checksums diverged");
|
|
413
557
|
}
|
|
414
558
|
}
|
|
415
559
|
}
|
|
@@ -71,6 +71,9 @@ export async function assertRunLedgerConforms(fixture, options = {}) {
|
|
|
71
71
|
id: "usage-1",
|
|
72
72
|
sessionId,
|
|
73
73
|
runId,
|
|
74
|
+
scope: "provider_turn",
|
|
75
|
+
turn: 1,
|
|
76
|
+
attempt: 2,
|
|
74
77
|
usage: { inputTokens: 3, outputTokens: 5, totalTokens: 8 },
|
|
75
78
|
recordedAt: "2026-01-01T00:00:01.000Z",
|
|
76
79
|
...scope,
|
|
@@ -114,8 +117,11 @@ export async function assertRunLedgerConforms(fixture, options = {}) {
|
|
|
114
117
|
}
|
|
115
118
|
if (fixture.readUsage) {
|
|
116
119
|
const usageRows = await fixture.readUsage();
|
|
117
|
-
|
|
120
|
+
const storedUsage = usageRows.find((row) => row.id === "usage-1");
|
|
121
|
+
if (!storedUsage)
|
|
118
122
|
throw new Error("RunLedger must persist UsageRecord rows");
|
|
123
|
+
if (storedUsage.scope !== "provider_turn" || storedUsage.turn !== 1 || storedUsage.attempt !== 2) {
|
|
124
|
+
throw new Error("RunLedger must preserve UsageRecord scope, turn, and attempt");
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
if (options.exerciseTenantIsolation && fixture.readRuns) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { JsonObject, ModelConfig, ProviderRequestOptions } from "./contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Portable thinking / reasoning effort levels shared across first-party providers.
|
|
4
|
+
* Model-dependent legality (which values a given model accepts) stays provider-owned.
|
|
5
|
+
*/
|
|
6
|
+
export declare const THINKING_LEVELS: readonly ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
7
|
+
export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
8
|
+
/**
|
|
9
|
+
* Compat mapping families used by ≥2 packages, or explicit no-op for host-owned adapters.
|
|
10
|
+
* Provider packages keep unique escape hatches (budgets, keep/all, tool_stream) local.
|
|
11
|
+
*/
|
|
12
|
+
export type ThinkingCompatFamily = "openai_reasoning" | "reasoning_effort" | "thinking_type" | "noop";
|
|
13
|
+
export declare function isThinkingLevel(value: unknown): value is ThinkingLevel;
|
|
14
|
+
/**
|
|
15
|
+
* Normalize a host thinkingLevel string. Known levels are lowercased; other non-empty
|
|
16
|
+
* strings pass through as opaque effort values for forward-compatible provider fields.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeThinkingLevel(level: string): ThinkingLevel | string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Build the `ProviderRequestOptions.compat` patch for a shared thinking level.
|
|
21
|
+
* Does not invent a second options tree — providers keep reading official fields from `compat`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function thinkingCompatFor(family: ThinkingCompatFamily, level: ThinkingLevel | string): JsonObject;
|
|
24
|
+
/**
|
|
25
|
+
* Merge a shared thinking level into `providerOptions.compat` for the given family.
|
|
26
|
+
* Per-turn patches win over prior compat via {@link mergeProviderRequestOptions}.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyThinkingLevel(options: ProviderRequestOptions | undefined, level: ThinkingLevel | string, family?: ThinkingCompatFamily): ProviderRequestOptions;
|
|
29
|
+
/**
|
|
30
|
+
* Best-effort family inference from model metadata without a second options tree.
|
|
31
|
+
* Prefer an explicit family in hosts/use-case workers when the provider is known.
|
|
32
|
+
*
|
|
33
|
+
* Heuristics (ordered):
|
|
34
|
+
* 1. Existing `compat.thinking` object → `thinking_type`
|
|
35
|
+
* 2. Existing `compat.reasoning` → `openai_reasoning`
|
|
36
|
+
* 3. Existing `compat.reasoning_effort` → `reasoning_effort`
|
|
37
|
+
* 4. Provider id starting with `openai` → `openai_reasoning`
|
|
38
|
+
* 5. Provider id `neuralwatt` → `reasoning_effort`
|
|
39
|
+
* 6. `capabilities.reasoning` → `reasoning_effort` (portable string field)
|
|
40
|
+
* 7. Else `noop`
|
|
41
|
+
*/
|
|
42
|
+
export declare function thinkingFamilyForModel(model: Pick<ModelConfig, "provider" | "compat" | "capabilities">): ThinkingCompatFamily;
|
package/dist/thinking.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mergeProviderRequestOptions } from "./provider-request-policy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Portable thinking / reasoning effort levels shared across first-party providers.
|
|
4
|
+
* Model-dependent legality (which values a given model accepts) stays provider-owned.
|
|
5
|
+
*/
|
|
6
|
+
export const THINKING_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
7
|
+
export function isThinkingLevel(value) {
|
|
8
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a host thinkingLevel string. Known levels are lowercased; other non-empty
|
|
12
|
+
* strings pass through as opaque effort values for forward-compatible provider fields.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeThinkingLevel(level) {
|
|
15
|
+
const normalized = level.trim().toLowerCase();
|
|
16
|
+
if (!normalized)
|
|
17
|
+
return undefined;
|
|
18
|
+
return isThinkingLevel(normalized) ? normalized : normalized;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the `ProviderRequestOptions.compat` patch for a shared thinking level.
|
|
22
|
+
* Does not invent a second options tree — providers keep reading official fields from `compat`.
|
|
23
|
+
*/
|
|
24
|
+
export function thinkingCompatFor(family, level) {
|
|
25
|
+
const normalized = typeof level === "string" ? normalizeThinkingLevel(level) : level;
|
|
26
|
+
if (!normalized || family === "noop")
|
|
27
|
+
return {};
|
|
28
|
+
switch (family) {
|
|
29
|
+
case "openai_reasoning":
|
|
30
|
+
return { reasoning: { effort: normalized } };
|
|
31
|
+
case "reasoning_effort":
|
|
32
|
+
return { reasoning_effort: normalized };
|
|
33
|
+
case "thinking_type":
|
|
34
|
+
return { thinking: { type: normalized === "none" ? "disabled" : "enabled" } };
|
|
35
|
+
default: {
|
|
36
|
+
const _exhaustive = family;
|
|
37
|
+
return _exhaustive;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Merge a shared thinking level into `providerOptions.compat` for the given family.
|
|
43
|
+
* Per-turn patches win over prior compat via {@link mergeProviderRequestOptions}.
|
|
44
|
+
*/
|
|
45
|
+
export function applyThinkingLevel(options, level, family = "reasoning_effort") {
|
|
46
|
+
const normalized = normalizeThinkingLevel(String(level));
|
|
47
|
+
if (!normalized || family === "noop")
|
|
48
|
+
return options ?? {};
|
|
49
|
+
const patch = thinkingCompatFor(family, normalized);
|
|
50
|
+
if (family === "openai_reasoning" && options?.compat?.reasoning && typeof options.compat.reasoning === "object" && !Array.isArray(options.compat.reasoning)) {
|
|
51
|
+
return mergeProviderRequestOptions(options, {
|
|
52
|
+
compat: {
|
|
53
|
+
reasoning: {
|
|
54
|
+
...options.compat.reasoning,
|
|
55
|
+
...patch.reasoning,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return mergeProviderRequestOptions(options, { compat: patch });
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Best-effort family inference from model metadata without a second options tree.
|
|
64
|
+
* Prefer an explicit family in hosts/use-case workers when the provider is known.
|
|
65
|
+
*
|
|
66
|
+
* Heuristics (ordered):
|
|
67
|
+
* 1. Existing `compat.thinking` object → `thinking_type`
|
|
68
|
+
* 2. Existing `compat.reasoning` → `openai_reasoning`
|
|
69
|
+
* 3. Existing `compat.reasoning_effort` → `reasoning_effort`
|
|
70
|
+
* 4. Provider id starting with `openai` → `openai_reasoning`
|
|
71
|
+
* 5. Provider id `neuralwatt` → `reasoning_effort`
|
|
72
|
+
* 6. `capabilities.reasoning` → `reasoning_effort` (portable string field)
|
|
73
|
+
* 7. Else `noop`
|
|
74
|
+
*/
|
|
75
|
+
export function thinkingFamilyForModel(model) {
|
|
76
|
+
const compat = model.compat ?? {};
|
|
77
|
+
if (compat.thinking != null && typeof compat.thinking === "object")
|
|
78
|
+
return "thinking_type";
|
|
79
|
+
if (compat.reasoning != null)
|
|
80
|
+
return "openai_reasoning";
|
|
81
|
+
if (compat.reasoning_effort != null)
|
|
82
|
+
return "reasoning_effort";
|
|
83
|
+
const provider = model.provider.trim().toLowerCase();
|
|
84
|
+
if (provider === "openai" || provider.startsWith("openai"))
|
|
85
|
+
return "openai_reasoning";
|
|
86
|
+
if (provider === "neuralwatt")
|
|
87
|
+
return "reasoning_effort";
|
|
88
|
+
if (model.capabilities?.reasoning)
|
|
89
|
+
return "reasoning_effort";
|
|
90
|
+
return "noop";
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=thinking.js.map
|
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isJsonObject } from "./config.js";
|
|
2
|
+
import { createId } from "./ids.js";
|
|
2
3
|
import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "./redaction.js";
|
|
3
4
|
import { assertCanRegister } from "./registry-options.js";
|
|
4
5
|
import { assertPermission } from "./security.js";
|
|
@@ -149,9 +150,7 @@ async function blocked(call, context, reason, error, options, startedAt) {
|
|
|
149
150
|
function toErrorInfo(value, secrets) {
|
|
150
151
|
return typeof value === "string" ? errorToErrorInfo(value, secrets) : redactSecrets(value, secrets);
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
-
return `${prefix}_${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
|
|
154
|
-
}
|
|
153
|
+
const randomId = createId;
|
|
155
154
|
function appendToolCallRecord(options, status, call, startedAt, fields) {
|
|
156
155
|
if (!options.ledger)
|
|
157
156
|
return undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ModelConfig, ProviderRequestOptions } from "./contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Host binding for a non-session LLM job (observational memory, LLM compaction,
|
|
4
|
+
* declarative agents, evals, etc.). Omitting `model` means "use the session model"
|
|
5
|
+
* when a session fallback is supplied to {@link resolveUseCaseModel}.
|
|
6
|
+
*
|
|
7
|
+
* Workers must not write `model_change` session entries; they resolve a model for
|
|
8
|
+
* their own provider calls only.
|
|
9
|
+
*/
|
|
10
|
+
export interface UseCaseModelBinding {
|
|
11
|
+
/** Explicit use-case model. When omitted, {@link resolveUseCaseModel} falls back to `sessionModel`. */
|
|
12
|
+
readonly model?: ModelConfig;
|
|
13
|
+
/**
|
|
14
|
+
* Optional provider id hint for docs / credential routing.
|
|
15
|
+
* When `model` is set, `model.provider` is authoritative.
|
|
16
|
+
*/
|
|
17
|
+
readonly provider?: string;
|
|
18
|
+
readonly providerOptions?: ProviderRequestOptions;
|
|
19
|
+
/** Portable thinking level; packages map via `applyThinkingLevel` into `compat`. */
|
|
20
|
+
readonly thinkingLevel?: string;
|
|
21
|
+
/**
|
|
22
|
+
* When true, do not fall back to `sessionModel` — leave resolution empty if
|
|
23
|
+
* `model` is omitted (preserves historical explicit-worker `missing_model` behavior).
|
|
24
|
+
*/
|
|
25
|
+
readonly requireExplicitModel?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface ResolveUseCaseModelInput {
|
|
28
|
+
/** Explicit use-case model (or `binding.model`). */
|
|
29
|
+
readonly configured?: ModelConfig;
|
|
30
|
+
/** Active session / agent model used when `configured` is omitted. */
|
|
31
|
+
readonly sessionModel?: ModelConfig;
|
|
32
|
+
/** When true, skip session fallback (OM `missing_model` escape hatch). */
|
|
33
|
+
readonly requireExplicitModel?: boolean;
|
|
34
|
+
readonly providerOptions?: ProviderRequestOptions;
|
|
35
|
+
readonly thinkingLevel?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ResolvedUseCaseModel {
|
|
38
|
+
readonly model: ModelConfig;
|
|
39
|
+
/** Whether the model came from the use-case binding or session fallback. */
|
|
40
|
+
readonly source: "configured" | "session";
|
|
41
|
+
readonly providerOptions?: ProviderRequestOptions;
|
|
42
|
+
readonly thinkingLevel?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the model for a non-session LLM job.
|
|
46
|
+
*
|
|
47
|
+
* Precedence:
|
|
48
|
+
* 1. `configured` → `source: "configured"`
|
|
49
|
+
* 2. Else `sessionModel` when `requireExplicitModel` is not set → `source: "session"`
|
|
50
|
+
* 3. Else `undefined` (caller skips / throws per package policy)
|
|
51
|
+
*
|
|
52
|
+
* O(1); no network. Does not mutate session history.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveUseCaseModel(input: ResolveUseCaseModelInput): ResolvedUseCaseModel | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve from a {@link UseCaseModelBinding} plus optional session fallback.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveUseCaseModelBinding(binding: UseCaseModelBinding | undefined, sessionModel?: ModelConfig): ResolvedUseCaseModel | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Provider id for credential requests: always the **resolved** model's provider.
|
|
61
|
+
* Optional `binding.provider` is only a hint when no model resolved yet.
|
|
62
|
+
*/
|
|
63
|
+
export declare function useCaseCredentialProviderId(resolved: ResolvedUseCaseModel | undefined, binding?: Pick<UseCaseModelBinding, "provider">): string | undefined;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the model for a non-session LLM job.
|
|
3
|
+
*
|
|
4
|
+
* Precedence:
|
|
5
|
+
* 1. `configured` → `source: "configured"`
|
|
6
|
+
* 2. Else `sessionModel` when `requireExplicitModel` is not set → `source: "session"`
|
|
7
|
+
* 3. Else `undefined` (caller skips / throws per package policy)
|
|
8
|
+
*
|
|
9
|
+
* O(1); no network. Does not mutate session history.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveUseCaseModel(input) {
|
|
12
|
+
const { providerOptions, thinkingLevel } = input;
|
|
13
|
+
if (input.configured) {
|
|
14
|
+
return {
|
|
15
|
+
model: input.configured,
|
|
16
|
+
source: "configured",
|
|
17
|
+
providerOptions,
|
|
18
|
+
thinkingLevel,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (input.requireExplicitModel)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (input.sessionModel) {
|
|
24
|
+
return {
|
|
25
|
+
model: input.sessionModel,
|
|
26
|
+
source: "session",
|
|
27
|
+
providerOptions,
|
|
28
|
+
thinkingLevel,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve from a {@link UseCaseModelBinding} plus optional session fallback.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveUseCaseModelBinding(binding, sessionModel) {
|
|
37
|
+
return resolveUseCaseModel({
|
|
38
|
+
configured: binding?.model,
|
|
39
|
+
sessionModel,
|
|
40
|
+
requireExplicitModel: binding?.requireExplicitModel,
|
|
41
|
+
providerOptions: binding?.providerOptions,
|
|
42
|
+
thinkingLevel: binding?.thinkingLevel,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Provider id for credential requests: always the **resolved** model's provider.
|
|
47
|
+
* Optional `binding.provider` is only a hint when no model resolved yet.
|
|
48
|
+
*/
|
|
49
|
+
export function useCaseCredentialProviderId(resolved, binding) {
|
|
50
|
+
return resolved?.model.provider ?? binding?.provider;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=use-case-model.js.map
|