@deepagents/text2sql 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/index.d.ts +3 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1334 -491
  4. package/dist/index.js.map +4 -4
  5. package/dist/lib/adapters/groundings/index.js +2034 -50
  6. package/dist/lib/adapters/groundings/index.js.map +4 -4
  7. package/dist/lib/adapters/groundings/report.grounding.d.ts.map +1 -1
  8. package/dist/lib/adapters/mysql/index.js +2034 -50
  9. package/dist/lib/adapters/mysql/index.js.map +4 -4
  10. package/dist/lib/adapters/postgres/index.js +2034 -50
  11. package/dist/lib/adapters/postgres/index.js.map +4 -4
  12. package/dist/lib/adapters/spreadsheet/index.js +35 -49
  13. package/dist/lib/adapters/spreadsheet/index.js.map +4 -4
  14. package/dist/lib/adapters/sqlite/index.js +2034 -50
  15. package/dist/lib/adapters/sqlite/index.js.map +4 -4
  16. package/dist/lib/adapters/sqlserver/column-stats.sqlserver.grounding.d.ts.map +1 -1
  17. package/dist/lib/adapters/sqlserver/index.js +2035 -53
  18. package/dist/lib/adapters/sqlserver/index.js.map +4 -4
  19. package/dist/lib/agents/developer.agent.d.ts.map +1 -1
  20. package/dist/lib/agents/explainer.agent.d.ts +4 -5
  21. package/dist/lib/agents/explainer.agent.d.ts.map +1 -1
  22. package/dist/lib/agents/question.agent.d.ts.map +1 -1
  23. package/dist/lib/agents/result-tools.d.ts +37 -0
  24. package/dist/lib/agents/result-tools.d.ts.map +1 -0
  25. package/dist/lib/agents/sql.agent.d.ts +1 -1
  26. package/dist/lib/agents/sql.agent.d.ts.map +1 -1
  27. package/dist/lib/agents/teachables.agent.d.ts.map +1 -1
  28. package/dist/lib/agents/text2sql.agent.d.ts +0 -21
  29. package/dist/lib/agents/text2sql.agent.d.ts.map +1 -1
  30. package/dist/lib/checkpoint.d.ts +1 -1
  31. package/dist/lib/checkpoint.d.ts.map +1 -1
  32. package/dist/lib/instructions.d.ts +9 -28
  33. package/dist/lib/instructions.d.ts.map +1 -1
  34. package/dist/lib/sql.d.ts +1 -1
  35. package/dist/lib/sql.d.ts.map +1 -1
  36. package/dist/lib/synthesis/extractors/base-contextual-extractor.d.ts +6 -7
  37. package/dist/lib/synthesis/extractors/base-contextual-extractor.d.ts.map +1 -1
  38. package/dist/lib/synthesis/extractors/last-query-extractor.d.ts.map +1 -1
  39. package/dist/lib/synthesis/extractors/segmented-context-extractor.d.ts +0 -6
  40. package/dist/lib/synthesis/extractors/segmented-context-extractor.d.ts.map +1 -1
  41. package/dist/lib/synthesis/extractors/sql-extractor.d.ts.map +1 -1
  42. package/dist/lib/synthesis/index.js +2394 -2132
  43. package/dist/lib/synthesis/index.js.map +4 -4
  44. package/dist/lib/synthesis/synthesizers/breadth-evolver.d.ts.map +1 -1
  45. package/dist/lib/synthesis/synthesizers/depth-evolver.d.ts +3 -3
  46. package/dist/lib/synthesis/synthesizers/depth-evolver.d.ts.map +1 -1
  47. package/dist/lib/synthesis/synthesizers/persona-generator.d.ts.map +1 -1
  48. package/dist/lib/synthesis/synthesizers/schema-synthesizer.d.ts +1 -1
  49. package/dist/lib/synthesis/synthesizers/schema-synthesizer.d.ts.map +1 -1
  50. package/package.json +9 -15
  51. package/dist/lib/instructions.js +0 -432
  52. package/dist/lib/instructions.js.map +0 -7
  53. package/dist/lib/teach/teachings.d.ts +0 -11
  54. package/dist/lib/teach/teachings.d.ts.map +0 -1
@@ -165,7 +165,7 @@ import {
165
165
  } from "ai";
166
166
 
167
167
  // packages/text2sql/src/lib/synthesis/extractors/base-contextual-extractor.ts
