@warlock.js/ai 4.5.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/cjs/index.cjs +20 -1
  3. package/cjs/{src-DFibP2FQ.cjs → src-Bmajk4Qg.cjs} +1 -1
  4. package/cjs/{src-C02yzsLs.cjs → src-OZyDYHxm.cjs} +2789 -691
  5. package/cjs/src-OZyDYHxm.cjs.map +1 -0
  6. package/esm/agent/agent-config.type.d.mts +29 -0
  7. package/esm/agent/agent-config.type.d.mts.map +1 -1
  8. package/esm/agent/agent.d.mts.map +1 -1
  9. package/esm/agent/agent.mjs +126 -7
  10. package/esm/agent/agent.mjs.map +1 -1
  11. package/esm/agent/signature.mjs +57 -0
  12. package/esm/agent/signature.mjs.map +1 -0
  13. package/esm/agent/snapshot.mjs +101 -0
  14. package/esm/agent/snapshot.mjs.map +1 -0
  15. package/esm/ai-openai/src/image.mjs +5 -0
  16. package/esm/ai-openai/src/index.mjs +3 -0
  17. package/esm/ai-openai/src/sdk.mjs +3 -0
  18. package/esm/ai-openai/src/speech.mjs +5 -0
  19. package/esm/ai-openai/src/transcription.mjs +6 -0
  20. package/esm/ai-openai/src/utils/index.mjs +1 -0
  21. package/esm/ai-openai/src/utils/to-openai-messages.mjs +3 -0
  22. package/esm/ai.d.mts +45 -0
  23. package/esm/ai.d.mts.map +1 -1
  24. package/esm/ai.mjs +37 -1
  25. package/esm/ai.mjs.map +1 -1
  26. package/esm/contracts/agent/agent-options.type.d.mts +22 -2
  27. package/esm/contracts/agent/agent-options.type.d.mts.map +1 -1
  28. package/esm/contracts/agent/agent-snapshot.type.d.mts +90 -0
  29. package/esm/contracts/agent/agent-snapshot.type.d.mts.map +1 -0
  30. package/esm/contracts/agent/agent.contract.d.mts +29 -1
  31. package/esm/contracts/agent/agent.contract.d.mts.map +1 -1
  32. package/esm/contracts/agent/index.d.mts +2 -1
  33. package/esm/contracts/image-model.contract.d.mts +156 -0
  34. package/esm/contracts/image-model.contract.d.mts.map +1 -0
  35. package/esm/contracts/index.d.mts +8 -3
  36. package/esm/contracts/planner/index.d.mts +3 -2
  37. package/esm/contracts/planner/planner-config.type.d.mts +30 -0
  38. package/esm/contracts/planner/planner-config.type.d.mts.map +1 -1
  39. package/esm/contracts/planner/planner-execute-options.type.d.mts +13 -1
  40. package/esm/contracts/planner/planner-execute-options.type.d.mts.map +1 -1
  41. package/esm/contracts/planner/planner-snapshot.type.d.mts +77 -0
  42. package/esm/contracts/planner/planner-snapshot.type.d.mts.map +1 -0
  43. package/esm/contracts/planner/planner.contract.d.mts +21 -1
  44. package/esm/contracts/planner/planner.contract.d.mts.map +1 -1
  45. package/esm/contracts/result/base-report.type.d.mts +1 -1
  46. package/esm/contracts/result/base-report.type.d.mts.map +1 -1
  47. package/esm/contracts/result/base-report.type.mjs.map +1 -1
  48. package/esm/contracts/sdk-adapter.contract.d.mts +37 -0
  49. package/esm/contracts/sdk-adapter.contract.d.mts.map +1 -1
  50. package/esm/contracts/speech-model.contract.d.mts +97 -0
  51. package/esm/contracts/speech-model.contract.d.mts.map +1 -0
  52. package/esm/contracts/transcription-model.contract.d.mts +101 -0
  53. package/esm/contracts/transcription-model.contract.d.mts.map +1 -0
  54. package/esm/errors/agent-drift-error.d.mts +32 -0
  55. package/esm/errors/agent-drift-error.d.mts.map +1 -0
  56. package/esm/errors/agent-drift-error.mjs +31 -0
  57. package/esm/errors/agent-drift-error.mjs.map +1 -0
  58. package/esm/errors/error-code.type.d.mts +1 -1
  59. package/esm/errors/index.d.mts +2 -0
  60. package/esm/errors/index.mjs +2 -0
  61. package/esm/errors/planner-drift-error.d.mts +34 -0
  62. package/esm/errors/planner-drift-error.d.mts.map +1 -0
  63. package/esm/errors/planner-drift-error.mjs +33 -0
  64. package/esm/errors/planner-drift-error.mjs.map +1 -0
  65. package/esm/image/image-cost.d.mts +32 -0
  66. package/esm/image/image-cost.d.mts.map +1 -0
  67. package/esm/image/image-cost.mjs +55 -0
  68. package/esm/image/image-cost.mjs.map +1 -0
  69. package/esm/image/image.d.mts +92 -0
  70. package/esm/image/image.d.mts.map +1 -0
  71. package/esm/image/image.mjs +113 -0
  72. package/esm/image/image.mjs.map +1 -0
  73. package/esm/image/index.mjs +4 -0
  74. package/esm/index.d.mts +26 -4
  75. package/esm/index.mjs +20 -1
  76. package/esm/mock/index.d.mts +3 -0
  77. package/esm/mock/index.mjs +3 -0
  78. package/esm/mock/mock-config.type.d.mts +22 -0
  79. package/esm/mock/mock-config.type.d.mts.map +1 -1
  80. package/esm/mock/mock-image-model.d.mts +41 -0
  81. package/esm/mock/mock-image-model.d.mts.map +1 -0
  82. package/esm/mock/mock-image-model.mjs +52 -0
  83. package/esm/mock/mock-image-model.mjs.map +1 -0
  84. package/esm/mock/mock-sdk.d.mts +7 -1
  85. package/esm/mock/mock-sdk.d.mts.map +1 -1
  86. package/esm/mock/mock-sdk.mjs +27 -0
  87. package/esm/mock/mock-sdk.mjs.map +1 -1
  88. package/esm/mock/mock-speech-model.d.mts +31 -0
  89. package/esm/mock/mock-speech-model.d.mts.map +1 -0
  90. package/esm/mock/mock-speech-model.mjs +39 -0
  91. package/esm/mock/mock-speech-model.mjs.map +1 -0
  92. package/esm/mock/mock-transcription-model.d.mts +32 -0
  93. package/esm/mock/mock-transcription-model.d.mts.map +1 -0
  94. package/esm/mock/mock-transcription-model.mjs +36 -0
  95. package/esm/mock/mock-transcription-model.mjs.map +1 -0
  96. package/esm/planner/planner-run.d.mts +8 -0
  97. package/esm/planner/planner-run.d.mts.map +1 -1
  98. package/esm/planner/planner-run.mjs +161 -6
  99. package/esm/planner/planner-run.mjs.map +1 -1
  100. package/esm/planner/planner.d.mts.map +1 -1
  101. package/esm/planner/planner.mjs +25 -1
  102. package/esm/planner/planner.mjs.map +1 -1
  103. package/esm/planner/snapshot.mjs +95 -0
  104. package/esm/planner/snapshot.mjs.map +1 -0
  105. package/esm/rag/index.d.mts +7 -0
  106. package/esm/rag/index.mjs +7 -0
  107. package/esm/rag/loaders/errors.d.mts +19 -0
  108. package/esm/rag/loaders/errors.d.mts.map +1 -0
  109. package/esm/rag/loaders/errors.mjs +25 -0
  110. package/esm/rag/loaders/errors.mjs.map +1 -0
  111. package/esm/rag/loaders/index.mjs +7 -0
  112. package/esm/rag/loaders/load-html.d.mts +26 -0
  113. package/esm/rag/loaders/load-html.d.mts.map +1 -0
  114. package/esm/rag/loaders/load-html.mjs +138 -0
  115. package/esm/rag/loaders/load-html.mjs.map +1 -0
  116. package/esm/rag/loaders/load-pdf.d.mts +38 -0
  117. package/esm/rag/loaders/load-pdf.d.mts.map +1 -0
  118. package/esm/rag/loaders/load-pdf.mjs +150 -0
  119. package/esm/rag/loaders/load-pdf.mjs.map +1 -0
  120. package/esm/rag/loaders/load-text.d.mts +47 -0
  121. package/esm/rag/loaders/load-text.d.mts.map +1 -0
  122. package/esm/rag/loaders/load-text.mjs +60 -0
  123. package/esm/rag/loaders/load-text.mjs.map +1 -0
  124. package/esm/rag/loaders/load-web.d.mts +42 -0
  125. package/esm/rag/loaders/load-web.d.mts.map +1 -0
  126. package/esm/rag/loaders/load-web.mjs +89 -0
  127. package/esm/rag/loaders/load-web.mjs.map +1 -0
  128. package/esm/rag/loaders/loader.type.d.mts +89 -0
  129. package/esm/rag/loaders/loader.type.d.mts.map +1 -0
  130. package/esm/rag/store/pg-vector-store.d.mts +139 -0
  131. package/esm/rag/store/pg-vector-store.d.mts.map +1 -0
  132. package/esm/rag/store/pg-vector-store.mjs +328 -0
  133. package/esm/rag/store/pg-vector-store.mjs.map +1 -0
  134. package/esm/speech/index.mjs +3 -0
  135. package/esm/speech/speech.d.mts +65 -0
  136. package/esm/speech/speech.d.mts.map +1 -0
  137. package/esm/speech/speech.mjs +123 -0
  138. package/esm/speech/speech.mjs.map +1 -0
  139. package/esm/supervisor/entries.mjs +2 -2
  140. package/esm/supervisor/entries.mjs.map +1 -1
  141. package/esm/transcribe/audio-input.d.mts +47 -0
  142. package/esm/transcribe/audio-input.d.mts.map +1 -0
  143. package/esm/transcribe/audio-input.mjs +84 -0
  144. package/esm/transcribe/audio-input.mjs.map +1 -0
  145. package/esm/transcribe/index.mjs +4 -0
  146. package/esm/transcribe/transcribe.d.mts +64 -0
  147. package/esm/transcribe/transcribe.d.mts.map +1 -0
  148. package/esm/transcribe/transcribe.mjs +128 -0
  149. package/esm/transcribe/transcribe.mjs.map +1 -0
  150. package/llms-full.txt +753 -0
  151. package/llms.txt +5 -0
  152. package/package.json +3 -3
  153. package/skills/README.md +4 -0
  154. package/skills/durable-agent-runs/SKILL.md +135 -0
  155. package/skills/generate-images/SKILL.md +138 -0
  156. package/skills/generate-speech/SKILL.md +139 -0
  157. package/skills/rag-loaders-and-stores/SKILL.md +164 -0
  158. package/skills/transcribe-audio/SKILL.md +157 -0
  159. package/cjs/src-C02yzsLs.cjs.map +0 -1
