@arnilo/prism 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +32 -20
  3. package/dist/agent-loops.d.ts +8 -1
  4. package/dist/agent-loops.js +57 -11
  5. package/dist/agents.js +70 -17
  6. package/dist/checkpoints.d.ts +11 -0
  7. package/dist/checkpoints.js +144 -0
  8. package/dist/compaction.js +9 -1
  9. package/dist/content.d.ts +102 -0
  10. package/dist/content.js +410 -0
  11. package/dist/contracts.d.ts +142 -2
  12. package/dist/event-multiplexer.d.ts +23 -0
  13. package/dist/event-multiplexer.js +136 -0
  14. package/dist/execution-policy.d.ts +28 -0
  15. package/dist/execution-policy.js +24 -0
  16. package/dist/index.d.ts +17 -5
  17. package/dist/index.js +11 -4
  18. package/dist/input.js +11 -1
  19. package/dist/leases.d.ts +8 -0
  20. package/dist/leases.js +111 -0
  21. package/dist/node/agent-definitions.js +3 -5
  22. package/dist/node/config.d.ts +1 -0
  23. package/dist/node/config.js +5 -3
  24. package/dist/node/contribution-discovery.js +5 -8
  25. package/dist/node/session-store-jsonl.js +8 -5
  26. package/dist/node/settings.js +2 -2
  27. package/dist/node/trust.js +2 -4
  28. package/dist/observability.d.ts +3 -0
  29. package/dist/observability.js +18 -0
  30. package/dist/providers/media.d.ts +42 -0
  31. package/dist/providers/media.js +116 -0
  32. package/dist/providers/openai-compatible.js +18 -119
  33. package/dist/providers/openai-primitives.d.ts +9 -0
  34. package/dist/providers/openai-primitives.js +129 -0
  35. package/dist/providers/transport.d.ts +40 -0
  36. package/dist/providers/transport.js +221 -0
  37. package/dist/redaction.js +40 -13
  38. package/dist/resources.d.ts +5 -0
  39. package/dist/resources.js +4 -0
  40. package/dist/structured-output.d.ts +11 -0
  41. package/dist/structured-output.js +59 -0
  42. package/dist/testing/persistence-schema.d.ts +102 -0
  43. package/dist/testing/persistence-schema.js +457 -0
  44. package/dist/testing/provider-conformance.js +10 -1
  45. package/dist/testing/run-ledger-conformance.d.ts +33 -0
  46. package/dist/testing/run-ledger-conformance.js +172 -0
  47. package/dist/testing/session-store-conformance.d.ts +16 -0
  48. package/dist/testing/session-store-conformance.js +73 -0
  49. package/dist/tools.d.ts +17 -0
  50. package/dist/tools.js +29 -2
  51. package/docs/agent-events.md +13 -4
  52. package/docs/agent-loops.md +10 -4
  53. package/docs/agent-session-runtime.md +1 -0
  54. package/docs/cli-rpc.md +3 -0
  55. package/docs/coding-agent-tools.md +41 -7
  56. package/docs/coding-security.md +84 -0
  57. package/docs/credential-storage.md +177 -0
  58. package/docs/credentials-and-redaction.md +2 -1
  59. package/docs/database-persistence.md +44 -2
  60. package/docs/host-security.md +15 -1
  61. package/docs/index.md +28 -12
  62. package/docs/input-and-prompt-assembly.md +6 -5
  63. package/docs/mcp-tools.md +139 -0
  64. package/docs/middleware-hooks.md +2 -0
  65. package/docs/migration.md +21 -28
  66. package/docs/model-registry.md +5 -3
  67. package/docs/multimodal-content.md +148 -0
  68. package/docs/observability.md +163 -0
  69. package/docs/performance.md +40 -1
  70. package/docs/persistence-credentials-multimodality-primitives.md +303 -0
  71. package/docs/postgres-persistence.md +141 -0
  72. package/docs/provider-conformance.md +17 -0
  73. package/docs/provider-layer.md +1 -1
  74. package/docs/provider-primitives.md +281 -0
  75. package/docs/providers/kimi.md +1 -0
  76. package/docs/providers/neuralwatt.md +1 -0
  77. package/docs/providers/openai-compatible.md +2 -1
  78. package/docs/providers/openai.md +8 -1
  79. package/docs/providers/opencode-go.md +1 -0
  80. package/docs/providers/openrouter.md +1 -0
  81. package/docs/providers/zai.md +1 -0
  82. package/docs/public-contracts.md +9 -2
  83. package/docs/release-and-install.md +209 -25
  84. package/docs/resource-loading.md +14 -4
  85. package/docs/review-coverage-2026-07-14.md +260 -0
  86. package/docs/run-ledger-conformance.md +96 -0
  87. package/docs/runs-and-usage.md +2 -0
  88. package/docs/session-store-conformance.md +16 -0
  89. package/docs/session-stores-and-branching.md +1 -0
  90. package/docs/settings-auth-trust-security.md +2 -1
  91. package/docs/sqlite-persistence.md +122 -0
  92. package/docs/structured-output.md +9 -0
  93. package/docs/tool-conformance.md +1 -0
  94. package/docs/tool-execution-primitives.md +374 -0
  95. package/docs/tools.md +39 -1
  96. package/docs/workflow-orchestration-primitives.md +565 -0
  97. package/docs/workflow-tui-primitives.md +5 -0
  98. package/docs/workflows.md +219 -0
  99. package/package.json +33 -5