168
- import { groq } from "@ai-sdk/groq";
168
+ import { groq as groq2 } from "@ai-sdk/groq";
169
169
  import {
170
170
  getToolOrDynamicToolName,
171
171
  isTextUIPart,
@@ -173,1643 +173,1185 @@ import {
173
173
  } from "ai";
174
174
  import dedent from "dedent";
175
175
  import z from "zod";
176
- import { agent, generate, user } from "@deepagents/agent";
177
- var contextResolverAgent = agent({
178
- name: "context_resolver",
179
- model: groq("openai/gpt-oss-20b"),
180
- output: z.object({
181
- question: z.string().describe(
182
- "A standalone natural language question that the SQL query answers"
183
- )
184
- }),
185
- prompt: (state) => dedent`
186
- <identity>
187
- You are an expert at understanding conversational context and generating clear,
188
- standalone questions from multi-turn conversations.
189
- </identity>
190
-
191
- ${state?.introspection ? `<schema>
192
- ${state.introspection}
193
- </schema>` : ""}
194
-
195
- <conversation>
196
- ${state?.conversation}
197
- </conversation>
198
-
199
- <sql>
200
- ${state?.sql}
201
- </sql>
202
-
203
- <task>
204
- Given the conversation above and the SQL query that was executed,
205
- generate a single, standalone natural language question that:
206
- 1. Fully captures the user's intent without needing prior context
207
- 2. Uses natural business language (not SQL terminology)
208
- 3. Could be asked by someone who hasn't seen the conversation
209
- 4. Accurately represents what the SQL query answers
210
- </task>
211
-
212
- <examples>
213
- Conversation: "Show me customers" → "Filter to NY" → "Sort by revenue"
214
- SQL: SELECT * FROM customers WHERE region = 'NY' ORDER BY revenue DESC
215
- Question: "Show me customers in the NY region sorted by revenue"
216
176
 
217
- Conversation: "What were sales last month?" → "Break it down by category"
218
- SQL: SELECT category, SUM(amount) FROM sales WHERE date >= '2024-11-01' GROUP BY category
219
- Question: "What were sales by category for last month?"
220
- </examples>
221
- `
222
- });
223
- function getMessageText(message2) {
224
- const textParts = message2.parts.filter(isTextUIPart).map((part) => part.text);
225
- return textParts.join(" ").trim();
226
- }
227
- function formatConversation(messages) {
228
- return messages.map((msg, i) => `[${i + 1}] ${msg}`).join("\n");
229
- }
230
- var BaseContextualExtractor = class extends PairProducer {
231
- context = [];
232
- results = [];
233
- messages;
234
- adapter;
235
- options;
236
- constructor(messages, adapter, options = {}) {
237
- super();
238
- this.messages = messages;
239
- this.adapter = adapter;
240
- this.options = options;
177
+ // packages/context/dist/index.js
178
+ import { encode } from "gpt-tokenizer";
179
+ import { generateId } from "ai";
180
+ import pluralize from "pluralize";
181
+ import { titlecase } from "stringcase";
182
+ import chalk from "chalk";
183
+ import { defineCommand } from "just-bash";
184
+ import spawn from "nano-spawn";
185
+ import "bash-tool";
186
+ import spawn2 from "nano-spawn";
187
+ import {
188
+ createBashTool
189
+ } from "bash-tool";
190
+ import YAML from "yaml";
191
+ import { DatabaseSync } from "node:sqlite";
192
+ import { groq } from "@ai-sdk/groq";
193
+ import {
194
+ NoSuchToolError,
195
+ Output,
196
+ convertToModelMessages,
197
+ createUIMessageStream,
198
+ generateId as generateId2,
199
+ generateText,
200
+ smoothStream,
201
+ stepCountIs,
202
+ streamText
203
+ } from "ai";
204
+ import chalk2 from "chalk";
205
+ import "zod";
206
+ import "@deepagents/agent";
207
+ var defaultTokenizer = {
208
+ encode(text) {
209
+ return encode(text);
210
+ },
211
+ count(text) {
212
+ return encode(text).length;
241
213
  }
214
+ };
215
+ var ModelsRegistry = class {
216
+ #cache = /* @__PURE__ */ new Map();
217
+ #loaded = false;
218
+ #tokenizers = /* @__PURE__ */ new Map();
219
+ #defaultTokenizer = defaultTokenizer;
242
220
  /**
243
- * Template method - defines the extraction algorithm skeleton.
244
- * Subclasses customize behavior via hooks, not by overriding this method.
221
+ * Load models data from models.dev API
245
222
  */
246
- async *produce() {
247
- this.context = [];
248
- this.results = [];
249
- const { includeFailures = false, toolName = "db_query" } = this.options;
250
- await this.extractSqlsWithContext(toolName, includeFailures);
251
- if (this.results.length === 0) {
252
- return;
223
+ async load() {
224
+ if (this.#loaded) return;
225
+ const response = await fetch("https://models.dev/api.json");
226
+ if (!response.ok) {
227
+ throw new Error(`Failed to fetch models: ${response.statusText}`);
253
228
  }
254
- const introspection = "";
255
- yield* this.resolveQuestions(introspection);
229
+ const data = await response.json();
230
+ for (const [providerId, provider] of Object.entries(data)) {
231
+ for (const [modelId, model] of Object.entries(provider.models)) {
232
+ const info = {
233
+ id: model.id,
234
+ name: model.name,
235
+ family: model.family,
236
+ cost: model.cost,
237
+ limit: model.limit,
238
+ provider: providerId
239
+ };
240
+ this.#cache.set(`${providerId}:${modelId}`, info);
241
+ }
242
+ }
243
+ this.#loaded = true;
256
244
  }
257
245
  /**
258
- * Core extraction loop - iterates through messages and calls hooks.
246
+ * Get model info by ID
247
+ * @param modelId - Model ID (e.g., "openai:gpt-4o")
259
248
  */
260
- async extractSqlsWithContext(toolName, includeFailures) {
261
- for (const message2 of this.messages) {
262
- if (message2.role === "user") {
263
- const text = getMessageText(message2);
264
- if (text) {
265
- await this.onUserMessage(text);
266
- }
267
- continue;
268
- }
269
- if (message2.role === "assistant") {
270
- await this.extractFromAssistant(message2, toolName, includeFailures);
271
- }
272
- }
249
+ get(modelId) {
250
+ return this.#cache.get(modelId);
273
251
  }
274
252
  /**
275
- * Extract SQL from assistant message parts.
253
+ * Check if a model exists in the registry
276
254
  */
277
- async extractFromAssistant(message2, toolName, includeFailures) {
278
- for (const part of message2.parts) {
279
- if (!isToolOrDynamicToolUIPart(part)) {
280
- continue;
281
- }
282
- if (getToolOrDynamicToolName(part) !== toolName) {
283
- continue;
284
- }
285
- const toolInput = "input" in part ? part.input : void 0;
286
- if (!toolInput?.sql) {
287
- continue;
288
- }
289
- const success = part.state === "output-available";
290
- const failed = part.state === "output-error";
291
- if (failed && !includeFailures) {
292
- continue;
293
- }
294
- if (!success && !failed) {
295
- continue;
296
- }
297
- const snapshot = this.getContextSnapshot();
298
- if (snapshot.length === 0) {
299
- continue;
300
- }
301
- this.results.push({
302
- sql: toolInput.sql,
303
- success,
304
- conversationContext: snapshot
305
- });
306
- }
307
- const assistantText = getMessageText(message2);
308
- if (assistantText) {
309
- this.context.push(`Assistant: ${assistantText}`);
310
- }
255
+ has(modelId) {
256
+ return this.#cache.has(modelId);
311
257
  }
312
258
  /**
313
- * Resolve extracted SQL contexts into standalone questions using LLM.
259
+ * List all available model IDs
314
260
  */
315
- async *resolveQuestions(introspection) {
316
- for (const item of this.results) {
317
- const { experimental_output } = await generate(
318
- contextResolverAgent,
319
- [user("Generate a standalone question for this SQL query.")],
320
- {
321
- conversation: formatConversation(item.conversationContext),
322
- sql: item.sql,
323
- introspection
324
- }
325
- );
326
- yield [
327
- {
328
- question: experimental_output.question,
329
- sql: item.sql,
330
- context: item.conversationContext,
331
- success: item.success
332
- }
333
- ];
334
- }
261
+ list() {
262
+ return [...this.#cache.keys()];
335
263
  }
336
- };
337
-
338
- // packages/text2sql/src/lib/synthesis/extractors/message-extractor.ts
339
- var MessageExtractor = class extends PairProducer {
340
- #messages;
341
- #options;
342
264
  /**
343
- * @param messages - Chat history to extract pairs from
344
- * @param options - Extraction configuration
265
+ * Register a custom tokenizer for specific model families
266
+ * @param family - Model family name (e.g., "llama", "claude")
267
+ * @param tokenizer - Tokenizer implementation
345
268
  */
346
- constructor(messages, options = {}) {
347
- super();
348
- this.#messages = messages;
349
- this.#options = options;
269
+ registerTokenizer(family, tokenizer) {
270
+ this.#tokenizers.set(family, tokenizer);
350
271
  }
351
272
  /**
352
- * Extracts question-SQL pairs by parsing tool calls and pairing with user messages.
353
- * @returns Pairs extracted from db_query tool invocations
273
+ * Set the default tokenizer used when no family-specific tokenizer is registered
354
274
  */
355
- async *produce() {
356
- const { includeFailures = false, toolName = "db_query" } = this.#options;
357
- let lastUserMessage = null;
358
- for (const message2 of this.#messages) {
359
- if (message2.role === "user") {
360
- lastUserMessage = message2;
361
- continue;
362
- }
363
- if (message2.role === "assistant" && lastUserMessage) {
364
- for (const part of message2.parts) {
365
- if (!isToolOrDynamicToolUIPart2(part)) {
366
- continue;
367
- }
368
- if (getToolOrDynamicToolName2(part) !== toolName) {
369
- continue;
370
- }
371
- const toolInput = "input" in part ? part.input : void 0;
372
- if (!toolInput?.sql) {
373
- continue;
374
- }
375
- const success = part.state === "output-available";
376
- const failed = part.state === "output-error";
377
- if (failed && !includeFailures) {
378
- continue;
379
- }
380
- if (!success && !failed) {
381
- continue;
382
- }
383
- const question = getMessageText(lastUserMessage);
384
- if (!question) {
385
- continue;
386
- }
387
- yield [
388
- {
389
- question,
390
- sql: toolInput.sql,
391
- success
392
- }
393
- ];
394
- }
275
+ setDefaultTokenizer(tokenizer) {
276
+ this.#defaultTokenizer = tokenizer;
277
+ }
278
+ /**
279
+ * Get the appropriate tokenizer for a model
280
+ */
281
+ getTokenizer(modelId) {
282
+ const model = this.get(modelId);
283
+ if (model) {
284
+ const familyTokenizer = this.#tokenizers.get(model.family);
285
+ if (familyTokenizer) {
286
+ return familyTokenizer;
395
287
  }
396
288
  }
397
- }
398
- };
399
-
400
- // packages/text2sql/src/lib/synthesis/extractors/sql-extractor.ts
401
- import { groq as groq2 } from "@ai-sdk/groq";
402
- import dedent2 from "dedent";
403
- import z2 from "zod";
404
- import { agent as agent2, generate as generate2, user as user2 } from "@deepagents/agent";
405
- var sqlToQuestionAgent = agent2({
406
- name: "sql_to_question",
407
- model: groq2("llama-3.3-70b-versatile"),
408
- output: z2.object({
409
- question: z2.string().describe("A natural language question that the SQL query answers")
410
- }),
411
- prompt: (state) => dedent2`
412
- <identity>
413
- You are an expert at understanding SQL queries and generating clear,
414
- natural language questions that describe what the query retrieves.
415
- </identity>
416
-
417
- <schema>
418
- ${state?.introspection}
419
- </schema>
420
-
421
- <sql>
422
- ${state?.sql}
423
- </sql>
424
-
425
- <task>
426
- Given the database schema and the SQL query above, generate a single
427
- natural language question that:
428
- 1. Accurately describes what information the query retrieves
429
- 2. Uses natural business language (not SQL terminology)
430
- 3. Could be asked by a non-technical user
431
- 4. Is concise but complete
432
- </task>
433
-
434
- <examples>
435
- SQL: SELECT COUNT(*) FROM customers WHERE region = 'NY'
436
- Question: "How many customers do we have in New York?"
437
-
438
- SQL: SELECT product_name, SUM(quantity) as total FROM orders GROUP BY product_name ORDER BY total DESC LIMIT 10
439
- Question: "What are our top 10 products by quantity sold?"
440
-
441
- SQL: SELECT c.name, COUNT(o.id) FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.id HAVING COUNT(o.id) = 0
442
- Question: "Which customers have never placed an order?"
443
- </examples>
444
- `
445
- });
446
- var SqlExtractor = class extends PairProducer {
447
- #sqls;
448
- #adapter;
449
- #options;
450
- /**
451
- * @param sql - SQL query or queries to generate questions for
452
- * @param adapter - Database adapter for validation and schema introspection
453
- * @param options - Extraction configuration
454
- */
455
- constructor(sql, adapter, options = {}) {
456
- super();
457
- this.#sqls = Array.isArray(sql) ? sql : [sql];
458
- this.#adapter = adapter;
459
- this.#options = options;
289
+ return this.#defaultTokenizer;
460
290
  }
461
291
  /**
462
- * Generates natural language questions for each SQL query using an LLM.
463
- * @returns Pairs with generated questions and original SQL
292
+ * Estimate token count and cost for given text and model
293
+ * @param modelId - Model ID to use for pricing (e.g., "openai:gpt-4o")
294
+ * @param input - Input text (prompt)
464
295
  */
465
- async *produce() {
466
- const { validateSql = true, skipInvalid = false } = this.#options;
467
- const introspection = "";
468
- for (const sql of this.#sqls) {
469
- let isValid = true;
470
- if (validateSql) {
471
- const error = await this.#adapter.validate(sql);
472
- isValid = error === void 0 || error === null;
473
- if (!isValid && skipInvalid) {
474
- continue;
475
- }
476
- }
477
- const { experimental_output } = await generate2(
478
- sqlToQuestionAgent,
479
- [user2("Generate a natural language question for this SQL query.")],
480
- {
481
- sql,
482
- introspection
483
- }
296
+ estimate(modelId, input) {
297
+ const model = this.get(modelId);
298
+ if (!model) {
299
+ throw new Error(
300
+ `Model "${modelId}" not found. Call load() first or check model ID.`
484
301
  );
485
- yield [
486
- {
487
- question: experimental_output.question,
488
- sql,
489
- success: isValid
490
- }
491
- ];
492
302
  }
303
+ const tokenizer = this.getTokenizer(modelId);
304
+ const tokens = tokenizer.count(input);
305
+ const cost = tokens / 1e6 * model.cost.input;
306
+ return {
307
+ model: model.id,
308
+ provider: model.provider,
309
+ tokens,
310
+ cost,
311
+ limits: {
312
+ context: model.limit.context,
313
+ output: model.limit.output,
314
+ exceedsContext: tokens > model.limit.context
315
+ },
316
+ fragments: []
317
+ };
493
318
  }
494
319
  };
495
-
496
- // packages/text2sql/src/lib/synthesis/extractors/full-context-extractor.ts
497
- var FullContextExtractor = class extends BaseContextualExtractor {
498
- constructor(messages, adapter, options = {}) {
499
- super(messages, adapter, options);
320
+ var _registry = null;
321
+ function getModelsRegistry() {
322
+ if (!_registry) {
323
+ _registry = new ModelsRegistry();
500
324
  }
501
- /**
502
- * Add user message to context (keeps all messages).
503
- */
504
- async onUserMessage(text) {
505
- this.context.push(`User: ${text}`);
325
+ return _registry;
326
+ }
327
+ function isFragment(data) {
328
+ return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
329
+ }
330
+ function isFragmentObject(data) {
331
+ return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
332
+ }
333
+ function isMessageFragment(fragment2) {
334
+ return fragment2.type === "message";
335
+ }
336
+ function fragment(name, ...children) {
337
+ return {
338
+ name,
339
+ data: children
340
+ };
341
+ }
342
+ function user(content) {
343
+ const message2 = typeof content === "string" ? {
344
+ id: generateId(),
345
+ role: "user",
346
+ parts: [{ type: "text", text: content }]
347
+ } : content;
348
+ return {
349
+ id: message2.id,
350
+ name: "user",
351
+ data: "content",
352
+ type: "message",
353
+ persist: true,
354
+ codec: {
355
+ decode() {
356
+ return message2;
357
+ },
358
+ encode() {
359
+ return message2;
360
+ }
361
+ }
362
+ };
363
+ }
364
+ function assistant(message2) {
365
+ return {
366
+ id: message2.id,
367
+ name: "assistant",
368
+ data: "content",
369
+ type: "message",
370
+ persist: true,
371
+ codec: {
372
+ decode() {
373
+ return message2;
374
+ },
375
+ encode() {
376
+ return message2;
377
+ }
378
+ }
379
+ };
380
+ }
381
+ function message(content) {
382
+ const message2 = typeof content === "string" ? {
383
+ id: generateId(),
384
+ role: "user",
385
+ parts: [{ type: "text", text: content }]
386
+ } : content;
387
+ return {
388
+ id: message2.id,
389
+ name: message2.role,
390
+ data: "content",
391
+ type: "message",
392
+ persist: true,
393
+ codec: {
394
+ decode() {
395
+ return message2;
396
+ },
397
+ encode() {
398
+ return message2;
399
+ }
400
+ }
401
+ };
402
+ }
403
+ function assistantText(content, options) {
404
+ const id = options?.id ?? crypto.randomUUID();
405
+ return assistant({
406
+ id,
407
+ role: "assistant",
408
+ parts: [{ type: "text", text: content }]
409
+ });
410
+ }
411
+ var LAZY_ID = Symbol("lazy-id");
412
+ function isLazyFragment(fragment2) {
413
+ return LAZY_ID in fragment2;
414
+ }
415
+ var ContextRenderer = class {
416
+ options;
417
+ constructor(options = {}) {
418
+ this.options = options;
506
419
  }
507
420
  /**
508
- * Return all context accumulated so far.
421
+ * Check if data is a primitive (string, number, boolean).
509
422
  */
510
- getContextSnapshot() {
511
- return [...this.context];
512
- }
513
- };
514
-
515
- // packages/text2sql/src/lib/synthesis/extractors/windowed-context-extractor.ts
516
- var WindowedContextExtractor = class extends BaseContextualExtractor {
517
- windowSize;
518
- constructor(messages, adapter, options) {
519
- super(messages, adapter, options);
520
- this.windowSize = options.windowSize;
423
+ isPrimitive(data) {
424
+ return typeof data === "string" || typeof data === "number" || typeof data === "boolean";
521
425
  }
522
426
  /**
523
- * Add user message to context (keeps all, windowing happens on snapshot).
427
+ * Group fragments by name for groupFragments option.
524
428
  */
525
- async onUserMessage(text) {
526
- this.context.push(`User: ${text}`);
429
+ groupByName(fragments) {
430
+ const groups = /* @__PURE__ */ new Map();
431
+ for (const fragment2 of fragments) {
432
+ const existing = groups.get(fragment2.name) ?? [];
433
+ existing.push(fragment2);
434
+ groups.set(fragment2.name, existing);
435
+ }
436
+ return groups;
527
437
  }
528
438
  /**
529
- * Return only the last N messages based on window size.
439
+ * Remove null/undefined from fragments and fragment data recursively.
440
+ * This protects renderers from nullish values and ensures they are ignored
441
+ * consistently across all output formats.
530
442
  */
531
- getContextSnapshot() {
532
- if (this.context.length <= this.windowSize) {
533
- return [...this.context];
443
+ sanitizeFragments(fragments) {
444
+ const sanitized = [];
445
+ for (const fragment2 of fragments) {
446
+ const cleaned = this.sanitizeFragment(fragment2, /* @__PURE__ */ new WeakSet());
447
+ if (cleaned) {
448
+ sanitized.push(cleaned);
449
+ }
534
450
  }
535
- return this.context.slice(-this.windowSize);
451
+ return sanitized;
536
452
  }
537
- };
538
-
539
- // packages/text2sql/src/lib/synthesis/extractors/segmented-context-extractor.ts
540
- import { groq as groq3 } from "@ai-sdk/groq";
541
- import dedent3 from "dedent";
542
- import z3 from "zod";
543
- import { agent as agent3, generate as generate3, user as user3 } from "@deepagents/agent";
544
- var topicChangeAgent = agent3({
545
- name: "topic_change_detector",
546
- model: groq3("openai/gpt-oss-20b"),
547
- output: z3.object({
548
- isTopicChange: z3.boolean().describe("Whether the new message represents a topic change"),
549
- reason: z3.string().describe("Brief explanation for the decision")
550
- }),
551
- prompt: (state) => dedent3`
552
- <identity>
553
- You are an expert at understanding conversational flow and detecting topic changes.
554
- </identity>
555
-
556
- <conversation_context>
557
- ${state?.context || "(no prior context)"}
558
- </conversation_context>
559
-
560
- <new_message>
561
- ${state?.newMessage}
562
- </new_message>
563
-
564
- <task>
565
- Determine if the new message represents a significant topic change from the
566
- prior conversation context. A topic change occurs when:
567
- 1. The user asks about a completely different entity/table/domain
568
- 2. The user starts a new analytical question unrelated to prior discussion
569
- 3. There's a clear shift in what data or metrics are being discussed
570
-
571
- NOT a topic change:
572
- - Follow-up questions refining the same query ("filter by...", "sort by...")
573
- - Questions about the same entities with different conditions
574
- - Requests for more details on the same topic
575
- </task>
576
-
577
- <examples>
578
- Context: "Show me customers in NY" → "Sort by revenue"
579
- New: "Filter to those with orders over $1000"
580
- Decision: NOT a topic change (still refining customer query)
581
-
582
- Context: "Show me customers in NY" → "Sort by revenue"
583
- New: "What were our total sales last quarter?"
584
- Decision: Topic change (shifted from customers to sales metrics)
585
-
586
- Context: "List all products"
587
- New: "How many orders did we have last month?"
588
- Decision: Topic change (products → orders/sales)
589
- </examples>
590
- `
591
- });
592
- var SegmentedContextExtractor = class extends BaseContextualExtractor {
593
- constructor(messages, adapter, options = {}) {
594
- super(messages, adapter, options);
453
+ sanitizeFragment(fragment2, seen) {
454
+ const data = this.sanitizeData(fragment2.data, seen);
455
+ if (data == null) {
456
+ return null;
457
+ }
458
+ return {
459
+ ...fragment2,
460
+ data
461
+ };
595
462
  }
596
- /**
597
- * Handle user message with topic change detection.
598
- * If topic changes, resolve the message to standalone form before resetting.
599
- *
600
- * Note: We capture context snapshot before async LLM calls to prevent race conditions
601
- * where context might be modified during the async operation.
602
- */
603
- async onUserMessage(text) {
604
- if (this.context.length >= 2) {
605
- const contextSnapshot = [...this.context];
606
- const isTopicChange = await this.detectTopicChange(text, contextSnapshot);
607
- if (isTopicChange) {
608
- const resolved = await this.resolveToStandalone(text, contextSnapshot);
609
- this.context = [`User: ${resolved}`];
610
- return;
463
+ sanitizeData(data, seen) {
464
+ if (data == null) {
465
+ return void 0;
466
+ }
467
+ if (isFragment(data)) {
468
+ return this.sanitizeFragment(data, seen) ?? void 0;
469
+ }
470
+ if (Array.isArray(data)) {
471
+ if (seen.has(data)) {
472
+ return void 0;
473
+ }
474
+ seen.add(data);
475
+ const cleaned = [];
476
+ for (const item of data) {
477
+ const sanitizedItem = this.sanitizeData(item, seen);
478
+ if (sanitizedItem != null) {
479
+ cleaned.push(sanitizedItem);
480
+ }
611
481
  }
482
+ return cleaned;
612
483
  }
613
- this.context.push(`User: ${text}`);
484
+ if (isFragmentObject(data)) {
485
+ if (seen.has(data)) {
486
+ return void 0;
487
+ }
488
+ seen.add(data);
489
+ const cleaned = {};
490
+ for (const [key, value] of Object.entries(data)) {
491
+ const sanitizedValue = this.sanitizeData(value, seen);
492
+ if (sanitizedValue != null) {
493
+ cleaned[key] = sanitizedValue;
494
+ }
495
+ }
496
+ return cleaned;
497
+ }
498
+ return data;
614
499
  }
615
500
  /**
616
- * Return all context in current topic segment.
501
+ * Template method - dispatches value to appropriate handler.
617
502
  */
618
- getContextSnapshot() {
619
- return [...this.context];
503
+ renderValue(key, value, ctx) {
504
+ if (value == null) {
505
+ return "";
506
+ }
507
+ if (isFragment(value)) {
508
+ return this.renderFragment(value, ctx);
509
+ }
510
+ if (Array.isArray(value)) {
511
+ return this.renderArray(key, value, ctx);
512
+ }
513
+ if (isFragmentObject(value)) {
514
+ return this.renderObject(key, value, ctx);
515
+ }
516
+ return this.renderPrimitive(key, String(value), ctx);
620
517
  }
621
518
  /**
622
- * Detect if a new message represents a topic change using LLM.
623
- * @param newMessage - The new user message to check
624
- * @param contextSnapshot - Snapshot of context captured before this async call
519
+ * Render all entries of an object.
625
520
  */
626
- async detectTopicChange(newMessage, contextSnapshot) {
627
- const { experimental_output } = await generate3(
628
- topicChangeAgent,
629
- [user3("Determine if this is a topic change.")],
630
- {
631
- context: formatConversation(contextSnapshot),
632
- newMessage
521
+ renderEntries(data, ctx) {
522
+ return Object.entries(data).map(([key, value]) => this.renderValue(key, value, ctx)).filter(Boolean);
523
+ }
524
+ };
525
+ var XmlRenderer = class extends ContextRenderer {
526
+ render(fragments) {
527
+ const sanitized = this.sanitizeFragments(fragments);
528
+ return sanitized.map((f) => this.#renderTopLevel(f)).filter(Boolean).join("\n");
529
+ }
530
+ #renderTopLevel(fragment2) {
531
+ if (this.isPrimitive(fragment2.data)) {
532
+ return this.#leafRoot(fragment2.name, String(fragment2.data));
533
+ }
534
+ if (Array.isArray(fragment2.data)) {
535
+ return this.#renderArray(fragment2.name, fragment2.data, 0);
536
+ }
537
+ if (isFragment(fragment2.data)) {
538
+ const child = this.renderFragment(fragment2.data, { depth: 1, path: [] });
539
+ return this.#wrap(fragment2.name, [child]);
540
+ }
541
+ if (isFragmentObject(fragment2.data)) {
542
+ return this.#wrap(
543
+ fragment2.name,
544
+ this.renderEntries(fragment2.data, { depth: 1, path: [] })
545
+ );
546
+ }
547
+ return "";
548
+ }
549
+ #renderArray(name, items, depth) {
550
+ const fragmentItems = items.filter(isFragment);
551
+ const nonFragmentItems = items.filter((item) => !isFragment(item));
552
+ const children = [];
553
+ for (const item of nonFragmentItems) {
554
+ if (item != null) {
555
+ if (isFragmentObject(item)) {
556
+ children.push(
557
+ this.#wrapIndented(
558
+ pluralize.singular(name),
559
+ this.renderEntries(item, { depth: depth + 2, path: [] }),
560
+ depth + 1
561
+ )
562
+ );
563
+ } else {
564
+ children.push(
565
+ this.#leaf(pluralize.singular(name), String(item), depth + 1)
566
+ );
567
+ }
633
568
  }
634
- );
635
- return experimental_output.isTopicChange;
569
+ }
570
+ if (this.options.groupFragments && fragmentItems.length > 0) {
571
+ const groups = this.groupByName(fragmentItems);
572
+ for (const [groupName, groupFragments] of groups) {
573
+ const groupChildren = groupFragments.map(
574
+ (frag) => this.renderFragment(frag, { depth: depth + 2, path: [] })
575
+ );
576
+ const pluralName = pluralize.plural(groupName);
577
+ children.push(this.#wrapIndented(pluralName, groupChildren, depth + 1));
578
+ }
579
+ } else {
580
+ for (const frag of fragmentItems) {
581
+ children.push(
582
+ this.renderFragment(frag, { depth: depth + 1, path: [] })
583
+ );
584
+ }
585
+ }
586
+ return this.#wrap(name, children);
636
587
  }
637
- /**
638
- * Resolve a context-dependent message into a standalone question.
639
- * Called when topic change is detected to preserve the meaning of
640
- * the triggering message before context is reset.
641
- * @param text - The user message to resolve
642
- * @param contextSnapshot - Snapshot of context captured before this async call
643
- */
644
- async resolveToStandalone(text, contextSnapshot) {
645
- const { experimental_output } = await generate3(
646
- contextResolverAgent,
647
- [user3("Generate a standalone question for this message.")],
648
- {
649
- conversation: formatConversation([...contextSnapshot, `User: ${text}`]),
650
- sql: ""
651
- // No SQL yet, just resolving the question
588
+ #leafRoot(tag, value) {
589
+ const safe = this.#escape(value);
590
+ if (safe.includes("\n")) {
591
+ return `<${tag}>
592
+ ${this.#indent(safe, 2)}
593
+ </${tag}>`;
594
+ }
595
+ return `<${tag}>${safe}</${tag}>`;
596
+ }
597
+ renderFragment(fragment2, ctx) {
598
+ const { name, data } = fragment2;
599
+ if (this.isPrimitive(data)) {
600
+ return this.#leaf(name, String(data), ctx.depth);
601
+ }
602
+ if (isFragment(data)) {
603
+ const child = this.renderFragment(data, { ...ctx, depth: ctx.depth + 1 });
604
+ return this.#wrapIndented(name, [child], ctx.depth);
605
+ }
606
+ if (Array.isArray(data)) {
607
+ return this.#renderArrayIndented(name, data, ctx.depth);
608
+ }
609
+ if (isFragmentObject(data)) {
610
+ const children = this.renderEntries(data, {
611
+ ...ctx,
612
+ depth: ctx.depth + 1
613
+ });
614
+ return this.#wrapIndented(name, children, ctx.depth);
615
+ }
616
+ return "";
617
+ }
618
+ #renderArrayIndented(name, items, depth) {
619
+ const fragmentItems = items.filter(isFragment);
620
+ const nonFragmentItems = items.filter((item) => !isFragment(item));
621
+ const children = [];
622
+ for (const item of nonFragmentItems) {
623
+ if (item != null) {
624
+ if (isFragmentObject(item)) {
625
+ children.push(
626
+ this.#wrapIndented(
627
+ pluralize.singular(name),
628
+ this.renderEntries(item, { depth: depth + 2, path: [] }),
629
+ depth + 1
630
+ )
631
+ );
632
+ } else {
633
+ children.push(
634
+ this.#leaf(pluralize.singular(name), String(item), depth + 1)
635
+ );
636
+ }
652
637
  }
653
- );
654
- return experimental_output.question;
638
+ }
639
+ if (this.options.groupFragments && fragmentItems.length > 0) {
640
+ const groups = this.groupByName(fragmentItems);
641
+ for (const [groupName, groupFragments] of groups) {
642
+ const groupChildren = groupFragments.map(
643
+ (frag) => this.renderFragment(frag, { depth: depth + 2, path: [] })
644
+ );
645
+ const pluralName = pluralize.plural(groupName);
646
+ children.push(this.#wrapIndented(pluralName, groupChildren, depth + 1));
647
+ }
648
+ } else {
649
+ for (const frag of fragmentItems) {
650
+ children.push(
651
+ this.renderFragment(frag, { depth: depth + 1, path: [] })
652
+ );
653
+ }
654
+ }
655
+ return this.#wrapIndented(name, children, depth);
656
+ }
657
+ renderPrimitive(key, value, ctx) {
658
+ return this.#leaf(key, value, ctx.depth);
659
+ }
660
+ renderArray(key, items, ctx) {
661
+ if (!items.length) {
662
+ return "";
663
+ }
664
+ const itemTag = pluralize.singular(key);
665
+ const children = items.filter((item) => item != null).map((item) => {
666
+ if (isFragment(item)) {
667
+ return this.renderFragment(item, { ...ctx, depth: ctx.depth + 1 });
668
+ }
669
+ if (isFragmentObject(item)) {
670
+ return this.#wrapIndented(
671
+ itemTag,
672
+ this.renderEntries(item, { ...ctx, depth: ctx.depth + 2 }),
673
+ ctx.depth + 1
674
+ );
675
+ }
676
+ return this.#leaf(itemTag, String(item), ctx.depth + 1);
677
+ });
678
+ return this.#wrapIndented(key, children, ctx.depth);
679
+ }
680
+ renderObject(key, obj, ctx) {
681
+ const children = this.renderEntries(obj, { ...ctx, depth: ctx.depth + 1 });
682
+ return this.#wrapIndented(key, children, ctx.depth);
683
+ }
684
+ #escape(value) {
685
+ if (value == null) {
686
+ return "";
687
+ }
688
+ return value.replaceAll(/&/g, "&amp;").replaceAll(/</g, "&lt;").replaceAll(/>/g, "&gt;").replaceAll(/"/g, "&quot;").replaceAll(/'/g, "&apos;");
689
+ }
690
+ #indent(text, spaces) {
691
+ if (!text.trim()) {
692
+ return "";
693
+ }
694
+ const padding = " ".repeat(spaces);
695
+ return text.split("\n").map((line) => line.length ? padding + line : padding).join("\n");
696
+ }
697
+ #leaf(tag, value, depth) {
698
+ const safe = this.#escape(value);
699
+ const pad = " ".repeat(depth);
700
+ if (safe.includes("\n")) {
701
+ return `${pad}<${tag}>
702
+ ${this.#indent(safe, (depth + 1) * 2)}
703
+ ${pad}</${tag}>`;
704
+ }
705
+ return `${pad}<${tag}>${safe}</${tag}>`;
706
+ }
707
+ #wrap(tag, children) {
708
+ const content = children.filter(Boolean).join("\n");
709
+ if (!content) {
710
+ return "";
711
+ }
712
+ return `<${tag}>
713
+ ${content}
714
+ </${tag}>`;
715
+ }
716
+ #wrapIndented(tag, children, depth) {
717
+ const content = children.filter(Boolean).join("\n");
718
+ if (!content) {
719
+ return "";
720
+ }
721
+ const pad = " ".repeat(depth);
722
+ return `${pad}<${tag}>
723
+ ${content}
724
+ ${pad}</${tag}>`;
655
725
  }
656
726
  };
657
-
658
- // packages/text2sql/src/lib/synthesis/extractors/last-query-extractor.ts
659
- import { generate as generate4, user as user4 } from "@deepagents/agent";
660
- var LastQueryExtractor = class extends BaseContextualExtractor {
661
- constructor(messages, adapter, options = {}) {
662
- super(messages, adapter, options);
727
+ var ContextStore = class {
728
+ };
729
+ var ContextEngine = class {
730
+ /** Non-message fragments (role, hints, etc.) - not persisted in graph */
731
+ #fragments = [];
732
+ /** Pending message fragments to be added to graph */
733
+ #pendingMessages = [];
734
+ #store;
735
+ #chatId;
736
+ #userId;
737
+ #branchName;
738
+ #branch = null;
739
+ #chatData = null;
740
+ #initialized = false;
741
+ constructor(options) {
742
+ if (!options.chatId) {
743
+ throw new Error("chatId is required");
744
+ }
745
+ if (!options.userId) {
746
+ throw new Error("userId is required");
747
+ }
748
+ this.#store = options.store;
749
+ this.#chatId = options.chatId;
750
+ this.#userId = options.userId;
751
+ this.#branchName = "main";
663
752
  }
664
753
  /**
665
- * Add user message to context (keeps all messages).
754
+ * Initialize the chat and branch if they don't exist.
666
755
  */
667
- async onUserMessage(text) {
668
- this.context.push(`User: ${text}`);
756
+ async #ensureInitialized() {
757
+ if (this.#initialized) {
758
+ return;
759
+ }
760
+ this.#chatData = await this.#store.upsertChat({
761
+ id: this.#chatId,
762
+ userId: this.#userId
763
+ });
764
+ this.#branch = await this.#store.getActiveBranch(this.#chatId);
765
+ this.#initialized = true;
669
766
  }
670
767
  /**
671
- * Return all context accumulated so far.
768
+ * Create a new branch from a specific message.
769
+ * Shared logic between rewind() and btw().
672
770
  */
673
- getContextSnapshot() {
674
- return [...this.context];
771
+ async #createBranchFrom(messageId, switchTo) {
772
+ const branches = await this.#store.listBranches(this.#chatId);
773
+ const samePrefix = branches.filter(
774
+ (b) => b.name === this.#branchName || b.name.startsWith(`${this.#branchName}-v`)
775
+ );
776
+ const newBranchName = `${this.#branchName}-v${samePrefix.length + 1}`;
777
+ const newBranch = {
778
+ id: crypto.randomUUID(),
779
+ chatId: this.#chatId,
780
+ name: newBranchName,
781
+ headMessageId: messageId,
782
+ isActive: false,
783
+ createdAt: Date.now()
784
+ };
785
+ await this.#store.createBranch(newBranch);
786
+ if (switchTo) {
787
+ await this.#store.setActiveBranch(this.#chatId, newBranch.id);
788
+ this.#branch = { ...newBranch, isActive: true };
789
+ this.#branchName = newBranchName;
790
+ this.#pendingMessages = [];
791
+ }
792
+ const chain = await this.#store.getMessageChain(messageId);
793
+ return {
794
+ id: newBranch.id,
795
+ name: newBranch.name,
796
+ headMessageId: newBranch.headMessageId,
797
+ isActive: switchTo,
798
+ messageCount: chain.length,
799
+ createdAt: newBranch.createdAt
800
+ };
675
801
  }
676
802
  /**
677
- * Override to only resolve the LAST query instead of all queries.
803
+ * Get the current chat ID.
678
804
  */
679
- async *resolveQuestions(introspection) {
680
- if (this.results.length === 0) {
681
- return;
682
- }
683
- const last = this.results.at(-1);
684
- const { experimental_output } = await generate4(
685
- contextResolverAgent,
686
- [user4("Generate a standalone question for this SQL query.")],
687
- {
688
- conversation: formatConversation(last.conversationContext),
689
- sql: last.sql,
690
- introspection
691
- }
692
- );
693
- yield [
694
- {
695
- question: experimental_output.question,
696
- sql: last.sql,
697
- context: last.conversationContext,
698
- success: last.success
699
- }
700
- ];
701
- }
702
- };
703
-
704
- // packages/text2sql/src/lib/synthesis/synthesizers/schema-synthesizer.ts
705
- import pLimit from "p-limit";
706
-
707
- // packages/text2sql/src/lib/agents/question.agent.ts
708
- import { groq as groq4 } from "@ai-sdk/groq";
709
- import { defaultSettingsMiddleware, wrapLanguageModel } from "ai";
710
- import dedent4 from "dedent";
711
- import z4 from "zod";
712
- import { agent as agent4, generate as generate5, user as user5 } from "@deepagents/agent";
713
- var complexityInstructions = {
714
- simple: dedent4`
715
- Generate simple questions that require:
716
- - Basic SELECT with single table
717
- - Simple WHERE clauses with one condition
718
- - COUNT(*) or basic aggregations
719
- - No joins required
720
- Examples: "How many customers do we have?", "List all products", "What is the total revenue?"
721
- `,
722
- moderate: dedent4`
723
- Generate moderate questions that require:
724
- - JOINs between 2-3 tables
725
- - Multiple WHERE conditions (AND/OR)
726
- - GROUP BY with HAVING clauses
727
- - ORDER BY with LIMIT
728
- - Basic subqueries
729
- Examples: "What are the top 5 customers by total orders?", "Which products have never been ordered?"
730
- `,
731
- complex: dedent4`
732
- Generate complex questions that require:
733
- - Multiple JOINs (3+ tables)
734
- - Nested subqueries or CTEs
735
- - Complex aggregations with multiple GROUP BY columns
736
- - CASE expressions
737
- - Date/time calculations
738
- Examples: "What is the month-over-month growth rate?", "Which customers have increased spending compared to last year?"
739
- `,
740
- "high complex": dedent4`
741
- Generate highly complex questions that require advanced SQL features:
742
- - Window functions (ROW_NUMBER, RANK, DENSE_RANK)
743
- - LAG, LEAD for comparisons
744
- - Running totals (SUM OVER)
745
- - Moving averages
746
- - PARTITION BY clauses
747
- - Complex CTEs with multiple levels
748
- Examples: "What is the running total of sales per month?", "Rank customers by their purchase frequency within each region"
749
- `
750
- };
751
- var questionGeneratorAgent = agent4({
752
- name: "question_generator",
753
- model: wrapLanguageModel({
754
- model: groq4("openai/gpt-oss-20b"),
755
- middleware: defaultSettingsMiddleware({
756
- settings: { temperature: 0.8, topP: 0.95 }
757
- })
758
- }),
759
- handoffDescription: "Generates natural language questions that users might ask about the database schema.",
760
- output: z4.object({
761
- questions: z4.array(z4.string().describe("A natural language question about the data")).min(1).describe("List of natural language questions a user might ask")
762
- }),
763
- prompt: (state) => {
764
- const count = state?.count;
765
- const complexity = state?.complexity ?? "moderate";
766
- return dedent4`
767
- <identity>
768
- You are a synthetic data generator specializing in creating realistic natural language questions
769
- that users might ask about a database. You understand database schemas and can generate diverse,
770
- practical questions that would require SQL queries to answer.
771
- </identity>
772
-
773
- ${state?.introspection || ""}
774
-
775
- <complexity level="${complexity}">
776
- ${complexityInstructions[complexity]}
777
- </complexity>
778
-
779
- <task>
780
- Generate exactly ${count} natural language questions at the "${complexity}" complexity level.
781
- The questions should:
782
- 1. Match the complexity requirements above
783
- 2. Use natural business language, not technical SQL terms
784
- 3. Be realistic questions a non-technical user would actually ask
785
- 4. Cover different tables and relationships when possible
786
- </task>
787
-
788
- <guardrails>
789
- - Questions MUST ONLY reference tables and columns that exist in the schema above
790
- - Before generating each question, verify that ALL entities (tables, columns, relationships) you reference are explicitly listed in the schema
791
- - DO NOT invent or assume tables/columns that aren't explicitly shown in the schema
792
- - Use natural language without SQL keywords like SELECT, WHERE, etc.
793
- - All questions must match the specified complexity level
794
- </guardrails>
795
- `;
796
- }
797
- });
798
- async function generateQuestions(params) {
799
- const { introspection, complexity, count, prompt, model } = params;
800
- const agentInstance = model ? questionGeneratorAgent.clone({ model }) : questionGeneratorAgent;
801
- const userPrompt = prompt ?? `Generate ${count} questions at ${complexity} complexity given db schema.`;
802
- const { experimental_output } = await generate5(
803
- agentInstance,
804
- [user5(userPrompt)],
805
- {
806
- introspection,
807
- complexity,
808
- count
809
- }
810
- );
811
- return { questions: experimental_output.questions };
812
- }
813
-
814
- // packages/text2sql/src/lib/agents/sql.agent.ts
815
- import { groq as groq5 } from "@ai-sdk/groq";
816
- import {
817
- APICallError,
818
- JSONParseError,
819
- NoContentGeneratedError,
820
- NoObjectGeneratedError,
821
- NoOutputGeneratedError,
822
- TypeValidationError,
823
- defaultSettingsMiddleware as defaultSettingsMiddleware2,
824
- wrapLanguageModel as wrapLanguageModel2
825
- } from "ai";
826
- import { Console } from "node:console";
827
- import { createWriteStream } from "node:fs";
828
- import pRetry from "p-retry";
829
- import z5 from "zod";
830
- import "@deepagents/agent";
831
-
832
- // packages/context/dist/index.js
833
- import { encode } from "gpt-tokenizer";
834
- import { generateId } from "ai";
835
- import pluralize from "pluralize";
836
- import { titlecase } from "stringcase";
837
- import { defineCommand } from "just-bash";
838
- import spawn from "nano-spawn";
839
- import "bash-tool";
840
- import spawn2 from "nano-spawn";
841
- import {
842
- createBashTool
843
- } from "bash-tool";
844
- import YAML from "yaml";
845
- import { DatabaseSync } from "node:sqlite";
846
- import {
847
- Output,
848
- convertToModelMessages,
849
- createUIMessageStream,
850
- generateId as generateId2,
851
- generateText,
852
- smoothStream,
853
- stepCountIs,
854
- streamText
855
- } from "ai";
856
- import chalk from "chalk";
857
- import "zod";
858
- import "@deepagents/agent";
859
- var defaultTokenizer = {
860
- encode(text) {
861
- return encode(text);
862
- },
863
- count(text) {
864
- return encode(text).length;
865
- }
866
- };
867
- var ModelsRegistry = class {
868
- #cache = /* @__PURE__ */ new Map();
869
- #loaded = false;
870
- #tokenizers = /* @__PURE__ */ new Map();
871
- #defaultTokenizer = defaultTokenizer;
872
- /**
873
- * Load models data from models.dev API
874
- */
875
- async load() {
876
- if (this.#loaded) return;
877
- const response = await fetch("https://models.dev/api.json");
878
- if (!response.ok) {
879
- throw new Error(`Failed to fetch models: ${response.statusText}`);
880
- }
881
- const data = await response.json();
882
- for (const [providerId, provider] of Object.entries(data)) {
883
- for (const [modelId, model] of Object.entries(provider.models)) {
884
- const info = {
885
- id: model.id,
886
- name: model.name,
887
- family: model.family,
888
- cost: model.cost,
889
- limit: model.limit,
890
- provider: providerId
891
- };
892
- this.#cache.set(`${providerId}:${modelId}`, info);
893
- }
894
- }
895
- this.#loaded = true;
805
+ get chatId() {
806
+ return this.#chatId;
896
807
  }
897
808
  /**
898
- * Get model info by ID
899
- * @param modelId - Model ID (e.g., "openai:gpt-4o")
809
+ * Get the current branch name.
900
810
  */
901
- get(modelId) {
902
- return this.#cache.get(modelId);
811
+ get branch() {
812
+ return this.#branchName;
903
813
  }
904
814
  /**
905
- * Check if a model exists in the registry
815
+ * Get metadata for the current chat.
816
+ * Returns null if the chat hasn't been initialized yet.
906
817
  */
907
- has(modelId) {
908
- return this.#cache.has(modelId);
818
+ get chat() {
819
+ if (!this.#chatData) {
820
+ return null;
821
+ }
822
+ return {
823
+ id: this.#chatData.id,
824
+ userId: this.#chatData.userId,
825
+ createdAt: this.#chatData.createdAt,
826
+ updatedAt: this.#chatData.updatedAt,
827
+ title: this.#chatData.title,
828
+ metadata: this.#chatData.metadata
829
+ };
909
830
  }
910
831
  /**
911
- * List all available model IDs
832
+ * Add fragments to the context.
833
+ *
834
+ * - Message fragments (user/assistant) are queued for persistence
835
+ * - Non-message fragments (role/hint) are kept in memory for system prompt
912
836
  */
913
- list() {
914
- return [...this.#cache.keys()];
837
+ set(...fragments) {
838
+ for (const fragment2 of fragments) {
839
+ if (isMessageFragment(fragment2)) {
840
+ this.#pendingMessages.push(fragment2);
841
+ } else {
842
+ this.#fragments.push(fragment2);
843
+ }
844
+ }
845
+ return this;
915
846
  }
916
- /**
917
- * Register a custom tokenizer for specific model families
918
- * @param family - Model family name (e.g., "llama", "claude")
919
- * @param tokenizer - Tokenizer implementation
920
- */
921
- registerTokenizer(family, tokenizer) {
922
- this.#tokenizers.set(family, tokenizer);
847
+ // Unset a fragment by ID (not implemented yet)
848
+ unset(fragmentId) {
923
849
  }
924
850
  /**
925
- * Set the default tokenizer used when no family-specific tokenizer is registered
851
+ * Render all fragments using the provided renderer.
852
+ * @internal Use resolve() instead for public API.
926
853
  */
927
- setDefaultTokenizer(tokenizer) {
928
- this.#defaultTokenizer = tokenizer;
854
+ render(renderer) {
855
+ return renderer.render(this.#fragments);
929
856
  }
930
857
  /**
931
- * Get the appropriate tokenizer for a model
858
+ * Resolve context into AI SDK-ready format.
859
+ *
860
+ * - Initializes chat and branch if needed
861
+ * - Loads message history from the graph (walking parent chain)
862
+ * - Separates context fragments for system prompt
863
+ * - Combines with pending messages
864
+ *
865
+ * @example
866
+ * ```ts
867
+ * const context = new ContextEngine({ store, chatId: 'chat-1', userId: 'user-1' })
868
+ * .set(role('You are helpful'), user('Hello'));
869
+ *
870
+ * const { systemPrompt, messages } = await context.resolve();
871
+ * await generateText({ system: systemPrompt, messages });
872
+ * ```
932
873
  */
933
- getTokenizer(modelId) {
934
- const model = this.get(modelId);
935
- if (model) {
936
- const familyTokenizer = this.#tokenizers.get(model.family);
937
- if (familyTokenizer) {
938
- return familyTokenizer;
874
+ async resolve(options) {
875
+ await this.#ensureInitialized();
876
+ const systemPrompt = options.renderer.render(this.#fragments);
877
+ const messages = [];
878
+ if (this.#branch?.headMessageId) {
879
+ const chain = await this.#store.getMessageChain(
880
+ this.#branch.headMessageId
881
+ );
882
+ for (const msg of chain) {
883
+ messages.push(message(msg.data).codec?.decode());
939
884
  }
940
885
  }
941
- return this.#defaultTokenizer;
886
+ for (const fragment2 of this.#pendingMessages) {
887
+ const decoded = fragment2.codec.decode();
888
+ messages.push(decoded);
889
+ }
890
+ return { systemPrompt, messages };
942
891
  }
943
892
  /**
944
- * Estimate token count and cost for given text and model
945
- * @param modelId - Model ID to use for pricing (e.g., "openai:gpt-4o")
946
- * @param input - Input text (prompt)
893
+ * Save pending messages to the graph.
894
+ *
895
+ * Each message is added as a node with parentId pointing to the previous message.
896
+ * The branch head is updated to point to the last message.
897
+ *
898
+ * @example
899
+ * ```ts
900
+ * context.set(user('Hello'));
901
+ * // AI responds...
902
+ * context.set(assistant('Hi there!'));
903
+ * await context.save(); // Persist to graph
904
+ * ```
947
905
  */
948
- estimate(modelId, input) {
949
- const model = this.get(modelId);
950
- if (!model) {
951
- throw new Error(
952
- `Model "${modelId}" not found. Call load() first or check model ID.`
953
- );
906
+ async save() {
907
+ await this.#ensureInitialized();
908
+ if (this.#pendingMessages.length === 0) {
909
+ return;
954
910
  }
955
- const tokenizer = this.getTokenizer(modelId);
956
- const tokens = tokenizer.count(input);
957
- const cost = tokens / 1e6 * model.cost.input;
958
- return {
959
- model: model.id,
960
- provider: model.provider,
961
- tokens,
962
- cost,
963
- limits: {
964
- context: model.limit.context,
965
- output: model.limit.output,
966
- exceedsContext: tokens > model.limit.context
967
- },
968
- fragments: []
969
- };
970
- }
971
- };
972
- var _registry = null;
973
- function getModelsRegistry() {
974
- if (!_registry) {
975
- _registry = new ModelsRegistry();
976
- }
977
- return _registry;
978
- }
979
- function isFragment(data) {
980
- return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
981
- }
982
- function isFragmentObject(data) {
983
- return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
984
- }
985
- function isMessageFragment(fragment2) {
986
- return fragment2.type === "message";
987
- }
988
- function role(content) {
989
- return {
990
- name: "role",
991
- data: content
992
- };
993
- }
994
- function user6(content) {
995
- const message2 = typeof content === "string" ? {
996
- id: generateId(),
997
- role: "user",
998
- parts: [{ type: "text", text: content }]
999
- } : content;
1000
- return {
1001
- id: message2.id,
1002
- name: "user",
1003
- data: "content",
1004
- type: "message",
1005
- persist: true,
1006
- codec: {
1007
- decode() {
1008
- return message2;
1009
- },
1010
- encode() {
1011
- return message2;
911
+ for (let i = 0; i < this.#pendingMessages.length; i++) {
912
+ const fragment2 = this.#pendingMessages[i];
913
+ if (isLazyFragment(fragment2)) {
914
+ this.#pendingMessages[i] = await this.#resolveLazyFragment(fragment2);
1012
915
  }
1013
916
  }
1014
- };
1015
- }
1016
- function message(content) {
1017
- const message2 = typeof content === "string" ? {
1018
- id: generateId(),
1019
- role: "user",
1020
- parts: [{ type: "text", text: content }]
1021
- } : content;
1022
- return {
1023
- id: message2.id,
1024
- name: "message",
1025
- data: "content",
1026
- type: "message",
1027
- persist: true,
1028
- codec: {
1029
- decode() {
1030
- return message2;
1031
- },
1032
- encode() {
1033
- return message2;
1034
- }
917
+ let parentId = this.#branch.headMessageId;
918
+ const now = Date.now();
919
+ for (const fragment2 of this.#pendingMessages) {
920
+ const messageData = {
921
+ id: fragment2.id ?? crypto.randomUUID(),
922
+ chatId: this.#chatId,
923
+ parentId,
924
+ name: fragment2.name,
925
+ type: fragment2.type,
926
+ data: fragment2.codec.encode(),
927
+ createdAt: now
928
+ };
929
+ await this.#store.addMessage(messageData);
930
+ parentId = messageData.id;
1035
931
  }
1036
- };
1037
- }
1038
- var ContextRenderer = class {
1039
- options;
1040
- constructor(options = {}) {
1041
- this.options = options;
932
+ await this.#store.updateBranchHead(this.#branch.id, parentId);
933
+ this.#branch.headMessageId = parentId;
934
+ this.#pendingMessages = [];
1042
935
  }
1043
936
  /**
1044
- * Check if data is a primitive (string, number, boolean).
937
+ * Resolve a lazy fragment by finding the appropriate ID.
1045
938
  */
1046
- isPrimitive(data) {
1047
- return typeof data === "string" || typeof data === "number" || typeof data === "boolean";
939
+ async #resolveLazyFragment(fragment2) {
940
+ const lazy = fragment2[LAZY_ID];
941
+ if (lazy.type === "last-assistant") {
942
+ const lastId = await this.#getLastAssistantId();
943
+ return assistantText(lazy.content, { id: lastId ?? crypto.randomUUID() });
944
+ }
945
+ throw new Error(`Unknown lazy fragment type: ${lazy.type}`);
1048
946
  }
1049
947
  /**
1050
- * Group fragments by name for groupFragments option.
948
+ * Find the most recent assistant message ID (pending or persisted).
1051
949
  */
1052
- groupByName(fragments) {
1053
- const groups = /* @__PURE__ */ new Map();
1054
- for (const fragment2 of fragments) {
1055
- const existing = groups.get(fragment2.name) ?? [];
1056
- existing.push(fragment2);
1057
- groups.set(fragment2.name, existing);
950
+ async #getLastAssistantId() {
951
+ for (let i = this.#pendingMessages.length - 1; i >= 0; i--) {
952
+ const msg = this.#pendingMessages[i];
953
+ if (msg.name === "assistant" && !isLazyFragment(msg)) {
954
+ return msg.id;
955
+ }
1058
956
  }
1059
- return groups;
957
+ if (this.#branch?.headMessageId) {
958
+ const chain = await this.#store.getMessageChain(
959
+ this.#branch.headMessageId
960
+ );
961
+ for (let i = chain.length - 1; i >= 0; i--) {
962
+ if (chain[i].name === "assistant") {
963
+ return chain[i].id;
964
+ }
965
+ }
966
+ }
967
+ return void 0;
1060
968
  }
1061
969
  /**
1062
- * Template method - dispatches value to appropriate handler.
970
+ * Estimate token count and cost for the full context.
971
+ *
972
+ * Includes:
973
+ * - System prompt fragments (role, hints, etc.)
974
+ * - Persisted chat messages (from store)
975
+ * - Pending messages (not yet saved)
976
+ *
977
+ * @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
978
+ * @param options - Optional settings
979
+ * @returns Estimate result with token counts, costs, and per-fragment breakdown
1063
980
  */
1064
- renderValue(key, value, ctx) {
1065
- if (value == null) {
1066
- return "";
981
+ async estimate(modelId, options = {}) {
982
+ await this.#ensureInitialized();
983
+ const renderer = options.renderer ?? new XmlRenderer();
984
+ const registry = getModelsRegistry();
985
+ await registry.load();
986
+ const model = registry.get(modelId);
987
+ if (!model) {
988
+ throw new Error(
989
+ `Model "${modelId}" not found. Call load() first or check model ID.`
990
+ );
1067
991
  }
1068
- if (isFragment(value)) {
1069
- return this.renderFragment(value, ctx);
992
+ const tokenizer = registry.getTokenizer(modelId);
993
+ const fragmentEstimates = [];
994
+ for (const fragment2 of this.#fragments) {
995
+ const rendered = renderer.render([fragment2]);
996
+ const tokens = tokenizer.count(rendered);
997
+ const cost = tokens / 1e6 * model.cost.input;
998
+ fragmentEstimates.push({
999
+ id: fragment2.id,
1000
+ name: fragment2.name,
1001
+ tokens,
1002
+ cost
1003
+ });
1070
1004
  }
1071
- if (Array.isArray(value)) {
1072
- return this.renderArray(key, value, ctx);
1005
+ if (this.#branch?.headMessageId) {
1006
+ const chain = await this.#store.getMessageChain(
1007
+ this.#branch.headMessageId
1008
+ );
1009
+ for (const msg of chain) {
1010
+ const content = String(msg.data);
1011
+ const tokens = tokenizer.count(content);
1012
+ const cost = tokens / 1e6 * model.cost.input;
1013
+ fragmentEstimates.push({
1014
+ name: msg.name,
1015
+ id: msg.id,
1016
+ tokens,
1017
+ cost
1018
+ });
1019
+ }
1073
1020
  }
1074
- if (isFragmentObject(value)) {
1075
- return this.renderObject(key, value, ctx);
1021
+ for (const fragment2 of this.#pendingMessages) {
1022
+ const content = String(fragment2.data);
1023
+ const tokens = tokenizer.count(content);
1024
+ const cost = tokens / 1e6 * model.cost.input;
1025
+ fragmentEstimates.push({
1026
+ name: fragment2.name,
1027
+ id: fragment2.id,
1028
+ tokens,
1029
+ cost
1030
+ });
1076
1031
  }
1077
- return this.renderPrimitive(key, String(value), ctx);
1032
+ const totalTokens = fragmentEstimates.reduce((sum, f) => sum + f.tokens, 0);
1033
+ const totalCost = fragmentEstimates.reduce((sum, f) => sum + f.cost, 0);
1034
+ return {
1035
+ model: model.id,
1036
+ provider: model.provider,
1037
+ tokens: totalTokens,
1038
+ cost: totalCost,
1039
+ limits: {
1040
+ context: model.limit.context,
1041
+ output: model.limit.output,
1042
+ exceedsContext: totalTokens > model.limit.context
1043
+ },
1044
+ fragments: fragmentEstimates
1045
+ };
1078
1046
  }
1079
1047
  /**
1080
- * Render all entries of an object.
1048
+ * Rewind to a specific message by ID.
1049
+ *
1050
+ * Creates a new branch from that message, preserving the original branch.
1051
+ * The new branch becomes active.
1052
+ *
1053
+ * @param messageId - The message ID to rewind to
1054
+ * @returns The new branch info
1055
+ *
1056
+ * @example
1057
+ * ```ts
1058
+ * context.set(user('What is 2 + 2?', { id: 'q1' }));
1059
+ * context.set(assistant('The answer is 5.', { id: 'wrong' })); // Oops!
1060
+ * await context.save();
1061
+ *
1062
+ * // Rewind to the question, creates new branch
1063
+ * const newBranch = await context.rewind('q1');
1064
+ *
1065
+ * // Now add correct answer on new branch
1066
+ * context.set(assistant('The answer is 4.'));
1067
+ * await context.save();
1068
+ * ```
1081
1069
  */
1082
- renderEntries(data, ctx) {
1083
- return Object.entries(data).map(([key, value]) => this.renderValue(key, value, ctx)).filter(Boolean);
1070
+ async rewind(messageId) {
1071
+ await this.#ensureInitialized();
1072
+ const message2 = await this.#store.getMessage(messageId);
1073
+ if (!message2) {
1074
+ throw new Error(`Message "${messageId}" not found`);
1075
+ }
1076
+ if (message2.chatId !== this.#chatId) {
1077
+ throw new Error(`Message "${messageId}" belongs to a different chat`);
1078
+ }
1079
+ return this.#createBranchFrom(messageId, true);
1084
1080
  }
1085
- };
1086
- var XmlRenderer = class extends ContextRenderer {
1087
- render(fragments) {
1088
- return fragments.map((f) => this.#renderTopLevel(f)).filter(Boolean).join("\n");
1089
- }
1090
- #renderTopLevel(fragment2) {
1091
- if (this.isPrimitive(fragment2.data)) {
1092
- return this.#leafRoot(fragment2.name, String(fragment2.data));
1093
- }
1094
- if (Array.isArray(fragment2.data)) {
1095
- return this.#renderArray(fragment2.name, fragment2.data, 0);
1096
- }
1097
- if (isFragment(fragment2.data)) {
1098
- const child = this.renderFragment(fragment2.data, { depth: 1, path: [] });
1099
- return this.#wrap(fragment2.name, [child]);
1081
+ /**
1082
+ * Create a checkpoint at the current position.
1083
+ *
1084
+ * A checkpoint is a named pointer to the current branch head.
1085
+ * Use restore() to return to this point later.
1086
+ *
1087
+ * @param name - Name for the checkpoint
1088
+ * @returns The checkpoint info
1089
+ *
1090
+ * @example
1091
+ * ```ts
1092
+ * context.set(user('I want to learn a new skill.'));
1093
+ * context.set(assistant('Would you like coding or cooking?'));
1094
+ * await context.save();
1095
+ *
1096
+ * // Save checkpoint before user's choice
1097
+ * const cp = await context.checkpoint('before-choice');
1098
+ * ```
1099
+ */
1100
+ async checkpoint(name) {
1101
+ await this.#ensureInitialized();
1102
+ if (!this.#branch?.headMessageId) {
1103
+ throw new Error("Cannot create checkpoint: no messages in conversation");
1100
1104
  }
1101
- return this.#wrap(
1102
- fragment2.name,
1103
- this.renderEntries(fragment2.data, { depth: 1, path: [] })
1104
- );
1105
+ const checkpoint = {
1106
+ id: crypto.randomUUID(),
1107
+ chatId: this.#chatId,
1108
+ name,
1109
+ messageId: this.#branch.headMessageId,
1110
+ createdAt: Date.now()
1111
+ };
1112
+ await this.#store.createCheckpoint(checkpoint);
1113
+ return {
1114
+ id: checkpoint.id,
1115
+ name: checkpoint.name,
1116
+ messageId: checkpoint.messageId,
1117
+ createdAt: checkpoint.createdAt
1118
+ };
1105
1119
  }
1106
- #renderArray(name, items, depth) {
1107
- const fragmentItems = items.filter(isFragment);
1108
- const nonFragmentItems = items.filter((item) => !isFragment(item));
1109
- const children = [];
1110
- for (const item of nonFragmentItems) {
1111
- if (item != null) {
1112
- children.push(
1113
- this.#leaf(pluralize.singular(name), String(item), depth + 1)
1114
- );
1115
- }
1116
- }
1117
- if (this.options.groupFragments && fragmentItems.length > 0) {
1118
- const groups = this.groupByName(fragmentItems);
1119
- for (const [groupName, groupFragments] of groups) {
1120
- const groupChildren = groupFragments.map(
1121
- (frag) => this.renderFragment(frag, { depth: depth + 2, path: [] })
1122
- );
1123
- const pluralName = pluralize.plural(groupName);
1124
- children.push(this.#wrapIndented(pluralName, groupChildren, depth + 1));
1125
- }
1126
- } else {
1127
- for (const frag of fragmentItems) {
1128
- children.push(
1129
- this.renderFragment(frag, { depth: depth + 1, path: [] })
1130
- );
1131
- }
1120
+ /**
1121
+ * Restore to a checkpoint by creating a new branch from that point.
1122
+ *
1123
+ * @param name - Name of the checkpoint to restore
1124
+ * @returns The new branch info
1125
+ *
1126
+ * @example
1127
+ * ```ts
1128
+ * // User chose cooking, but wants to try coding path
1129
+ * await context.restore('before-choice');
1130
+ *
1131
+ * context.set(user('I want to learn coding.'));
1132
+ * context.set(assistant('Python is a great starting language!'));
1133
+ * await context.save();
1134
+ * ```
1135
+ */
1136
+ async restore(name) {
1137
+ await this.#ensureInitialized();
1138
+ const checkpoint = await this.#store.getCheckpoint(this.#chatId, name);
1139
+ if (!checkpoint) {
1140
+ throw new Error(
1141
+ `Checkpoint "${name}" not found in chat "${this.#chatId}"`
1142
+ );
1132
1143
  }
1133
- return this.#wrap(name, children);
1144
+ return this.rewind(checkpoint.messageId);
1134
1145
  }
1135
- #leafRoot(tag, value) {
1136
- const safe = this.#escape(value);
1137
- if (safe.includes("\n")) {
1138
- return `<${tag}>
1139
- ${this.#indent(safe, 2)}
1140
- </${tag}>`;
1146
+ /**
1147
+ * Switch to a different branch by name.
1148
+ *
1149
+ * @param name - Branch name to switch to
1150
+ *
1151
+ * @example
1152
+ * ```ts
1153
+ * // List branches (via store)
1154
+ * const branches = await store.listBranches(context.chatId);
1155
+ * console.log(branches); // [{name: 'main', ...}, {name: 'main-v2', ...}]
1156
+ *
1157
+ * // Switch to original branch
1158
+ * await context.switchBranch('main');
1159
+ * ```
1160
+ */
1161
+ async switchBranch(name) {
1162
+ await this.#ensureInitialized();
1163
+ const branch = await this.#store.getBranch(this.#chatId, name);
1164
+ if (!branch) {
1165
+ throw new Error(`Branch "${name}" not found in chat "${this.#chatId}"`);
1141
1166
  }
1142
- return `<${tag}>${safe}</${tag}>`;
1167
+ await this.#store.setActiveBranch(this.#chatId, branch.id);
1168
+ this.#branch = { ...branch, isActive: true };
1169
+ this.#branchName = name;
1170
+ this.#pendingMessages = [];
1143
1171
  }
1144
- renderFragment(fragment2, ctx) {
1145
- const { name, data } = fragment2;
1146
- if (this.isPrimitive(data)) {
1147
- return this.#leaf(name, String(data), ctx.depth);
1148
- }
1149
- if (isFragment(data)) {
1150
- const child = this.renderFragment(data, { ...ctx, depth: ctx.depth + 1 });
1151
- return this.#wrapIndented(name, [child], ctx.depth);
1152
- }
1153
- if (Array.isArray(data)) {
1154
- return this.#renderArrayIndented(name, data, ctx.depth);
1172
+ /**
1173
+ * Create a parallel branch from the current position ("by the way").
1174
+ *
1175
+ * Use this when you want to fork the conversation without leaving
1176
+ * the current branch. Common use case: user wants to ask another
1177
+ * question while waiting for the model to respond.
1178
+ *
1179
+ * Unlike rewind(), this method:
1180
+ * - Uses the current HEAD (no messageId needed)
1181
+ * - Does NOT switch to the new branch
1182
+ * - Keeps pending messages intact
1183
+ *
1184
+ * @returns The new branch info (does not switch to it)
1185
+ * @throws Error if no messages exist in the conversation
1186
+ *
1187
+ * @example
1188
+ * ```ts
1189
+ * // User asked a question, model is generating...
1190
+ * context.set(user('What is the weather?'));
1191
+ * await context.save();
1192
+ *
1193
+ * // User wants to ask something else without waiting
1194
+ * const newBranch = await context.btw();
1195
+ * // newBranch = { name: 'main-v2', ... }
1196
+ *
1197
+ * // Later, switch to the new branch and add the question
1198
+ * await context.switchBranch(newBranch.name);
1199
+ * context.set(user('Also, what time is it?'));
1200
+ * await context.save();
1201
+ * ```
1202
+ */
1203
+ async btw() {
1204
+ await this.#ensureInitialized();
1205
+ if (!this.#branch?.headMessageId) {
1206
+ throw new Error("Cannot create btw branch: no messages in conversation");
1155
1207
  }
1156
- const children = this.renderEntries(data, { ...ctx, depth: ctx.depth + 1 });
1157
- return this.#wrapIndented(name, children, ctx.depth);
1208
+ return this.#createBranchFrom(this.#branch.headMessageId, false);
1158
1209
  }
1159
- #renderArrayIndented(name, items, depth) {
1160
- const fragmentItems = items.filter(isFragment);
1161
- const nonFragmentItems = items.filter((item) => !isFragment(item));
1162
- const children = [];
1163
- for (const item of nonFragmentItems) {
1164
- if (item != null) {
1165
- children.push(
1166
- this.#leaf(pluralize.singular(name), String(item), depth + 1)
1167
- );
1168
- }
1210
+ /**
1211
+ * Update metadata for the current chat.
1212
+ *
1213
+ * @param updates - Partial metadata to merge (title, metadata)
1214
+ *
1215
+ * @example
1216
+ * ```ts
1217
+ * await context.updateChat({
1218
+ * title: 'Coding Help Session',
1219
+ * metadata: { tags: ['python', 'debugging'] }
1220
+ * });
1221
+ * ```
1222
+ */
1223
+ async updateChat(updates) {
1224
+ await this.#ensureInitialized();
1225
+ const storeUpdates = {};
1226
+ if (updates.title !== void 0) {
1227
+ storeUpdates.title = updates.title;
1169
1228
  }
1170
- if (this.options.groupFragments && fragmentItems.length > 0) {
1171
- const groups = this.groupByName(fragmentItems);
1172
- for (const [groupName, groupFragments] of groups) {
1173
- const groupChildren = groupFragments.map(
1174
- (frag) => this.renderFragment(frag, { depth: depth + 2, path: [] })
1175
- );
1176
- const pluralName = pluralize.plural(groupName);
1177
- children.push(this.#wrapIndented(pluralName, groupChildren, depth + 1));
1178
- }
1179
- } else {
1180
- for (const frag of fragmentItems) {
1181
- children.push(
1182
- this.renderFragment(frag, { depth: depth + 1, path: [] })
1183
- );
1184
- }
1229
+ if (updates.metadata !== void 0) {
1230
+ storeUpdates.metadata = {
1231
+ ...this.#chatData?.metadata,
1232
+ ...updates.metadata
1233
+ };
1185
1234
  }
1186
- return this.#wrapIndented(name, children, depth);
1235
+ this.#chatData = await this.#store.updateChat(this.#chatId, storeUpdates);
1187
1236
  }
1188
- renderPrimitive(key, value, ctx) {
1189
- return this.#leaf(key, value, ctx.depth);
1237
+ /**
1238
+ * Consolidate context fragments (no-op for now).
1239
+ *
1240
+ * This is a placeholder for future functionality that merges context fragments
1241
+ * using specific rules. Currently, it does nothing.
1242
+ *
1243
+ * @experimental
1244
+ */
1245
+ consolidate() {
1246
+ return void 0;
1190
1247
  }
1191
- renderArray(key, items, ctx) {
1192
- if (!items.length) {
1193
- return "";
1248
+ /**
1249
+ * Extract skill path mappings from available_skills fragments.
1250
+ * Returns array of { host, sandbox } for mounting in sandbox filesystem.
1251
+ *
1252
+ * Reads the original `paths` configuration stored in fragment metadata
1253
+ * by the skills() fragment helper.
1254
+ *
1255
+ * @example
1256
+ * ```ts
1257
+ * const context = new ContextEngine({ store, chatId, userId })
1258
+ * .set(skills({ paths: [{ host: './skills', sandbox: '/skills' }] }));
1259
+ *
1260
+ * const mounts = context.getSkillMounts();
1261
+ * // [{ host: './skills', sandbox: '/skills' }]
1262
+ * ```
1263
+ */
1264
+ getSkillMounts() {
1265
+ const mounts = [];
1266
+ for (const fragment2 of this.#fragments) {
1267
+ if (fragment2.name === "available_skills" && fragment2.metadata && Array.isArray(fragment2.metadata.paths)) {
1268
+ for (const mapping of fragment2.metadata.paths) {
1269
+ if (typeof mapping === "object" && mapping !== null && typeof mapping.host === "string" && typeof mapping.sandbox === "string") {
1270
+ mounts.push({ host: mapping.host, sandbox: mapping.sandbox });
1271
+ }
1272
+ }
1273
+ }
1194
1274
  }
1195
- const itemTag = pluralize.singular(key);
1196
- const children = items.filter((item) => item != null).map((item) => this.#leaf(itemTag, String(item), ctx.depth + 1));
1197
- return this.#wrapIndented(key, children, ctx.depth);
1198
- }
1199
- renderObject(key, obj, ctx) {
1200
- const children = this.renderEntries(obj, { ...ctx, depth: ctx.depth + 1 });
1201
- return this.#wrapIndented(key, children, ctx.depth);
1202
- }
1203
- #escape(value) {
1204
- if (value == null) {
1205
- return "";
1206
- }
1207
- return value.replaceAll(/&/g, "&amp;").replaceAll(/</g, "&lt;").replaceAll(/>/g, "&gt;").replaceAll(/"/g, "&quot;").replaceAll(/'/g, "&apos;");
1208
- }
1209
- #indent(text, spaces) {
1210
- if (!text.trim()) {
1211
- return "";
1212
- }
1213
- const padding = " ".repeat(spaces);
1214
- return text.split("\n").map((line) => line.length ? padding + line : padding).join("\n");
1215
- }
1216
- #leaf(tag, value, depth) {
1217
- const safe = this.#escape(value);
1218
- const pad = " ".repeat(depth);
1219
- if (safe.includes("\n")) {
1220
- return `${pad}<${tag}>
1221
- ${this.#indent(safe, (depth + 1) * 2)}
1222
- ${pad}</${tag}>`;
1223
- }
1224
- return `${pad}<${tag}>${safe}</${tag}>`;
1225
- }
1226
- #wrap(tag, children) {
1227
- const content = children.filter(Boolean).join("\n");
1228
- if (!content) {
1229
- return "";
1230
- }
1231
- return `<${tag}>
1232
- ${content}
1233
- </${tag}>`;
1234
- }
1235
- #wrapIndented(tag, children, depth) {
1236
- const content = children.filter(Boolean).join("\n");
1237
- if (!content) {
1238
- return "";
1239
- }
1240
- const pad = " ".repeat(depth);
1241
- return `${pad}<${tag}>
1242
- ${content}
1243
- ${pad}</${tag}>`;
1244
- }
1245
- };
1246
- var ContextStore = class {
1247
- };
1248
- var ContextEngine = class {
1249
- /** Non-message fragments (role, hints, etc.) - not persisted in graph */
1250
- #fragments = [];
1251
- /** Pending message fragments to be added to graph */
1252
- #pendingMessages = [];
1253
- #store;
1254
- #chatId;
1255
- #branchName;
1256
- #branch = null;
1257
- #chatData = null;
1258
- #initialized = false;
1259
- constructor(options) {
1260
- if (!options.chatId) {
1261
- throw new Error("chatId is required");
1262
- }
1263
- this.#store = options.store;
1264
- this.#chatId = options.chatId;
1265
- this.#branchName = options.branch ?? "main";
1266
- }
1267
- /**
1268
- * Initialize the chat and branch if they don't exist.
1269
- */
1270
- async #ensureInitialized() {
1271
- if (this.#initialized) {
1272
- return;
1273
- }
1274
- this.#chatData = await this.#store.upsertChat({ id: this.#chatId });
1275
- const existingBranch = await this.#store.getBranch(
1276
- this.#chatId,
1277
- this.#branchName
1278
- );
1279
- if (existingBranch) {
1280
- this.#branch = existingBranch;
1281
- } else {
1282
- this.#branch = {
1283
- id: crypto.randomUUID(),
1284
- chatId: this.#chatId,
1285
- name: this.#branchName,
1286
- headMessageId: null,
1287
- isActive: true,
1288
- createdAt: Date.now()
1289
- };
1290
- await this.#store.createBranch(this.#branch);
1291
- }
1292
- this.#initialized = true;
1293
- }
1294
- /**
1295
- * Create a new branch from a specific message.
1296
- * Shared logic between rewind() and btw().
1297
- */
1298
- async #createBranchFrom(messageId, switchTo) {
1299
- const branches = await this.#store.listBranches(this.#chatId);
1300
- const samePrefix = branches.filter(
1301
- (b) => b.name === this.#branchName || b.name.startsWith(`${this.#branchName}-v`)
1302
- );
1303
- const newBranchName = `${this.#branchName}-v${samePrefix.length + 1}`;
1304
- const newBranch = {
1305
- id: crypto.randomUUID(),
1306
- chatId: this.#chatId,
1307
- name: newBranchName,
1308
- headMessageId: messageId,
1309
- isActive: false,
1310
- createdAt: Date.now()
1311
- };
1312
- await this.#store.createBranch(newBranch);
1313
- if (switchTo) {
1314
- await this.#store.setActiveBranch(this.#chatId, newBranch.id);
1315
- this.#branch = { ...newBranch, isActive: true };
1316
- this.#branchName = newBranchName;
1317
- this.#pendingMessages = [];
1318
- }
1319
- const chain = await this.#store.getMessageChain(messageId);
1320
- return {
1321
- id: newBranch.id,
1322
- name: newBranch.name,
1323
- headMessageId: newBranch.headMessageId,
1324
- isActive: switchTo,
1325
- messageCount: chain.length,
1326
- createdAt: newBranch.createdAt
1327
- };
1328
- }
1329
- /**
1330
- * Get the current chat ID.
1331
- */
1332
- get chatId() {
1333
- return this.#chatId;
1334
- }
1335
- /**
1336
- * Get the current branch name.
1337
- */
1338
- get branch() {
1339
- return this.#branchName;
1340
- }
1341
- /**
1342
- * Get metadata for the current chat.
1343
- * Returns null if the chat hasn't been initialized yet.
1344
- */
1345
- get chat() {
1346
- if (!this.#chatData) {
1347
- return null;
1348
- }
1349
- return {
1350
- id: this.#chatData.id,
1351
- createdAt: this.#chatData.createdAt,
1352
- updatedAt: this.#chatData.updatedAt,
1353
- title: this.#chatData.title,
1354
- metadata: this.#chatData.metadata
1355
- };
1356
- }
1357
- /**
1358
- * Add fragments to the context.
1359
- *
1360
- * - Message fragments (user/assistant) are queued for persistence
1361
- * - Non-message fragments (role/hint) are kept in memory for system prompt
1362
- */
1363
- set(...fragments) {
1364
- for (const fragment2 of fragments) {
1365
- if (isMessageFragment(fragment2)) {
1366
- this.#pendingMessages.push(fragment2);
1367
- } else {
1368
- this.#fragments.push(fragment2);
1369
- }
1370
- }
1371
- return this;
1372
- }
1373
- // Unset a fragment by ID (not implemented yet)
1374
- unset(fragmentId) {
1375
- }
1376
- /**
1377
- * Render all fragments using the provided renderer.
1378
- * @internal Use resolve() instead for public API.
1379
- */
1380
- render(renderer) {
1381
- return renderer.render(this.#fragments);
1275
+ return mounts;
1382
1276
  }
1383
1277
  /**
1384
- * Resolve context into AI SDK-ready format.
1278
+ * Inspect the full context state for debugging.
1279
+ * Returns a JSON-serializable object with context information.
1385
1280
  *
1386
- * - Initializes chat and branch if needed
1387
- * - Loads message history from the graph (walking parent chain)
1388
- * - Separates context fragments for system prompt
1389
- * - Combines with pending messages
1281
+ * @param options - Inspection options (modelId and renderer required)
1282
+ * @returns Complete inspection data including estimates, rendered output, fragments, and graph
1390
1283
  *
1391
1284
  * @example
1392
1285
  * ```ts
1393
- * const context = new ContextEngine({ store, chatId: 'chat-1' })
1394
- * .set(role('You are helpful'), user('Hello'));
1286
+ * const inspection = await context.inspect({
1287
+ * modelId: 'openai:gpt-4o',
1288
+ * renderer: new XmlRenderer(),
1289
+ * });
1290
+ * console.log(JSON.stringify(inspection, null, 2));
1395
1291
  *
1396
- * const { systemPrompt, messages } = await context.resolve();
1397
- * await generateText({ system: systemPrompt, messages });
1292
+ * // Or write to file for analysis
1293
+ * await fs.writeFile('context-debug.json', JSON.stringify(inspection, null, 2));
1398
1294
  * ```
1399
1295
  */
1400
- async resolve(options) {
1296
+ async inspect(options) {
1401
1297
  await this.#ensureInitialized();
1402
- const systemPrompt = options.renderer.render(this.#fragments);
1403
- const messages = [];
1298
+ const { renderer } = options;
1299
+ const estimateResult = await this.estimate(options.modelId, { renderer });
1300
+ const rendered = renderer.render(this.#fragments);
1301
+ const persistedMessages = [];
1404
1302
  if (this.#branch?.headMessageId) {
1405
1303
  const chain = await this.#store.getMessageChain(
1406
1304
  this.#branch.headMessageId
1407
1305
  );
1408
- for (const msg of chain) {
1409
- messages.push(message(msg.data).codec?.decode());
1306
+ persistedMessages.push(...chain);
1307
+ }
1308
+ const graph = await this.#store.getGraph(this.#chatId);
1309
+ return {
1310
+ estimate: estimateResult,
1311
+ rendered,
1312
+ fragments: {
1313
+ context: [...this.#fragments],
1314
+ pending: [...this.#pendingMessages],
1315
+ persisted: persistedMessages
1316
+ },
1317
+ graph,
1318
+ meta: {
1319
+ chatId: this.#chatId,
1320
+ branch: this.#branchName,
1321
+ timestamp: Date.now()
1410
1322
  }
1323
+ };
1324
+ }
1325
+ };
1326
+ function term(name, definition) {
1327
+ return {
1328
+ name: "term",
1329
+ data: { name, definition }
1330
+ };
1331
+ }
1332
+ function hint(text) {
1333
+ return {
1334
+ name: "hint",
1335
+ data: text
1336
+ };
1337
+ }
1338
+ function guardrail(input) {
1339
+ return {
1340
+ name: "guardrail",
1341
+ data: {
1342
+ rule: input.rule,
1343
+ ...input.reason && { reason: input.reason },
1344
+ ...input.action && { action: input.action }
1411
1345
  }
1412
- for (const fragment2 of this.#pendingMessages) {
1413
- const decoded = fragment2.codec.decode();
1414
- messages.push(decoded);
1415
- }
1416
- return { systemPrompt, messages };
1417
- }
1418
- /**
1419
- * Save pending messages to the graph.
1420
- *
1421
- * Each message is added as a node with parentId pointing to the previous message.
1422
- * The branch head is updated to point to the last message.
1423
- *
1424
- * @example
1425
- * ```ts
1426
- * context.set(user('Hello'));
1427
- * // AI responds...
1428
- * context.set(assistant('Hi there!'));
1429
- * await context.save(); // Persist to graph
1430
- * ```
1431
- */
1432
- async save() {
1433
- await this.#ensureInitialized();
1434
- if (this.#pendingMessages.length === 0) {
1435
- return;
1436
- }
1437
- let parentId = this.#branch.headMessageId;
1438
- const now = Date.now();
1439
- for (const fragment2 of this.#pendingMessages) {
1440
- const messageData = {
1441
- id: fragment2.id ?? crypto.randomUUID(),
1442
- chatId: this.#chatId,
1443
- parentId,
1444
- name: fragment2.name,
1445
- type: fragment2.type,
1446
- data: fragment2.codec.encode(),
1447
- createdAt: now
1448
- };
1449
- await this.#store.addMessage(messageData);
1450
- parentId = messageData.id;
1451
- }
1452
- await this.#store.updateBranchHead(this.#branch.id, parentId);
1453
- this.#branch.headMessageId = parentId;
1454
- this.#pendingMessages = [];
1455
- }
1456
- /**
1457
- * Estimate token count and cost for the full context.
1458
- *
1459
- * Includes:
1460
- * - System prompt fragments (role, hints, etc.)
1461
- * - Persisted chat messages (from store)
1462
- * - Pending messages (not yet saved)
1463
- *
1464
- * @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
1465
- * @param options - Optional settings
1466
- * @returns Estimate result with token counts, costs, and per-fragment breakdown
1467
- */
1468
- async estimate(modelId, options = {}) {
1469
- await this.#ensureInitialized();
1470
- const renderer = options.renderer ?? new XmlRenderer();
1471
- const registry = getModelsRegistry();
1472
- await registry.load();
1473
- const model = registry.get(modelId);
1474
- if (!model) {
1475
- throw new Error(
1476
- `Model "${modelId}" not found. Call load() first or check model ID.`
1477
- );
1478
- }
1479
- const tokenizer = registry.getTokenizer(modelId);
1480
- const fragmentEstimates = [];
1481
- for (const fragment2 of this.#fragments) {
1482
- const rendered = renderer.render([fragment2]);
1483
- const tokens = tokenizer.count(rendered);
1484
- const cost = tokens / 1e6 * model.cost.input;
1485
- fragmentEstimates.push({
1486
- id: fragment2.id,
1487
- name: fragment2.name,
1488
- tokens,
1489
- cost
1490
- });
1491
- }
1492
- if (this.#branch?.headMessageId) {
1493
- const chain = await this.#store.getMessageChain(
1494
- this.#branch.headMessageId
1495
- );
1496
- for (const msg of chain) {
1497
- const content = String(msg.data);
1498
- const tokens = tokenizer.count(content);
1499
- const cost = tokens / 1e6 * model.cost.input;
1500
- fragmentEstimates.push({
1501
- name: msg.name,
1502
- id: msg.id,
1503
- tokens,
1504
- cost
1505
- });
1506
- }
1507
- }
1508
- for (const fragment2 of this.#pendingMessages) {
1509
- const content = String(fragment2.data);
1510
- const tokens = tokenizer.count(content);
1511
- const cost = tokens / 1e6 * model.cost.input;
1512
- fragmentEstimates.push({
1513
- name: fragment2.name,
1514
- id: fragment2.id,
1515
- tokens,
1516
- cost
1517
- });
1518
- }
1519
- const totalTokens = fragmentEstimates.reduce((sum, f) => sum + f.tokens, 0);
1520
- const totalCost = fragmentEstimates.reduce((sum, f) => sum + f.cost, 0);
1521
- return {
1522
- model: model.id,
1523
- provider: model.provider,
1524
- tokens: totalTokens,
1525
- cost: totalCost,
1526
- limits: {
1527
- context: model.limit.context,
1528
- output: model.limit.output,
1529
- exceedsContext: totalTokens > model.limit.context
1530
- },
1531
- fragments: fragmentEstimates
1532
- };
1533
- }
1534
- /**
1535
- * Rewind to a specific message by ID.
1536
- *
1537
- * Creates a new branch from that message, preserving the original branch.
1538
- * The new branch becomes active.
1539
- *
1540
- * @param messageId - The message ID to rewind to
1541
- * @returns The new branch info
1542
- *
1543
- * @example
1544
- * ```ts
1545
- * context.set(user('What is 2 + 2?', { id: 'q1' }));
1546
- * context.set(assistant('The answer is 5.', { id: 'wrong' })); // Oops!
1547
- * await context.save();
1548
- *
1549
- * // Rewind to the question, creates new branch
1550
- * const newBranch = await context.rewind('q1');
1551
- *
1552
- * // Now add correct answer on new branch
1553
- * context.set(assistant('The answer is 4.'));
1554
- * await context.save();
1555
- * ```
1556
- */
1557
- async rewind(messageId) {
1558
- await this.#ensureInitialized();
1559
- const message2 = await this.#store.getMessage(messageId);
1560
- if (!message2) {
1561
- throw new Error(`Message "${messageId}" not found`);
1562
- }
1563
- if (message2.chatId !== this.#chatId) {
1564
- throw new Error(`Message "${messageId}" belongs to a different chat`);
1565
- }
1566
- return this.#createBranchFrom(messageId, true);
1567
- }
1568
- /**
1569
- * Create a checkpoint at the current position.
1570
- *
1571
- * A checkpoint is a named pointer to the current branch head.
1572
- * Use restore() to return to this point later.
1573
- *
1574
- * @param name - Name for the checkpoint
1575
- * @returns The checkpoint info
1576
- *
1577
- * @example
1578
- * ```ts
1579
- * context.set(user('I want to learn a new skill.'));
1580
- * context.set(assistant('Would you like coding or cooking?'));
1581
- * await context.save();
1582
- *
1583
- * // Save checkpoint before user's choice
1584
- * const cp = await context.checkpoint('before-choice');
1585
- * ```
1586
- */
1587
- async checkpoint(name) {
1588
- await this.#ensureInitialized();
1589
- if (!this.#branch?.headMessageId) {
1590
- throw new Error("Cannot create checkpoint: no messages in conversation");
1591
- }
1592
- const checkpoint = {
1593
- id: crypto.randomUUID(),
1594
- chatId: this.#chatId,
1595
- name,
1596
- messageId: this.#branch.headMessageId,
1597
- createdAt: Date.now()
1598
- };
1599
- await this.#store.createCheckpoint(checkpoint);
1600
- return {
1601
- id: checkpoint.id,
1602
- name: checkpoint.name,
1603
- messageId: checkpoint.messageId,
1604
- createdAt: checkpoint.createdAt
1605
- };
1606
- }
1607
- /**
1608
- * Restore to a checkpoint by creating a new branch from that point.
1609
- *
1610
- * @param name - Name of the checkpoint to restore
1611
- * @returns The new branch info
1612
- *
1613
- * @example
1614
- * ```ts
1615
- * // User chose cooking, but wants to try coding path
1616
- * await context.restore('before-choice');
1617
- *
1618
- * context.set(user('I want to learn coding.'));
1619
- * context.set(assistant('Python is a great starting language!'));
1620
- * await context.save();
1621
- * ```
1622
- */
1623
- async restore(name) {
1624
- await this.#ensureInitialized();
1625
- const checkpoint = await this.#store.getCheckpoint(this.#chatId, name);
1626
- if (!checkpoint) {
1627
- throw new Error(
1628
- `Checkpoint "${name}" not found in chat "${this.#chatId}"`
1629
- );
1630
- }
1631
- return this.rewind(checkpoint.messageId);
1632
- }
1633
- /**
1634
- * Switch to a different branch by name.
1635
- *
1636
- * @param name - Branch name to switch to
1637
- *
1638
- * @example
1639
- * ```ts
1640
- * // List branches (via store)
1641
- * const branches = await store.listBranches(context.chatId);
1642
- * console.log(branches); // [{name: 'main', ...}, {name: 'main-v2', ...}]
1643
- *
1644
- * // Switch to original branch
1645
- * await context.switchBranch('main');
1646
- * ```
1647
- */
1648
- async switchBranch(name) {
1649
- await this.#ensureInitialized();
1650
- const branch = await this.#store.getBranch(this.#chatId, name);
1651
- if (!branch) {
1652
- throw new Error(`Branch "${name}" not found in chat "${this.#chatId}"`);
1653
- }
1654
- await this.#store.setActiveBranch(this.#chatId, branch.id);
1655
- this.#branch = { ...branch, isActive: true };
1656
- this.#branchName = name;
1657
- this.#pendingMessages = [];
1658
- }
1659
- /**
1660
- * Create a parallel branch from the current position ("by the way").
1661
- *
1662
- * Use this when you want to fork the conversation without leaving
1663
- * the current branch. Common use case: user wants to ask another
1664
- * question while waiting for the model to respond.
1665
- *
1666
- * Unlike rewind(), this method:
1667
- * - Uses the current HEAD (no messageId needed)
1668
- * - Does NOT switch to the new branch
1669
- * - Keeps pending messages intact
1670
- *
1671
- * @returns The new branch info (does not switch to it)
1672
- * @throws Error if no messages exist in the conversation
1673
- *
1674
- * @example
1675
- * ```ts
1676
- * // User asked a question, model is generating...
1677
- * context.set(user('What is the weather?'));
1678
- * await context.save();
1679
- *
1680
- * // User wants to ask something else without waiting
1681
- * const newBranch = await context.btw();
1682
- * // newBranch = { name: 'main-v2', ... }
1683
- *
1684
- * // Later, switch to the new branch and add the question
1685
- * await context.switchBranch(newBranch.name);
1686
- * context.set(user('Also, what time is it?'));
1687
- * await context.save();
1688
- * ```
1689
- */
1690
- async btw() {
1691
- await this.#ensureInitialized();
1692
- if (!this.#branch?.headMessageId) {
1693
- throw new Error("Cannot create btw branch: no messages in conversation");
1694
- }
1695
- return this.#createBranchFrom(this.#branch.headMessageId, false);
1696
- }
1697
- /**
1698
- * Update metadata for the current chat.
1699
- *
1700
- * @param updates - Partial metadata to merge (title, metadata)
1701
- *
1702
- * @example
1703
- * ```ts
1704
- * await context.updateChat({
1705
- * title: 'Coding Help Session',
1706
- * metadata: { tags: ['python', 'debugging'] }
1707
- * });
1708
- * ```
1709
- */
1710
- async updateChat(updates) {
1711
- await this.#ensureInitialized();
1712
- const storeUpdates = {};
1713
- if (updates.title !== void 0) {
1714
- storeUpdates.title = updates.title;
1715
- }
1716
- if (updates.metadata !== void 0) {
1717
- storeUpdates.metadata = {
1718
- ...this.#chatData?.metadata,
1719
- ...updates.metadata
1720
- };
1721
- }
1722
- this.#chatData = await this.#store.updateChat(this.#chatId, storeUpdates);
1723
- }
1724
- /**
1725
- * Consolidate context fragments (no-op for now).
1726
- *
1727
- * This is a placeholder for future functionality that merges context fragments
1728
- * using specific rules. Currently, it does nothing.
1729
- *
1730
- * @experimental
1731
- */
1732
- consolidate() {
1733
- return void 0;
1734
- }
1735
- /**
1736
- * Inspect the full context state for debugging.
1737
- * Returns a comprehensive JSON-serializable object with all context information.
1738
- *
1739
- * @param options - Inspection options (modelId and renderer required)
1740
- * @returns Complete inspection data including estimates, rendered output, fragments, and graph
1741
- *
1742
- * @example
1743
- * ```ts
1744
- * const inspection = await context.inspect({
1745
- * modelId: 'openai:gpt-4o',
1746
- * renderer: new XmlRenderer(),
1747
- * });
1748
- * console.log(JSON.stringify(inspection, null, 2));
1749
- *
1750
- * // Or write to file for analysis
1751
- * await fs.writeFile('context-debug.json', JSON.stringify(inspection, null, 2));
1752
- * ```
1753
- */
1754
- async inspect(options) {
1755
- await this.#ensureInitialized();
1756
- const { renderer } = options;
1757
- const estimateResult = await this.estimate(options.modelId, { renderer });
1758
- const rendered = renderer.render(this.#fragments);
1759
- const persistedMessages = [];
1760
- if (this.#branch?.headMessageId) {
1761
- const chain = await this.#store.getMessageChain(
1762
- this.#branch.headMessageId
1763
- );
1764
- persistedMessages.push(...chain);
1765
- }
1766
- const graph = await this.#store.getGraph(this.#chatId);
1767
- return {
1768
- estimate: estimateResult,
1769
- rendered,
1770
- fragments: {
1771
- context: [...this.#fragments],
1772
- pending: [...this.#pendingMessages],
1773
- persisted: persistedMessages
1774
- },
1775
- graph,
1776
- meta: {
1777
- chatId: this.#chatId,
1778
- branch: this.#branchName,
1779
- timestamp: Date.now()
1780
- }
1781
- };
1782
- }
1783
- };
1784
- function term(name, definition) {
1785
- return {
1786
- name: "term",
1787
- data: { name, definition }
1788
- };
1789
- }
1790
- function hint(text) {
1791
- return {
1792
- name: "hint",
1793
- data: text
1794
- };
1795
- }
1796
- function guardrail(input) {
1797
- return {
1798
- name: "guardrail",
1799
- data: {
1800
- rule: input.rule,
1801
- ...input.reason && { reason: input.reason },
1802
- ...input.action && { action: input.action }
1803
- }
1804
- };
1805
- }
1806
- function explain(input) {
1807
- return {
1808
- name: "explain",
1809
- data: {
1810
- concept: input.concept,
1811
- explanation: input.explanation,
1812
- ...input.therefore && { therefore: input.therefore }
1346
+ };
1347
+ }
1348
+ function explain(input) {
1349
+ return {
1350
+ name: "explain",
1351
+ data: {
1352
+ concept: input.concept,
1353
+ explanation: input.explanation,
1354
+ ...input.therefore && { therefore: input.therefore }
1813
1355
  }
1814
1356
  };
1815
1357
  }
@@ -1880,7 +1422,8 @@ function persona(input) {
1880
1422
  name: "persona",
1881
1423
  data: {
1882
1424
  name: input.name,
1883
- role: input.role,
1425
+ ...input.role && { role: input.role },
1426
+ ...input.objective && { objective: input.objective },
1884
1427
  ...input.tone && { tone: input.tone }
1885
1428
  }
1886
1429
  };
@@ -1890,6 +1433,7 @@ var STORE_DDL = `
1890
1433
  -- createdAt/updatedAt: DEFAULT for insert, inline SET for updates
1891
1434
  CREATE TABLE IF NOT EXISTS chats (
1892
1435
  id TEXT PRIMARY KEY,
1436
+ userId TEXT NOT NULL,
1893
1437
  title TEXT,
1894
1438
  metadata TEXT,
1895
1439
  createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
@@ -1897,6 +1441,7 @@ CREATE TABLE IF NOT EXISTS chats (
1897
1441
  );
1898
1442
 
1899
1443
  CREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);
1444
+ CREATE INDEX IF NOT EXISTS idx_chats_userId ON chats(userId);
1900
1445
 
1901
1446
  -- Messages table (nodes in the DAG)
1902
1447
  CREATE TABLE IF NOT EXISTS messages (
@@ -1962,37 +1507,67 @@ var SqliteContextStore = class extends ContextStore {
1962
1507
  this.#db.exec("PRAGMA foreign_keys = ON");
1963
1508
  this.#db.exec(STORE_DDL);
1964
1509
  }
1510
+ /**
1511
+ * Execute a function within a transaction.
1512
+ * Automatically commits on success or rolls back on error.
1513
+ */
1514
+ #useTransaction(fn) {
1515
+ this.#db.exec("BEGIN TRANSACTION");
1516
+ try {
1517
+ const result = fn();
1518
+ this.#db.exec("COMMIT");
1519
+ return result;
1520
+ } catch (error) {
1521
+ this.#db.exec("ROLLBACK");
1522
+ throw error;
1523
+ }
1524
+ }
1965
1525
  // ==========================================================================
1966
1526
  // Chat Operations
1967
1527
  // ==========================================================================
1968
1528
  async createChat(chat) {
1969
- this.#db.prepare(
1970
- `INSERT INTO chats (id, title, metadata)
1971
- VALUES (?, ?, ?)`
1972
- ).run(
1973
- chat.id,
1974
- chat.title ?? null,
1975
- chat.metadata ? JSON.stringify(chat.metadata) : null
1976
- );
1529
+ this.#useTransaction(() => {
1530
+ this.#db.prepare(
1531
+ `INSERT INTO chats (id, userId, title, metadata)
1532
+ VALUES (?, ?, ?, ?)`
1533
+ ).run(
1534
+ chat.id,
1535
+ chat.userId,
1536
+ chat.title ?? null,
1537
+ chat.metadata ? JSON.stringify(chat.metadata) : null
1538
+ );
1539
+ this.#db.prepare(
1540
+ `INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
1541
+ VALUES (?, ?, 'main', NULL, 1, ?)`
1542
+ ).run(crypto.randomUUID(), chat.id, Date.now());
1543
+ });
1977
1544
  }
1978
1545
  async upsertChat(chat) {
1979
- const row = this.#db.prepare(
1980
- `INSERT INTO chats (id, title, metadata)
1981
- VALUES (?, ?, ?)
1982
- ON CONFLICT(id) DO UPDATE SET id = excluded.id
1983
- RETURNING *`
1984
- ).get(
1985
- chat.id,
1986
- chat.title ?? null,
1987
- chat.metadata ? JSON.stringify(chat.metadata) : null
1988
- );
1989
- return {
1990
- id: row.id,
1991
- title: row.title ?? void 0,
1992
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
1993
- createdAt: row.createdAt,
1994
- updatedAt: row.updatedAt
1995
- };
1546
+ return this.#useTransaction(() => {
1547
+ const row = this.#db.prepare(
1548
+ `INSERT INTO chats (id, userId, title, metadata)
1549
+ VALUES (?, ?, ?, ?)
1550
+ ON CONFLICT(id) DO UPDATE SET id = excluded.id
1551
+ RETURNING *`
1552
+ ).get(
1553
+ chat.id,
1554
+ chat.userId,
1555
+ chat.title ?? null,
1556
+ chat.metadata ? JSON.stringify(chat.metadata) : null
1557
+ );
1558
+ this.#db.prepare(
1559
+ `INSERT OR IGNORE INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
1560
+ VALUES (?, ?, 'main', NULL, 1, ?)`
1561
+ ).run(crypto.randomUUID(), chat.id, Date.now());
1562
+ return {
1563
+ id: row.id,
1564
+ userId: row.userId,
1565
+ title: row.title ?? void 0,
1566
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
1567
+ createdAt: row.createdAt,
1568
+ updatedAt: row.updatedAt
1569
+ };
1570
+ });
1996
1571
  }
1997
1572
  async getChat(chatId) {
1998
1573
  const row = this.#db.prepare("SELECT * FROM chats WHERE id = ?").get(chatId);
@@ -2001,6 +1576,7 @@ var SqliteContextStore = class extends ContextStore {
2001
1576
  }
2002
1577
  return {
2003
1578
  id: row.id,
1579
+ userId: row.userId,
2004
1580
  title: row.title ?? void 0,
2005
1581
  metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
2006
1582
  createdAt: row.createdAt,
@@ -2024,16 +1600,33 @@ var SqliteContextStore = class extends ContextStore {
2024
1600
  ).get(...params);
2025
1601
  return {
2026
1602
  id: row.id,
1603
+ userId: row.userId,
2027
1604
  title: row.title ?? void 0,
2028
1605
  metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
2029
1606
  createdAt: row.createdAt,
2030
1607
  updatedAt: row.updatedAt
2031
1608
  };
2032
1609
  }
2033
- async listChats() {
1610
+ async listChats(options) {
1611
+ const params = [];
1612
+ let whereClause = "";
1613
+ let limitClause = "";
1614
+ if (options?.userId) {
1615
+ whereClause = "WHERE c.userId = ?";
1616
+ params.push(options.userId);
1617
+ }
1618
+ if (options?.limit !== void 0) {
1619
+ limitClause = " LIMIT ?";
1620
+ params.push(options.limit);
1621
+ if (options.offset !== void 0) {
1622
+ limitClause += " OFFSET ?";
1623
+ params.push(options.offset);
1624
+ }
1625
+ }
2034
1626
  const rows = this.#db.prepare(
2035
1627
  `SELECT
2036
1628
  c.id,
1629
+ c.userId,
2037
1630
  c.title,
2038
1631
  c.createdAt,
2039
1632
  c.updatedAt,
@@ -2042,11 +1635,13 @@ var SqliteContextStore = class extends ContextStore {
2042
1635
  FROM chats c
2043
1636
  LEFT JOIN messages m ON m.chatId = c.id
2044
1637
  LEFT JOIN branches b ON b.chatId = c.id
1638
+ ${whereClause}
2045
1639
  GROUP BY c.id
2046
- ORDER BY c.updatedAt DESC`
2047
- ).all();
1640
+ ORDER BY c.updatedAt DESC${limitClause}`
1641
+ ).all(...params);
2048
1642
  return rows.map((row) => ({
2049
1643
  id: row.id,
1644
+ userId: row.userId,
2050
1645
  title: row.title ?? void 0,
2051
1646
  messageCount: row.messageCount,
2052
1647
  branchCount: row.branchCount,
@@ -2054,371 +1649,1062 @@ var SqliteContextStore = class extends ContextStore {
2054
1649
  updatedAt: row.updatedAt
2055
1650
  }));
2056
1651
  }
1652
+ async deleteChat(chatId, options) {
1653
+ return this.#useTransaction(() => {
1654
+ const messageIds = this.#db.prepare("SELECT id FROM messages WHERE chatId = ?").all(chatId);
1655
+ let sql = "DELETE FROM chats WHERE id = ?";
1656
+ const params = [chatId];
1657
+ if (options?.userId !== void 0) {
1658
+ sql += " AND userId = ?";
1659
+ params.push(options.userId);
1660
+ }
1661
+ const result = this.#db.prepare(sql).run(...params);
1662
+ if (result.changes > 0 && messageIds.length > 0) {
1663
+ const placeholders = messageIds.map(() => "?").join(", ");
1664
+ this.#db.prepare(
1665
+ `DELETE FROM messages_fts WHERE messageId IN (${placeholders})`
1666
+ ).run(...messageIds.map((m) => m.id));
1667
+ }
1668
+ return result.changes > 0;
1669
+ });
1670
+ }
1671
+ // ==========================================================================
1672
+ // Message Operations (Graph Nodes)
1673
+ // ==========================================================================
1674
+ async addMessage(message2) {
1675
+ const existingParent = message2.parentId === message2.id ? this.#db.prepare("SELECT parentId FROM messages WHERE id = ?").get(message2.id) : void 0;
1676
+ const parentId = message2.parentId === message2.id ? existingParent?.parentId ?? null : message2.parentId;
1677
+ this.#db.prepare(
1678
+ `INSERT INTO messages (id, chatId, parentId, name, type, data, createdAt)
1679
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1680
+ ON CONFLICT(id) DO UPDATE SET
1681
+ name = excluded.name,
1682
+ type = excluded.type,
1683
+ data = excluded.data`
1684
+ ).run(
1685
+ message2.id,
1686
+ message2.chatId,
1687
+ parentId,
1688
+ message2.name,
1689
+ message2.type ?? null,
1690
+ JSON.stringify(message2.data),
1691
+ message2.createdAt
1692
+ );
1693
+ const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
1694
+ this.#db.prepare(`DELETE FROM messages_fts WHERE messageId = ?`).run(message2.id);
1695
+ this.#db.prepare(
1696
+ `INSERT INTO messages_fts(messageId, chatId, name, content)
1697
+ VALUES (?, ?, ?, ?)`
1698
+ ).run(message2.id, message2.chatId, message2.name, content);
1699
+ }
1700
+ async getMessage(messageId) {
1701
+ const row = this.#db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
1702
+ if (!row) {
1703
+ return void 0;
1704
+ }
1705
+ return {
1706
+ id: row.id,
1707
+ chatId: row.chatId,
1708
+ parentId: row.parentId,
1709
+ name: row.name,
1710
+ type: row.type ?? void 0,
1711
+ data: JSON.parse(row.data),
1712
+ createdAt: row.createdAt
1713
+ };
1714
+ }
1715
+ async getMessageChain(headId) {
1716
+ const rows = this.#db.prepare(
1717
+ `WITH RECURSIVE chain AS (
1718
+ SELECT *, 0 as depth FROM messages WHERE id = ?
1719
+ UNION ALL
1720
+ SELECT m.*, c.depth + 1 FROM messages m
1721
+ INNER JOIN chain c ON m.id = c.parentId
1722
+ )
1723
+ SELECT * FROM chain
1724
+ ORDER BY depth DESC`
1725
+ ).all(headId);
1726
+ return rows.map((row) => ({
1727
+ id: row.id,
1728
+ chatId: row.chatId,
1729
+ parentId: row.parentId,
1730
+ name: row.name,
1731
+ type: row.type ?? void 0,
1732
+ data: JSON.parse(row.data),
1733
+ createdAt: row.createdAt
1734
+ }));
1735
+ }
1736
+ async hasChildren(messageId) {
1737
+ const row = this.#db.prepare(
1738
+ "SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = ?) as hasChildren"
1739
+ ).get(messageId);
1740
+ return row.hasChildren === 1;
1741
+ }
1742
+ async getMessages(chatId) {
1743
+ const chat = await this.getChat(chatId);
1744
+ if (!chat) {
1745
+ throw new Error(`Chat "${chatId}" not found`);
1746
+ }
1747
+ const activeBranch = await this.getActiveBranch(chatId);
1748
+ if (!activeBranch?.headMessageId) {
1749
+ return [];
1750
+ }
1751
+ return this.getMessageChain(activeBranch.headMessageId);
1752
+ }
1753
+ // ==========================================================================
1754
+ // Branch Operations
1755
+ // ==========================================================================
1756
+ async createBranch(branch) {
1757
+ this.#db.prepare(
1758
+ `INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
1759
+ VALUES (?, ?, ?, ?, ?, ?)`
1760
+ ).run(
1761
+ branch.id,
1762
+ branch.chatId,
1763
+ branch.name,
1764
+ branch.headMessageId,
1765
+ branch.isActive ? 1 : 0,
1766
+ branch.createdAt
1767
+ );
1768
+ }
1769
+ async getBranch(chatId, name) {
1770
+ const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND name = ?").get(chatId, name);
1771
+ if (!row) {
1772
+ return void 0;
1773
+ }
1774
+ return {
1775
+ id: row.id,
1776
+ chatId: row.chatId,
1777
+ name: row.name,
1778
+ headMessageId: row.headMessageId,
1779
+ isActive: row.isActive === 1,
1780
+ createdAt: row.createdAt
1781
+ };
1782
+ }
1783
+ async getActiveBranch(chatId) {
1784
+ const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND isActive = 1").get(chatId);
1785
+ if (!row) {
1786
+ return void 0;
1787
+ }
1788
+ return {
1789
+ id: row.id,
1790
+ chatId: row.chatId,
1791
+ name: row.name,
1792
+ headMessageId: row.headMessageId,
1793
+ isActive: true,
1794
+ createdAt: row.createdAt
1795
+ };
1796
+ }
1797
+ async setActiveBranch(chatId, branchId) {
1798
+ this.#db.prepare("UPDATE branches SET isActive = 0 WHERE chatId = ?").run(chatId);
1799
+ this.#db.prepare("UPDATE branches SET isActive = 1 WHERE id = ?").run(branchId);
1800
+ }
1801
+ async updateBranchHead(branchId, messageId) {
1802
+ this.#db.prepare("UPDATE branches SET headMessageId = ? WHERE id = ?").run(messageId, branchId);
1803
+ }
1804
+ async listBranches(chatId) {
1805
+ const branches = this.#db.prepare(
1806
+ `SELECT
1807
+ b.id,
1808
+ b.name,
1809
+ b.headMessageId,
1810
+ b.isActive,
1811
+ b.createdAt
1812
+ FROM branches b
1813
+ WHERE b.chatId = ?
1814
+ ORDER BY b.createdAt ASC`
1815
+ ).all(chatId);
1816
+ const result = [];
1817
+ for (const branch of branches) {
1818
+ let messageCount = 0;
1819
+ if (branch.headMessageId) {
1820
+ const countRow = this.#db.prepare(
1821
+ `WITH RECURSIVE chain AS (
1822
+ SELECT id, parentId FROM messages WHERE id = ?
1823
+ UNION ALL
1824
+ SELECT m.id, m.parentId FROM messages m
1825
+ INNER JOIN chain c ON m.id = c.parentId
1826
+ )
1827
+ SELECT COUNT(*) as count FROM chain`
1828
+ ).get(branch.headMessageId);
1829
+ messageCount = countRow.count;
1830
+ }
1831
+ result.push({
1832
+ id: branch.id,
1833
+ name: branch.name,
1834
+ headMessageId: branch.headMessageId,
1835
+ isActive: branch.isActive === 1,
1836
+ messageCount,
1837
+ createdAt: branch.createdAt
1838
+ });
1839
+ }
1840
+ return result;
1841
+ }
2057
1842
  // ==========================================================================
2058
- // Message Operations (Graph Nodes)
1843
+ // Checkpoint Operations
2059
1844
  // ==========================================================================
2060
- async addMessage(message2) {
1845
+ async createCheckpoint(checkpoint) {
2061
1846
  this.#db.prepare(
2062
- `INSERT INTO messages (id, chatId, parentId, name, type, data, createdAt)
2063
- VALUES (?, ?, ?, ?, ?, ?, ?)
2064
- ON CONFLICT(id) DO UPDATE SET
2065
- parentId = excluded.parentId,
2066
- name = excluded.name,
2067
- type = excluded.type,
2068
- data = excluded.data`
1847
+ `INSERT INTO checkpoints (id, chatId, name, messageId, createdAt)
1848
+ VALUES (?, ?, ?, ?, ?)
1849
+ ON CONFLICT(chatId, name) DO UPDATE SET
1850
+ messageId = excluded.messageId,
1851
+ createdAt = excluded.createdAt`
2069
1852
  ).run(
2070
- message2.id,
2071
- message2.chatId,
2072
- message2.parentId,
2073
- message2.name,
2074
- message2.type ?? null,
2075
- JSON.stringify(message2.data),
2076
- message2.createdAt
1853
+ checkpoint.id,
1854
+ checkpoint.chatId,
1855
+ checkpoint.name,
1856
+ checkpoint.messageId,
1857
+ checkpoint.createdAt
2077
1858
  );
2078
- const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
2079
- this.#db.prepare(`DELETE FROM messages_fts WHERE messageId = ?`).run(message2.id);
2080
- this.#db.prepare(
2081
- `INSERT INTO messages_fts(messageId, chatId, name, content)
2082
- VALUES (?, ?, ?, ?)`
2083
- ).run(message2.id, message2.chatId, message2.name, content);
2084
1859
  }
2085
- async getMessage(messageId) {
2086
- const row = this.#db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
1860
+ async getCheckpoint(chatId, name) {
1861
+ const row = this.#db.prepare("SELECT * FROM checkpoints WHERE chatId = ? AND name = ?").get(chatId, name);
2087
1862
  if (!row) {
2088
1863
  return void 0;
2089
1864
  }
2090
1865
  return {
2091
1866
  id: row.id,
2092
1867
  chatId: row.chatId,
2093
- parentId: row.parentId,
2094
1868
  name: row.name,
2095
- type: row.type ?? void 0,
2096
- data: JSON.parse(row.data),
1869
+ messageId: row.messageId,
2097
1870
  createdAt: row.createdAt
2098
1871
  };
2099
1872
  }
2100
- async getMessageChain(headId) {
1873
+ async listCheckpoints(chatId) {
2101
1874
  const rows = this.#db.prepare(
2102
- `WITH RECURSIVE chain AS (
2103
- SELECT *, 0 as depth FROM messages WHERE id = ?
2104
- UNION ALL
2105
- SELECT m.*, c.depth + 1 FROM messages m
2106
- INNER JOIN chain c ON m.id = c.parentId
2107
- )
2108
- SELECT * FROM chain
2109
- ORDER BY depth DESC`
2110
- ).all(headId);
1875
+ `SELECT id, name, messageId, createdAt
1876
+ FROM checkpoints
1877
+ WHERE chatId = ?
1878
+ ORDER BY createdAt DESC`
1879
+ ).all(chatId);
2111
1880
  return rows.map((row) => ({
2112
1881
  id: row.id,
2113
- chatId: row.chatId,
2114
- parentId: row.parentId,
2115
1882
  name: row.name,
2116
- type: row.type ?? void 0,
2117
- data: JSON.parse(row.data),
1883
+ messageId: row.messageId,
2118
1884
  createdAt: row.createdAt
2119
1885
  }));
2120
1886
  }
2121
- async hasChildren(messageId) {
2122
- const row = this.#db.prepare(
2123
- "SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = ?) as hasChildren"
2124
- ).get(messageId);
2125
- return row.hasChildren === 1;
1887
+ async deleteCheckpoint(chatId, name) {
1888
+ this.#db.prepare("DELETE FROM checkpoints WHERE chatId = ? AND name = ?").run(chatId, name);
1889
+ }
1890
+ // ==========================================================================
1891
+ // Search Operations
1892
+ // ==========================================================================
1893
+ async searchMessages(chatId, query, options) {
1894
+ const limit = options?.limit ?? 20;
1895
+ const roles = options?.roles;
1896
+ let sql = `
1897
+ SELECT
1898
+ m.id,
1899
+ m.chatId,
1900
+ m.parentId,
1901
+ m.name,
1902
+ m.type,
1903
+ m.data,
1904
+ m.createdAt,
1905
+ fts.rank,
1906
+ snippet(messages_fts, 3, '<mark>', '</mark>', '...', 32) as snippet
1907
+ FROM messages_fts fts
1908
+ JOIN messages m ON m.id = fts.messageId
1909
+ WHERE messages_fts MATCH ?
1910
+ AND fts.chatId = ?
1911
+ `;
1912
+ const params = [query, chatId];
1913
+ if (roles && roles.length > 0) {
1914
+ const placeholders = roles.map(() => "?").join(", ");
1915
+ sql += ` AND fts.name IN (${placeholders})`;
1916
+ params.push(...roles);
1917
+ }
1918
+ sql += " ORDER BY fts.rank LIMIT ?";
1919
+ params.push(limit);
1920
+ const rows = this.#db.prepare(sql).all(...params);
1921
+ return rows.map((row) => ({
1922
+ message: {
1923
+ id: row.id,
1924
+ chatId: row.chatId,
1925
+ parentId: row.parentId,
1926
+ name: row.name,
1927
+ type: row.type ?? void 0,
1928
+ data: JSON.parse(row.data),
1929
+ createdAt: row.createdAt
1930
+ },
1931
+ rank: row.rank,
1932
+ snippet: row.snippet
1933
+ }));
1934
+ }
1935
+ // ==========================================================================
1936
+ // Visualization Operations
1937
+ // ==========================================================================
1938
+ async getGraph(chatId) {
1939
+ const messageRows = this.#db.prepare(
1940
+ `SELECT id, parentId, name, data, createdAt
1941
+ FROM messages
1942
+ WHERE chatId = ?
1943
+ ORDER BY createdAt ASC`
1944
+ ).all(chatId);
1945
+ const nodes = messageRows.map((row) => {
1946
+ const data = JSON.parse(row.data);
1947
+ const content = typeof data === "string" ? data : JSON.stringify(data);
1948
+ return {
1949
+ id: row.id,
1950
+ parentId: row.parentId,
1951
+ role: row.name,
1952
+ content: content.length > 50 ? content.slice(0, 50) + "..." : content,
1953
+ createdAt: row.createdAt
1954
+ };
1955
+ });
1956
+ const branchRows = this.#db.prepare(
1957
+ `SELECT name, headMessageId, isActive
1958
+ FROM branches
1959
+ WHERE chatId = ?
1960
+ ORDER BY createdAt ASC`
1961
+ ).all(chatId);
1962
+ const branches = branchRows.map((row) => ({
1963
+ name: row.name,
1964
+ headMessageId: row.headMessageId,
1965
+ isActive: row.isActive === 1
1966
+ }));
1967
+ const checkpointRows = this.#db.prepare(
1968
+ `SELECT name, messageId
1969
+ FROM checkpoints
1970
+ WHERE chatId = ?
1971
+ ORDER BY createdAt ASC`
1972
+ ).all(chatId);
1973
+ const checkpoints = checkpointRows.map((row) => ({
1974
+ name: row.name,
1975
+ messageId: row.messageId
1976
+ }));
1977
+ return {
1978
+ chatId,
1979
+ nodes,
1980
+ branches,
1981
+ checkpoints
1982
+ };
1983
+ }
1984
+ };
1985
+ var InMemoryContextStore = class extends SqliteContextStore {
1986
+ constructor() {
1987
+ super(":memory:");
1988
+ }
1989
+ };
1990
+ function structuredOutput(options) {
1991
+ return {
1992
+ async generate(contextVariables, config) {
1993
+ if (!options.context) {
1994
+ throw new Error(`structuredOutput is missing a context.`);
1995
+ }
1996
+ if (!options.model) {
1997
+ throw new Error(`structuredOutput is missing a model.`);
1998
+ }
1999
+ const { messages, systemPrompt } = await options.context.resolve({
2000
+ renderer: new XmlRenderer()
2001
+ });
2002
+ const result = await generateText({
2003
+ abortSignal: config?.abortSignal,
2004
+ providerOptions: options.providerOptions,
2005
+ model: options.model,
2006
+ system: systemPrompt,
2007
+ messages: await convertToModelMessages(messages),
2008
+ stopWhen: stepCountIs(25),
2009
+ experimental_repairToolCall: repairToolCall,
2010
+ experimental_context: contextVariables,
2011
+ output: Output.object({ schema: options.schema }),
2012
+ tools: options.tools
2013
+ });
2014
+ return result.output;
2015
+ },
2016
+ async stream(contextVariables, config) {
2017
+ if (!options.context) {
2018
+ throw new Error(`structuredOutput is missing a context.`);
2019
+ }
2020
+ if (!options.model) {
2021
+ throw new Error(`structuredOutput is missing a model.`);
2022
+ }
2023
+ const { messages, systemPrompt } = await options.context.resolve({
2024
+ renderer: new XmlRenderer()
2025
+ });
2026
+ return streamText({
2027
+ abortSignal: config?.abortSignal,
2028
+ providerOptions: options.providerOptions,
2029
+ model: options.model,
2030
+ system: systemPrompt,
2031
+ experimental_repairToolCall: repairToolCall,
2032
+ messages: await convertToModelMessages(messages),
2033
+ stopWhen: stepCountIs(50),
2034
+ experimental_transform: config?.transform ?? smoothStream(),
2035
+ experimental_context: contextVariables,
2036
+ output: Output.object({ schema: options.schema }),
2037
+ tools: options.tools
2038
+ });
2039
+ }
2040
+ };
2041
+ }
2042
+ var repairToolCall = async ({
2043
+ toolCall,
2044
+ tools,
2045
+ inputSchema,
2046
+ error
2047
+ }) => {
2048
+ console.log(
2049
+ `Debug: ${chalk2.yellow("RepairingToolCall")}: ${toolCall.toolName}`,
2050
+ error.name
2051
+ );
2052
+ if (NoSuchToolError.isInstance(error)) {
2053
+ return null;
2054
+ }
2055
+ const tool = tools[toolCall.toolName];
2056
+ const { output } = await generateText({
2057
+ model: groq("openai/gpt-oss-20b"),
2058
+ output: Output.object({ schema: tool.inputSchema }),
2059
+ prompt: [
2060
+ `The model tried to call the tool "${toolCall.toolName}" with the following inputs:`,
2061
+ JSON.stringify(toolCall.input),
2062
+ `The tool accepts the following schema:`,
2063
+ JSON.stringify(inputSchema(toolCall)),
2064
+ "Please fix the inputs."
2065
+ ].join("\n")
2066
+ });
2067
+ return { ...toolCall, input: JSON.stringify(output) };
2068
+ };
2069
+
2070
+ // packages/text2sql/src/lib/synthesis/extractors/base-contextual-extractor.ts
2071
+ var contextResolverSchema = z.object({
2072
+ question: z.string().describe(
2073
+ "A standalone natural language question that the SQL query answers"
2074
+ )
2075
+ });
2076
+ async function resolveContext(params) {
2077
+ const context = new ContextEngine({
2078
+ store: new InMemoryContextStore(),
2079
+ chatId: `context-resolver-${crypto.randomUUID()}`,
2080
+ userId: "system"
2081
+ });
2082
+ context.set(
2083
+ persona({
2084
+ name: "context_resolver",
2085
+ role: "You are an expert at understanding conversational context and generating clear, standalone questions from multi-turn conversations.",
2086
+ objective: "Transform context-dependent messages into standalone questions that fully capture user intent"
2087
+ }),
2088
+ ...params.introspection ? [fragment("database_schema", params.introspection)] : [],
2089
+ fragment("conversation", params.conversation),
2090
+ fragment("sql", params.sql),
2091
+ fragment(
2092
+ "task",
2093
+ dedent`
2094
+ Given the conversation above and the SQL query that was executed,
2095
+ generate a single, standalone natural language question that:
2096
+ 1. Fully captures the user's intent without needing prior context
2097
+ 2. Uses natural business language (not SQL terminology)
2098
+ 3. Could be asked by someone who hasn't seen the conversation
2099
+ 4. Accurately represents what the SQL query answers
2100
+ `
2101
+ ),
2102
+ fragment(
2103
+ "examples",
2104
+ dedent`
2105
+ Conversation: "Show me customers" → "Filter to NY" → "Sort by revenue"
2106
+ SQL: SELECT * FROM customers WHERE region = 'NY' ORDER BY revenue DESC
2107
+ Question: "Show me customers in the NY region sorted by revenue"
2108
+
2109
+ Conversation: "What were sales last month?" → "Break it down by category"
2110
+ SQL: SELECT category, SUM(amount) FROM sales WHERE date >= '2024-11-01' GROUP BY category
2111
+ Question: "What were sales by category for last month?"
2112
+ `
2113
+ ),
2114
+ user("Generate a standalone question for this SQL query.")
2115
+ );
2116
+ const resolverOutput = structuredOutput({
2117
+ model: groq2("openai/gpt-oss-20b"),
2118
+ context,
2119
+ schema: contextResolverSchema
2120
+ });
2121
+ return resolverOutput.generate();
2122
+ }
2123
+ function getMessageText(message2) {
2124
+ const textParts = message2.parts.filter(isTextUIPart).map((part) => part.text);
2125
+ return textParts.join(" ").trim();
2126
+ }
2127
+ function formatConversation(messages) {
2128
+ return messages.map((msg, i) => `[${i + 1}] ${msg}`).join("\n");
2129
+ }
2130
+ var BaseContextualExtractor = class extends PairProducer {
2131
+ context = [];
2132
+ results = [];
2133
+ messages;
2134
+ adapter;
2135
+ options;
2136
+ constructor(messages, adapter, options = {}) {
2137
+ super();
2138
+ this.messages = messages;
2139
+ this.adapter = adapter;
2140
+ this.options = options;
2126
2141
  }
2127
- // ==========================================================================
2128
- // Branch Operations
2129
- // ==========================================================================
2130
- async createBranch(branch) {
2131
- this.#db.prepare(
2132
- `INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
2133
- VALUES (?, ?, ?, ?, ?, ?)`
2134
- ).run(
2135
- branch.id,
2136
- branch.chatId,
2137
- branch.name,
2138
- branch.headMessageId,
2139
- branch.isActive ? 1 : 0,
2140
- branch.createdAt
2141
- );
2142
+ /**
2143
+ * Template method - defines the extraction algorithm skeleton.
2144
+ * Subclasses customize behavior via hooks, not by overriding this method.
2145
+ */
2146
+ async *produce() {
2147
+ this.context = [];
2148
+ this.results = [];
2149
+ const { includeFailures = false, toolName = "db_query" } = this.options;
2150
+ await this.extractSqlsWithContext(toolName, includeFailures);
2151
+ if (this.results.length === 0) {
2152
+ return;
2153
+ }
2154
+ const introspection = "";
2155
+ yield* this.resolveQuestions(introspection);
2142
2156
  }
2143
- async getBranch(chatId, name) {
2144
- const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND name = ?").get(chatId, name);
2145
- if (!row) {
2146
- return void 0;
2157
+ /**
2158
+ * Core extraction loop - iterates through messages and calls hooks.
2159
+ */
2160
+ async extractSqlsWithContext(toolName, includeFailures) {
2161
+ for (const message2 of this.messages) {
2162
+ if (message2.role === "user") {
2163
+ const text = getMessageText(message2);
2164
+ if (text) {
2165
+ await this.onUserMessage(text);
2166
+ }
2167
+ continue;
2168
+ }
2169
+ if (message2.role === "assistant") {
2170
+ await this.extractFromAssistant(message2, toolName, includeFailures);
2171
+ }
2147
2172
  }
2148
- return {
2149
- id: row.id,
2150
- chatId: row.chatId,
2151
- name: row.name,
2152
- headMessageId: row.headMessageId,
2153
- isActive: row.isActive === 1,
2154
- createdAt: row.createdAt
2155
- };
2156
2173
  }
2157
- async getActiveBranch(chatId) {
2158
- const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND isActive = 1").get(chatId);
2159
- if (!row) {
2160
- return void 0;
2174
+ /**
2175
+ * Extract SQL from assistant message parts.
2176
+ */
2177
+ async extractFromAssistant(message2, toolName, includeFailures) {
2178
+ for (const part of message2.parts) {
2179
+ if (!isToolOrDynamicToolUIPart(part)) {
2180
+ continue;
2181
+ }
2182
+ if (getToolOrDynamicToolName(part) !== toolName) {
2183
+ continue;
2184
+ }
2185
+ const toolInput = "input" in part ? part.input : void 0;
2186
+ if (!toolInput?.sql) {
2187
+ continue;
2188
+ }
2189
+ const success = part.state === "output-available";
2190
+ const failed = part.state === "output-error";
2191
+ if (failed && !includeFailures) {
2192
+ continue;
2193
+ }
2194
+ if (!success && !failed) {
2195
+ continue;
2196
+ }
2197
+ const snapshot = this.getContextSnapshot();
2198
+ if (snapshot.length === 0) {
2199
+ continue;
2200
+ }
2201
+ this.results.push({
2202
+ sql: toolInput.sql,
2203
+ success,
2204
+ conversationContext: snapshot
2205
+ });
2206
+ }
2207
+ const assistantText2 = getMessageText(message2);
2208
+ if (assistantText2) {
2209
+ this.context.push(`Assistant: ${assistantText2}`);
2161
2210
  }
2162
- return {
2163
- id: row.id,
2164
- chatId: row.chatId,
2165
- name: row.name,
2166
- headMessageId: row.headMessageId,
2167
- isActive: true,
2168
- createdAt: row.createdAt
2169
- };
2170
2211
  }
2171
- async setActiveBranch(chatId, branchId) {
2172
- this.#db.prepare("UPDATE branches SET isActive = 0 WHERE chatId = ?").run(chatId);
2173
- this.#db.prepare("UPDATE branches SET isActive = 1 WHERE id = ?").run(branchId);
2212
+ /**
2213
+ * Resolve extracted SQL contexts into standalone questions using LLM.
2214
+ */
2215
+ async *resolveQuestions(introspection) {
2216
+ for (const item of this.results) {
2217
+ const output = await resolveContext({
2218
+ conversation: formatConversation(item.conversationContext),
2219
+ sql: item.sql,
2220
+ introspection
2221
+ });
2222
+ yield [
2223
+ {
2224
+ question: output.question,
2225
+ sql: item.sql,
2226
+ context: item.conversationContext,
2227
+ success: item.success
2228
+ }
2229
+ ];
2230
+ }
2174
2231
  }
2175
- async updateBranchHead(branchId, messageId) {
2176
- this.#db.prepare("UPDATE branches SET headMessageId = ? WHERE id = ?").run(messageId, branchId);
2232
+ };
2233
+
2234
+ // packages/text2sql/src/lib/synthesis/extractors/message-extractor.ts
2235
+ var MessageExtractor = class extends PairProducer {
2236
+ #messages;
2237
+ #options;
2238
+ /**
2239
+ * @param messages - Chat history to extract pairs from
2240
+ * @param options - Extraction configuration
2241
+ */
2242
+ constructor(messages, options = {}) {
2243
+ super();
2244
+ this.#messages = messages;
2245
+ this.#options = options;
2177
2246
  }
2178
- async listBranches(chatId) {
2179
- const branches = this.#db.prepare(
2180
- `SELECT
2181
- b.id,
2182
- b.name,
2183
- b.headMessageId,
2184
- b.isActive,
2185
- b.createdAt
2186
- FROM branches b
2187
- WHERE b.chatId = ?
2188
- ORDER BY b.createdAt ASC`
2189
- ).all(chatId);
2190
- const result = [];
2191
- for (const branch of branches) {
2192
- let messageCount = 0;
2193
- if (branch.headMessageId) {
2194
- const countRow = this.#db.prepare(
2195
- `WITH RECURSIVE chain AS (
2196
- SELECT id, parentId FROM messages WHERE id = ?
2197
- UNION ALL
2198
- SELECT m.id, m.parentId FROM messages m
2199
- INNER JOIN chain c ON m.id = c.parentId
2200
- )
2201
- SELECT COUNT(*) as count FROM chain`
2202
- ).get(branch.headMessageId);
2203
- messageCount = countRow.count;
2247
+ /**
2248
+ * Extracts question-SQL pairs by parsing tool calls and pairing with user messages.
2249
+ * @returns Pairs extracted from db_query tool invocations
2250
+ */
2251
+ async *produce() {
2252
+ const { includeFailures = false, toolName = "db_query" } = this.#options;
2253
+ let lastUserMessage = null;
2254
+ for (const message2 of this.#messages) {
2255
+ if (message2.role === "user") {
2256
+ lastUserMessage = message2;
2257
+ continue;
2204
2258
  }
2205
- result.push({
2206
- id: branch.id,
2207
- name: branch.name,
2208
- headMessageId: branch.headMessageId,
2209
- isActive: branch.isActive === 1,
2210
- messageCount,
2211
- createdAt: branch.createdAt
2259
+ if (message2.role === "assistant" && lastUserMessage) {
2260
+ for (const part of message2.parts) {
2261
+ if (!isToolOrDynamicToolUIPart2(part)) {
2262
+ continue;
2263
+ }
2264
+ if (getToolOrDynamicToolName2(part) !== toolName) {
2265
+ continue;
2266
+ }
2267
+ const toolInput = "input" in part ? part.input : void 0;
2268
+ if (!toolInput?.sql) {
2269
+ continue;
2270
+ }
2271
+ const success = part.state === "output-available";
2272
+ const failed = part.state === "output-error";
2273
+ if (failed && !includeFailures) {
2274
+ continue;
2275
+ }
2276
+ if (!success && !failed) {
2277
+ continue;
2278
+ }
2279
+ const question = getMessageText(lastUserMessage);
2280
+ if (!question) {
2281
+ continue;
2282
+ }
2283
+ yield [
2284
+ {
2285
+ question,
2286
+ sql: toolInput.sql,
2287
+ success
2288
+ }
2289
+ ];
2290
+ }
2291
+ }
2292
+ }
2293
+ }
2294
+ };
2295
+
2296
+ // packages/text2sql/src/lib/synthesis/extractors/sql-extractor.ts
2297
+ import { groq as groq3 } from "@ai-sdk/groq";
2298
+ import dedent2 from "dedent";
2299
+ import z2 from "zod";
2300
+ var outputSchema = z2.object({
2301
+ question: z2.string().describe("A natural language question that the SQL query answers")
2302
+ });
2303
+ var SqlExtractor = class extends PairProducer {
2304
+ #sqls;
2305
+ #adapter;
2306
+ #options;
2307
+ /**
2308
+ * @param sql - SQL query or queries to generate questions for
2309
+ * @param adapter - Database adapter for validation and schema introspection
2310
+ * @param options - Extraction configuration
2311
+ */
2312
+ constructor(sql, adapter, options = {}) {
2313
+ super();
2314
+ this.#sqls = Array.isArray(sql) ? sql : [sql];
2315
+ this.#adapter = adapter;
2316
+ this.#options = options;
2317
+ }
2318
+ /**
2319
+ * Generates natural language questions for each SQL query using an LLM.
2320
+ * @returns Pairs with generated questions and original SQL
2321
+ */
2322
+ async *produce() {
2323
+ const { validateSql = true, skipInvalid = false } = this.#options;
2324
+ const introspection = "";
2325
+ for (const sql of this.#sqls) {
2326
+ let isValid = true;
2327
+ if (validateSql) {
2328
+ const error = await this.#adapter.validate(sql);
2329
+ isValid = error === void 0 || error === null;
2330
+ if (!isValid && skipInvalid) {
2331
+ continue;
2332
+ }
2333
+ }
2334
+ const context = new ContextEngine({
2335
+ store: new InMemoryContextStore(),
2336
+ chatId: `sql-to-question-${crypto.randomUUID()}`,
2337
+ userId: "system"
2212
2338
  });
2339
+ context.set(
2340
+ persona({
2341
+ name: "sql_to_question",
2342
+ role: "You are an expert at understanding SQL queries and generating clear, natural language questions that describe what the query retrieves.",
2343
+ objective: "Generate clear, natural language questions that describe what SQL queries retrieve"
2344
+ }),
2345
+ fragment("database_schema", introspection),
2346
+ fragment("sql", sql),
2347
+ fragment(
2348
+ "task",
2349
+ dedent2`
2350
+ Given the database schema and the SQL query above, generate a single
2351
+ natural language question that:
2352
+ 1. Accurately describes what information the query retrieves
2353
+ 2. Uses natural business language (not SQL terminology)
2354
+ 3. Could be asked by a non-technical user
2355
+ 4. Is concise but complete
2356
+ `
2357
+ ),
2358
+ fragment(
2359
+ "examples",
2360
+ dedent2`
2361
+ SQL: SELECT COUNT(*) FROM customers WHERE region = 'NY'
2362
+ Question: "How many customers do we have in New York?"
2363
+
2364
+ SQL: SELECT product_name, SUM(quantity) as total FROM orders GROUP BY product_name ORDER BY total DESC LIMIT 10
2365
+ Question: "What are our top 10 products by quantity sold?"
2366
+
2367
+ SQL: SELECT c.name, COUNT(o.id) FROM customers c LEFT JOIN orders o ON c.id = o.customer_id GROUP BY c.id HAVING COUNT(o.id) = 0
2368
+ Question: "Which customers have never placed an order?"
2369
+ `
2370
+ ),
2371
+ user("Generate a natural language question for this SQL query.")
2372
+ );
2373
+ const sqlToQuestionOutput = structuredOutput({
2374
+ model: groq3("openai/gpt-oss-20b"),
2375
+ context,
2376
+ schema: outputSchema
2377
+ });
2378
+ const output = await sqlToQuestionOutput.generate();
2379
+ yield [
2380
+ {
2381
+ question: output.question,
2382
+ sql,
2383
+ success: isValid
2384
+ }
2385
+ ];
2213
2386
  }
2214
- return result;
2215
2387
  }
2216
- // ==========================================================================
2217
- // Checkpoint Operations
2218
- // ==========================================================================
2219
- async createCheckpoint(checkpoint) {
2220
- this.#db.prepare(
2221
- `INSERT INTO checkpoints (id, chatId, name, messageId, createdAt)
2222
- VALUES (?, ?, ?, ?, ?)
2223
- ON CONFLICT(chatId, name) DO UPDATE SET
2224
- messageId = excluded.messageId,
2225
- createdAt = excluded.createdAt`
2226
- ).run(
2227
- checkpoint.id,
2228
- checkpoint.chatId,
2229
- checkpoint.name,
2230
- checkpoint.messageId,
2231
- checkpoint.createdAt
2232
- );
2388
+ };
2389
+
2390
+ // packages/text2sql/src/lib/synthesis/extractors/full-context-extractor.ts
2391
+ var FullContextExtractor = class extends BaseContextualExtractor {
2392
+ constructor(messages, adapter, options = {}) {
2393
+ super(messages, adapter, options);
2233
2394
  }
2234
- async getCheckpoint(chatId, name) {
2235
- const row = this.#db.prepare("SELECT * FROM checkpoints WHERE chatId = ? AND name = ?").get(chatId, name);
2236
- if (!row) {
2237
- return void 0;
2238
- }
2239
- return {
2240
- id: row.id,
2241
- chatId: row.chatId,
2242
- name: row.name,
2243
- messageId: row.messageId,
2244
- createdAt: row.createdAt
2245
- };
2395
+ /**
2396
+ * Add user message to context (keeps all messages).
2397
+ */
2398
+ async onUserMessage(text) {
2399
+ this.context.push(`User: ${text}`);
2246
2400
  }
2247
- async listCheckpoints(chatId) {
2248
- const rows = this.#db.prepare(
2249
- `SELECT id, name, messageId, createdAt
2250
- FROM checkpoints
2251
- WHERE chatId = ?
2252
- ORDER BY createdAt DESC`
2253
- ).all(chatId);
2254
- return rows.map((row) => ({
2255
- id: row.id,
2256
- name: row.name,
2257
- messageId: row.messageId,
2258
- createdAt: row.createdAt
2259
- }));
2401
+ /**
2402
+ * Return all context accumulated so far.
2403
+ */
2404
+ getContextSnapshot() {
2405
+ return [...this.context];
2260
2406
  }
2261
- async deleteCheckpoint(chatId, name) {
2262
- this.#db.prepare("DELETE FROM checkpoints WHERE chatId = ? AND name = ?").run(chatId, name);
2407
+ };
2408
+
2409
+ // packages/text2sql/src/lib/synthesis/extractors/windowed-context-extractor.ts
2410
+ var WindowedContextExtractor = class extends BaseContextualExtractor {
2411
+ windowSize;
2412
+ constructor(messages, adapter, options) {
2413
+ super(messages, adapter, options);
2414
+ this.windowSize = options.windowSize;
2263
2415
  }
2264
- // ==========================================================================
2265
- // Search Operations
2266
- // ==========================================================================
2267
- async searchMessages(chatId, query, options) {
2268
- const limit = options?.limit ?? 20;
2269
- const roles = options?.roles;
2270
- let sql = `
2271
- SELECT
2272
- m.id,
2273
- m.chatId,
2274
- m.parentId,
2275
- m.name,
2276
- m.type,
2277
- m.data,
2278
- m.createdAt,
2279
- fts.rank,
2280
- snippet(messages_fts, 3, '<mark>', '</mark>', '...', 32) as snippet
2281
- FROM messages_fts fts
2282
- JOIN messages m ON m.id = fts.messageId
2283
- WHERE messages_fts MATCH ?
2284
- AND fts.chatId = ?
2285
- `;
2286
- const params = [query, chatId];
2287
- if (roles && roles.length > 0) {
2288
- const placeholders = roles.map(() => "?").join(", ");
2289
- sql += ` AND fts.name IN (${placeholders})`;
2290
- params.push(...roles);
2416
+ /**
2417
+ * Add user message to context (keeps all, windowing happens on snapshot).
2418
+ */
2419
+ async onUserMessage(text) {
2420
+ this.context.push(`User: ${text}`);
2421
+ }
2422
+ /**
2423
+ * Return only the last N messages based on window size.
2424
+ */
2425
+ getContextSnapshot() {
2426
+ if (this.context.length <= this.windowSize) {
2427
+ return [...this.context];
2291
2428
  }
2292
- sql += " ORDER BY fts.rank LIMIT ?";
2293
- params.push(limit);
2294
- const rows = this.#db.prepare(sql).all(...params);
2295
- return rows.map((row) => ({
2296
- message: {
2297
- id: row.id,
2298
- chatId: row.chatId,
2299
- parentId: row.parentId,
2300
- name: row.name,
2301
- type: row.type ?? void 0,
2302
- data: JSON.parse(row.data),
2303
- createdAt: row.createdAt
2304
- },
2305
- rank: row.rank,
2306
- snippet: row.snippet
2307
- }));
2429
+ return this.context.slice(-this.windowSize);
2308
2430
  }
2309
- // ==========================================================================
2310
- // Visualization Operations
2311
- // ==========================================================================
2312
- async getGraph(chatId) {
2313
- const messageRows = this.#db.prepare(
2314
- `SELECT id, parentId, name, data, createdAt
2315
- FROM messages
2316
- WHERE chatId = ?
2317
- ORDER BY createdAt ASC`
2318
- ).all(chatId);
2319
- const nodes = messageRows.map((row) => {
2320
- const data = JSON.parse(row.data);
2321
- const content = typeof data === "string" ? data : JSON.stringify(data);
2322
- return {
2323
- id: row.id,
2324
- parentId: row.parentId,
2325
- role: row.name,
2326
- content: content.length > 50 ? content.slice(0, 50) + "..." : content,
2327
- createdAt: row.createdAt
2328
- };
2431
+ };
2432
+
2433
+ // packages/text2sql/src/lib/synthesis/extractors/segmented-context-extractor.ts
2434
+ import { groq as groq4 } from "@ai-sdk/groq";
2435
+ import dedent3 from "dedent";
2436
+ import z3 from "zod";
2437
+ var topicChangeSchema = z3.object({
2438
+ isTopicChange: z3.boolean().describe("Whether the new message represents a topic change"),
2439
+ reason: z3.string().describe("Brief explanation for the decision")
2440
+ });
2441
+ async function detectTopicChange(params) {
2442
+ const context = new ContextEngine({
2443
+ store: new InMemoryContextStore(),
2444
+ chatId: `topic-change-${crypto.randomUUID()}`,
2445
+ userId: "system"
2446
+ });
2447
+ context.set(
2448
+ persona({
2449
+ name: "topic_change_detector",
2450
+ role: "You are an expert at understanding conversational flow and detecting topic changes.",
2451
+ objective: "Detect significant topic changes in database conversations"
2452
+ }),
2453
+ fragment("conversation_context", params.context || "(no prior context)"),
2454
+ fragment("new_message", params.newMessage),
2455
+ fragment(
2456
+ "task",
2457
+ dedent3`
2458
+ Determine if the new message represents a significant topic change from the
2459
+ prior conversation context. A topic change occurs when:
2460
+ 1. The user asks about a completely different entity/table/domain
2461
+ 2. The user starts a new analytical question unrelated to prior discussion
2462
+ 3. There's a clear shift in what data or metrics are being discussed
2463
+
2464
+ NOT a topic change:
2465
+ - Follow-up questions refining the same query ("filter by...", "sort by...")
2466
+ - Questions about the same entities with different conditions
2467
+ - Requests for more details on the same topic
2468
+ `
2469
+ ),
2470
+ fragment(
2471
+ "examples",
2472
+ dedent3`
2473
+ Context: "Show me customers in NY" → "Sort by revenue"
2474
+ New: "Filter to those with orders over $1000"
2475
+ Decision: NOT a topic change (still refining customer query)
2476
+
2477
+ Context: "Show me customers in NY" → "Sort by revenue"
2478
+ New: "What were our total sales last quarter?"
2479
+ Decision: Topic change (shifted from customers to sales metrics)
2480
+
2481
+ Context: "List all products"
2482
+ New: "How many orders did we have last month?"
2483
+ Decision: Topic change (products → orders/sales)
2484
+ `
2485
+ ),
2486
+ user("Determine if this is a topic change.")
2487
+ );
2488
+ const topicOutput = structuredOutput({
2489
+ model: groq4("openai/gpt-oss-20b"),
2490
+ context,
2491
+ schema: topicChangeSchema
2492
+ });
2493
+ return topicOutput.generate();
2494
+ }
2495
+ var SegmentedContextExtractor = class extends BaseContextualExtractor {
2496
+ constructor(messages, adapter, options = {}) {
2497
+ super(messages, adapter, options);
2498
+ }
2499
+ /**
2500
+ * Handle user message with topic change detection.
2501
+ * If topic changes, resolve the message to standalone form before resetting.
2502
+ *
2503
+ * Note: We capture context snapshot before async LLM calls to prevent race conditions
2504
+ * where context might be modified during the async operation.
2505
+ */
2506
+ async onUserMessage(text) {
2507
+ if (this.context.length >= 2) {
2508
+ const contextSnapshot = [...this.context];
2509
+ const { isTopicChange } = await detectTopicChange({
2510
+ context: formatConversation(contextSnapshot),
2511
+ newMessage: text
2512
+ });
2513
+ if (isTopicChange) {
2514
+ const resolved = await this.resolveToStandalone(text, contextSnapshot);
2515
+ this.context = [`User: ${resolved}`];
2516
+ return;
2517
+ }
2518
+ }
2519
+ this.context.push(`User: ${text}`);
2520
+ }
2521
+ /**
2522
+ * Return all context in current topic segment.
2523
+ */
2524
+ getContextSnapshot() {
2525
+ return [...this.context];
2526
+ }
2527
+ /**
2528
+ * Resolve a context-dependent message into a standalone question.
2529
+ * Called when topic change is detected to preserve the meaning of
2530
+ * the triggering message before context is reset.
2531
+ * @param text - The user message to resolve
2532
+ * @param contextSnapshot - Snapshot of context captured before this async call
2533
+ */
2534
+ async resolveToStandalone(text, contextSnapshot) {
2535
+ const output = await resolveContext({
2536
+ conversation: formatConversation([...contextSnapshot, `User: ${text}`]),
2537
+ sql: ""
2538
+ // No SQL yet, just resolving the question
2329
2539
  });
2330
- const branchRows = this.#db.prepare(
2331
- `SELECT name, headMessageId, isActive
2332
- FROM branches
2333
- WHERE chatId = ?
2334
- ORDER BY createdAt ASC`
2335
- ).all(chatId);
2336
- const branches = branchRows.map((row) => ({
2337
- name: row.name,
2338
- headMessageId: row.headMessageId,
2339
- isActive: row.isActive === 1
2340
- }));
2341
- const checkpointRows = this.#db.prepare(
2342
- `SELECT name, messageId
2343
- FROM checkpoints
2344
- WHERE chatId = ?
2345
- ORDER BY createdAt ASC`
2346
- ).all(chatId);
2347
- const checkpoints = checkpointRows.map((row) => ({
2348
- name: row.name,
2349
- messageId: row.messageId
2350
- }));
2351
- return {
2352
- chatId,
2353
- nodes,
2354
- branches,
2355
- checkpoints
2356
- };
2540
+ return output.question;
2541
+ }
2542
+ };
2543
+
2544
+ // packages/text2sql/src/lib/synthesis/extractors/last-query-extractor.ts
2545
+ var LastQueryExtractor = class extends BaseContextualExtractor {
2546
+ constructor(messages, adapter, options = {}) {
2547
+ super(messages, adapter, options);
2548
+ }
2549
+ /**
2550
+ * Add user message to context (keeps all messages).
2551
+ */
2552
+ async onUserMessage(text) {
2553
+ this.context.push(`User: ${text}`);
2554
+ }
2555
+ /**
2556
+ * Return all context accumulated so far.
2557
+ */
2558
+ getContextSnapshot() {
2559
+ return [...this.context];
2357
2560
  }
2358
- };
2359
- var InMemoryContextStore = class extends SqliteContextStore {
2360
- constructor() {
2361
- super(":memory:");
2561
+ /**
2562
+ * Override to only resolve the LAST query instead of all queries.
2563
+ */
2564
+ async *resolveQuestions(introspection) {
2565
+ if (this.results.length === 0) {
2566
+ return;
2567
+ }
2568
+ const last = this.results.at(-1);
2569
+ const output = await resolveContext({
2570
+ conversation: formatConversation(last.conversationContext),
2571
+ sql: last.sql,
2572
+ introspection
2573
+ });
2574
+ yield [
2575
+ {
2576
+ question: output.question,
2577
+ sql: last.sql,
2578
+ context: last.conversationContext,
2579
+ success: last.success
2580
+ }
2581
+ ];
2362
2582
  }
2363
2583
  };
2364
- function structuredOutput(options) {
2365
- return {
2366
- async generate(contextVariables, config) {
2367
- if (!options.context) {
2368
- throw new Error(
2369
- `structuredOutput "${options.name}" is missing a context.`
2370
- );
2371
- }
2372
- if (!options.model) {
2373
- throw new Error(
2374
- `structuredOutput "${options.name}" is missing a model.`
2375
- );
2376
- }
2377
- const { messages, systemPrompt } = await options.context.resolve({
2378
- renderer: new XmlRenderer()
2379
- });
2380
- const result = await generateText({
2381
- abortSignal: config?.abortSignal,
2382
- providerOptions: options.providerOptions,
2383
- model: options.model,
2384
- system: systemPrompt,
2385
- messages: convertToModelMessages(messages),
2386
- stopWhen: stepCountIs(25),
2387
- experimental_context: contextVariables,
2388
- experimental_output: Output.object({ schema: options.schema })
2389
- });
2390
- return result.experimental_output;
2391
- },
2392
- async stream(contextVariables, config) {
2393
- if (!options.context) {
2394
- throw new Error(
2395
- `structuredOutput "${options.name}" is missing a context.`
2396
- );
2397
- }
2398
- if (!options.model) {
2399
- throw new Error(
2400
- `structuredOutput "${options.name}" is missing a model.`
2401
- );
2402
- }
2403
- const { messages, systemPrompt } = await options.context.resolve({
2404
- renderer: new XmlRenderer()
2405
- });
2406
- return streamText({
2407
- abortSignal: config?.abortSignal,
2408
- providerOptions: options.providerOptions,
2409
- model: options.model,
2410
- system: systemPrompt,
2411
- messages: convertToModelMessages(messages),
2412
- stopWhen: stepCountIs(25),
2413
- experimental_transform: config?.transform ?? smoothStream(),
2414
- experimental_context: contextVariables,
2415
- experimental_output: Output.object({ schema: options.schema })
2416
- });
2417
- }
2418
- };
2584
+
2585
+ // packages/text2sql/src/lib/synthesis/synthesizers/schema-synthesizer.ts
2586
+ import pLimit from "p-limit";
2587
+
2588
+ // packages/text2sql/src/lib/agents/question.agent.ts
2589
+ import { groq as groq5 } from "@ai-sdk/groq";
2590
+ import dedent4 from "dedent";
2591
+ import z4 from "zod";
2592
+ import "@deepagents/agent";
2593
+ var complexityInstructions = {
2594
+ simple: dedent4`
2595
+ Generate simple questions that require:
2596
+ - Basic SELECT with single table
2597
+ - Simple WHERE clauses with one condition
2598
+ - COUNT(*) or basic aggregations
2599
+ - No joins required
2600
+ Examples: "How many customers do we have?", "List all products", "What is the total revenue?"
2601
+ `,
2602
+ moderate: dedent4`
2603
+ Generate moderate questions that require:
2604
+ - JOINs between 2-3 tables
2605
+ - Multiple WHERE conditions (AND/OR)
2606
+ - GROUP BY with HAVING clauses
2607
+ - ORDER BY with LIMIT
2608
+ - Basic subqueries
2609
+ Examples: "What are the top 5 customers by total orders?", "Which products have never been ordered?"
2610
+ `,
2611
+ complex: dedent4`
2612
+ Generate complex questions that require:
2613
+ - Multiple JOINs (3+ tables)
2614
+ - Nested subqueries or CTEs
2615
+ - Complex aggregations with multiple GROUP BY columns
2616
+ - CASE expressions
2617
+ - Date/time calculations
2618
+ Examples: "What is the month-over-month growth rate?", "Which customers have increased spending compared to last year?"
2619
+ `,
2620
+ "high complex": dedent4`
2621
+ Generate highly complex questions that require advanced SQL features:
2622
+ - Window functions (ROW_NUMBER, RANK, DENSE_RANK)
2623
+ - LAG, LEAD for comparisons
2624
+ - Running totals (SUM OVER)
2625
+ - Moving averages
2626
+ - PARTITION BY clauses
2627
+ - Complex CTEs with multiple levels
2628
+ Examples: "What is the running total of sales per month?", "Rank customers by their purchase frequency within each region"
2629
+ `
2630
+ };
2631
+ var outputSchema2 = z4.object({
2632
+ questions: z4.array(z4.string().describe("A natural language question about the data")).min(1).describe("List of natural language questions a user might ask")
2633
+ });
2634
+ async function generateQuestions(params) {
2635
+ const { introspection, complexity, count, prompt, model } = params;
2636
+ const context = new ContextEngine({
2637
+ store: new InMemoryContextStore(),
2638
+ chatId: `question-gen-${crypto.randomUUID()}`,
2639
+ userId: "system"
2640
+ });
2641
+ context.set(
2642
+ persona({
2643
+ name: "question_generator",
2644
+ role: "You are a synthetic data generator specializing in creating realistic natural language questions that users might ask about a database.",
2645
+ objective: "Generate diverse, realistic natural language questions that match the specified complexity level"
2646
+ }),
2647
+ fragment("database_schema", introspection || ""),
2648
+ fragment(
2649
+ "complexity",
2650
+ { level: complexity },
2651
+ complexityInstructions[complexity]
2652
+ ),
2653
+ fragment(
2654
+ "task",
2655
+ dedent4`
2656
+ Generate exactly ${count} natural language questions at the "${complexity}" complexity level.
2657
+ The questions should:
2658
+ 1. Match the complexity requirements above
2659
+ 2. Use natural business language, not technical SQL terms
2660
+ 3. Be realistic questions a non-technical user would actually ask
2661
+ 4. Cover different tables and relationships when possible
2662
+ `
2663
+ ),
2664
+ guardrail({
2665
+ rule: "Questions MUST ONLY reference tables and columns that exist in the schema above"
2666
+ }),
2667
+ guardrail({
2668
+ rule: "Before generating each question, verify that ALL entities (tables, columns, relationships) you reference are explicitly listed in the schema"
2669
+ }),
2670
+ guardrail({
2671
+ rule: "DO NOT invent or assume tables/columns that are not explicitly shown in the schema"
2672
+ }),
2673
+ guardrail({
2674
+ rule: "Use natural language without SQL keywords like SELECT, WHERE, etc."
2675
+ }),
2676
+ guardrail({
2677
+ rule: "All questions must match the specified complexity level"
2678
+ }),
2679
+ user(
2680
+ prompt ?? `Generate ${count} questions at ${complexity} complexity given db schema.`
2681
+ )
2682
+ );
2683
+ const questionOutput = structuredOutput({
2684
+ model: model ?? groq5("openai/gpt-oss-20b"),
2685
+ context,
2686
+ schema: outputSchema2
2687
+ });
2688
+ return questionOutput.generate();
2419
2689
  }
2420
2690
 
2421
2691
  // packages/text2sql/src/lib/agents/sql.agent.ts
2692
+ import { groq as groq6 } from "@ai-sdk/groq";
2693
+ import {
2694
+ APICallError,
2695
+ JSONParseError,
2696
+ NoContentGeneratedError,
2697
+ NoObjectGeneratedError,
2698
+ NoOutputGeneratedError,
2699
+ TypeValidationError,
2700
+ defaultSettingsMiddleware,
2701
+ wrapLanguageModel
2702
+ } from "ai";
2703
+ import { Console } from "node:console";
2704
+ import { createWriteStream } from "node:fs";
2705
+ import pRetry from "p-retry";
2706
+ import z5 from "zod";
2707
+ import "@deepagents/agent";
2422
2708
  var logger = new Console({
2423
2709
  stdout: createWriteStream("./sql-agent.log", { flags: "a" }),
2424
2710
  stderr: createWriteStream("./sql-agent-error.log", { flags: "a" }),
@@ -2456,37 +2742,36 @@ async function toSql(options) {
2456
2742
  async (attemptNumber, errors, attempts) => {
2457
2743
  const context = new ContextEngine({
2458
2744
  store: new InMemoryContextStore(),
2459
- chatId: `sql-gen-${crypto.randomUUID()}`
2745
+ chatId: `sql-gen-${crypto.randomUUID()}`,
2746
+ userId: "system"
2460
2747
  });
2461
2748
  context.set(
2462
2749
  persona({
2463
2750
  name: "Freya",
2464
- role: "You are an expert SQL query generator. You translate natural language questions into precise, efficient SQL queries based on the provided database schema."
2751
+ role: "You are an expert SQL query generator. You translate natural language questions into precise, efficient SQL queries based on the provided database schema.",
2752
+ objective: "Translate natural language questions into precise, efficient SQL queries"
2465
2753
  }),
2466
2754
  ...options.instructions,
2467
2755
  ...options.schemaFragments
2468
2756
  );
2469
2757
  if (errors.length) {
2470
2758
  context.set(
2471
- user6(options.input),
2472
- user6(
2759
+ user(options.input),
2760
+ user(
2473
2761
  `<validation_error>Your previous SQL query had the following error: ${errors.at(-1)?.message}. Please fix the query.</validation_error>`
2474
2762
  )
2475
2763
  );
2476
2764
  } else {
2477
- context.set(user6(options.input));
2765
+ context.set(user(options.input));
2478
2766
  }
2767
+ const temperature = RETRY_TEMPERATURES[attemptNumber - 1] ?? RETRY_TEMPERATURES[RETRY_TEMPERATURES.length - 1];
2768
+ const baseModel = options.model ?? groq6("openai/gpt-oss-20b");
2769
+ const model = wrapLanguageModel({
2770
+ model: baseModel,
2771
+ middleware: defaultSettingsMiddleware({ settings: { temperature } })
2772
+ });
2479
2773
  const sqlOutput = structuredOutput({
2480
- name: "text2sql",
2481
- model: wrapLanguageModel2({
2482
- model: options.model ?? groq5("openai/gpt-oss-20b"),
2483
- middleware: defaultSettingsMiddleware2({
2484
- settings: {
2485
- temperature: RETRY_TEMPERATURES[attemptNumber - 1] ?? 0.3,
2486
- topP: 1
2487
- }
2488
- })
2489
- }),
2774
+ model,
2490
2775
  context,
2491
2776
  schema: z5.union([
2492
2777
  z5.object({
@@ -2669,17 +2954,11 @@ Generate ${this.options.count} questions at ${complexity} complexity.` : void 0;
2669
2954
  };
2670
2955
 
2671
2956
  // packages/text2sql/src/lib/synthesis/synthesizers/breadth-evolver.ts
2672
- import { groq as groq6 } from "@ai-sdk/groq";
2673
- import { defaultSettingsMiddleware as defaultSettingsMiddleware3, wrapLanguageModel as wrapLanguageModel3 } from "ai";
2957
+ import { groq as groq7 } from "@ai-sdk/groq";
2674
2958
  import dedent5 from "dedent";
2675
2959
  import pLimit2 from "p-limit";
2676
2960
  import z6 from "zod";
2677
- import {
2678
- agent as agent5,
2679
- generate as generate6,
2680
- toOutput,
2681
- user as user7
2682
- } from "@deepagents/agent";
2961
+ import "@deepagents/agent";
2683
2962
 
2684
2963
  // packages/text2sql/src/lib/synthesis/synthesizers/styles.ts
2685
2964
  var ALL_STYLES = [
@@ -2715,63 +2994,53 @@ var styleInstructions = {
2715
2994
  };
2716
2995
 
2717
2996
  // packages/text2sql/src/lib/synthesis/synthesizers/breadth-evolver.ts
2718
- var paraphraserAgent = agent5({
2719
- name: "question_paraphraser",
2720
- model: wrapLanguageModel3({
2721
- model: groq6("openai/gpt-oss-20b"),
2722
- middleware: defaultSettingsMiddleware3({
2723
- settings: { temperature: 0.9, topP: 0.95, frequencyPenalty: 0.2 }
2724
- })
2725
- }),
2726
- logging: process.env.AGENT_LOGGING === "true",
2727
- output: z6.object({
2728
- paraphrases: z6.array(
2729
- z6.string().describe("A paraphrased version of the original question")
2730
- ).min(1).describe(
2731
- "List of paraphrased questions that would produce the same SQL"
2732
- )
2733
- }),
2734
- prompt: (state) => {
2735
- const personaInstruction = state?.persona ? dedent5`
2736
- <persona role="${state.persona.role}">
2737
- ${state.persona.perspective}
2997
+ var paraphraserOutputSchema = z6.object({
2998
+ paraphrases: z6.array(
2999
+ z6.string().describe("A paraphrased version of the original question")
3000
+ ).min(1).describe("List of paraphrased questions that would produce the same SQL")
3001
+ });
3002
+ async function paraphraseQuestion(params) {
3003
+ const context = new ContextEngine({
3004
+ store: new InMemoryContextStore(),
3005
+ chatId: `paraphraser-${crypto.randomUUID()}`,
3006
+ userId: "system"
3007
+ });
3008
+ const personaInstruction = params.persona ? dedent5`
3009
+ <persona role="${params.persona.role}">
3010
+ ${params.persona.perspective}
2738
3011
 
2739
3012
  Paraphrase the question as this persona would naturally ask it.
2740
3013
  Use their vocabulary, priorities, and framing style.
2741
3014
  </persona>
2742
3015
  ` : "";
2743
- const styleInstruction = state?.persona?.styles && state.persona.styles.length > 0 ? dedent5`
3016
+ const styleInstruction = params.persona?.styles && params.persona.styles.length > 0 ? dedent5`
2744
3017
  <communication_styles>
2745
- Generate paraphrases using these communication styles: ${state.persona.styles.join(", ")}
3018
+ Generate paraphrases using these communication styles: ${params.persona.styles.join(", ")}
2746
3019
 
2747
3020
  Style definitions:
2748
- ${state.persona.styles.map((s) => `- ${s}: ${styleInstructions[s]}`).join("\n")}
3021
+ ${params.persona.styles.map((s) => `- ${s}: ${styleInstructions[s]}`).join("\n")}
2749
3022
 
2750
3023
  Distribute paraphrases across these styles for variety.
2751
3024
  </communication_styles>
2752
3025
  ` : "";
2753
- return dedent5`
2754
- <identity>
2755
- You are a linguistic expert specializing in paraphrasing database questions.
2756
- Your task is to generate alternative phrasings of questions that preserve
2757
- the exact same semantic meaning - they must all produce the identical SQL query.
2758
- </identity>
2759
-
2760
- <original_question>
2761
- ${state?.question}
2762
- </original_question>
2763
-
2764
- <reference_sql>
2765
- ${state?.sql}
2766
- (This SQL shows what the question is really asking - all paraphrases must ask for exactly this)
2767
- </reference_sql>
2768
-
2769
- ${personaInstruction}
2770
-
2771
- ${styleInstruction}
2772
-
2773
- <task>
2774
- Generate exactly ${state?.count} paraphrased versions of the original question.
3026
+ context.set(
3027
+ persona({
3028
+ name: "question_paraphraser",
3029
+ role: "You are a linguistic expert specializing in paraphrasing database questions. Your task is to generate alternative phrasings of questions that preserve the exact same semantic meaning - they must all produce the identical SQL query.",
3030
+ objective: "Generate paraphrased versions of questions that preserve exact semantic meaning and produce identical SQL"
3031
+ }),
3032
+ fragment("original_question", params.question),
3033
+ fragment(
3034
+ "reference_sql",
3035
+ params.sql,
3036
+ "This SQL shows what the question is really asking - all paraphrases must ask for exactly this"
3037
+ ),
3038
+ ...personaInstruction ? [fragment("persona", personaInstruction)] : [],
3039
+ ...styleInstruction ? [fragment("communication_styles", styleInstruction)] : [],
3040
+ fragment(
3041
+ "task",
3042
+ dedent5`
3043
+ Generate exactly ${params.count} paraphrased versions of the original question.
2775
3044
 
2776
3045
  Requirements:
2777
3046
  1. Each paraphrase must be semantically equivalent - it should produce the EXACT same SQL
@@ -2779,18 +3048,30 @@ var paraphraserAgent = agent5({
2779
3048
  3. Use natural language without SQL keywords (SELECT, WHERE, JOIN, etc.)
2780
3049
  4. Keep paraphrases realistic - how actual users would ask
2781
3050
  5. Do not add or remove any conditions, filters, or requirements from the original
2782
- ${state?.persona?.styles?.length ? "6. Apply the specified communication styles to create diverse phrasings" : ""}
2783
- </task>
2784
-
2785
- <guardrails>
2786
- - NEVER change what data is being requested
2787
- - NEVER add filters, aggregations, or conditions not in the original
2788
- - NEVER remove any specificity from the original question
2789
- - All paraphrases must be answerable by the exact same SQL query
2790
- </guardrails>
2791
- `;
2792
- }
2793
- });
3051
+ ${params.persona?.styles?.length ? "6. Apply the specified communication styles to create diverse phrasings" : ""}
3052
+ `
3053
+ ),
3054
+ guardrail({ rule: "NEVER change what data is being requested" }),
3055
+ guardrail({
3056
+ rule: "NEVER add filters, aggregations, or conditions not in the original"
3057
+ }),
3058
+ guardrail({
3059
+ rule: "NEVER remove any specificity from the original question"
3060
+ }),
3061
+ guardrail({
3062
+ rule: "All paraphrases must be answerable by the exact same SQL query"
3063
+ }),
3064
+ user(
3065
+ `Paraphrase this question ${params.count} times: "${params.question}"`
3066
+ )
3067
+ );
3068
+ const paraphraserOutput = structuredOutput({
3069
+ model: params.model ?? groq7("openai/gpt-oss-20b"),
3070
+ context,
3071
+ schema: paraphraserOutputSchema
3072
+ });
3073
+ return paraphraserOutput.generate();
3074
+ }
2794
3075
  var BreadthEvolver = class extends PairProducer {
2795
3076
  /**
2796
3077
  * @param source - Source pairs or producer to evolve
@@ -2811,23 +3092,14 @@ var BreadthEvolver = class extends PairProducer {
2811
3092
  for await (const chunk of this.from(this.source)) {
2812
3093
  const tasks = chunk.map(
2813
3094
  (pair) => this.#limit(async () => {
2814
- const { paraphrases } = await toOutput(
2815
- generate6(
2816
- paraphraserAgent.clone({ model: this.options.model }),
2817
- [
2818
- user7(
2819
- `Paraphrase this question ${this.options.count} times: "${pair.question}"`
2820
- )
2821
- ],
2822
- {
2823
- question: pair.question,
2824
- sql: pair.sql,
2825
- count: this.options.count,
2826
- persona: this.options.persona
2827
- }
2828
- )
2829
- );
2830
- return paraphrases.map((paraphrase) => ({
3095
+ const result = await paraphraseQuestion({
3096
+ question: pair.question,
3097
+ sql: pair.sql,
3098
+ count: this.options.count,
3099
+ persona: this.options.persona,
3100
+ model: this.options.model
3101
+ });
3102
+ return result.paraphrases.map((paraphrase) => ({
2831
3103
  question: paraphrase,
2832
3104
  sql: pair.sql,
2833
3105
  context: pair.context,
@@ -2842,18 +3114,13 @@ var BreadthEvolver = class extends PairProducer {
2842
3114
  };
2843
3115
 
2844
3116
  // packages/text2sql/src/lib/synthesis/synthesizers/depth-evolver.ts
2845
- import { groq as groq7 } from "@ai-sdk/groq";
2846
- import {
2847
- NoObjectGeneratedError as NoObjectGeneratedError2,
2848
- NoOutputGeneratedError as NoOutputGeneratedError2,
2849
- defaultSettingsMiddleware as defaultSettingsMiddleware4,
2850
- wrapLanguageModel as wrapLanguageModel4
2851
- } from "ai";
3117
+ import { groq as groq8 } from "@ai-sdk/groq";
3118
+ import { NoObjectGeneratedError as NoObjectGeneratedError2, NoOutputGeneratedError as NoOutputGeneratedError2 } from "ai";
2852
3119
  import dedent6 from "dedent";
2853
3120
  import pLimit3 from "p-limit";
2854
3121
  import pRetry2 from "p-retry";
2855
3122
  import z7 from "zod";
2856
- import { agent as agent6, generate as generate7, user as user8 } from "@deepagents/agent";
3123
+ import "@deepagents/agent";
2857
3124
  var techniqueInstructions = {
2858
3125
  "add-aggregation": dedent6`
2859
3126
  Add aggregation requirements to the question.
@@ -2896,44 +3163,37 @@ var techniqueInstructions = {
2896
3163
  - "Get costs" → "What would be the impact of a 10% discount on profit margins?"
2897
3164
  `
2898
3165
  };
2899
- var questionEvolverAgent = agent6({
2900
- name: "question_evolver",
2901
- model: wrapLanguageModel4({
2902
- model: groq7("openai/gpt-oss-20b"),
2903
- middleware: defaultSettingsMiddleware4({
2904
- settings: { temperature: 0.7, topP: 0.95 }
2905
- })
2906
- }),
2907
- output: z7.object({
2908
- evolvedQuestion: z7.string().describe("The evolved, more complex version of the original question")
2909
- }),
2910
- prompt: (state) => {
2911
- return dedent6`
2912
- <identity>
2913
- You are an expert at evolving simple database questions into more complex ones.
2914
- Your task is to take a basic question and transform it into a more sophisticated
2915
- version that requires advanced SQL techniques to answer.
2916
- </identity>
2917
-
2918
- <original_question>
2919
- ${state?.question}
2920
- </original_question>
2921
-
2922
- <original_sql>
2923
- ${state?.sql}
2924
- (This shows what the original question required)
2925
- </original_sql>
2926
-
2927
- <database_schema>
2928
- ${state?.schema}
2929
- </database_schema>
2930
-
2931
- <technique name="${state?.technique}">
2932
- ${state?.techniqueInstruction}
2933
- </technique>
2934
-
2935
- <task>
2936
- Evolve the original question using the "${state?.technique}" technique.
3166
+ var evolverOutputSchema = z7.object({
3167
+ evolvedQuestion: z7.string().describe("The evolved, more complex version of the original question")
3168
+ });
3169
+ async function evolveQuestion(params) {
3170
+ const context = new ContextEngine({
3171
+ store: new InMemoryContextStore(),
3172
+ chatId: `evolver-${crypto.randomUUID()}`,
3173
+ userId: "system"
3174
+ });
3175
+ context.set(
3176
+ persona({
3177
+ name: "question_evolver",
3178
+ role: "You are an expert at evolving simple database questions into more complex ones. Your task is to take a basic question and transform it into a more sophisticated version that requires advanced SQL techniques to answer.",
3179
+ objective: "Transform simple questions into complex versions requiring advanced SQL techniques"
3180
+ }),
3181
+ fragment("original_question", params.question),
3182
+ fragment(
3183
+ "original_sql",
3184
+ params.sql,
3185
+ "(This shows what the original question required)"
3186
+ ),
3187
+ fragment("database_schema", params.schema),
3188
+ fragment(
3189
+ "technique",
3190
+ { name: params.technique },
3191
+ params.techniqueInstruction
3192
+ ),
3193
+ fragment(
3194
+ "task",
3195
+ dedent6`
3196
+ Evolve the original question using the "${params.technique}" technique.
2937
3197
 
2938
3198
  Requirements:
2939
3199
  1. The evolved question must be MORE COMPLEX than the original
@@ -2942,17 +3202,29 @@ var questionEvolverAgent = agent6({
2942
3202
  4. Use natural language - no SQL keywords
2943
3203
  5. Keep the question realistic and practical
2944
3204
  6. The evolved question should build upon the original topic/domain
2945
- </task>
2946
-
2947
- <guardrails>
2948
- - The evolved question MUST require more complex SQL than the original
2949
- - Do not ask for data that doesn't exist in the schema
2950
- - Keep the question grounded in the same domain as the original
2951
- - Make sure the question is clear and unambiguous
2952
- </guardrails>
2953
- `;
2954
- }
2955
- });
3205
+ `
3206
+ ),
3207
+ guardrail({
3208
+ rule: "The evolved question MUST require more complex SQL than the original"
3209
+ }),
3210
+ guardrail({
3211
+ rule: "Do not ask for data that does not exist in the schema"
3212
+ }),
3213
+ guardrail({
3214
+ rule: "Keep the question grounded in the same domain as the original"
3215
+ }),
3216
+ guardrail({ rule: "Make sure the question is clear and unambiguous" }),
3217
+ user(
3218
+ `Evolve this question using "${params.technique}": "${params.question}"`
3219
+ )
3220
+ );
3221
+ const evolverOutput = structuredOutput({
3222
+ model: params.model ?? groq8("openai/gpt-oss-20b"),
3223
+ context,
3224
+ schema: evolverOutputSchema
3225
+ });
3226
+ return evolverOutput.generate();
3227
+ }
2956
3228
  var ALL_TECHNIQUES = [
2957
3229
  "add-aggregation",
2958
3230
  "add-filter",
@@ -2998,22 +3270,17 @@ var DepthEvolver = class extends PairProducer {
2998
3270
  }
2999
3271
  }
3000
3272
  async #processTask(pair, technique, introspection) {
3001
- const { experimental_output } = await withRetry2(
3002
- () => generate7(
3003
- questionEvolverAgent.clone({
3004
- model: this.options?.model
3005
- }),
3006
- [user8(`Evolve this question using "${technique}": "${pair.question}"`)],
3007
- {
3008
- question: pair.question,
3009
- sql: pair.sql,
3010
- schema: introspection,
3011
- technique,
3012
- techniqueInstruction: techniqueInstructions[technique]
3013
- }
3014
- )
3273
+ const output = await withRetry2(
3274
+ () => evolveQuestion({
3275
+ question: pair.question,
3276
+ sql: pair.sql,
3277
+ schema: introspection,
3278
+ technique,
3279
+ techniqueInstruction: techniqueInstructions[technique],
3280
+ model: this.options?.model
3281
+ })
3015
3282
  );
3016
- const evolvedQuestion = experimental_output.evolvedQuestion;
3283
+ const evolvedQuestion = output.evolvedQuestion;
3017
3284
  try {
3018
3285
  const sqlResult = await toSql({
3019
3286
  input: evolvedQuestion,
@@ -3069,12 +3336,11 @@ async function withRetry2(computation) {
3069
3336
  }
3070
3337
 
3071
3338
  // packages/text2sql/src/lib/synthesis/synthesizers/persona-generator.ts
3072
- import { groq as groq8 } from "@ai-sdk/groq";
3073
- import { defaultSettingsMiddleware as defaultSettingsMiddleware5, wrapLanguageModel as wrapLanguageModel5 } from "ai";
3339
+ import { groq as groq9 } from "@ai-sdk/groq";
3074
3340
  import dedent7 from "dedent";
3075
3341
  import z8 from "zod";
3076
3342
  import "@deepagents/agent";
3077
- var outputSchema = z8.object({
3343
+ var outputSchema3 = z8.object({
3078
3344
  personas: z8.array(
3079
3345
  z8.object({
3080
3346
  role: z8.string().describe("The job title or role of this persona"),
@@ -3092,48 +3358,52 @@ async function generatePersonas(schemaFragments, options) {
3092
3358
  const count = options?.count ?? 5;
3093
3359
  const context = new ContextEngine({
3094
3360
  store: new InMemoryContextStore(),
3095
- chatId: `persona-gen-${crypto.randomUUID()}`
3361
+ chatId: `persona-gen-${crypto.randomUUID()}`,
3362
+ userId: "system"
3096
3363
  });
3097
3364
  context.set(
3098
3365
  persona({
3099
3366
  name: "persona_generator",
3100
- role: "You are an expert at understanding database schemas and inferring who would use them."
3367
+ role: "You are an expert at understanding database schemas and inferring who would use them.",
3368
+ objective: "Generate realistic personas representing users who would query this database"
3101
3369
  }),
3102
- role(dedent7`
3103
- Your task is to analyze a database schema and generate realistic personas representing
3104
- the different types of users who would query this database.
3105
-
3106
- <database_schema>
3107
- ${schema}
3108
- </database_schema>
3370
+ fragment("database_schema", schema),
3371
+ fragment(
3372
+ "task",
3373
+ dedent7`
3374
+ Analyze the database schema and generate realistic personas representing
3375
+ the different types of users who would query this database.
3376
+
3377
+ For each persona, provide:
3378
+ 1. **role**: Their job title or role (e.g., "Financial Analyst", "Customer Support Rep")
3379
+ 2. **perspective**: A rich description of what they care about, including:
3380
+ - What questions they typically ask
3381
+ - What metrics/data points matter to them
3382
+ - How they prefer data formatted or presented
3383
+ - Their priorities (speed vs accuracy, detail vs summary)
3384
+ - Domain-specific concerns relevant to their role
3385
+ 3. **styles**: 1-3 communication styles typical for this persona. Choose from:
3386
+ - formal: Professional business language, complete sentences
3387
+ - colloquial: Casual everyday speech, contractions
3388
+ - imperative: Commands like "Show me...", "Get...", "List..."
3389
+ - interrogative: Questions like "What is...", "How many..."
3390
+ - descriptive: Verbose, detailed phrasing
3391
+ - concise: Brief, minimal words
3392
+ - vague: Ambiguous, hedging language
3393
+ - metaphorical: Figurative language, analogies
3394
+ - conversational: Chat-like, casual tone
3109
3395
 
3110
- For each persona, provide:
3111
- 1. **role**: Their job title or role (e.g., "Financial Analyst", "Customer Support Rep")
3112
- 2. **perspective**: A rich description of what they care about, including:
3113
- - What questions they typically ask
3114
- - What metrics/data points matter to them
3115
- - How they prefer data formatted or presented
3116
- - Their priorities (speed vs accuracy, detail vs summary)
3117
- - Domain-specific concerns relevant to their role
3118
- 3. **styles**: 1-3 communication styles typical for this persona. Choose from:
3119
- - formal: Professional business language, complete sentences
3120
- - colloquial: Casual everyday speech, contractions
3121
- - imperative: Commands like "Show me...", "Get...", "List..."
3122
- - interrogative: Questions like "What is...", "How many..."
3123
- - descriptive: Verbose, detailed phrasing
3124
- - concise: Brief, minimal words
3125
- - vague: Ambiguous, hedging language
3126
- - metaphorical: Figurative language, analogies
3127
- - conversational: Chat-like, casual tone
3128
-
3129
- Requirements:
3130
- - Personas should be realistic for the given schema
3131
- - Each persona should have distinct concerns and priorities
3132
- - Perspectives should be detailed enough to guide question paraphrasing
3133
- - Cover different levels of technical expertise (some technical, some business-focused)
3134
- - Styles should match how this persona would naturally communicate
3135
-
3136
- <example>
3396
+ Requirements:
3397
+ - Personas should be realistic for the given schema
3398
+ - Each persona should have distinct concerns and priorities
3399
+ - Perspectives should be detailed enough to guide question paraphrasing
3400
+ - Cover different levels of technical expertise (some technical, some business-focused)
3401
+ - Styles should match how this persona would naturally communicate
3402
+ `
3403
+ ),
3404
+ fragment(
3405
+ "example",
3406
+ dedent7`
3137
3407
  For an e-commerce schema with orders, customers, products tables:
3138
3408
 
3139
3409
  {
@@ -3147,40 +3417,36 @@ async function generatePersonas(schemaFragments, options) {
3147
3417
  "perspective": "As inventory manager, I care about:\\n- Current stock levels and reorder points\\n- Product availability across warehouses\\n- Slow-moving inventory identification\\n- Supplier lead times and pending orders\\n- I need accurate counts, often aggregated by location",
3148
3418
  "styles": ["formal", "interrogative"]
3149
3419
  }
3150
- </example>
3151
-
3152
- <guardrails>
3153
- - Only generate personas relevant to the actual schema provided
3154
- - Do not invent tables or data that don't exist in the schema
3155
- - Ensure perspectives are specific to the domain, not generic
3156
- </guardrails>
3157
- `),
3158
- user6(
3420
+ `
3421
+ ),
3422
+ guardrail({
3423
+ rule: "Only generate personas relevant to the actual schema provided"
3424
+ }),
3425
+ guardrail({
3426
+ rule: "Do not invent tables or data that do not exist in the schema"
3427
+ }),
3428
+ guardrail({
3429
+ rule: "Ensure perspectives are specific to the domain, not generic"
3430
+ }),
3431
+ user(
3159
3432
  `Generate exactly ${count} distinct personas who would query this database.`
3160
3433
  )
3161
3434
  );
3162
3435
  const personaOutput = structuredOutput({
3163
- name: "persona_generator",
3164
- model: wrapLanguageModel5({
3165
- model: options?.model ?? groq8("openai/gpt-oss-20b"),
3166
- middleware: defaultSettingsMiddleware5({
3167
- settings: { temperature: 0.8, topP: 0.95, presencePenalty: 0.2 }
3168
- })
3169
- }),
3436
+ model: options?.model ?? groq9("openai/gpt-oss-20b"),
3170
3437
  context,
3171
- schema: outputSchema
3438
+ schema: outputSchema3
3172
3439
  });
3173
3440
  const output = await personaOutput.generate();
3174
3441
  return output.personas;
3175
3442
  }
3176
3443
 
3177
3444
  // packages/text2sql/src/lib/agents/teachables.agent.ts
3178
- import { groq as groq9 } from "@ai-sdk/groq";
3179
- import { defaultSettingsMiddleware as defaultSettingsMiddleware6, wrapLanguageModel as wrapLanguageModel6 } from "ai";
3445
+ import { groq as groq10 } from "@ai-sdk/groq";
3180
3446
  import dedent8 from "dedent";
3181
3447
  import z9 from "zod";
3182
- import { agent as agent7, generate as generate8, user as user9 } from "@deepagents/agent";
3183
- var outputSchema2 = z9.object({
3448
+ import "@deepagents/agent";
3449
+ var outputSchema4 = z9.object({
3184
3450
  terms: z9.array(z9.object({ name: z9.string(), definition: z9.string() })).optional().describe("Domain terminology definitions"),
3185
3451
  hints: z9.array(z9.object({ text: z9.string() })).optional().describe("Helpful hints for SQL generation"),
3186
3452
  guardrails: z9.array(
@@ -3231,62 +3497,58 @@ var outputSchema2 = z9.object({
3231
3497
  })
3232
3498
  ).optional().describe("Concept analogies")
3233
3499
  });
3234
- var teachablesAuthorAgent = agent7({
3235
- name: "teachables-author",
3236
- model: wrapLanguageModel6({
3237
- model: groq9("openai/gpt-oss-20b"),
3238
- middleware: defaultSettingsMiddleware6({
3239
- settings: { temperature: 0.4, topP: 0.95 }
3240
- })
3241
- }),
3242
- output: outputSchema2,
3243
- prompt: (state) => dedent8`
3244
- <identity>
3245
- You design "fragments" for a Text2SQL system. Fragments become structured XML instructions.
3246
- Choose only high-impact items that improve accuracy, safety, or clarity for this database.
3247
- </identity>
3248
-
3249
- <database_schema>
3250
- ${state?.schema}
3251
- </database_schema>
3252
-
3253
- ${state?.context ? `<additional_context>${state.context}</additional_context>` : ""}
3254
-
3255
- <output_structure>
3256
- Output a JSON object with these optional arrays (include only relevant ones):
3257
- - terms: [{ name: string, definition: string }] - Domain terminology
3258
- - hints: [{ text: string }] - Helpful SQL generation hints
3259
- - guardrails: [{ rule: string, reason?: string, action?: string }] - Safety constraints
3260
- - explains: [{ concept: string, explanation: string, therefore?: string }] - Concept explanations
3261
- - examples: [{ question: string, answer: string, note?: string }] - Q&A examples
3262
- - clarifications: [{ when: string, ask: string, reason: string }] - Clarification triggers
3263
- - workflows: [{ task: string, steps: string[], triggers?: string[], notes?: string }] - Multi-step tasks
3264
- - quirks: [{ issue: string, workaround: string }] - Known issues
3265
- - styleGuides: [{ prefer: string, never?: string, always?: string }] - SQL style rules
3266
- - analogies: [{ concepts: string[], relationship: string, insight?: string, therefore?: string, pitfall?: string }]
3267
- </output_structure>
3268
-
3269
- <instructions>
3270
- 1. Analyze the schema to infer domain, relationships, and sensitive columns.
3271
- 2. Generate 3-10 fragments total across all categories, prioritizing:
3272
- - guardrails for PII columns (email, ssn, phone, etc)
3273
- - hints for status/enum columns
3274
- - clarifications for ambiguous terms
3275
- 3. Ground everything in the schema - do not invent tables/columns.
3276
- 4. Only include categories that are relevant to this schema.
3277
- </instructions>
3278
- `
3279
- });
3280
3500
  async function toTeachings(input, options) {
3281
- const { experimental_output: result } = await generate8(
3282
- teachablesAuthorAgent.clone({ model: options?.model }),
3283
- [
3284
- user9(
3285
- `Analyze this database schema and generate fragments that will help an AI generate accurate SQL queries.`
3286
- )
3287
- ],
3288
- input
3501
+ const context = new ContextEngine({
3502
+ store: new InMemoryContextStore(),
3503
+ chatId: `teachables-gen-${crypto.randomUUID()}`,
3504
+ userId: "system"
3505
+ });
3506
+ context.set(
3507
+ persona({
3508
+ name: "teachables-author",
3509
+ role: 'You design "fragments" for a Text2SQL system. Fragments become structured XML instructions.',
3510
+ objective: "Choose only high-impact items that improve accuracy, safety, or clarity for this database"
3511
+ }),
3512
+ fragment("database_schema", input.schema),
3513
+ ...input.context ? [fragment("additional_context", input.context)] : [],
3514
+ fragment(
3515
+ "output_structure",
3516
+ dedent8`
3517
+ Output a JSON object with these optional arrays (include only relevant ones):
3518
+ - terms: [{ name: string, definition: string }] - Domain terminology
3519
+ - hints: [{ text: string }] - Helpful SQL generation hints
3520
+ - guardrails: [{ rule: string, reason?: string, action?: string }] - Safety constraints
3521
+ - explains: [{ concept: string, explanation: string, therefore?: string }] - Concept explanations
3522
+ - examples: [{ question: string, answer: string, note?: string }] - Q&A examples
3523
+ - clarifications: [{ when: string, ask: string, reason: string }] - Clarification triggers
3524
+ - workflows: [{ task: string, steps: string[], triggers?: string[], notes?: string }] - Multi-step tasks
3525
+ - quirks: [{ issue: string, workaround: string }] - Known issues
3526
+ - styleGuides: [{ prefer: string, never?: string, always?: string }] - SQL style rules
3527
+ - analogies: [{ concepts: string[], relationship: string, insight?: string, therefore?: string, pitfall?: string }]
3528
+ `
3529
+ ),
3530
+ fragment(
3531
+ "task",
3532
+ dedent8`
3533
+ 1. Analyze the schema to infer domain, relationships, and sensitive columns.
3534
+ 2. Generate 3-10 fragments total across all categories, prioritizing:
3535
+ - guardrails for PII columns (email, ssn, phone, etc)
3536
+ - hints for status/enum columns
3537
+ - clarifications for ambiguous terms
3538
+ 3. Ground everything in the schema - do not invent tables/columns.
3539
+ 4. Only include categories that are relevant to this schema.
3540
+ `
3541
+ ),
3542
+ user(
3543
+ `Analyze this database schema and generate fragments that will help an AI generate accurate SQL queries.`
3544
+ )
3289
3545
  );
3546
+ const teachablesOutput = structuredOutput({
3547
+ model: options?.model ?? groq10("openai/gpt-oss-20b"),
3548
+ context,
3549
+ schema: outputSchema4
3550
+ });
3551
+ const result = await teachablesOutput.generate();
3290
3552
  const fragments = [];
3291
3553
  result.terms?.forEach((t) => fragments.push(term(t.name, t.definition)));
3292
3554
  result.hints?.forEach((h) => fragments.push(hint(h.text)));
@@ -3383,11 +3645,11 @@ export {
3383
3645
  SqlExtractor,
3384
3646
  ValidatedProducer,
3385
3647
  WindowedContextExtractor,
3386
- contextResolverAgent,
3387
3648
  formatConversation,
3388
3649
  generatePersonas,
3389
3650
  generateTeachings,
3390
3651
  getMessageText,
3652
+ resolveContext,
3391
3653
  styleInstructions,
3392
3654
  toPairs
3393
3655
  };