@@ -0,0 +1,328 @@
1
+ //#region ../@warlock.js/ai/src/rag/store/pg-vector-store.ts
2
+ /**
3
+ * Default backing table — provisions the store with no extra config when
4
+ * the dev runs {@link VectorStore.schema | ensureSchema} through their
5
+ * migration tool.
6
+ */
7
+ const DEFAULT_TABLE = "warlock_ai_rag_vectors";
8
+ /** Default embedding width baked into the DDL (OpenAI `text-embedding-3-small`). */
9
+ const DEFAULT_DIMENSIONS = 1536;
10
+ /** Default `ivfflat` list count when that index strategy is chosen. */
11
+ const DEFAULT_IVFFLAT_LISTS = 100;
12
+ /**
13
+ * Allowed characters in a Postgres identifier (table name). The table name
14
+ * is interpolated into DDL/DML, so anything outside this conservative
15
+ * ASCII subset is rejected — interpolating an arbitrary string would be a
16
+ * SQL-injection footgun (mirrors the snapshot / human-interrupt pg stores
17
+ * and `@warlock.js/cache`'s `PgCacheDriver`).
18
+ */
19
+ const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
20
+ /**
21
+ * Module specifier for the optional `pg` driver. Held in a `string`
22
+ * variable so the dynamic `import()` is not statically resolved at compile
23
+ * time — `pg` is an optional peer that need not be installed for this
24
+ * package to type-check or for a cache-only consumer to run.
25
+ */
26
+ const PG_MODULE = "pg";
27
+ /**
28
+ * Curated install string surfaced (at use time) when a `connectionString`
29
+ * is configured but the optional `pg` driver is absent. Never thrown at
30
+ * import — a cache-only consumer must be able to load this module.
31
+ */
32
+ const PG_INSTALL_INSTRUCTIONS = `
33
+ The @warlock.js/ai Postgres vector store requires the pg package and a
34
+ Postgres database with the pgvector extension. Install the driver with:
35
+
36
+ npm install pg
37
+
38
+ Or with your preferred package manager:
39
+
40
+ pnpm add pg
41
+ yarn add pg
42
+ `.trim();
43
+ /**
44
+ * Lazily import `pg` and return a `Pool` built from `connectionString`. A
45
+ * bare `catch` rethrows the curated install string — a missing optional
46
+ * peer surfaces as actionable guidance, never a raw resolution error.
47
+ */
48
+ async function buildPgClient(connectionString) {
49
+ let sdk;
50
+ try {
51
+ sdk = await import(PG_MODULE);
52
+ } catch {
53
+ throw new Error(PG_INSTALL_INSTRUCTIONS);
54
+ }
55
+ return new sdk.Pool({ connectionString });
56
+ }
57
+ /**
58
+ * Serialize a JS `number[]` to the pgvector text literal: `[1,2,3]`.
59
+ * pgvector accepts a vector either as this bracketed literal or via a
60
+ * typed parameter; passing the literal string + an explicit `::vector`
61
+ * cast keeps the store driver-agnostic (no dependency on a registered
62
+ * `pg` type parser).
63
+ *
64
+ * Non-finite components (`NaN` / `±Infinity`) are rejected — pgvector
65
+ * stores only finite floats, and silently coercing them would corrupt the
66
+ * index. The check is cheap relative to the embed call that produced the
67
+ * vector.
68
+ *
69
+ * @example
70
+ * vectorLiteral([1, 0.5, -2]); // "[1,0.5,-2]"
71
+ */
72
+ function vectorLiteral(vector) {
73
+ let literal = "[";
74
+ for (let index = 0; index < vector.length; index++) {
75
+ const component = vector[index];
76
+ if (!Number.isFinite(component)) throw new TypeError(`pgVectorStore: embedding component at index ${index} is not finite (${component}); pgvector stores only finite floats.`);
77
+ if (index > 0) literal += ",";
78
+ literal += String(component);
79
+ }
80
+ return literal + "]";
81
+ }
82
+ /**
83
+ * Coerce a `value` JSONB column back into the stored payload. node-postgres
84
+ * parses `JSONB` into a JS value already, but some pool wrappers hand back
85
+ * the raw string — be defensive across both (mirrors the snapshot store's
86
+ * `parsePayload`).
87
+ */
88
+ function parseValue(value) {
89
+ if (typeof value === "string") return JSON.parse(value);
90
+ return value;
91
+ }
92
+ /**
93
+ * Coerce a pgvector cosine **distance** (`<=>`, in `[0, 2]`, 0 = identical)
94
+ * into the cosine **similarity** score the {@link VectorStore} contract
95
+ * declares (`[0, 1]`, 1 = identical). `pg` returns the computed distance
96
+ * column as a string for `double precision`; parse then map `1 - distance`,
97
+ * clamped to `[0, 1]` so a tiny floating-point overshoot never yields a
98
+ * score just outside the contract's range.
99
+ */
100
+ function distanceToScore(distance) {
101
+ const score = 1 - (typeof distance === "string" ? Number(distance) : distance);
102
+ if (score < 0) return 0;
103
+ if (score > 1) return 1;
104
+ return score;
105
+ }
106
+ /**
107
+ * Postgres + pgvector-backed {@link VectorStore} — one durable row per
108
+ * indexed chunk, keyed by the RAG pipeline's dotted `key`
109
+ * (`ai.rag.<name>.<sourceId>.<chunkIndex>`), with the chunk payload in a
110
+ * `value` JSONB column and the embedding in a `vector` column.
111
+ *
112
+ * Owns: the three RAG vector operations against a pgvector index —
113
+ * `upsert` (INSERT … ON CONFLICT DO UPDATE), `query` (cosine
114
+ * `ORDER BY embedding <=> $vec` with a `threshold` floor + optional `tags`
115
+ * overlap filter, capped at `topK`), and `removeNamespace` (prefix DELETE).
116
+ * Also emits the reference DDL via {@link PgVectorStore.schema} (alias
117
+ * {@link PgVectorStore.ensureSchema}).
118
+ *
119
+ * Does NOT own: the connection lifecycle (a dev-supplied `client` is never
120
+ * closed; a store-built `Pool` from a `connectionString` is also left open
121
+ * for the process to reuse) or schema migration — the dev runs the DDL
122
+ * through their own tool; the framework never auto-migrates.
123
+ *
124
+ * Front it with the {@link pgVectorStore} factory — callers never `new` it.
125
+ */
126
+ var PgVectorStore = class {
127
+ constructor(options) {
128
+ const table = options.table ?? DEFAULT_TABLE;
129
+ if (!SAFE_IDENTIFIER.test(table)) throw new TypeError(`pgVectorStore: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`);
130
+ this.table = table;
131
+ this.dimensions = options.dimensions ?? DEFAULT_DIMENSIONS;
132
+ this.index = options.index ?? "hnsw";
133
+ this.ivfflatLists = options.ivfflatLists ?? DEFAULT_IVFFLAT_LISTS;
134
+ if (options.client) {
135
+ if (typeof options.client.query !== "function") throw new TypeError("pgVectorStore requires a 'client' option implementing { query(text, params) } — pass a pg.Pool or pg.Client.");
136
+ this.clientPromise = Promise.resolve(options.client);
137
+ return;
138
+ }
139
+ if (options.connectionString) {
140
+ this.clientPromise = buildPgClient(options.connectionString);
141
+ return;
142
+ }
143
+ throw new TypeError("pgVectorStore requires either a 'client' or a 'connectionString' option.");
144
+ }
145
+ /**
146
+ * Resolve the backing client, surfacing the lazy `pg` import's curated
147
+ * install string on the first call that needs it.
148
+ */
149
+ client() {
150
+ return this.clientPromise;
151
+ }
152
+ /**
153
+ * Index `value` under `key` with its embedding `vector`. Upserts on the
154
+ * `key` primary key — re-indexing the same chunk overwrites its payload,
155
+ * embedding, and tags rather than appending a duplicate row. Optional
156
+ * `tags` ride a `text[]` column so {@link query} can restrict the
157
+ * candidate set with an array-overlap filter.
158
+ *
159
+ * The embedding is sent as a pgvector text literal (`$3`) cast to
160
+ * `::vector`, so the store needs no registered `pg` type parser. `tags`
161
+ * defaults to an empty array (never `NULL`) to keep the overlap filter's
162
+ * `&&` semantics simple.
163
+ */
164
+ async upsert(key, value, vector, tags) {
165
+ await (await this.client()).query(`INSERT INTO ${this.table} (key, value, embedding, tags)
166
+ VALUES ($1, $2::jsonb, $3::vector, $4::text[])
167
+ ON CONFLICT (key) DO UPDATE
168
+ SET value = EXCLUDED.value,
169
+ embedding = EXCLUDED.embedding,
170
+ tags = EXCLUDED.tags`, [
171
+ key,
172
+ JSON.stringify(value),
173
+ vectorLiteral(vector),
174
+ tags ?? []
175
+ ]);
176
+ }
177
+ /**
178
+ * Return the cosine-nearest rows to `vector`, mapped to the contract's
179
+ * `{ key, value, score }` shape. The SQL:
180
+ *
181
+ * - computes `embedding <=> $1::vector` (cosine **distance**) once, aliased
182
+ * `distance`, and `ORDER BY` it ascending (nearest first);
183
+ * - applies the `threshold` floor as `distance <= 1 - threshold`
184
+ * (similarity `>=` threshold), so the default `0.5` floor maps to a
185
+ * `<= 0.5` distance bound — the filter runs in SQL, not in JS, so a
186
+ * below-floor row never crosses the wire;
187
+ * - when `tags` are given, restricts to rows whose `tags` array overlaps
188
+ * the requested set via `tags && $tags::text[]` (one-of semantics,
189
+ * matching the cache store);
190
+ * - caps the result at `topK` with `LIMIT`.
191
+ *
192
+ * The returned `score` is `1 - distance`, clamped to `[0, 1]`, so callers
193
+ * see the same cosine-similarity scale the cache store emits.
194
+ */
195
+ async query(vector, options) {
196
+ const client = await this.client();
197
+ const params = [vectorLiteral(vector), options.topK];
198
+ const conditions = [];
199
+ if (options.threshold !== void 0) {
200
+ params.push(1 - options.threshold);
201
+ conditions.push(`(embedding <=> $1::vector) <= $${params.length}`);
202
+ }
203
+ if (options.tags !== void 0 && options.tags.length > 0) {
204
+ params.push(options.tags);
205
+ conditions.push(`tags && $${params.length}::text[]`);
206
+ }
207
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
208
+ const { rows } = await client.query(`SELECT key, value, (embedding <=> $1::vector) AS distance
209
+ FROM ${this.table}
210
+ ${where}
211
+ ORDER BY embedding <=> $1::vector
212
+ LIMIT $2`, params);
213
+ return rows.map((row) => ({
214
+ key: row.key,
215
+ value: parseValue(row.value),
216
+ score: distanceToScore(row.distance)
217
+ }));
218
+ }
219
+ /**
220
+ * Drop every entry written under `namespace`. The RAG pipeline keys
221
+ * chunks as `<namespace>.<sourceId>.<chunkIndex>`, so a row belongs to
222
+ * the namespace when its `key` equals it OR begins with `<namespace>.`
223
+ * — deleting `ai.rag.docs` must not also catch `ai.rag.docs2`. The `_`
224
+ * and `%` LIKE wildcards in the namespace are escaped so a namespace
225
+ * that happens to contain them is matched literally.
226
+ */
227
+ async removeNamespace(namespace) {
228
+ const client = await this.client();
229
+ const escaped = namespace.replace(/\\/g, "\\\\").replace(/_/g, "\\_").replace(/%/g, "\\%");
230
+ await client.query(`DELETE FROM ${this.table}
231
+ WHERE key = $1 OR key LIKE $2 ESCAPE '\\'`, [namespace, `${escaped}.%`]);
232
+ }
233
+ /**
234
+ * Return the reference migration DDL for this store's backing table,
235
+ * interpolating the configured table name, embedding width, and ANN
236
+ * index strategy. The dev runs it once through their migration tool —
237
+ * the framework never auto-migrates.
238
+ *
239
+ * The emitted statements:
240
+ * 1. `CREATE EXTENSION IF NOT EXISTS vector;` — enables pgvector (needs
241
+ * a superuser or a role with `CREATE` on the database the first time).
242
+ * 2. `CREATE TABLE IF NOT EXISTS <table> (key TEXT PRIMARY KEY, value
243
+ * JSONB NOT NULL, embedding vector(<dimensions>) NOT NULL, tags
244
+ * text[] NOT NULL DEFAULT '{}');`
245
+ * 3. A GIN index on `tags` so the array-overlap filter stays sargable.
246
+ * 4. The chosen ANN index over `embedding` using `vector_cosine_ops`:
247
+ * - `"hnsw"` → `USING hnsw (embedding vector_cosine_ops)`;
248
+ * - `"ivfflat"` → `USING ivfflat (embedding vector_cosine_ops)
249
+ * WITH (lists = <ivfflatLists>)`;
250
+ * - `"none"` → emitted as a comment (exact scan, no ANN index).
251
+ *
252
+ * @example
253
+ * const store = pgVectorStore({ client: pool, dimensions: 1536 });
254
+ * await pool.query(store.ensureSchema());
255
+ */
256
+ schema() {
257
+ const lines = [
258
+ `CREATE EXTENSION IF NOT EXISTS vector;`,
259
+ `CREATE TABLE IF NOT EXISTS ${this.table} (`,
260
+ ` key TEXT PRIMARY KEY,`,
261
+ ` value JSONB NOT NULL,`,
262
+ ` embedding vector(${this.dimensions}) NOT NULL,`,
263
+ ` tags TEXT[] NOT NULL DEFAULT '{}'`,
264
+ `);`,
265
+ `CREATE INDEX IF NOT EXISTS idx_${this.table}_tags`,
266
+ ` ON ${this.table} USING gin (tags);`
267
+ ];
268
+ if (this.index === "hnsw") lines.push(`CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding`, ` ON ${this.table} USING hnsw (embedding vector_cosine_ops);`);
269
+ else if (this.index === "ivfflat") lines.push(`CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding`, ` ON ${this.table} USING ivfflat (embedding vector_cosine_ops)`, ` WITH (lists = ${this.ivfflatLists});`);
270
+ else lines.push(`-- No ANN index requested (index: "none"): cosine queries fall back`, `-- to an exact sequential scan, which is correct but linear in rows.`);
271
+ return lines.join("\n");
272
+ }
273
+ /**
274
+ * Alias for {@link PgVectorStore.schema} — reads more naturally in a
275
+ * migration script (`await pool.query(store.ensureSchema())`). Returns
276
+ * the identical DDL string; it does NOT execute anything against the
277
+ * database (the store never auto-migrates).
278
+ */
279
+ ensureSchema() {
280
+ return this.schema();
281
+ }
282
+ };
283
+ /**
284
+ * Create a Postgres + pgvector-backed {@link VectorStore} for the RAG
285
+ * pipeline. Either pass a live `pg.Pool` / `pg.Client` (`{ client }`) —
286
+ * `@warlock.js/ai` never imports `pg` in that case — or a
287
+ * `{ connectionString }` and let the store lazily `import("pg")` to build
288
+ * its own pool. When `pg` is not installed, a curated install string
289
+ * surfaces on first use, never at import.
290
+ *
291
+ * Run {@link PgVectorStoreInstance.ensureSchema} through your migration
292
+ * tool once before use (it enables the `vector` extension, creates the
293
+ * table, and builds the tag + ANN indexes); the store never auto-migrates.
294
+ *
295
+ * Index and query MUST use the same embedding model — the `vector(N)`
296
+ * column width is fixed at table-creation time from `dimensions`.
297
+ *
298
+ * @example
299
+ * import { Pool } from "pg";
300
+ * import { ai } from "@warlock.js/ai";
301
+ *
302
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
303
+ * const store = ai.rag.pgVectorStore({ client: pool, dimensions: 1536 });
304
+ *
305
+ * // Once, via your migration tooling:
306
+ * // await pool.query(store.ensureSchema());
307
+ *
308
+ * const kb = ai.rag({
309
+ * name: "docs",
310
+ * embedder: openai.embedder({ name: "text-embedding-3-small" }),
311
+ * store,
312
+ * });
313
+ *
314
+ * @example
315
+ * // Let the store build its own pool from a connection string:
316
+ * const store = ai.rag.pgVectorStore({
317
+ * connectionString: process.env.DATABASE_URL,
318
+ * index: "ivfflat",
319
+ * ivfflatLists: 200,
320
+ * });
321
+ */
322
+ function pgVectorStore(options) {
323
+ return new PgVectorStore(options);
324
+ }
325
+
326
+ //#endregion
327
+ export { pgVectorStore, vectorLiteral };
328
+ //# sourceMappingURL=pg-vector-store.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pg-vector-store.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai/src/rag/store/pg-vector-store.ts"],"sourcesContent":["import type { VectorStore } from \"./vector-store.contract\";\n\n/**\n * Minimal `pg`-compatible client surface the Postgres {@link VectorStore}\n * depends on. Both `pg.Pool` and `pg.Client` satisfy it — the store only\n * ever calls `query`.\n *\n * `@warlock.js/ai` takes **no** hard dependency on `pg`; the dev installs\n * it (an optional peer) and passes the client in. Structurally identical\n * to the snapshot / human-interrupt stores' `PgClientLike`, so a single\n * pool can back the orchestrator checkpoint/snapshot tables, the\n * interrupt table, and this vectors table alike.\n */\nexport interface PgClientLike {\n query(text: string, params?: unknown[]): Promise<{ rows: unknown[] }>;\n}\n\n/**\n * Options for the Postgres {@link VectorStore}.\n *\n * Two mutually-supportive ways to supply the connection (mirroring\n * `ai.human.interrupt.pg`):\n * - **`client`** — pass an already-built `pg.Pool` / `pg.Client` (anything\n * satisfying {@link PgClientLike}). The store only ever calls `query`\n * and never opens or closes it; one pool can back several stores.\n * - **`connectionString`** — let the store lazily `import(\"pg\")` and build\n * its own `Pool`. `@warlock.js/ai` takes **no** hard dependency on\n * `pg` (an optional peer); when it is absent the store throws a curated\n * install string at first use, never a raw module-resolution stack trace\n * at import.\n *\n * Exactly one of the two must be present.\n */\nexport interface PgVectorStoreOptions {\n /** An already-built `pg.Pool` / `pg.Client` — anything matching {@link PgClientLike}. */\n client?: PgClientLike;\n\n /** Connection string the store passes to a lazily-imported `pg.Pool`. */\n connectionString?: string;\n\n /**\n * Backing table name. Defaults to `warlock_ai_rag_vectors`. Must be a\n * safe SQL identifier — it is interpolated into DDL/DML.\n */\n table?: string;\n\n /**\n * Embedding dimensionality used in the `CREATE TABLE` DDL emitted by\n * {@link VectorStore.schema | ensureSchema}. Defaults to `1536`\n * (OpenAI `text-embedding-3-small`). The column is declared\n * `vector(N)`; queries and upserts never re-state it, so an existing\n * table provisioned at a different size is unaffected — only the DDL\n * helper reads this.\n */\n dimensions?: number;\n\n /**\n * Approximate-nearest-neighbour index strategy baked into the DDL\n * emitted by {@link VectorStore.schema | ensureSchema}. Defaults to\n * `\"hnsw\"` (better recall/latency on modern pgvector). Use `\"ivfflat\"`\n * for the classic list-partitioned index, or `\"none\"` to emit no ANN\n * index (exact scan — correct, but linear in row count).\n */\n index?: \"hnsw\" | \"ivfflat\" | \"none\";\n\n /**\n * `lists` parameter for an `ivfflat` index (ignored for `hnsw` / `none`).\n * Defaults to `100`. Tune toward `rows / 1000` for large tables.\n */\n ivfflatLists?: number;\n}\n\n/**\n * Default backing table — provisions the store with no extra config when\n * the dev runs {@link VectorStore.schema | ensureSchema} through their\n * migration tool.\n */\nconst DEFAULT_TABLE = \"warlock_ai_rag_vectors\";\n\n/** Default embedding width baked into the DDL (OpenAI `text-embedding-3-small`). */\nconst DEFAULT_DIMENSIONS = 1536;\n\n/** Default `ivfflat` list count when that index strategy is chosen. */\nconst DEFAULT_IVFFLAT_LISTS = 100;\n\n/**\n * Allowed characters in a Postgres identifier (table name). The table name\n * is interpolated into DDL/DML, so anything outside this conservative\n * ASCII subset is rejected — interpolating an arbitrary string would be a\n * SQL-injection footgun (mirrors the snapshot / human-interrupt pg stores\n * and `@warlock.js/cache`'s `PgCacheDriver`).\n */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Module specifier for the optional `pg` driver. Held in a `string`\n * variable so the dynamic `import()` is not statically resolved at compile\n * time — `pg` is an optional peer that need not be installed for this\n * package to type-check or for a cache-only consumer to run.\n */\nconst PG_MODULE = \"pg\";\n\n/**\n * Curated install string surfaced (at use time) when a `connectionString`\n * is configured but the optional `pg` driver is absent. Never thrown at\n * import — a cache-only consumer must be able to load this module.\n */\nconst PG_INSTALL_INSTRUCTIONS = `\nThe @warlock.js/ai Postgres vector store requires the pg package and a\nPostgres database with the pgvector extension. Install the driver with:\n\n npm install pg\n\nOr with your preferred package manager:\n\n pnpm add pg\n yarn add pg\n`.trim();\n\n/**\n * Minimal structural view of a `pg.Pool` constructor — just enough of the\n * `pg` module surface for the store to build a client when handed a\n * `connectionString`. Declared locally (rather than `typeof import(\"pg\")`)\n * so this module type-checks even when `pg` is not installed.\n */\ninterface PgModuleLike {\n Pool: new (config: { connectionString: string }) => PgClientLike;\n}\n\n/**\n * Lazily import `pg` and return a `Pool` built from `connectionString`. A\n * bare `catch` rethrows the curated install string — a missing optional\n * peer surfaces as actionable guidance, never a raw resolution error.\n */\nasync function buildPgClient(connectionString: string): Promise<PgClientLike> {\n let sdk: PgModuleLike;\n\n try {\n sdk = (await import(PG_MODULE)) as unknown as PgModuleLike;\n } catch {\n throw new Error(PG_INSTALL_INSTRUCTIONS);\n }\n\n return new sdk.Pool({ connectionString });\n}\n\n/**\n * Serialize a JS `number[]` to the pgvector text literal: `[1,2,3]`.\n * pgvector accepts a vector either as this bracketed literal or via a\n * typed parameter; passing the literal string + an explicit `::vector`\n * cast keeps the store driver-agnostic (no dependency on a registered\n * `pg` type parser).\n *\n * Non-finite components (`NaN` / `±Infinity`) are rejected — pgvector\n * stores only finite floats, and silently coercing them would corrupt the\n * index. The check is cheap relative to the embed call that produced the\n * vector.\n *\n * @example\n * vectorLiteral([1, 0.5, -2]); // \"[1,0.5,-2]\"\n */\nexport function vectorLiteral(vector: number[]): string {\n let literal = \"[\";\n\n for (let index = 0; index < vector.length; index++) {\n const component = vector[index];\n\n if (!Number.isFinite(component)) {\n throw new TypeError(\n `pgVectorStore: embedding component at index ${index} is not finite (${component}); pgvector stores only finite floats.`,\n );\n }\n\n if (index > 0) {\n literal += \",\";\n }\n\n literal += String(component);\n }\n\n return literal + \"]\";\n}\n\n/**\n * Coerce a `value` JSONB column back into the stored payload. node-postgres\n * parses `JSONB` into a JS value already, but some pool wrappers hand back\n * the raw string — be defensive across both (mirrors the snapshot store's\n * `parsePayload`).\n */\nfunction parseValue<T>(value: unknown): T {\n if (typeof value === \"string\") {\n return JSON.parse(value) as T;\n }\n\n return value as T;\n}\n\n/**\n * Coerce a pgvector cosine **distance** (`<=>`, in `[0, 2]`, 0 = identical)\n * into the cosine **similarity** score the {@link VectorStore} contract\n * declares (`[0, 1]`, 1 = identical). `pg` returns the computed distance\n * column as a string for `double precision`; parse then map `1 - distance`,\n * clamped to `[0, 1]` so a tiny floating-point overshoot never yields a\n * score just outside the contract's range.\n */\nfunction distanceToScore(distance: unknown): number {\n const value = typeof distance === \"string\" ? Number(distance) : (distance as number);\n const score = 1 - value;\n\n if (score < 0) {\n return 0;\n }\n\n if (score > 1) {\n return 1;\n }\n\n return score;\n}\n\n/**\n * Postgres + pgvector-backed {@link VectorStore} — one durable row per\n * indexed chunk, keyed by the RAG pipeline's dotted `key`\n * (`ai.rag.<name>.<sourceId>.<chunkIndex>`), with the chunk payload in a\n * `value` JSONB column and the embedding in a `vector` column.\n *\n * Owns: the three RAG vector operations against a pgvector index —\n * `upsert` (INSERT … ON CONFLICT DO UPDATE), `query` (cosine\n * `ORDER BY embedding <=> $vec` with a `threshold` floor + optional `tags`\n * overlap filter, capped at `topK`), and `removeNamespace` (prefix DELETE).\n * Also emits the reference DDL via {@link PgVectorStore.schema} (alias\n * {@link PgVectorStore.ensureSchema}).\n *\n * Does NOT own: the connection lifecycle (a dev-supplied `client` is never\n * closed; a store-built `Pool` from a `connectionString` is also left open\n * for the process to reuse) or schema migration — the dev runs the DDL\n * through their own tool; the framework never auto-migrates.\n *\n * Front it with the {@link pgVectorStore} factory — callers never `new` it.\n */\nclass PgVectorStore implements VectorStore {\n /** Validated backing table name, safe to interpolate into SQL. */\n private readonly table: string;\n\n /** Embedding width baked into the DDL. */\n private readonly dimensions: number;\n\n /** ANN index strategy baked into the DDL. */\n private readonly index: \"hnsw\" | \"ivfflat\" | \"none\";\n\n /** `lists` parameter for an `ivfflat` index. */\n private readonly ivfflatLists: number;\n\n /**\n * A ready client, or a promise resolving to one the store builds lazily\n * from a `connectionString`. Resolved once and memoized so the optional\n * `pg` import happens at most once.\n */\n private readonly clientPromise: Promise<PgClientLike>;\n\n public constructor(options: PgVectorStoreOptions) {\n const table = options.table ?? DEFAULT_TABLE;\n\n if (!SAFE_IDENTIFIER.test(table)) {\n throw new TypeError(\n `pgVectorStore: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`,\n );\n }\n\n this.table = table;\n this.dimensions = options.dimensions ?? DEFAULT_DIMENSIONS;\n this.index = options.index ?? \"hnsw\";\n this.ivfflatLists = options.ivfflatLists ?? DEFAULT_IVFFLAT_LISTS;\n\n if (options.client) {\n if (typeof options.client.query !== \"function\") {\n throw new TypeError(\n \"pgVectorStore requires a 'client' option implementing { query(text, params) } — pass a pg.Pool or pg.Client.\",\n );\n }\n\n this.clientPromise = Promise.resolve(options.client);\n\n return;\n }\n\n if (options.connectionString) {\n // Defer the optional `pg` import to first use — a curated install\n // string surfaces from `buildPgClient`, not at construction.\n this.clientPromise = buildPgClient(options.connectionString);\n\n return;\n }\n\n throw new TypeError(\n \"pgVectorStore requires either a 'client' or a 'connectionString' option.\",\n );\n }\n\n /**\n * Resolve the backing client, surfacing the lazy `pg` import's curated\n * install string on the first call that needs it.\n */\n private client(): Promise<PgClientLike> {\n return this.clientPromise;\n }\n\n /**\n * Index `value` under `key` with its embedding `vector`. Upserts on the\n * `key` primary key — re-indexing the same chunk overwrites its payload,\n * embedding, and tags rather than appending a duplicate row. Optional\n * `tags` ride a `text[]` column so {@link query} can restrict the\n * candidate set with an array-overlap filter.\n *\n * The embedding is sent as a pgvector text literal (`$3`) cast to\n * `::vector`, so the store needs no registered `pg` type parser. `tags`\n * defaults to an empty array (never `NULL`) to keep the overlap filter's\n * `&&` semantics simple.\n */\n public async upsert(\n key: string,\n value: unknown,\n vector: number[],\n tags?: string[],\n ): Promise<void> {\n const client = await this.client();\n\n await client.query(\n `INSERT INTO ${this.table} (key, value, embedding, tags)\n VALUES ($1, $2::jsonb, $3::vector, $4::text[])\n ON CONFLICT (key) DO UPDATE\n SET value = EXCLUDED.value,\n embedding = EXCLUDED.embedding,\n tags = EXCLUDED.tags`,\n [key, JSON.stringify(value), vectorLiteral(vector), tags ?? []],\n );\n }\n\n /**\n * Return the cosine-nearest rows to `vector`, mapped to the contract's\n * `{ key, value, score }` shape. The SQL:\n *\n * - computes `embedding <=> $1::vector` (cosine **distance**) once, aliased\n * `distance`, and `ORDER BY` it ascending (nearest first);\n * - applies the `threshold` floor as `distance <= 1 - threshold`\n * (similarity `>=` threshold), so the default `0.5` floor maps to a\n * `<= 0.5` distance bound — the filter runs in SQL, not in JS, so a\n * below-floor row never crosses the wire;\n * - when `tags` are given, restricts to rows whose `tags` array overlaps\n * the requested set via `tags && $tags::text[]` (one-of semantics,\n * matching the cache store);\n * - caps the result at `topK` with `LIMIT`.\n *\n * The returned `score` is `1 - distance`, clamped to `[0, 1]`, so callers\n * see the same cosine-similarity scale the cache store emits.\n */\n public async query<T>(\n vector: number[],\n options: { topK: number; threshold?: number; tags?: string[] },\n ): Promise<{ key: string; value: T; score: number }[]> {\n const client = await this.client();\n const queryVector = vectorLiteral(vector);\n\n // $1 = query vector, $2 = topK. Optional threshold + tags are appended\n // as $3 / $4 only when present, so the prepared statement carries no\n // unused placeholders.\n const params: unknown[] = [queryVector, options.topK];\n const conditions: string[] = [];\n\n if (options.threshold !== undefined) {\n params.push(1 - options.threshold);\n conditions.push(`(embedding <=> $1::vector) <= $${params.length}`);\n }\n\n if (options.tags !== undefined && options.tags.length > 0) {\n params.push(options.tags);\n conditions.push(`tags && $${params.length}::text[]`);\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n\n const { rows } = await client.query(\n `SELECT key, value, (embedding <=> $1::vector) AS distance\n FROM ${this.table}\n ${where}\n ORDER BY embedding <=> $1::vector\n LIMIT $2`,\n params,\n );\n\n return (rows as Record<string, unknown>[]).map((row) => ({\n key: row.key as string,\n value: parseValue<T>(row.value),\n score: distanceToScore(row.distance),\n }));\n }\n\n /**\n * Drop every entry written under `namespace`. The RAG pipeline keys\n * chunks as `<namespace>.<sourceId>.<chunkIndex>`, so a row belongs to\n * the namespace when its `key` equals it OR begins with `<namespace>.`\n * — deleting `ai.rag.docs` must not also catch `ai.rag.docs2`. The `_`\n * and `%` LIKE wildcards in the namespace are escaped so a namespace\n * that happens to contain them is matched literally.\n */\n public async removeNamespace(namespace: string): Promise<void> {\n const client = await this.client();\n\n const escaped = namespace\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/_/g, \"\\\\_\")\n .replace(/%/g, \"\\\\%\");\n\n await client.query(\n `DELETE FROM ${this.table}\n WHERE key = $1 OR key LIKE $2 ESCAPE '\\\\'`,\n [namespace, `${escaped}.%`],\n );\n }\n\n /**\n * Return the reference migration DDL for this store's backing table,\n * interpolating the configured table name, embedding width, and ANN\n * index strategy. The dev runs it once through their migration tool —\n * the framework never auto-migrates.\n *\n * The emitted statements:\n * 1. `CREATE EXTENSION IF NOT EXISTS vector;` — enables pgvector (needs\n * a superuser or a role with `CREATE` on the database the first time).\n * 2. `CREATE TABLE IF NOT EXISTS <table> (key TEXT PRIMARY KEY, value\n * JSONB NOT NULL, embedding vector(<dimensions>) NOT NULL, tags\n * text[] NOT NULL DEFAULT '{}');`\n * 3. A GIN index on `tags` so the array-overlap filter stays sargable.\n * 4. The chosen ANN index over `embedding` using `vector_cosine_ops`:\n * - `\"hnsw\"` → `USING hnsw (embedding vector_cosine_ops)`;\n * - `\"ivfflat\"` → `USING ivfflat (embedding vector_cosine_ops)\n * WITH (lists = <ivfflatLists>)`;\n * - `\"none\"` → emitted as a comment (exact scan, no ANN index).\n *\n * @example\n * const store = pgVectorStore({ client: pool, dimensions: 1536 });\n * await pool.query(store.ensureSchema());\n */\n public schema(): string {\n const lines = [\n `CREATE EXTENSION IF NOT EXISTS vector;`,\n `CREATE TABLE IF NOT EXISTS ${this.table} (`,\n ` key TEXT PRIMARY KEY,`,\n ` value JSONB NOT NULL,`,\n ` embedding vector(${this.dimensions}) NOT NULL,`,\n ` tags TEXT[] NOT NULL DEFAULT '{}'`,\n `);`,\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_tags`,\n ` ON ${this.table} USING gin (tags);`,\n ];\n\n if (this.index === \"hnsw\") {\n lines.push(\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding`,\n ` ON ${this.table} USING hnsw (embedding vector_cosine_ops);`,\n );\n } else if (this.index === \"ivfflat\") {\n lines.push(\n `CREATE INDEX IF NOT EXISTS idx_${this.table}_embedding`,\n ` ON ${this.table} USING ivfflat (embedding vector_cosine_ops)`,\n ` WITH (lists = ${this.ivfflatLists});`,\n );\n } else {\n lines.push(\n `-- No ANN index requested (index: \"none\"): cosine queries fall back`,\n `-- to an exact sequential scan, which is correct but linear in rows.`,\n );\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * Alias for {@link PgVectorStore.schema} — reads more naturally in a\n * migration script (`await pool.query(store.ensureSchema())`). Returns\n * the identical DDL string; it does NOT execute anything against the\n * database (the store never auto-migrates).\n */\n public ensureSchema(): string {\n return this.schema();\n }\n}\n\n/**\n * The {@link VectorStore} surface plus the pg store's extra DDL helpers.\n * `schema()` / `ensureSchema()` are not part of the base contract (the\n * cache store has no backing table), so the factory's return type widens\n * it for callers that want the migration SQL.\n */\nexport interface PgVectorStoreInstance extends VectorStore {\n /** Reference migration DDL (extension + table + indexes). Never executed. */\n schema(): string;\n /** Alias for {@link PgVectorStoreInstance.schema} — reads better in a migration script. */\n ensureSchema(): string;\n}\n\n/**\n * Create a Postgres + pgvector-backed {@link VectorStore} for the RAG\n * pipeline. Either pass a live `pg.Pool` / `pg.Client` (`{ client }`) —\n * `@warlock.js/ai` never imports `pg` in that case — or a\n * `{ connectionString }` and let the store lazily `import(\"pg\")` to build\n * its own pool. When `pg` is not installed, a curated install string\n * surfaces on first use, never at import.\n *\n * Run {@link PgVectorStoreInstance.ensureSchema} through your migration\n * tool once before use (it enables the `vector` extension, creates the\n * table, and builds the tag + ANN indexes); the store never auto-migrates.\n *\n * Index and query MUST use the same embedding model — the `vector(N)`\n * column width is fixed at table-creation time from `dimensions`.\n *\n * @example\n * import { Pool } from \"pg\";\n * import { ai } from \"@warlock.js/ai\";\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n * const store = ai.rag.pgVectorStore({ client: pool, dimensions: 1536 });\n *\n * // Once, via your migration tooling:\n * // await pool.query(store.ensureSchema());\n *\n * const kb = ai.rag({\n * name: \"docs\",\n * embedder: openai.embedder({ name: \"text-embedding-3-small\" }),\n * store,\n * });\n *\n * @example\n * // Let the store build its own pool from a connection string:\n * const store = ai.rag.pgVectorStore({\n * connectionString: process.env.DATABASE_URL,\n * index: \"ivfflat\",\n * ivfflatLists: 200,\n * });\n */\nexport function pgVectorStore(options: PgVectorStoreOptions): PgVectorStoreInstance {\n return new PgVectorStore(options);\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { speech } from "./speech.mjs";
2
+
3
+ export { };
@@ -0,0 +1,65 @@
1
+ import { BaseReport } from "../contracts/result/base-report.type.mjs";
2
+ import { ExecuteResult } from "../contracts/result/execute-result.type.mjs";
3
+ import { FlowObserveOption } from "../observe/resolve-observers.mjs";
4
+ import { GeneratedAudio, SpeechModelContract } from "../contracts/speech-model.contract.mjs";
5
+
6
+ //#region ../@warlock.js/ai/src/speech/speech.d.ts
7
+ /** Parameters for {@link speech}. `model` comes from `sdk.speech({ name })`. */
8
+ type SpeechParams = {
9
+ /** The TTS model to synthesize with. */model: SpeechModelContract; /** The text to speak. */
10
+ text: string; /** Voice id/name; overrides the model's default voice. */
11
+ voice?: string; /** Output container (`"mp3"` / `"opus"` / `"aac"` / `"flac"` / `"wav"` / `"pcm"`). */
12
+ format?: string; /** Playback speed multiplier. */
13
+ speed?: number; /** Extra tone/delivery steering (model-dependent). */
14
+ instructions?: string; /** Cancellation handle. */
15
+ signal?: AbortSignal; /** Observability routing — same `observe` seam as agents. */
16
+ observe?: FlowObserveOption; /** Groups this call into a session for flat cost/trace queries. */
17
+ sessionId?: string; /** Report node name (defaults to `"speech"`). */
18
+ name?: string; /** Provider-specific options forwarded verbatim to the adapter. */
19
+ options?: Record<string, unknown>;
20
+ };
21
+ /** Success payload of a {@link speech} run. */
22
+ type SpeechData = {
23
+ /** The synthesized audio, normalized to the discriminated shape. */audio: GeneratedAudio;
24
+ };
25
+ /** The report node a {@link speech} run produces (`type: "speech"`). */
26
+ type SpeechReport = BaseReport & {
27
+ type: "speech"; /** Identity of the TTS model this run used. */
28
+ model: {
29
+ name: string;
30
+ provider: string;
31
+ }; /** Number of input characters synthesized (0 on failure). */
32
+ characters: number;
33
+ };
34
+ /** Result envelope of {@link speech} — the uniform `{ data, error, usage, report }`. */
35
+ type SpeechResult = ExecuteResult<SpeechData> & {
36
+ type: "speech";
37
+ report: SpeechReport;
38
+ };
39
+ /**
40
+ * Synthesize speech from text — the text-to-speech verb of the
41
+ * output-modality track (Theme I), sibling to `ai.image()`. Wraps a
42
+ * {@link SpeechModelContract} (from `openai.speech(...)`) in the
43
+ * framework's uniform result contract:
44
+ *
45
+ * - **Never throws.** Provider failures surface as a typed `AIError` on
46
+ * `result.error`; `result.data` is then `undefined`.
47
+ * - **Cost-truth.** `result.usage.cost` is filled per-character
48
+ * (`tts-1`) or per-token (`gpt-4o-mini-tts`), folding into the same
49
+ * `Usage.cost` rollup as text.
50
+ * - **Observable.** The completed {@link SpeechReport} routes to any
51
+ * registered `Observer` (panoptic, OTel, …) via the `observe` seam.
52
+ *
53
+ * @example
54
+ * const openai = new OpenAISDK({ apiKey });
55
+ * const { data, error } = await ai.speech({
56
+ * model: openai.speech({ name: "tts-1", voice: "alloy" }),
57
+ * text: "Your order has shipped.",
58
+ * format: "mp3",
59
+ * });
60
+ * if (!error) await fs.writeFile("ship.mp3", Buffer.from(data.audio.base64, "base64"));
61
+ */
62
+ declare function speech(params: SpeechParams): Promise<SpeechResult>;
63
+ //#endregion
64
+ export { SpeechData, SpeechParams, SpeechReport, SpeechResult, speech };
65
+ //# sourceMappingURL=speech.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai/src/speech/speech.ts"],"mappings":";;;;;;;KAmBY,YAAA;EAAA,wCAEV,KAAA,EAAO,mBAAA;EAEP,IAAA,UAUS;EART,KAAA,WAgBU;EAdV,MAAA,WAcgB;EAZhB,KAAA,WARO;EAUP,YAAA,WANA;EAQA,MAAA,GAAS,WAAA,EAJT;EAMA,OAAA,GAAU,iBAAA,EAFV;EAIA,SAAA,WAFA;EAIA,IAAA,WAFA;EAIA,OAAA,GAAU,MAAA;AAAA;;KAIA,UAAA;EAJM,oEAMhB,KAAA,EAAO,cAAc;AAAA;;KAIX,YAAA,GAAe,UAAU;EACnC,IAAA,YADU;EAGV,KAAA;IAAS,IAAA;IAAc,QAAA;EAAA,GAFvB;EAIA,UAAA;AAAA;;KAIU,YAAA,GAAe,aAAA,CAAc,UAAA;EACvC,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;;;;;;;;;;;;;;AAAY;AA0BtB;;;;;;;;;iBAAsB,MAAA,CAAO,MAAA,EAAQ,YAAA,GAAe,OAAA,CAAQ,YAAA"}
@@ -0,0 +1,123 @@
1
+ import { AIError } from "../errors/ai-error.mjs";
2
+ import { ProviderError } from "../errors/provider-error.mjs";
3
+ import { computeCost } from "../utils/compute-cost.mjs";
4
+ import { generateRunId } from "../utils/generate-run-id.mjs";
5
+ import { REPORT_SCHEMA_VERSION } from "../contracts/result/base-report.type.mjs";
6
+ import { stampReportLineage } from "../utils/stamp-report-lineage.mjs";
7
+ import { notifyObservers } from "../observe/resolve-observers.mjs";
8
+
9
+ //#region ../@warlock.js/ai/src/speech/speech.ts
10
+ /**
11
+ * Synthesize speech from text — the text-to-speech verb of the
12
+ * output-modality track (Theme I), sibling to `ai.image()`. Wraps a
13
+ * {@link SpeechModelContract} (from `openai.speech(...)`) in the
14
+ * framework's uniform result contract:
15
+ *
16
+ * - **Never throws.** Provider failures surface as a typed `AIError` on
17
+ * `result.error`; `result.data` is then `undefined`.
18
+ * - **Cost-truth.** `result.usage.cost` is filled per-character
19
+ * (`tts-1`) or per-token (`gpt-4o-mini-tts`), folding into the same
20
+ * `Usage.cost` rollup as text.
21
+ * - **Observable.** The completed {@link SpeechReport} routes to any
22
+ * registered `Observer` (panoptic, OTel, …) via the `observe` seam.
23
+ *
24
+ * @example
25
+ * const openai = new OpenAISDK({ apiKey });
26
+ * const { data, error } = await ai.speech({
27
+ * model: openai.speech({ name: "tts-1", voice: "alloy" }),
28
+ * text: "Your order has shipped.",
29
+ * format: "mp3",
30
+ * });
31
+ * if (!error) await fs.writeFile("ship.mp3", Buffer.from(data.audio.base64, "base64"));
32
+ */
33
+ async function speech(params) {
34
+ const { model, text } = params;
35
+ const runId = generateRunId("speech");
36
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
37
+ const startPerf = performance.now();
38
+ const usage = {
39
+ input: 0,
40
+ output: 0,
41
+ total: 0
42
+ };
43
+ let data;
44
+ let error;
45
+ let status = "completed";
46
+ let characters = 0;
47
+ try {
48
+ const response = await model.generate(text, {
49
+ voice: params.voice,
50
+ format: params.format,
51
+ speed: params.speed,
52
+ instructions: params.instructions,
53
+ signal: params.signal,
54
+ ...params.options
55
+ });
56
+ Object.assign(usage, response.usage);
57
+ characters = response.characters;
58
+ if (usage.cost === void 0) {
59
+ const cost = computeSpeechCost(usage, characters, model.pricing);
60
+ if (cost !== void 0) usage.cost = cost;
61
+ }
62
+ data = { audio: response.audio };
63
+ } catch (thrown) {
64
+ error = thrown instanceof AIError ? thrown : new ProviderError(toMessage(thrown), { cause: thrown });
65
+ status = params.signal?.aborted ? "cancelled" : "failed";
66
+ }
67
+ const report = {
68
+ runId,
69
+ rootRunId: runId,
70
+ name: params.name ?? "speech",
71
+ type: "speech",
72
+ status,
73
+ error,
74
+ startedAt,
75
+ endedAt: (/* @__PURE__ */ new Date()).toISOString(),
76
+ duration: performance.now() - startPerf,
77
+ usage,
78
+ children: [],
79
+ model: {
80
+ name: model.name,
81
+ provider: model.provider
82
+ },
83
+ characters,
84
+ reportSchemaVersion: 1
85
+ };
86
+ stampReportLineage(report, {
87
+ rootRunId: runId,
88
+ sessionId: params.sessionId
89
+ });
90
+ await notifyObservers(params.observe, report);
91
+ return {
92
+ type: "speech",
93
+ data,
94
+ error,
95
+ usage,
96
+ report
97
+ };
98
+ }
99
+ /**
100
+ * Price a TTS run: `perMillionCharacters × characters` (per-character
101
+ * metering, attributed to `cost.input`) wins when configured, otherwise
102
+ * the standard token math. Returns `undefined` when no usable pricing
103
+ * is present.
104
+ */
105
+ function computeSpeechCost(usage, characters, pricing) {
106
+ if (!pricing) return;
107
+ if (pricing.perMillionCharacters !== void 0) return {
108
+ input: characters * pricing.perMillionCharacters / 1e6,
109
+ output: 0
110
+ };
111
+ if (pricing.input !== void 0 && pricing.output !== void 0) return computeCost(usage, {
112
+ input: pricing.input,
113
+ output: pricing.output
114
+ });
115
+ }
116
+ /** Best-effort message for a non-`AIError` thrown value. */
117
+ function toMessage(thrown) {
118
+ return thrown instanceof Error ? thrown.message : String(thrown);
119
+ }
120
+
121
+ //#endregion
122
+ export { speech };
123
+ //# sourceMappingURL=speech.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai/src/speech/speech.ts"],"sourcesContent":["import type {\n GeneratedAudio,\n SpeechModelContract,\n SpeechModelPricing,\n} from \"../contracts/speech-model.contract\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport { REPORT_SCHEMA_VERSION } from \"../contracts/result/base-report.type\";\nimport type { ExecuteResult } from \"../contracts/result/execute-result.type\";\nimport type { ModelPricing } from \"../contracts/result/model-pricing.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport { AIError } from \"../errors/ai-error\";\nimport { ProviderError } from \"../errors/provider-error\";\nimport type { FlowObserveOption } from \"../observe/resolve-observers\";\nimport { notifyObservers } from \"../observe/resolve-observers\";\nimport { computeCost } from \"../utils/compute-cost\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport { stampReportLineage } from \"../utils/stamp-report-lineage\";\n\n/** Parameters for {@link speech}. `model` comes from `sdk.speech({ name })`. */\nexport type SpeechParams = {\n /** The TTS model to synthesize with. */\n model: SpeechModelContract;\n /** The text to speak. */\n text: string;\n /** Voice id/name; overrides the model's default voice. */\n voice?: string;\n /** Output container (`\"mp3\"` / `\"opus\"` / `\"aac\"` / `\"flac\"` / `\"wav\"` / `\"pcm\"`). */\n format?: string;\n /** Playback speed multiplier. */\n speed?: number;\n /** Extra tone/delivery steering (model-dependent). */\n instructions?: string;\n /** Cancellation handle. */\n signal?: AbortSignal;\n /** Observability routing — same `observe` seam as agents. */\n observe?: FlowObserveOption;\n /** Groups this call into a session for flat cost/trace queries. */\n sessionId?: string;\n /** Report node name (defaults to `\"speech\"`). */\n name?: string;\n /** Provider-specific options forwarded verbatim to the adapter. */\n options?: Record<string, unknown>;\n};\n\n/** Success payload of a {@link speech} run. */\nexport type SpeechData = {\n /** The synthesized audio, normalized to the discriminated shape. */\n audio: GeneratedAudio;\n};\n\n/** The report node a {@link speech} run produces (`type: \"speech\"`). */\nexport type SpeechReport = BaseReport & {\n type: \"speech\";\n /** Identity of the TTS model this run used. */\n model: { name: string; provider: string };\n /** Number of input characters synthesized (0 on failure). */\n characters: number;\n};\n\n/** Result envelope of {@link speech} — the uniform `{ data, error, usage, report }`. */\nexport type SpeechResult = ExecuteResult<SpeechData> & {\n type: \"speech\";\n report: SpeechReport;\n};\n\n/**\n * Synthesize speech from text — the text-to-speech verb of the\n * output-modality track (Theme I), sibling to `ai.image()`. Wraps a\n * {@link SpeechModelContract} (from `openai.speech(...)`) in the\n * framework's uniform result contract:\n *\n * - **Never throws.** Provider failures surface as a typed `AIError` on\n * `result.error`; `result.data` is then `undefined`.\n * - **Cost-truth.** `result.usage.cost` is filled per-character\n * (`tts-1`) or per-token (`gpt-4o-mini-tts`), folding into the same\n * `Usage.cost` rollup as text.\n * - **Observable.** The completed {@link SpeechReport} routes to any\n * registered `Observer` (panoptic, OTel, …) via the `observe` seam.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey });\n * const { data, error } = await ai.speech({\n * model: openai.speech({ name: \"tts-1\", voice: \"alloy\" }),\n * text: \"Your order has shipped.\",\n * format: \"mp3\",\n * });\n * if (!error) await fs.writeFile(\"ship.mp3\", Buffer.from(data.audio.base64, \"base64\"));\n */\nexport async function speech(params: SpeechParams): Promise<SpeechResult> {\n const { model, text } = params;\n\n const runId = generateRunId(\"speech\");\n const startedAt = new Date().toISOString();\n const startPerf = performance.now();\n\n const usage: Usage = { input: 0, output: 0, total: 0 };\n let data: SpeechData | undefined;\n let error: AIError | undefined;\n let status: SpeechReport[\"status\"] = \"completed\";\n let characters = 0;\n\n try {\n const response = await model.generate(text, {\n voice: params.voice,\n format: params.format,\n speed: params.speed,\n instructions: params.instructions,\n signal: params.signal,\n ...params.options,\n });\n\n Object.assign(usage, response.usage);\n characters = response.characters;\n\n if (usage.cost === undefined) {\n const cost = computeSpeechCost(usage, characters, model.pricing);\n if (cost !== undefined) {\n usage.cost = cost;\n }\n }\n\n data = { audio: response.audio };\n } catch (thrown) {\n error =\n thrown instanceof AIError ? thrown : new ProviderError(toMessage(thrown), { cause: thrown });\n status = params.signal?.aborted ? \"cancelled\" : \"failed\";\n }\n\n const report: SpeechReport = {\n runId,\n rootRunId: runId,\n name: params.name ?? \"speech\",\n type: \"speech\",\n status,\n error,\n startedAt,\n endedAt: new Date().toISOString(),\n duration: performance.now() - startPerf,\n usage,\n children: [],\n model: { name: model.name, provider: model.provider },\n characters,\n reportSchemaVersion: REPORT_SCHEMA_VERSION,\n };\n\n stampReportLineage(report, { rootRunId: runId, sessionId: params.sessionId });\n\n await notifyObservers(params.observe, report);\n\n return { type: \"speech\", data, error, usage, report };\n}\n\n/**\n * Price a TTS run: `perMillionCharacters × characters` (per-character\n * metering, attributed to `cost.input`) wins when configured, otherwise\n * the standard token math. Returns `undefined` when no usable pricing\n * is present.\n */\nfunction computeSpeechCost(\n usage: Usage,\n characters: number,\n pricing: SpeechModelPricing | undefined,\n): ModelPricing | undefined {\n if (!pricing) {\n return undefined;\n }\n\n if (pricing.perMillionCharacters !== undefined) {\n return { input: (characters * pricing.perMillionCharacters) / 1_000_000, output: 0 };\n }\n\n if (pricing.input !== undefined && pricing.output !== undefined) {\n return computeCost(usage, { input: pricing.input, output: pricing.output });\n }\n\n return undefined;\n}\n\n/** Best-effort message for a non-`AIError` thrown value. */\nfunction toMessage(thrown: unknown): string {\n return thrown instanceof Error ? thrown.message : String(thrown);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwFA,eAAsB,OAAO,QAA6C;CACxE,MAAM,EAAE,OAAO,SAAS;CAExB,MAAM,QAAQ,cAAc,QAAQ;CACpC,MAAM,6BAAY,IAAI,KAAK,EAAC,CAAC,YAAY;CACzC,MAAM,YAAY,YAAY,IAAI;CAElC,MAAM,QAAe;EAAE,OAAO;EAAG,QAAQ;EAAG,OAAO;CAAE;CACrD,IAAI;CACJ,IAAI;CACJ,IAAI,SAAiC;CACrC,IAAI,aAAa;CAEjB,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,SAAS,MAAM;GAC1C,OAAO,OAAO;GACd,QAAQ,OAAO;GACf,OAAO,OAAO;GACd,cAAc,OAAO;GACrB,QAAQ,OAAO;GACf,GAAG,OAAO;EACZ,CAAC;EAED,OAAO,OAAO,OAAO,SAAS,KAAK;EACnC,aAAa,SAAS;EAEtB,IAAI,MAAM,SAAS,QAAW;GAC5B,MAAM,OAAO,kBAAkB,OAAO,YAAY,MAAM,OAAO;GAC/D,IAAI,SAAS,QACX,MAAM,OAAO;EAEjB;EAEA,OAAO,EAAE,OAAO,SAAS,MAAM;CACjC,SAAS,QAAQ;EACf,QACE,kBAAkB,UAAU,SAAS,IAAI,cAAc,UAAU,MAAM,GAAG,EAAE,OAAO,OAAO,CAAC;EAC7F,SAAS,OAAO,QAAQ,UAAU,cAAc;CAClD;CAEA,MAAM,SAAuB;EAC3B;EACA,WAAW;EACX,MAAM,OAAO,QAAQ;EACrB,MAAM;EACN;EACA;EACA;EACA,0BAAS,IAAI,KAAK,EAAC,CAAC,YAAY;EAChC,UAAU,YAAY,IAAI,IAAI;EAC9B;EACA,UAAU,CAAC;EACX,OAAO;GAAE,MAAM,MAAM;GAAM,UAAU,MAAM;EAAS;EACpD;EACA;CACF;CAEA,mBAAmB,QAAQ;EAAE,WAAW;EAAO,WAAW,OAAO;CAAU,CAAC;CAE5E,MAAM,gBAAgB,OAAO,SAAS,MAAM;CAE5C,OAAO;EAAE,MAAM;EAAU;EAAM;EAAO;EAAO;CAAO;AACtD;;;;;;;AAQA,SAAS,kBACP,OACA,YACA,SAC0B;CAC1B,IAAI,CAAC,SACH;CAGF,IAAI,QAAQ,yBAAyB,QACnC,OAAO;EAAE,OAAQ,aAAa,QAAQ,uBAAwB;EAAW,QAAQ;CAAE;CAGrF,IAAI,QAAQ,UAAU,UAAa,QAAQ,WAAW,QACpD,OAAO,YAAY,OAAO;EAAE,OAAO,QAAQ;EAAO,QAAQ,QAAQ;CAAO,CAAC;AAI9E;;AAGA,SAAS,UAAU,QAAyB;CAC1C,OAAO,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM;AACjE"}
@@ -178,8 +178,8 @@ function isDispatchableUnit(value) {
178
178
  return typeof candidate.name === "string" && typeof candidate.execute === "function";
179
179
  }
180
180
  function detectType(unit) {
181
- if (typeof unit.signature === "string") return "workflow";
182
- return "agent";
181
+ if (typeof unit.stream === "function") return "agent";
182
+ return "workflow";
183
183
  }
184
184
  function resolveAgentLikeDescription(intent, entryForm, unit, supervisorName) {
185
185
  const entryOverride = entryForm?.description;