@@ -0,0 +1,102 @@
1
+ import type { PersistencePage, SessionEntry, SessionEntryQuery } from "../contracts.js";
2
+ /** Current shared persistence schema version for production database adapters. */
3
+ export declare const PERSISTENCE_SCHEMA_VERSION = 1;
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_tool_calls" | "prism_usage" | "prism_retention_policies" | "prism_migrations";
5
+ export type PersistenceColumnType = "text" | "integer" | "boolean" | "json" | "timestamp";
6
+ export interface PersistenceColumnDefinition {
7
+ readonly name: string;
8
+ readonly type: PersistenceColumnType;
9
+ readonly nullable?: boolean;
10
+ /** Column participates in tenant isolation boundaries when true. */
11
+ readonly tenantScoped?: boolean;
12
+ }
13
+ export interface PersistenceForeignKeyDefinition {
14
+ readonly columns: readonly string[];
15
+ readonly referencesTable: PersistenceTableName;
16
+ readonly referencesColumns: readonly string[];
17
+ /** When true, tenant_id must participate in the FK boundary for scoped tables. */
18
+ readonly tenantBound?: boolean;
19
+ }
20
+ export interface PersistenceTableDefinition {
21
+ readonly name: PersistenceTableName;
22
+ readonly columns: readonly PersistenceColumnDefinition[];
23
+ readonly primaryKey: readonly string[];
24
+ readonly uniqueKeys?: readonly (readonly string[])[];
25
+ readonly foreignKeys?: readonly PersistenceForeignKeyDefinition[];
26
+ }
27
+ export interface PersistenceIndexDefinition {
28
+ readonly name: string;
29
+ readonly table: PersistenceTableName;
30
+ readonly columns: readonly string[];
31
+ readonly unique?: boolean;
32
+ /** Human-readable query-plan purpose for adapter authors and tests. */
33
+ readonly purpose: string;
34
+ }
35
+ /** Dialect-neutral schema model shared by SQLite and PostgreSQL adapters. */
36
+ export interface PersistenceSchemaModel {
37
+ readonly version: number;
38
+ readonly tables: readonly PersistenceTableDefinition[];
39
+ readonly indexes: readonly PersistenceIndexDefinition[];
40
+ }
41
+ export interface PersistenceMigrationStep {
42
+ readonly version: number;
43
+ readonly name: string;
44
+ readonly description?: string;
45
+ }
46
+ /** Versioned migration expectations shared by production database adapters. */
47
+ export interface PersistenceMigrationContract {
48
+ readonly targetSchemaVersion: number;
49
+ readonly appliedMigrationsTable: PersistenceTableName;
50
+ readonly steps: readonly PersistenceMigrationStep[];
51
+ /** Advisory-lock or equivalent guidance for concurrent migration setup. */
52
+ readonly lockGuidance: string;
53
+ /** Least-privilege role guidance for migration vs runtime credentials. */
54
+ readonly leastPrivilegeGuidance: string;
55
+ }
56
+ /** Cursor key shapes adapters must index so pagination avoids full scans. */
57
+ export interface PersistencePaginationCursor {
58
+ readonly table: PersistenceTableName;
59
+ readonly columns: readonly string[];
60
+ readonly supportsOrder: readonly ("asc" | "desc")[];
61
+ readonly purpose: string;
62
+ }
63
+ /** Guidance adapters must follow: values are bound parameters, never interpolated. */
64
+ export declare 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.";
65
+ /** Canonical shared schema model for Tasks 2–3 adapter packages. */
66
+ export declare function createPersistenceSchemaModel(): PersistenceSchemaModel;
67
+ /** Canonical migration contract for production adapters. */
68
+ export declare function createPersistenceMigrationContract(): PersistenceMigrationContract;
69
+ /** Indexed cursor columns adapters must support for paginated reads. */
70
+ export declare function getPersistencePaginationCursors(): readonly PersistencePaginationCursor[];
71
+ /** Build a tenant-scoped unique key column list for adapter DDL. */
72
+ export declare function tenantScopedUniqueKey(baseColumns: readonly string[], tenantColumn?: "tenant_id" | "account_id" | "user_id"): readonly string[];
73
+ /** Assert a schema model includes required tables, tenant boundaries, and indexes. */
74
+ export declare function assertPersistenceSchemaModel(model: PersistenceSchemaModel): void;
75
+ /** Assert migration steps are strictly increasing and end at the target schema version. */
76
+ export declare function assertPersistenceMigrationContract(contract: PersistenceMigrationContract): void;
77
+ /**
78
+ * Assert a dialect-local adapter exposes the canonical table and index names.
79
+ * Adapters pass the table/index names their migration runner created.
80
+ */
81
+ export declare function assertAdapterSchemaMatchesModel(adapterTables: readonly string[], adapterIndexes: readonly string[], model?: PersistenceSchemaModel): void;
82
+ /** Guard adapter SQL tests: reject obvious value interpolation into statement text. */
83
+ export declare function assertParameterizedQuery(sql: string, boundValues: readonly unknown[]): void;
84
+ /** Simulate migration up + reopen: applied steps must match the contract in order. */
85
+ export declare function assertMigrationUpAndReopen(contract: PersistenceMigrationContract, appliedAfterUp: readonly {
86
+ readonly name: string;
87
+ readonly version: string;
88
+ }[], appliedAfterReopen: readonly {
89
+ readonly name: string;
90
+ readonly version: string;
91
+ }[]): void;
92
+ export interface PersistenceQueryConformanceFixture {
93
+ readonly seedEntries: (entries: readonly SessionEntry[]) => Promise<void> | void;
94
+ readonly queryEntries: (query: SessionEntryQuery) => Promise<PersistencePage<SessionEntry>>;
95
+ }
96
+ /** Assert cursor pagination returns stable pages without repeating rows. */
97
+ export declare function assertPersistenceQueryPaginationConforms(fixture: PersistenceQueryConformanceFixture, sessionId?: string): Promise<void>;
98
+ /** Assert tenant-filtered queries do not return rows from another tenant. */
99
+ export declare function assertTenantScopedQueryIsolation(queryByTenant: (tenantId: string) => Promise<readonly {
100
+ readonly tenantId?: string;
101
+ readonly id: string;
102
+ }[]>): Promise<void>;
@@ -0,0 +1,457 @@
1
+ // ponytail: dialect-neutral persistence schema model and migration contracts for
2
+ // SQLite/PostgreSQL adapter packages (Plan 056 Task 1). SQL stays package-local;
3
+ // this module defines the shared table/index/pagination/migration expectations
4
+ // adapter authors implement and test against before shipping dialect-specific DDL.
5
+ /** Current shared persistence schema version for production database adapters. */
6
+ export const PERSISTENCE_SCHEMA_VERSION = 1;
7
+ /** Guidance adapters must follow: values are bound parameters, never interpolated. */
8
+ 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
+ const TENANT_COLUMNS = [
10
+ { name: "tenant_id", type: "text", nullable: true, tenantScoped: true },
11
+ { name: "account_id", type: "text", nullable: true, tenantScoped: true },
12
+ { name: "user_id", type: "text", nullable: true, tenantScoped: true },
13
+ ];
14
+ /** Canonical shared schema model for Tasks 2–3 adapter packages. */
15
+ export function createPersistenceSchemaModel() {
16
+ return {
17
+ version: PERSISTENCE_SCHEMA_VERSION,
18
+ tables: [
19
+ {
20
+ name: "prism_tenants",
21
+ primaryKey: ["id"],
22
+ columns: [
23
+ { name: "id", type: "text" },
24
+ { name: "name", type: "text" },
25
+ { name: "created_at", type: "timestamp" },
26
+ { name: "metadata", type: "json", nullable: true },
27
+ ],
28
+ },
29
+ {
30
+ name: "prism_accounts",
31
+ primaryKey: ["id"],
32
+ columns: [
33
+ { name: "id", type: "text" },
34
+ { name: "tenant_id", type: "text", tenantScoped: true },
35
+ { name: "name", type: "text" },
36
+ { name: "created_at", type: "timestamp" },
37
+ { name: "metadata", type: "json", nullable: true },
38
+ ],
39
+ foreignKeys: [{ columns: ["tenant_id"], referencesTable: "prism_tenants", referencesColumns: ["id"], tenantBound: true }],
40
+ },
41
+ {
42
+ name: "prism_users",
43
+ primaryKey: ["id"],
44
+ columns: [
45
+ { name: "id", type: "text" },
46
+ { name: "tenant_id", type: "text", tenantScoped: true },
47
+ { name: "account_id", type: "text", nullable: true, tenantScoped: true },
48
+ { name: "name", type: "text" },
49
+ { name: "created_at", type: "timestamp" },
50
+ { name: "metadata", type: "json", nullable: true },
51
+ ],
52
+ },
53
+ {
54
+ name: "prism_agent_definitions",
55
+ primaryKey: ["id"],
56
+ columns: [
57
+ { name: "id", type: "text" },
58
+ { name: "name", type: "text" },
59
+ { name: "version", type: "text" },
60
+ { name: "source", type: "text", nullable: true },
61
+ { name: "agent_definition", type: "json" },
62
+ ...TENANT_COLUMNS,
63
+ { name: "created_at", type: "timestamp" },
64
+ { name: "created_by", type: "text", nullable: true },
65
+ { name: "metadata", type: "json", nullable: true },
66
+ ],
67
+ uniqueKeys: [["name", "version"]],
68
+ },
69
+ {
70
+ name: "prism_sessions",
71
+ primaryKey: ["id"],
72
+ columns: [
73
+ { name: "id", type: "text" },
74
+ ...TENANT_COLUMNS,
75
+ { name: "parent_session_id", type: "text", nullable: true },
76
+ { name: "agent_definition_id", type: "text", nullable: true },
77
+ { name: "agent_definition_version", type: "text", nullable: true },
78
+ { name: "created_at", type: "timestamp" },
79
+ { name: "updated_at", type: "timestamp" },
80
+ { name: "expires_at", type: "timestamp", nullable: true },
81
+ { name: "retention_policy_id", type: "text", nullable: true },
82
+ { name: "metadata", type: "json", nullable: true },
83
+ ],
84
+ },
85
+ {
86
+ name: "prism_branches",
87
+ primaryKey: ["id"],
88
+ columns: [
89
+ { name: "id", type: "text" },
90
+ { name: "session_id", type: "text" },
91
+ { name: "name", type: "text", nullable: true },
92
+ { name: "root_entry_id", type: "text", nullable: true },
93
+ { name: "parent_branch_id", type: "text", nullable: true },
94
+ { name: "leaf_entry_id", type: "text", nullable: true },
95
+ { name: "created_at", type: "timestamp" },
96
+ { name: "metadata", type: "json", nullable: true },
97
+ ],
98
+ foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
99
+ },
100
+ {
101
+ name: "prism_session_entries",
102
+ primaryKey: ["id"],
103
+ columns: [
104
+ { name: "id", type: "text" },
105
+ { name: "session_id", type: "text" },
106
+ { name: "parent_id", type: "text", nullable: true },
107
+ { name: "run_id", type: "text", nullable: true },
108
+ { name: "timestamp", type: "timestamp" },
109
+ { name: "kind", type: "text" },
110
+ { name: "schema_version", type: "integer" },
111
+ { name: "message", type: "json", nullable: true },
112
+ { name: "event", type: "json", nullable: true },
113
+ { name: "model", type: "json", nullable: true },
114
+ { name: "previous_model", type: "json", nullable: true },
115
+ { name: "label", type: "text", nullable: true },
116
+ { name: "summary", type: "text", nullable: true },
117
+ { name: "data", type: "json", nullable: true },
118
+ { name: "metadata", type: "json", nullable: true },
119
+ ],
120
+ foreignKeys: [
121
+ { columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] },
122
+ { columns: ["parent_id"], referencesTable: "prism_session_entries", referencesColumns: ["id"] },
123
+ ],
124
+ },
125
+ {
126
+ name: "prism_session_append_idempotency",
127
+ primaryKey: ["session_id", "expected_parent_id", "idempotency_key"],
128
+ columns: [
129
+ { name: "session_id", type: "text" },
130
+ { name: "expected_parent_id", type: "text" },
131
+ { name: "idempotency_key", type: "text" },
132
+ { name: "entry_id", type: "text" },
133
+ { name: "created_at", type: "timestamp" },
134
+ ...TENANT_COLUMNS,
135
+ ],
136
+ uniqueKeys: [["session_id", "expected_parent_id", "idempotency_key"]],
137
+ foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
138
+ },
139
+ {
140
+ name: "prism_runs",
141
+ primaryKey: ["id"],
142
+ columns: [
143
+ { name: "id", type: "text" },
144
+ { name: "session_id", type: "text" },
145
+ { name: "branch_id", type: "text", nullable: true },
146
+ { name: "agent_definition_id", type: "text", nullable: true },
147
+ { name: "agent_definition_version", type: "text", nullable: true },
148
+ { name: "status", type: "text", nullable: true },
149
+ { name: "started_at", type: "timestamp" },
150
+ { name: "finished_at", type: "timestamp", nullable: true },
151
+ { name: "model", type: "json", nullable: true },
152
+ { name: "provider", type: "text", nullable: true },
153
+ { name: "idempotency_key", type: "text", nullable: true },
154
+ { name: "abort_reason", type: "text", nullable: true },
155
+ { name: "error", type: "json", nullable: true },
156
+ ...TENANT_COLUMNS,
157
+ { name: "metadata", type: "json", nullable: true },
158
+ ],
159
+ uniqueKeys: [["tenant_id", "idempotency_key"]],
160
+ foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
161
+ },
162
+ {
163
+ name: "prism_agent_events",
164
+ primaryKey: ["id"],
165
+ columns: [
166
+ { name: "id", type: "text" },
167
+ { name: "session_id", type: "text" },
168
+ { name: "run_id", type: "text", nullable: true },
169
+ { name: "entry_id", type: "text", nullable: true },
170
+ { name: "sequence", type: "integer" },
171
+ { name: "type", type: "text" },
172
+ { name: "timestamp", type: "timestamp" },
173
+ { name: "event", type: "json" },
174
+ { name: "redacted", type: "boolean" },
175
+ ...TENANT_COLUMNS,
176
+ { name: "metadata", type: "json", nullable: true },
177
+ ],
178
+ foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
179
+ },
180
+ {
181
+ name: "prism_tool_calls",
182
+ primaryKey: ["id"],
183
+ columns: [
184
+ { name: "id", type: "text" },
185
+ { name: "session_id", type: "text" },
186
+ { name: "run_id", type: "text", nullable: true },
187
+ { name: "entry_id", type: "text", nullable: true },
188
+ { name: "tool_call_id", type: "text" },
189
+ { name: "name", type: "text" },
190
+ { name: "arguments", type: "json" },
191
+ { name: "result", type: "json", nullable: true },
192
+ { name: "status", type: "text", nullable: true },
193
+ { name: "reason", type: "text", nullable: true },
194
+ { name: "progress", type: "json", nullable: true },
195
+ { name: "progress_metadata", type: "json", nullable: true },
196
+ { name: "progress_at", type: "timestamp", nullable: true },
197
+ { name: "started_at", type: "timestamp" },
198
+ { name: "finished_at", type: "timestamp", nullable: true },
199
+ { name: "redacted", type: "boolean" },
200
+ ...TENANT_COLUMNS,
201
+ { name: "metadata", type: "json", nullable: true },
202
+ ],
203
+ foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
204
+ },
205
+ {
206
+ name: "prism_usage",
207
+ primaryKey: ["id"],
208
+ columns: [
209
+ { name: "id", type: "text" },
210
+ { name: "session_id", type: "text" },
211
+ { name: "run_id", type: "text", nullable: true },
212
+ { name: "entry_id", type: "text", nullable: true },
213
+ { name: "usage", type: "json" },
214
+ { name: "recorded_at", type: "timestamp" },
215
+ ...TENANT_COLUMNS,
216
+ { name: "metadata", type: "json", nullable: true },
217
+ ],
218
+ foreignKeys: [{ columns: ["session_id"], referencesTable: "prism_sessions", referencesColumns: ["id"] }],
219
+ },
220
+ {
221
+ name: "prism_retention_policies",
222
+ primaryKey: ["id"],
223
+ columns: [
224
+ { name: "id", type: "text" },
225
+ ...TENANT_COLUMNS,
226
+ { name: "name", type: "text", nullable: true },
227
+ { name: "max_age_days", type: "integer", nullable: true },
228
+ { name: "max_entries_per_session", type: "integer", nullable: true },
229
+ { name: "max_total_bytes", type: "integer", nullable: true },
230
+ { name: "archive_store", type: "text", nullable: true },
231
+ { name: "applied_kinds", type: "json", nullable: true },
232
+ { name: "created_at", type: "timestamp" },
233
+ { name: "metadata", type: "json", nullable: true },
234
+ ],
235
+ },
236
+ {
237
+ name: "prism_migrations",
238
+ primaryKey: ["id"],
239
+ columns: [
240
+ { name: "id", type: "text" },
241
+ { name: "name", type: "text" },
242
+ { name: "version", type: "text" },
243
+ { name: "applied_at", type: "timestamp" },
244
+ { name: "applied_by", type: "text", nullable: true },
245
+ { name: "checksum", type: "text", nullable: true },
246
+ { name: "metadata", type: "json", nullable: true },
247
+ ],
248
+ uniqueKeys: [["name", "version"]],
249
+ },
250
+ ],
251
+ indexes: [
252
+ { name: "prism_sessions_tenant_created_idx", table: "prism_sessions", columns: ["tenant_id", "account_id", "user_id", "created_at"], purpose: "tenant-scoped session listing" },
253
+ { name: "prism_sessions_expires_idx", table: "prism_sessions", columns: ["expires_at"], purpose: "retention expiry scans" },
254
+ { name: "prism_branches_session_name_idx", table: "prism_branches", columns: ["session_id", "name"], purpose: "named branch lookup" },
255
+ { name: "prism_branches_leaf_idx", table: "prism_branches", columns: ["leaf_entry_id"], purpose: "leaf-to-branch resolution" },
256
+ { name: "prism_session_entries_session_parent_idx", table: "prism_session_entries", columns: ["session_id", "parent_id"], purpose: "parent existence checks and child lookups" },
257
+ { name: "prism_session_entries_session_kind_ts_idx", table: "prism_session_entries", columns: ["session_id", "kind", "timestamp"], purpose: "kind-filtered entry listing" },
258
+ { name: "prism_session_entries_session_run_ts_idx", table: "prism_session_entries", columns: ["session_id", "run_id", "timestamp"], purpose: "run-scoped entry listing" },
259
+ { name: "prism_session_entries_session_ts_id_idx", table: "prism_session_entries", columns: ["session_id", "timestamp", "id"], purpose: "cursor pagination without full scans" },
260
+ { 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"], unique: true, purpose: "append retry deduplication" },
262
+ { name: "prism_runs_session_started_idx", table: "prism_runs", columns: ["session_id", "started_at", "id"], purpose: "run history pagination" },
263
+ { name: "prism_runs_branch_started_idx", table: "prism_runs", columns: ["branch_id", "started_at", "id"], purpose: "branch-scoped runs" },
264
+ { name: "prism_runs_tenant_idempotency_unique", table: "prism_runs", columns: ["tenant_id", "idempotency_key"], unique: true, purpose: "run-level idempotency deduplication per tenant" },
265
+ { name: "prism_agent_events_run_sequence_idx", table: "prism_agent_events", columns: ["run_id", "sequence"], purpose: "stable per-run event timeline pagination" },
266
+ { name: "prism_agent_events_session_ts_id_idx", table: "prism_agent_events", columns: ["session_id", "timestamp", "id"], purpose: "event stream pagination" },
267
+ { name: "prism_tool_calls_session_name_started_idx", table: "prism_tool_calls", columns: ["session_id", "name", "started_at"], purpose: "tool usage by name" },
268
+ { name: "prism_tool_calls_run_started_idx", table: "prism_tool_calls", columns: ["run_id", "started_at"], purpose: "run tool-call listing" },
269
+ { name: "prism_usage_run_recorded_idx", table: "prism_usage", columns: ["run_id", "recorded_at", "id"], purpose: "run usage pagination" },
270
+ { name: "prism_usage_session_recorded_idx", table: "prism_usage", columns: ["session_id", "recorded_at"], purpose: "usage aggregation" },
271
+ { 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"], unique: true, purpose: "applied-migration uniqueness" },
273
+ ],
274
+ };
275
+ }
276
+ /** Canonical migration contract for production adapters. */
277
+ export function createPersistenceMigrationContract() {
278
+ return {
279
+ targetSchemaVersion: PERSISTENCE_SCHEMA_VERSION,
280
+ appliedMigrationsTable: "prism_migrations",
281
+ steps: [
282
+ { version: 1, name: "001_init", description: "Create core session, branch, entry, idempotency, run, ledger, and migration tables." },
283
+ ],
284
+ 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
+ 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.",
286
+ };
287
+ }
288
+ /** Indexed cursor columns adapters must support for paginated reads. */
289
+ export function getPersistencePaginationCursors() {
290
+ return [
291
+ { table: "prism_session_entries", columns: ["session_id", "timestamp", "id"], supportsOrder: ["asc", "desc"], purpose: "entry listing and branch paging" },
292
+ { table: "prism_runs", columns: ["session_id", "started_at", "id"], supportsOrder: ["asc", "desc"], purpose: "run history" },
293
+ { table: "prism_agent_events", columns: ["run_id", "sequence"], supportsOrder: ["asc", "desc"], purpose: "stable per-run event timeline" },
294
+ { table: "prism_agent_events", columns: ["session_id", "timestamp", "id"], supportsOrder: ["asc", "desc"], purpose: "session event stream" },
295
+ { table: "prism_usage", columns: ["run_id", "recorded_at", "id"], supportsOrder: ["asc", "desc"], purpose: "run usage totals" },
296
+ { table: "prism_tool_calls", columns: ["run_id", "started_at"], supportsOrder: ["asc", "desc"], purpose: "run tool-call listing" },
297
+ ];
298
+ }
299
+ /** Build a tenant-scoped unique key column list for adapter DDL. */
300
+ export function tenantScopedUniqueKey(baseColumns, tenantColumn = "tenant_id") {
301
+ return [tenantColumn, ...baseColumns];
302
+ }
303
+ /** Assert a schema model includes required tables, tenant boundaries, and indexes. */
304
+ export function assertPersistenceSchemaModel(model) {
305
+ const canonical = createPersistenceSchemaModel();
306
+ if (model.version !== canonical.version) {
307
+ throw new Error(`Persistence schema version mismatch: expected ${canonical.version}, got ${model.version}`);
308
+ }
309
+ const tableNames = new Set(model.tables.map((table) => table.name));
310
+ for (const required of canonical.tables.map((table) => table.name)) {
311
+ if (!tableNames.has(required))
312
+ throw new Error(`Persistence schema missing required table: ${required}`);
313
+ }
314
+ const idempotency = model.tables.find((table) => table.name === "prism_session_append_idempotency");
315
+ if (!idempotency)
316
+ throw new Error("Persistence schema missing idempotency side table");
317
+ if (!idempotency.columns.some((column) => column.name === "tenant_id" && column.tenantScoped)) {
318
+ throw new Error("Idempotency table must include tenant-scoped tenant_id for isolation boundaries");
319
+ }
320
+ const hasIdempotencyUnique = (idempotency.uniqueKeys ?? []).some((key) => key.includes("session_id") && key.includes("expected_parent_id") && key.includes("idempotency_key"));
321
+ if (!hasIdempotencyUnique) {
322
+ throw new Error("Idempotency table must declare unique (session_id, expected_parent_id, idempotency_key)");
323
+ }
324
+ const runs = model.tables.find((table) => table.name === "prism_runs");
325
+ if (!runs?.columns.some((column) => column.name === "tenant_id" && column.tenantScoped)) {
326
+ throw new Error("Runs table must include tenant-scoped tenant_id");
327
+ }
328
+ 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"]) {
330
+ if (!indexTables.has(requiredIndex)) {
331
+ throw new Error(`Persistence schema missing indexes for ${requiredIndex}`);
332
+ }
333
+ }
334
+ const paginationTables = new Set(getPersistencePaginationCursors().map((cursor) => cursor.table));
335
+ for (const table of paginationTables) {
336
+ if (!tableNames.has(table))
337
+ throw new Error(`Pagination cursor references missing table: ${table}`);
338
+ }
339
+ }
340
+ /** Assert migration steps are strictly increasing and end at the target schema version. */
341
+ export function assertPersistenceMigrationContract(contract) {
342
+ if (contract.targetSchemaVersion !== PERSISTENCE_SCHEMA_VERSION) {
343
+ throw new Error(`Migration contract target version must be ${PERSISTENCE_SCHEMA_VERSION}`);
344
+ }
345
+ if (contract.appliedMigrationsTable !== "prism_migrations") {
346
+ throw new Error("Migration contract must record applied steps in prism_migrations");
347
+ }
348
+ if (!contract.lockGuidance.trim() || !contract.leastPrivilegeGuidance.trim()) {
349
+ throw new Error("Migration contract must document lock and least-privilege guidance");
350
+ }
351
+ if (contract.steps.length === 0)
352
+ throw new Error("Migration contract must include at least one step");
353
+ let previous = 0;
354
+ const names = new Set();
355
+ for (const step of contract.steps) {
356
+ if (step.version <= previous)
357
+ throw new Error(`Migration steps must be strictly increasing; ${step.name} is out of order`);
358
+ if (names.has(step.name))
359
+ throw new Error(`Duplicate migration step name: ${step.name}`);
360
+ names.add(step.name);
361
+ previous = step.version;
362
+ }
363
+ if (previous !== contract.targetSchemaVersion) {
364
+ throw new Error(`Last migration step version ${previous} must equal targetSchemaVersion ${contract.targetSchemaVersion}`);
365
+ }
366
+ }
367
+ /**
368
+ * Assert a dialect-local adapter exposes the canonical table and index names.
369
+ * Adapters pass the table/index names their migration runner created.
370
+ */
371
+ export function assertAdapterSchemaMatchesModel(adapterTables, adapterIndexes, model = createPersistenceSchemaModel()) {
372
+ assertPersistenceSchemaModel(model);
373
+ const tables = new Set(adapterTables);
374
+ for (const table of model.tables.map((item) => item.name)) {
375
+ if (!tables.has(table))
376
+ throw new Error(`Adapter schema missing table ${table}`);
377
+ }
378
+ for (const index of model.indexes) {
379
+ if (!adapterIndexes.includes(index.name)) {
380
+ throw new Error(`Adapter schema missing required index ${index.name}`);
381
+ }
382
+ }
383
+ }
384
+ /** Guard adapter SQL tests: reject obvious value interpolation into statement text. */
385
+ export function assertParameterizedQuery(sql, boundValues) {
386
+ for (const value of boundValues) {
387
+ if (typeof value !== "string" || value.length === 0)
388
+ continue;
389
+ if (sql.includes(value)) {
390
+ throw new Error("SQL statement appears to interpolate a bound string value; use parameters instead");
391
+ }
392
+ }
393
+ }
394
+ /** Simulate migration up + reopen: applied steps must match the contract in order. */
395
+ export function assertMigrationUpAndReopen(contract, appliedAfterUp, appliedAfterReopen) {
396
+ assertPersistenceMigrationContract(contract);
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
+ }
406
+ }
407
+ if (appliedAfterReopen.length !== appliedAfterUp.length) {
408
+ throw new Error("Reopened adapter must not re-apply migrations; applied row count changed");
409
+ }
410
+ for (let i = 0; i < appliedAfterUp.length; i++) {
411
+ if (appliedAfterReopen[i].name !== appliedAfterUp[i].name) {
412
+ throw new Error("Reopened adapter migration history diverged");
413
+ }
414
+ }
415
+ }
416
+ /** Assert cursor pagination returns stable pages without repeating rows. */
417
+ export async function assertPersistenceQueryPaginationConforms(fixture, sessionId = "pagination-conformance") {
418
+ const entries = [0, 1, 2, 3, 4].map((index) => ({
419
+ id: `page-${index}`,
420
+ sessionId,
421
+ timestamp: `2026-01-01T00:00:0${index}.000Z`,
422
+ kind: "label",
423
+ label: `e${index}`,
424
+ }));
425
+ await fixture.seedEntries(entries);
426
+ const first = await fixture.queryEntries({ sessionId, limit: 2, order: "asc" });
427
+ if (first.items.length !== 2)
428
+ throw new Error("Pagination first page must honor limit");
429
+ if (!first.nextCursor)
430
+ throw new Error("Pagination must return nextCursor when more rows exist");
431
+ const second = await fixture.queryEntries({ sessionId, limit: 2, order: "asc", cursor: first.nextCursor });
432
+ const seen = new Set(first.items.map((row) => row.id));
433
+ for (const row of second.items) {
434
+ if (seen.has(row.id))
435
+ throw new Error("Pagination cursor returned overlapping rows");
436
+ seen.add(row.id);
437
+ }
438
+ if (seen.size < 3)
439
+ throw new Error("Pagination did not advance past the first page");
440
+ }
441
+ /** Assert tenant-filtered queries do not return rows from another tenant. */
442
+ export async function assertTenantScopedQueryIsolation(queryByTenant) {
443
+ const tenantA = await queryByTenant("tenant-a");
444
+ const tenantB = await queryByTenant("tenant-b");
445
+ if (tenantA.some((row) => row.tenantId === "tenant-b")) {
446
+ throw new Error("Tenant-a query leaked tenant-b rows");
447
+ }
448
+ if (tenantB.some((row) => row.tenantId === "tenant-a")) {
449
+ throw new Error("Tenant-b query leaked tenant-a rows");
450
+ }
451
+ const aIds = new Set(tenantA.map((row) => row.id));
452
+ for (const row of tenantB) {
453
+ if (aIds.has(row.id))
454
+ throw new Error("Tenant collision: the same primary id appeared in two tenant queries");
455
+ }
456
+ }
457
+ //# sourceMappingURL=persistence-schema.js.map
@@ -107,7 +107,16 @@ function contentBlockCanaries(block) {
107
107
  case "thinking":
108
108
  return block.text ? [block.text] : [];
109
109
  case "image":
110
- return [block.url, block.data, block.mimeType].filter((value) => typeof value === "string" && value.length > 0);
110
+ return [block.url, block.resourceUri, block.data, block.mimeType, block.name].filter((value) => typeof value === "string" && value.length > 0);
111
+ case "audio":
112
+ // Providers map mediaType to wire format tokens (e.g. audio/wav → "wav") and
113
+ // typically omit display names from input_audio payloads; identity canaries are
114
+ // source bytes/refs plus optional transcript text.
115
+ return [block.url, block.resourceUri, block.data, block.transcript].filter((value) => typeof value === "string" && value.length > 0);
116
+ case "file":
117
+ return [block.url, block.resourceUri, block.data, block.mediaType, block.name].filter((value) => typeof value === "string" && value.length > 0);
118
+ case "document":
119
+ return [block.url, block.resourceUri, block.data, block.mediaType, block.name, block.transcript].filter((value) => typeof value === "string" && value.length > 0);
111
120
  case "tool_call":
112
121
  return [block.id, block.name, ...jsonPrimitives(block.arguments)];
113
122
  case "tool_result": {
@@ -0,0 +1,33 @@
1
+ import type { AgentEventRecord, RunLedger, RunRecord, ToolCallRecord, UsageRecord } from "../contracts.js";
2
+ export interface RunLedgerConformanceFixture {
3
+ readonly ledger: RunLedger;
4
+ readonly readRuns?: () => Promise<readonly RunRecord[]> | readonly RunRecord[];
5
+ readonly readEvents?: () => Promise<readonly AgentEventRecord[]> | readonly AgentEventRecord[];
6
+ readonly readToolCalls?: () => Promise<readonly ToolCallRecord[]> | readonly ToolCallRecord[];
7
+ readonly readUsage?: () => Promise<readonly UsageRecord[]> | readonly UsageRecord[];
8
+ }
9
+ export interface RunLedgerConformanceOptions {
10
+ readonly sessionId?: string;
11
+ readonly runId?: string;
12
+ readonly tenantId?: string;
13
+ readonly accountId?: string;
14
+ readonly userId?: string;
15
+ /** When true, requires read* callbacks and asserts tenant-scoped rows do not leak. */
16
+ readonly exerciseTenantIsolation?: boolean;
17
+ /** When true, factory is invoked twice to assert durable writes survive reopen. */
18
+ readonly exerciseReopen?: boolean;
19
+ }
20
+ export type RunLedgerConformanceFactory = () => RunLedgerConformanceFixture | Promise<RunLedgerConformanceFixture>;
21
+ /**
22
+ * Assert that a `RunLedger` implementation satisfies the write contract:
23
+ * all record kinds round-trip via optional read callbacks, per-run event order
24
+ * is preserved, and tenant-scoped rows store `tenant_id` when
25
+ * `exerciseTenantIsolation` is enabled.
26
+ */
27
+ export declare function assertRunLedgerConforms(fixture: RunLedgerConformanceFixture, options?: RunLedgerConformanceOptions): Promise<void>;
28
+ /**
29
+ * Factory-based conformance entry point for durable adapters. Invokes
30
+ * `assertRunLedgerConforms` against a fresh fixture and optionally reopens via
31
+ * the same factory to assert writes survive process/database reopen.
32
+ */
33
+ export declare function runRunLedgerConformance(factory: RunLedgerConformanceFactory, options?: RunLedgerConformanceOptions): Promise<void>;