@duckcodeailabs/dql-cli 1.6.30 → 1.6.32

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 (85) hide show
  1. package/dist/apps-api.d.ts.map +1 -1
  2. package/dist/apps-api.js +3 -1
  3. package/dist/apps-api.js.map +1 -1
  4. package/dist/args.d.ts +18 -0
  5. package/dist/args.d.ts.map +1 -1
  6. package/dist/args.js +37 -0
  7. package/dist/args.js.map +1 -1
  8. package/dist/assets/dql-notebook/assets/index-B6x1cRJo.js +5698 -0
  9. package/dist/assets/dql-notebook/index.html +1 -1
  10. package/dist/commands/agent.d.ts +47 -1
  11. package/dist/commands/agent.d.ts.map +1 -1
  12. package/dist/commands/agent.js +635 -27
  13. package/dist/commands/agent.js.map +1 -1
  14. package/dist/commands/app.d.ts.map +1 -1
  15. package/dist/commands/app.js +3 -1
  16. package/dist/commands/app.js.map +1 -1
  17. package/dist/commands/diff.d.ts.map +1 -1
  18. package/dist/commands/diff.js +111 -22
  19. package/dist/commands/diff.js.map +1 -1
  20. package/dist/commands/eval-judge.d.ts +37 -0
  21. package/dist/commands/eval-judge.d.ts.map +1 -0
  22. package/dist/commands/eval-judge.js +99 -0
  23. package/dist/commands/eval-judge.js.map +1 -0
  24. package/dist/commands/eval.d.ts +27 -1
  25. package/dist/commands/eval.d.ts.map +1 -1
  26. package/dist/commands/eval.js +143 -6
  27. package/dist/commands/eval.js.map +1 -1
  28. package/dist/commands/mcp.js +27 -3
  29. package/dist/commands/mcp.js.map +1 -1
  30. package/dist/index.js +6 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/llm/analytics-tools.d.ts.map +1 -1
  33. package/dist/llm/analytics-tools.js +12 -7
  34. package/dist/llm/analytics-tools.js.map +1 -1
  35. package/dist/llm/answer-loop-tools.d.ts +7 -0
  36. package/dist/llm/answer-loop-tools.d.ts.map +1 -0
  37. package/dist/llm/answer-loop-tools.js +67 -0
  38. package/dist/llm/answer-loop-tools.js.map +1 -0
  39. package/dist/llm/proposal-metadata.d.ts +5 -0
  40. package/dist/llm/proposal-metadata.d.ts.map +1 -0
  41. package/dist/llm/proposal-metadata.js +27 -0
  42. package/dist/llm/proposal-metadata.js.map +1 -0
  43. package/dist/llm/providers/claude-agent-sdk.d.ts +6 -0
  44. package/dist/llm/providers/claude-agent-sdk.d.ts.map +1 -1
  45. package/dist/llm/providers/claude-agent-sdk.js +17 -5
  46. package/dist/llm/providers/claude-agent-sdk.js.map +1 -1
  47. package/dist/llm/providers/claude-code.d.ts +7 -1
  48. package/dist/llm/providers/claude-code.d.ts.map +1 -1
  49. package/dist/llm/providers/claude-code.js +11 -2
  50. package/dist/llm/providers/claude-code.js.map +1 -1
  51. package/dist/llm/providers/dql-agent-provider.d.ts +29 -0
  52. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  53. package/dist/llm/providers/dql-agent-provider.js +777 -32
  54. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  55. package/dist/llm/providers/native-sdk-provider.d.ts.map +1 -1
  56. package/dist/llm/providers/native-sdk-provider.js +7 -1
  57. package/dist/llm/providers/native-sdk-provider.js.map +1 -1
  58. package/dist/llm/tools.d.ts.map +1 -1
  59. package/dist/llm/tools.js +34 -137
  60. package/dist/llm/tools.js.map +1 -1
  61. package/dist/llm/types.d.ts +73 -2
  62. package/dist/llm/types.d.ts.map +1 -1
  63. package/dist/local-runtime.d.ts +160 -5
  64. package/dist/local-runtime.d.ts.map +1 -1
  65. package/dist/local-runtime.js +1862 -181
  66. package/dist/local-runtime.js.map +1 -1
  67. package/dist/package.json +10 -10
  68. package/dist/providers/oauth/claude-oauth.d.ts +80 -0
  69. package/dist/providers/oauth/claude-oauth.d.ts.map +1 -0
  70. package/dist/providers/oauth/claude-oauth.js +352 -0
  71. package/dist/providers/oauth/claude-oauth.js.map +1 -0
  72. package/dist/providers/oauth/codex-oauth.d.ts +70 -0
  73. package/dist/providers/oauth/codex-oauth.d.ts.map +1 -0
  74. package/dist/providers/oauth/codex-oauth.js +413 -0
  75. package/dist/providers/oauth/codex-oauth.js.map +1 -0
  76. package/dist/providers/oauth/oauth-store.d.ts +31 -0
  77. package/dist/providers/oauth/oauth-store.d.ts.map +1 -0
  78. package/dist/providers/oauth/oauth-store.js +61 -0
  79. package/dist/providers/oauth/oauth-store.js.map +1 -0
  80. package/dist/settings/provider-settings.d.ts +18 -0
  81. package/dist/settings/provider-settings.d.ts.map +1 -1
  82. package/dist/settings/provider-settings.js +44 -10
  83. package/dist/settings/provider-settings.js.map +1 -1
  84. package/package.json +11 -11
  85. package/dist/assets/dql-notebook/assets/index-QJKCrLXe.js +0 -5648
@@ -6,6 +6,13 @@
6
6
  * [--user alice@acme.com] (filters Skills + records feedback as this user)
7
7
  * [--domain growth] (scopes KG search)
8
8
  * [--format json] (emits structured JSON instead of prose)
9
+ * [--thread <id>] (continue a persisted conversation thread: the
10
+ * question runs through the runtime's agent-run
11
+ * engine, which injects prior turns and records
12
+ * this one server-side)
13
+ *
14
+ * dql agent threads
15
+ * Lists persisted conversation threads (id, updated, title) from the runtime.
9
16
  *
10
17
  * dql agent reindex [path]
11
18
  * Rebuilds .dql/cache/agent-kg.sqlite and metadata.sqlite from the
@@ -17,9 +24,11 @@
17
24
  import { existsSync, readFileSync } from 'node:fs';
18
25
  import { join, resolve } from 'node:path';
19
26
  import { load as loadYaml } from 'js-yaml';
20
- import { KGStore, MemoryStore, defaultKgPath, defaultMemoryPath, reindexProject, loadSkills, pickProvider, answer, buildLocalContextPack, deriveGeneratedDraftSlug, recordQueryRun, upsertGeneratedDraft, validateSqlAgainstLocalContext, } from '@duckcodeailabs/dql-agent';
27
+ import { KGStore, MemoryStore, defaultKgPath, defaultMemoryPath, reindexProject, loadSkills, pickProvider, answer, buildAnalysisQuestionPlan, buildLocalContextPack, coerceReasoningEffort, contextRetrievalBudgetForQuestion, deriveGeneratedDraftSlug, loadAgentSemanticLayer, recordQueryRun, recordRuntimeSchemaSnapshot, upsertGeneratedDqlArtifactDraft, upsertGeneratedDraft, validateSqlAgainstLocalContext, } from '@duckcodeailabs/dql-agent';
21
28
  import { buildManifest, resolveDbtManifestPath } from '@duckcodeailabs/dql-core';
22
29
  import { findProjectRoot } from '../local-runtime.js';
30
+ import { buildAnswerLoopTools, createGroundingContextExpander } from '../llm/answer-loop-tools.js';
31
+ import { judgeAnswer } from './eval-judge.js';
23
32
  import { startProjectRuntime } from './notebook.js';
24
33
  /**
25
34
  * Resolve the runtime the agent posts certified blocks / generated SQL to.
@@ -46,6 +55,91 @@ async function resolveAgentRuntime(projectRoot, flags) {
46
55
  const handle = await startProjectRuntime(projectRoot, { preferredPort: 0 });
47
56
  return { runtimeBase: handle.url, close: handle.close };
48
57
  }
58
+ async function fetchRuntimeSchemaContext(runtimeBase) {
59
+ try {
60
+ const response = await fetch(`${runtimeBase.replace(/\/$/, '')}/api/schema`);
61
+ if (!response.ok)
62
+ return [];
63
+ const raw = await response.json();
64
+ if (!Array.isArray(raw))
65
+ return [];
66
+ return raw
67
+ .map(normalizeRuntimeSchemaTable)
68
+ .filter((table) => Boolean(table))
69
+ .slice(0, 500);
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ function normalizeRuntimeSchemaTable(raw) {
76
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
77
+ return undefined;
78
+ const table = raw;
79
+ const relation = cleanRuntimeSchemaString(table.path) ?? cleanRuntimeSchemaString(table.name);
80
+ if (!relation)
81
+ return undefined;
82
+ const columns = Array.isArray(table.columns)
83
+ ? table.columns
84
+ .map(normalizeRuntimeSchemaColumn)
85
+ .filter((column) => Boolean(column))
86
+ .slice(0, 120)
87
+ : [];
88
+ return {
89
+ relation,
90
+ name: cleanRuntimeSchemaString(table.name) ?? relation.split('.').pop() ?? relation,
91
+ source: cleanRuntimeSchemaString(table.source) ?? 'runtime schema',
92
+ columns,
93
+ };
94
+ }
95
+ function normalizeRuntimeSchemaColumn(raw) {
96
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
97
+ return undefined;
98
+ const column = raw;
99
+ const name = cleanRuntimeSchemaString(column.name);
100
+ if (!name)
101
+ return undefined;
102
+ return {
103
+ name,
104
+ type: cleanRuntimeSchemaString(column.type),
105
+ description: cleanRuntimeSchemaString(column.description),
106
+ };
107
+ }
108
+ function recordCliRuntimeSchemaSnapshot(projectRoot, schemaContext, source) {
109
+ if (schemaContext.length === 0)
110
+ return;
111
+ try {
112
+ recordRuntimeSchemaSnapshot(projectRoot, {
113
+ source,
114
+ tables: schemaContext.map((table) => ({
115
+ relation: table.relation,
116
+ schema: table.schema,
117
+ name: table.name,
118
+ description: table.description,
119
+ source: table.source,
120
+ columns: table.columns.map((column) => ({
121
+ name: column.name,
122
+ type: column.type,
123
+ description: column.description,
124
+ sampleValues: column.sampleValues?.slice(0, 8),
125
+ })),
126
+ })),
127
+ });
128
+ }
129
+ catch {
130
+ // Runtime schema snapshots are advisory local metadata and must not block answers.
131
+ }
132
+ }
133
+ function cleanRuntimeSchemaString(value) {
134
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
135
+ }
136
+ function cliReasoningEffort(flags) {
137
+ return coerceReasoningEffort(flags.reasoningEffort);
138
+ }
139
+ function cliAnalysisDepth(flags) {
140
+ const value = flags.analysisDepth?.trim().toLowerCase();
141
+ return value === 'quick' || value === 'deep' ? value : undefined;
142
+ }
49
143
  /** A DQL runtime answers `/api/connections` with a connector/connection payload. */
50
144
  async function isDqlRuntime(base) {
51
145
  try {
@@ -63,6 +157,8 @@ export async function runAgent(sub, rest, flags) {
63
157
  switch (sub) {
64
158
  case 'ask':
65
159
  return runAsk(rest, flags);
160
+ case 'threads':
161
+ return runThreads(flags);
66
162
  case 'reindex':
67
163
  return runReindex(rest, flags);
68
164
  case 'feedback':
@@ -70,8 +166,9 @@ export async function runAgent(sub, rest, flags) {
70
166
  case 'eval':
71
167
  return runEval(rest, flags);
72
168
  default:
73
- throw new Error('Usage: dql agent <ask|reindex|feedback|eval> [args]\n' +
74
- ' dql agent ask "<question>" [--provider claude|openai|gemini|ollama] [--user <id>] [--domain <d>]\n' +
169
+ throw new Error('Usage: dql agent <ask|threads|reindex|feedback|eval> [args]\n' +
170
+ ' dql agent ask "<question>" [--provider claude|openai|gemini|ollama] [--user <id>] [--domain <d>] [--thread <id>]\n' +
171
+ ' dql agent threads [--runtime-url <url>]\n' +
75
172
  ' dql agent reindex [path]\n' +
76
173
  ' dql agent feedback up|down --block <id> --question "..."\n' +
77
174
  ' dql agent eval agent-evals.yml [--provider claude|openai|gemini|ollama] [--execute] [--save]');
@@ -81,6 +178,12 @@ async function runAsk(rest, flags) {
81
178
  const question = rest.join(' ').trim();
82
179
  if (!question)
83
180
  throw new Error('Usage: dql agent ask "<question>"');
181
+ // Thread-scoped ask: hand the question to the runtime's agent-run engine with
182
+ // the thread id, so the SERVER injects prior turns and persists this run as a
183
+ // new turn (the same conversation store the notebook UI uses).
184
+ const threadId = flags.thread;
185
+ if (threadId)
186
+ return runThreadAsk(question, threadId, flags);
84
187
  const projectRoot = findProjectRoot(process.cwd());
85
188
  const kgPath = defaultKgPath(projectRoot);
86
189
  await reindexProject(projectRoot, { kgPath });
@@ -88,6 +191,8 @@ async function runAsk(rest, flags) {
88
191
  const userId = flags.user;
89
192
  const domain = flags.domain;
90
193
  const format = flags.format;
194
+ const reasoningEffort = cliReasoningEffort(flags);
195
+ const requestedDepth = cliAnalysisDepth(flags);
91
196
  const provider = await pickProvider(providerName);
92
197
  const kg = new KGStore(kgPath);
93
198
  const memory = new MemoryStore(defaultMemoryPath(projectRoot));
@@ -99,10 +204,31 @@ async function runAsk(rest, flags) {
99
204
  scopes: ['project', 'user', 'artifact'],
100
205
  limit: 6,
101
206
  });
102
- const contextPack = await buildLocalContextPack(projectRoot, { question, limit: 80 }).catch(() => undefined);
207
+ const semanticLayer = loadAgentSemanticLayer(projectRoot);
208
+ const questionPlan = buildAnalysisQuestionPlan(question);
209
+ const contextBudget = contextRetrievalBudgetForQuestion({
210
+ questionPlan,
211
+ requestedDepth,
212
+ reasoningEffort,
213
+ });
103
214
  const manifest = buildManifest({ projectRoot, dbtManifestPath: resolveDbtManifestPath(projectRoot) ?? undefined });
104
215
  const { runtimeBase, close } = await resolveAgentRuntime(projectRoot, flags);
105
216
  closeRuntime = close;
217
+ const schemaContext = await fetchRuntimeSchemaContext(runtimeBase);
218
+ recordCliRuntimeSchemaSnapshot(projectRoot, schemaContext, 'direct CLI runtime schema');
219
+ const contextPack = await buildLocalContextPack(projectRoot, {
220
+ question,
221
+ surface: 'cli',
222
+ strictness: contextBudget.strictness,
223
+ limit: contextBudget.limit,
224
+ runtimeSchemaSnapshot: schemaContext.length > 0
225
+ ? {
226
+ source: 'direct CLI runtime schema',
227
+ tables: schemaContext,
228
+ }
229
+ : undefined,
230
+ }).catch(() => undefined);
231
+ const answerLoopTools = buildAnswerLoopTools(projectRoot, { serverUrl: runtimeBase });
106
232
  const result = await answer({
107
233
  question,
108
234
  provider,
@@ -111,7 +237,13 @@ async function runAsk(rest, flags) {
111
237
  userId,
112
238
  domain,
113
239
  memoryContext,
240
+ semanticLayer,
241
+ schemaContext,
114
242
  contextPack,
243
+ reasoningEffort,
244
+ analysisDepth: contextBudget.analysisDepth,
245
+ expandGroundingContext: createGroundingContextExpander(projectRoot),
246
+ answerLoopTools,
115
247
  executeCertifiedBlock: async (node) => {
116
248
  const block = manifest.blocks[node.name] ?? manifest.blocks[node.nodeId.replace(/^block:/, '')];
117
249
  if (!block)
@@ -187,20 +319,39 @@ async function runAsk(rest, flags) {
187
319
  });
188
320
  return result;
189
321
  },
190
- captureGeneratedDraft: ({ question: draftQuestion, sql, intent, followUp, contextPack, sourceBlock, validationWarnings }) => {
322
+ captureGeneratedDraft: ({ question: draftQuestion, sql, intent, followUp, contextPack, sourceBlock, sourceDqlArtifact, dqlArtifact, proposedEntity, requestedFilters, requestedDimensions, validationWarnings, outputs }) => {
191
323
  const slug = deriveGeneratedDraftSlug(draftQuestion);
192
324
  const proposedDomain = sourceBlock?.domain ?? contextPack?.objects.find((object) => object.domain)?.domain ?? domain ?? 'misc';
325
+ if (dqlArtifact?.kind === 'semantic_block') {
326
+ return upsertGeneratedDqlArtifactDraft(projectRoot, {
327
+ slug,
328
+ question: draftQuestion,
329
+ proposedContractId: `${proposedDomain}.Unknown.${slug}`,
330
+ proposedDomain,
331
+ dqlArtifact,
332
+ sourceQuestion: followUp?.sourceQuestion,
333
+ sourceBlock: followUp?.sourceBlockName ?? sourceBlock?.name,
334
+ followupKind: followUp?.kind,
335
+ outputs,
336
+ contextPackId: contextPack?.id,
337
+ routeIntent: String(intent),
338
+ validationWarnings,
339
+ });
340
+ }
193
341
  return upsertGeneratedDraft(projectRoot, {
194
342
  slug,
195
343
  question: draftQuestion,
196
344
  proposedSql: sql,
197
345
  proposedContractId: `${proposedDomain}.Unknown.${slug}`,
198
346
  proposedDomain,
347
+ proposedEntity,
348
+ sourceDqlArtifact,
199
349
  sourceQuestion: followUp?.sourceQuestion,
200
350
  sourceBlock: followUp?.sourceBlockName ?? sourceBlock?.name,
201
351
  followupKind: followUp?.kind,
202
- requestedFilters: followUp?.filters,
203
- requestedDimensions: followUp?.dimensions,
352
+ requestedFilters,
353
+ requestedDimensions,
354
+ outputs,
204
355
  contextPackId: contextPack?.id,
205
356
  routeIntent: String(intent),
206
357
  validationWarnings,
@@ -224,15 +375,7 @@ async function runAsk(rest, flags) {
224
375
  console.log(`\nRows: ${result.result.rowCount}`);
225
376
  console.log(JSON.stringify(result.result.rows.slice(0, 5), null, 2));
226
377
  }
227
- if (result.proposedSql) {
228
- console.log(`\n--- Proposed SQL (review before saving as a block) ---\n${result.proposedSql}`);
229
- if (result.suggestedViz)
230
- console.log(`Viz: ${result.suggestedViz}`);
231
- if (result.draftBlock?.path)
232
- console.log(`Draft: ${result.draftBlock.path}`);
233
- if (result.promoteCommand)
234
- console.log(`Promote: ${result.promoteCommand}`);
235
- }
378
+ printDqlArtifactPreview(result);
236
379
  }
237
380
  finally {
238
381
  kg.close();
@@ -241,6 +384,103 @@ async function runAsk(rest, flags) {
241
384
  await closeRuntime();
242
385
  }
243
386
  }
387
+ function printDqlArtifactPreview(result) {
388
+ const dqlSource = result.dqlArtifact?.source?.trim();
389
+ if (dqlSource) {
390
+ console.log(`\n--- DQL artifact (${result.dqlArtifact?.kind ?? 'draft'}) ---\n${dqlSource}`);
391
+ }
392
+ if (result.proposedSql) {
393
+ const label = dqlSource
394
+ ? 'Compiled SQL preview'
395
+ : 'Proposed SQL (review before saving as a block)';
396
+ console.log(`\n--- ${label} ---\n${result.proposedSql}`);
397
+ if (result.suggestedViz)
398
+ console.log(`Viz: ${result.suggestedViz}`);
399
+ if (result.draftBlock?.path)
400
+ console.log(`Draft: ${result.draftBlock.path}`);
401
+ if (result.promoteCommand)
402
+ console.log(`Promote: ${result.promoteCommand}`);
403
+ }
404
+ }
405
+ /**
406
+ * `dql agent ask --thread <id>` — POST the question to the runtime's
407
+ * `/api/agent-runs` with the threadId in the body. The server injects the
408
+ * thread's prior turns into the conversation context and records the completed
409
+ * run as the next turn, so follow-ups resolve "those"/"that product" correctly
410
+ * across CLI invocations (and across the notebook UI, which shares the store).
411
+ */
412
+ async function runThreadAsk(question, threadId, flags) {
413
+ const projectRoot = findProjectRoot(process.cwd());
414
+ const format = flags.format;
415
+ const { runtimeBase, close } = await resolveAgentRuntime(projectRoot, flags);
416
+ try {
417
+ const reasoningEffort = cliReasoningEffort(flags);
418
+ const analysisDepth = cliAnalysisDepth(flags);
419
+ const response = await fetch(`${runtimeBase.replace(/\/$/, '')}/api/agent-runs`, {
420
+ method: 'POST',
421
+ headers: { 'Content-Type': 'application/json' },
422
+ body: JSON.stringify({
423
+ question,
424
+ threadId,
425
+ ...(reasoningEffort ? { reasoningEffort } : {}),
426
+ ...(analysisDepth ? { analysisDepth } : {}),
427
+ }),
428
+ });
429
+ if (!response.ok)
430
+ throw new Error(`Runtime returned ${response.status}: ${await response.text()}`);
431
+ const payload = (await response.json());
432
+ if (!payload.run)
433
+ throw new Error('Runtime did not return an agent run.');
434
+ if (format === 'json') {
435
+ console.log(JSON.stringify(payload.run, null, 2));
436
+ return;
437
+ }
438
+ const run = payload.run;
439
+ const badge = run.trustState === 'certified'
440
+ ? '✓ Certified'
441
+ : run.trustState === 'grounded'
442
+ ? '✓ Verified (grounded)'
443
+ : run.trustState === 'review_required'
444
+ ? '! AI-generated · review required'
445
+ : run.trustState === 'blocked'
446
+ ? '✕ Blocked'
447
+ : '· Reply';
448
+ console.log(`${badge}\n\n${(run.answer ?? run.summary ?? '').trim()}`);
449
+ console.log(`\nThread: ${threadId}`);
450
+ }
451
+ finally {
452
+ await close();
453
+ }
454
+ }
455
+ /** `dql agent threads` — list server-persisted conversation threads. */
456
+ async function runThreads(flags) {
457
+ const projectRoot = findProjectRoot(process.cwd());
458
+ const format = flags.format;
459
+ const { runtimeBase, close } = await resolveAgentRuntime(projectRoot, flags);
460
+ try {
461
+ const response = await fetch(`${runtimeBase.replace(/\/$/, '')}/api/agent/threads?limit=50`);
462
+ if (!response.ok)
463
+ throw new Error(`Runtime returned ${response.status}: ${await response.text()}`);
464
+ const payload = (await response.json());
465
+ const threads = Array.isArray(payload.threads) ? payload.threads : [];
466
+ if (format === 'json') {
467
+ console.log(JSON.stringify({ threads }, null, 2));
468
+ return;
469
+ }
470
+ if (threads.length === 0) {
471
+ console.log('No conversation threads yet. Ask from the notebook UI, or continue one here with `dql agent ask "<question>" --thread <id>`.');
472
+ return;
473
+ }
474
+ for (const thread of threads) {
475
+ const updated = thread.updatedAt ? new Date(thread.updatedAt).toISOString().replace('T', ' ').slice(0, 16) : 'unknown';
476
+ console.log(` ${thread.id} ${updated} [${thread.surface ?? 'notebook'}] ${thread.title ?? '(untitled)'}`);
477
+ }
478
+ console.log(`\n${threads.length} thread(s). Continue one: dql agent ask "<question>" --thread <id>`);
479
+ }
480
+ finally {
481
+ await close();
482
+ }
483
+ }
244
484
  function recordCliQueryRun(projectRoot, run) {
245
485
  try {
246
486
  recordQueryRun(projectRoot, run);
@@ -256,7 +496,9 @@ async function runReindex(rest, flags) {
256
496
  console.log(JSON.stringify({ ok: true, ...stats }, null, 2));
257
497
  return;
258
498
  }
259
- console.log(` ✓ KG and metadata catalog rebuilt — ${stats.nodes} nodes, ${stats.edges} edges, ${stats.skills} skill(s).`);
499
+ const kgStatus = stats.kgRebuilt ? 'KG rebuilt' : 'KG fresh';
500
+ const catalogStatus = stats.metadataRefreshed ? 'metadata refreshed' : 'metadata fresh';
501
+ console.log(` ✓ ${kgStatus}; ${catalogStatus} — ${stats.nodes} nodes, ${stats.edges} edges, ${stats.skills} skill(s).`);
260
502
  }
261
503
  async function runFeedback(rest, flags) {
262
504
  const rating = rest[0];
@@ -304,14 +546,27 @@ async function runEval(rest, flags) {
304
546
  await reindexProject(projectRoot, { kgPath });
305
547
  const providerName = flags.provider;
306
548
  const provider = await pickProvider(providerName);
549
+ const reasoningEffort = cliReasoningEffort(flags);
550
+ const requestedDepth = cliAnalysisDepth(flags);
307
551
  const kg = new KGStore(kgPath);
308
552
  const memory = new MemoryStore(defaultMemoryPath(projectRoot));
309
553
  const { skills } = loadSkills(projectRoot);
310
554
  const execute = Boolean(flags.execute);
555
+ // R3.2: optional LLM-as-judge. Uses the same provider's completion; skipped
556
+ // gracefully when no provider is available so offline eval stays deterministic.
557
+ const judge = Boolean(flags.judge);
558
+ const judgeComplete = async ({ system, user }) => provider.generate([{ role: 'system', content: system }, { role: 'user', content: user }], {});
311
559
  const runtimeBase = flags.runtimeUrl
312
560
  ?? flags.runtime
313
561
  ?? process.env.DQL_RUNTIME_URL
314
562
  ?? 'http://127.0.0.1:3474';
563
+ const semanticLayer = loadAgentSemanticLayer(projectRoot);
564
+ const expandGroundingContext = createGroundingContextExpander(projectRoot);
565
+ const answerLoopTools = buildAnswerLoopTools(projectRoot, { serverUrl: runtimeBase });
566
+ const schemaContext = execute
567
+ ? await fetchRuntimeSchemaContext(runtimeBase)
568
+ : [];
569
+ recordCliRuntimeSchemaSnapshot(projectRoot, schemaContext, 'CLI eval runtime schema');
315
570
  const manifest = execute
316
571
  ? buildManifest({ projectRoot, dbtManifestPath: resolveDbtManifestPath(projectRoot) ?? undefined })
317
572
  : null;
@@ -324,12 +579,25 @@ async function runEval(rest, flags) {
324
579
  scopes: ['project', 'user', 'artifact'],
325
580
  limit: 6,
326
581
  });
582
+ const questionPlan = buildAnalysisQuestionPlan(testCase.question, testCase.followUp);
583
+ const contextBudget = contextRetrievalBudgetForQuestion({
584
+ questionPlan,
585
+ requestedDepth,
586
+ reasoningEffort,
587
+ });
327
588
  const contextPack = await buildLocalContextPack(projectRoot, {
328
589
  question: testCase.question,
329
590
  surface: 'cli-eval',
330
591
  followUp: testCase.followUp,
331
592
  selectedContext: testCase.selectedContext,
332
- limit: 80,
593
+ strictness: contextBudget.strictness,
594
+ limit: contextBudget.limit,
595
+ runtimeSchemaSnapshot: schemaContext.length > 0
596
+ ? {
597
+ source: 'CLI eval runtime schema',
598
+ tables: schemaContext,
599
+ }
600
+ : undefined,
333
601
  }).catch(() => undefined);
334
602
  const result = await answer({
335
603
  question: testCase.question,
@@ -339,16 +607,45 @@ async function runEval(rest, flags) {
339
607
  skills,
340
608
  memoryContext,
341
609
  followUp: testCase.followUp,
610
+ semanticLayer,
611
+ schemaContext,
342
612
  contextPack,
613
+ reasoningEffort,
614
+ analysisDepth: contextBudget.analysisDepth,
615
+ expandGroundingContext,
616
+ answerLoopTools,
343
617
  executeCertifiedBlock: execute && manifest
344
618
  ? createCertifiedBlockExecutor(projectRoot, manifest, runtimeBase)
345
619
  : undefined,
346
620
  executeGeneratedSql: execute
347
621
  ? createGeneratedSqlExecutor(runtimeBase)
348
622
  : undefined,
349
- captureGeneratedDraft: ({ question: draftQuestion, sql, intent, followUp, contextPack: draftContextPack, sourceBlock, validationWarnings }) => {
623
+ captureGeneratedDraft: ({ question: draftQuestion, sql, intent, followUp, contextPack: draftContextPack, sourceBlock, sourceDqlArtifact, dqlArtifact, proposedEntity, requestedFilters, requestedDimensions, validationWarnings, outputs }) => {
350
624
  const slug = deriveGeneratedDraftSlug(draftQuestion);
351
625
  const proposedDomain = sourceBlock?.domain ?? draftContextPack?.objects.find((object) => object.domain)?.domain ?? testCase.domain ?? 'misc';
626
+ if (dqlArtifact?.kind === 'semantic_block') {
627
+ if (!flags.save) {
628
+ return {
629
+ path: previewGeneratedDraftPath(projectRoot, proposedDomain, slug),
630
+ askedTimes: 0,
631
+ proposedContractId: `${proposedDomain}.Unknown.${slug}`,
632
+ };
633
+ }
634
+ return upsertGeneratedDqlArtifactDraft(projectRoot, {
635
+ slug,
636
+ question: draftQuestion,
637
+ proposedContractId: `${proposedDomain}.Unknown.${slug}`,
638
+ proposedDomain,
639
+ dqlArtifact,
640
+ sourceQuestion: followUp?.sourceQuestion,
641
+ sourceBlock: followUp?.sourceBlockName ?? sourceBlock?.name,
642
+ followupKind: followUp?.kind,
643
+ outputs,
644
+ contextPackId: draftContextPack?.id,
645
+ routeIntent: String(intent),
646
+ validationWarnings,
647
+ });
648
+ }
352
649
  if (!flags.save) {
353
650
  return {
354
651
  path: previewGeneratedDraftPath(projectRoot, proposedDomain, slug),
@@ -362,11 +659,14 @@ async function runEval(rest, flags) {
362
659
  proposedSql: sql,
363
660
  proposedContractId: `${proposedDomain}.Unknown.${slug}`,
364
661
  proposedDomain,
662
+ proposedEntity,
663
+ sourceDqlArtifact,
365
664
  sourceQuestion: followUp?.sourceQuestion,
366
665
  sourceBlock: followUp?.sourceBlockName ?? sourceBlock?.name,
367
666
  followupKind: followUp?.kind,
368
- requestedFilters: followUp?.filters,
369
- requestedDimensions: followUp?.dimensions,
667
+ requestedFilters,
668
+ requestedDimensions,
669
+ outputs,
370
670
  contextPackId: draftContextPack?.id,
371
671
  routeIntent: String(intent),
372
672
  validationWarnings,
@@ -374,21 +674,42 @@ async function runEval(rest, flags) {
374
674
  },
375
675
  });
376
676
  const evaluation = evaluateCase(testCase, result);
677
+ const durationMs = Date.now() - startedAt;
678
+ const draftSaved = Boolean(result.draftBlock?.path ?? result.draftBlockId);
679
+ const judgeVerdict = judge
680
+ ? await judgeAnswer({
681
+ question: testCase.question,
682
+ sql: result.proposedSql ?? result.sql,
683
+ answerText: result.text,
684
+ trustLabel: result.trustLabelInfo?.display ?? result.certification,
685
+ resultSample: result.result?.rows,
686
+ }, judgeComplete)
687
+ : undefined;
377
688
  results.push({
378
689
  name: testCase.name ?? testCase.question,
379
690
  passed: evaluation.failures.length === 0,
380
691
  failures: evaluation.failures,
381
- durationMs: Date.now() - startedAt,
692
+ durationMs,
382
693
  executionMs: result.result?.executionTime,
694
+ executionMatched: evaluation.executionMatched,
695
+ ...(judgeVerdict ? { judgeScore: judgeVerdict.score, judgePass: judgeVerdict.pass } : {}),
383
696
  kind: result.kind,
384
697
  route: result.contextPack?.routeDecision.route,
385
698
  intent: result.contextPack?.routeDecision.intent,
386
699
  reviewStatus: result.reviewStatus,
387
700
  contextObjects: result.contextPack?.objects.length ?? 0,
388
701
  followUp: Boolean(testCase.followUp),
389
- draftSaved: Boolean(result.draftBlock?.path ?? result.draftBlockId),
702
+ draftSaved,
703
+ toolCalls: result.evidence?.toolCalls?.length ?? 0,
390
704
  expected: testCase.expected,
391
705
  validationCode: evaluation.validationCode,
706
+ trace: buildEvalTrace({
707
+ testCase,
708
+ result,
709
+ evaluation,
710
+ durationMs,
711
+ draftSaved,
712
+ }),
392
713
  });
393
714
  }
394
715
  }
@@ -398,8 +719,18 @@ async function runEval(rest, flags) {
398
719
  }
399
720
  const passed = results.filter((r) => r.passed).length;
400
721
  const metrics = computeEvalMetrics(results);
722
+ const thresholds = {
723
+ minToolRequirement: flags.minToolRequirement ?? null,
724
+ minExecutionMatch: flags.minExecutionMatch ?? null,
725
+ minJudgePass: flags.minJudgePass ?? null,
726
+ maxWrongCertified: flags.maxWrongCertified ?? null,
727
+ };
728
+ const thresholdsPassed = agentEvalThresholdsPass(metrics, thresholds);
729
+ const ok = passed === results.length && thresholdsPassed;
401
730
  if (flags.format === 'json') {
402
- console.log(JSON.stringify({ ok: passed === results.length, passed, total: results.length, metrics, results }, null, 2));
731
+ console.log(JSON.stringify({ ok, passed, total: results.length, thresholds, metrics, results }, null, 2));
732
+ if (!ok)
733
+ process.exitCode = 1;
403
734
  return;
404
735
  }
405
736
  for (const result of results) {
@@ -411,9 +742,25 @@ async function runEval(rest, flags) {
411
742
  console.log(`Certified hit rate: ${formatRate(metrics.certified_hit_rate)}`);
412
743
  console.log(`Generated follow-up pass rate: ${formatRate(metrics.generated_followup_pass_rate)}`);
413
744
  console.log(`Safe refusal rate: ${formatRate(metrics.safe_refusal_rate)}`);
745
+ console.log(`Execution match rate: ${formatRate(metrics.execution_match_rate)}`);
746
+ console.log(`Tool requirement pass rate: ${formatRate(metrics.tool_requirement_pass_rate)}`);
747
+ console.log(`Tool-observed case count: ${metrics.tool_observed_case_count}`);
748
+ console.log(`Average tool calls: ${metrics.avg_tool_calls}`);
414
749
  console.log(`Wrong certified count: ${metrics.wrong_certified_count}`);
415
750
  console.log(`Draft saved count: ${metrics.draft_saved_count}`);
416
- if (passed !== results.length)
751
+ if (thresholds.minToolRequirement !== null) {
752
+ console.log(`Tool requirement threshold: ${thresholds.minToolRequirement} (actual ${formatRate(metrics.tool_requirement_pass_rate)})`);
753
+ }
754
+ if (thresholds.minExecutionMatch !== null) {
755
+ console.log(`Execution-match threshold: ${thresholds.minExecutionMatch} (actual ${formatRate(metrics.execution_match_rate)})`);
756
+ }
757
+ if (thresholds.minJudgePass !== null) {
758
+ console.log(`Judge-pass threshold: ${thresholds.minJudgePass} (actual ${formatRate(metrics.judge_pass_rate)})`);
759
+ }
760
+ if (thresholds.maxWrongCertified !== null) {
761
+ console.log(`Wrong-certified ceiling: ${thresholds.maxWrongCertified} (actual ${metrics.wrong_certified_count})`);
762
+ }
763
+ if (!ok)
417
764
  process.exitCode = 1;
418
765
  }
419
766
  function previewGeneratedDraftPath(projectRoot, domain, slug) {
@@ -433,6 +780,7 @@ function evaluateCase(testCase, result) {
433
780
  return { failures: [] };
434
781
  const failures = [];
435
782
  let validationCode;
783
+ let executionMatched;
436
784
  if (expected.kind && result.kind !== expected.kind)
437
785
  failures.push(`kind expected ${expected.kind}, got ${result.kind}`);
438
786
  if (expected.sourceTier && result.sourceTier !== expected.sourceTier)
@@ -466,6 +814,12 @@ function evaluateCase(testCase, result) {
466
814
  if (saved !== expected.draftSaved)
467
815
  failures.push(`draftSaved expected ${expected.draftSaved}, got ${saved}`);
468
816
  }
817
+ if (typeof expected.minToolCalls === 'number') {
818
+ const actualToolCalls = result.evidence?.toolCalls?.length ?? 0;
819
+ if (actualToolCalls < expected.minToolCalls) {
820
+ failures.push(`toolCalls expected at least ${expected.minToolCalls}, got ${actualToolCalls}`);
821
+ }
822
+ }
469
823
  if ((expected.allowedRelationsOnly || expected.allowedColumnsOnly) && result.proposedSql) {
470
824
  const validation = validateSqlAgainstLocalContext(result.proposedSql, result.contextPack, {
471
825
  question: testCase.question,
@@ -479,10 +833,11 @@ function evaluateCase(testCase, result) {
479
833
  }
480
834
  if (expected.rows) {
481
835
  const actualRows = result.result?.rows ?? [];
482
- if (!rowsEqual(actualRows, expected.rows))
836
+ executionMatched = rowsEqual(actualRows, expected.rows);
837
+ if (!executionMatched)
483
838
  failures.push('executed rows did not match expected rows');
484
839
  }
485
- return { failures, validationCode };
840
+ return { failures, validationCode, executionMatched };
486
841
  }
487
842
  function computeEvalMetrics(results) {
488
843
  const certifiedCases = results.filter((result) => result.expected?.kind === 'certified' ||
@@ -494,18 +849,256 @@ function computeEvalMetrics(results) {
494
849
  const executionTimes = results
495
850
  .map((result) => result.executionMs)
496
851
  .filter((value) => typeof value === 'number' && Number.isFinite(value));
852
+ const executionMatchCases = results.filter((result) => result.executionMatched !== undefined);
853
+ const toolRequiredCases = results.filter((result) => typeof result.expected?.minToolCalls === 'number');
854
+ const toolCallCounts = results.map((result) => result.toolCalls);
855
+ const judged = results.filter((result) => typeof result.judgeScore === 'number');
497
856
  return {
498
857
  certified_hit_rate: ratio(certifiedCases.filter((result) => result.passed && result.kind === 'certified').length, certifiedCases.length),
858
+ judge_mean_score: judged.length ? average(judged.map((result) => result.judgeScore ?? 0)) : null,
859
+ judge_pass_rate: judged.length ? ratio(judged.filter((result) => result.judgePass).length, judged.length) : null,
499
860
  generated_followup_pass_rate: ratio(generatedFollowUpCases.filter((result) => result.passed).length, generatedFollowUpCases.length),
500
861
  safe_refusal_rate: ratio(refusalCases.filter((result) => result.passed && result.kind === 'no_answer').length, refusalCases.length),
862
+ execution_match_rate: ratio(executionMatchCases.filter((result) => result.executionMatched).length, executionMatchCases.length),
863
+ tool_requirement_pass_rate: ratio(toolRequiredCases.filter((result) => result.toolCalls >= (result.expected?.minToolCalls ?? 0)).length, toolRequiredCases.length),
501
864
  wrong_certified_count: results.filter((result) => result.kind === 'certified' &&
502
865
  (result.expected?.kind ? result.expected.kind !== 'certified' : result.followUp)).length,
503
866
  outside_context_rejection_count: results.filter((result) => result.validationCode === 'unknown_relation' || result.validationCode === 'unknown_column').length,
504
867
  draft_saved_count: results.filter((result) => result.draftSaved).length,
868
+ tool_observed_case_count: results.filter((result) => result.toolCalls > 0).length,
869
+ avg_tool_calls: average(toolCallCounts),
505
870
  avg_context_objects: average(results.map((result) => result.contextObjects)),
506
871
  avg_execution_ms: executionTimes.length ? average(executionTimes) : null,
507
872
  };
508
873
  }
874
+ function agentEvalThresholdsPass(metrics, thresholds) {
875
+ // A rate threshold with no applicable cases (metric === null) is vacuously
876
+ // satisfied — you only fail when the metric exists and falls below the bar.
877
+ const rateOk = (metric, min) => min === null || min === undefined || metric === null || metric >= min;
878
+ return rateOk(metrics.tool_requirement_pass_rate, thresholds.minToolRequirement)
879
+ && rateOk(metrics.execution_match_rate, thresholds.minExecutionMatch)
880
+ && rateOk(metrics.judge_pass_rate, thresholds.minJudgePass)
881
+ && (thresholds.maxWrongCertified === null
882
+ || thresholds.maxWrongCertified === undefined
883
+ || metrics.wrong_certified_count <= thresholds.maxWrongCertified);
884
+ }
885
+ function buildEvalTrace(input) {
886
+ const { testCase, result, evaluation, durationMs, draftSaved } = input;
887
+ const routeDecision = result.contextPack?.routeDecision;
888
+ const selectedRelations = result.contextPack?.retrievalDiagnostics.selectedRelations ?? [];
889
+ const allowedRelations = result.contextPack?.allowedSqlContext?.relations ?? [];
890
+ const followUp = testCase.followUp;
891
+ const toolCalls = result.evidence?.toolCalls ?? [];
892
+ const routeEvidence = result.evidence?.route ?? [];
893
+ const executionStatus = result.executionError
894
+ ? 'failed'
895
+ : result.result
896
+ ? 'passed'
897
+ : 'not_run';
898
+ const validationExpected = Boolean(testCase.expected?.allowedRelationsOnly || testCase.expected?.allowedColumnsOnly);
899
+ const validationStatus = evaluation.validationCode
900
+ ? 'failed'
901
+ : validationExpected
902
+ ? 'passed'
903
+ : 'not_run';
904
+ const rowsExpected = testCase.expected?.rows !== undefined;
905
+ const expectedMinToolCalls = testCase.expected?.minToolCalls;
906
+ const toolStatus = typeof expectedMinToolCalls === 'number'
907
+ ? toolCalls.length >= expectedMinToolCalls ? 'passed' : 'failed'
908
+ : toolCalls.length > 0 ? 'passed' : routeEvidence.length > 0 ? 'info' : 'not_run';
909
+ const toolMessage = typeof expectedMinToolCalls === 'number'
910
+ ? toolCalls.length >= expectedMinToolCalls
911
+ ? `Observed ${toolCalls.length} provider tool call(s), meeting the minimum of ${expectedMinToolCalls}.`
912
+ : `Observed ${toolCalls.length} provider tool call(s), below the minimum of ${expectedMinToolCalls}.`
913
+ : toolCalls.length > 0
914
+ ? `Observed ${toolCalls.length} provider tool call(s).`
915
+ : routeEvidence.length > 0
916
+ ? `Captured ${routeEvidence.length} deterministic route evidence step(s).`
917
+ : 'No provider tool calls were observed for this answer.';
918
+ return [
919
+ {
920
+ stage: 'context',
921
+ status: result.contextPack ? 'passed' : 'not_run',
922
+ message: result.contextPack
923
+ ? `Context pack ${result.contextPack.id} selected ${result.contextPack.objects.length} object(s).`
924
+ : 'No context pack was attached to the answer.',
925
+ payload: result.contextPack
926
+ ? {
927
+ contextPackId: result.contextPack.id,
928
+ selectedObjectCount: result.contextPack.objects.length,
929
+ allowedRelationCount: allowedRelations.length,
930
+ selectedRelations: selectedRelations.slice(0, 12).map((relation) => relation.relation),
931
+ missingContext: result.contextPack.missingContext,
932
+ }
933
+ : undefined,
934
+ },
935
+ {
936
+ stage: 'rewrite',
937
+ status: followUp ? 'passed' : 'not_run',
938
+ message: followUp
939
+ ? `Follow-up context attached (${followUp.kind}).`
940
+ : 'No follow-up rewrite/context was supplied for this case.',
941
+ payload: summarizeFollowUpForTrace(followUp),
942
+ },
943
+ {
944
+ stage: 'lane',
945
+ status: routeDecision ? 'passed' : 'not_run',
946
+ message: routeDecision
947
+ ? `Lane ${routeDecision.route} / ${routeDecision.intent}.`
948
+ : 'No lane decision was attached to the answer.',
949
+ payload: routeDecision
950
+ ? {
951
+ route: routeDecision.route,
952
+ intent: routeDecision.intent,
953
+ reason: routeDecision.reason,
954
+ trustLabel: routeDecision.trustLabel,
955
+ reviewStatus: routeDecision.reviewStatus,
956
+ exactObjectKey: routeDecision.exactObjectKey,
957
+ }
958
+ : undefined,
959
+ },
960
+ {
961
+ stage: 'tools',
962
+ status: toolStatus,
963
+ message: toolMessage,
964
+ payload: {
965
+ observedToolCalls: toolCalls.length,
966
+ expectedMinToolCalls,
967
+ providerToolCalls: toolCalls.slice(0, 12).map((call) => ({
968
+ order: call.order,
969
+ name: call.name,
970
+ status: call.status,
971
+ inputSummary: call.inputSummary,
972
+ outputSummary: call.outputSummary,
973
+ })),
974
+ routeEvidence: routeEvidence.slice(0, 12).map((step) => ({
975
+ tool: step.tool,
976
+ status: step.status,
977
+ label: step.label,
978
+ detail: step.detail,
979
+ })),
980
+ },
981
+ },
982
+ {
983
+ stage: 'answer',
984
+ status: result.kind === 'no_answer' ? 'failed' : 'passed',
985
+ message: `Answer kind ${result.kind}${result.sourceTier ? ` from ${result.sourceTier}` : ''}.`,
986
+ payload: {
987
+ kind: result.kind,
988
+ sourceTier: result.sourceTier,
989
+ certification: result.certification,
990
+ reviewStatus: result.reviewStatus,
991
+ route: result.route?.tier,
992
+ refusalCode: result.refusalCode,
993
+ sourceCertifiedBlock: result.sourceCertifiedBlock,
994
+ dqlArtifactKind: result.dqlArtifact?.kind,
995
+ providerUsed: result.providerUsed,
996
+ },
997
+ },
998
+ {
999
+ stage: 'validation',
1000
+ status: validationStatus,
1001
+ message: evaluation.validationCode
1002
+ ? `SQL context validation failed with ${evaluation.validationCode}.`
1003
+ : validationExpected
1004
+ ? 'SQL context validation passed.'
1005
+ : 'SQL context validation was not required by this case.',
1006
+ payload: {
1007
+ validationCode: evaluation.validationCode,
1008
+ failures: evaluation.failures,
1009
+ expectedAllowedRelationsOnly: testCase.expected?.allowedRelationsOnly,
1010
+ expectedAllowedColumnsOnly: testCase.expected?.allowedColumnsOnly,
1011
+ },
1012
+ },
1013
+ {
1014
+ stage: 'execution',
1015
+ status: executionStatus,
1016
+ message: result.executionError
1017
+ ? result.executionError
1018
+ : result.result
1019
+ ? `Executed and returned ${result.result.rowCount} row(s).`
1020
+ : 'No SQL/block execution result was captured.',
1021
+ payload: {
1022
+ rowCount: result.result?.rowCount,
1023
+ executionTime: result.result?.executionTime,
1024
+ executionMatched: rowsExpected ? evaluation.executionMatched : undefined,
1025
+ expectedRows: rowsExpected ? testCase.expected?.rows?.length : undefined,
1026
+ columns: summarizeResultColumns(result.result?.columns),
1027
+ },
1028
+ },
1029
+ {
1030
+ stage: 'draft',
1031
+ status: draftSaved ? 'passed' : 'not_run',
1032
+ message: draftSaved
1033
+ ? `Draft captured at ${result.draftBlock?.path ?? result.draftBlockId}.`
1034
+ : 'No generated draft was captured.',
1035
+ payload: {
1036
+ draftBlockId: result.draftBlockId,
1037
+ draftPath: result.draftBlock?.path,
1038
+ promoteCommand: result.promoteCommand,
1039
+ },
1040
+ },
1041
+ {
1042
+ stage: 'scoring',
1043
+ status: evaluation.failures.length === 0 ? 'passed' : 'failed',
1044
+ message: evaluation.failures.length === 0
1045
+ ? `Case passed in ${durationMs}ms.`
1046
+ : `Case failed ${evaluation.failures.length} check(s) in ${durationMs}ms.`,
1047
+ payload: {
1048
+ durationMs,
1049
+ expected: testCase.expected,
1050
+ },
1051
+ },
1052
+ ];
1053
+ }
1054
+ function summarizeFollowUpForTrace(followUp) {
1055
+ if (!followUp)
1056
+ return undefined;
1057
+ const priorResultRef = followUp.priorResultRef
1058
+ ? {
1059
+ id: followUp.priorResultRef.id,
1060
+ question: followUp.priorResultRef.question,
1061
+ columns: followUp.priorResultRef.columns,
1062
+ rowCount: followUp.priorResultRef.rowCount,
1063
+ sourceSql: truncateTraceText(followUp.priorResultRef.sourceSql, 4000),
1064
+ }
1065
+ : undefined;
1066
+ const priorDqlArtifact = followUp.priorDqlArtifact
1067
+ ? {
1068
+ kind: followUp.priorDqlArtifact.kind,
1069
+ name: followUp.priorDqlArtifact.name,
1070
+ sourcePath: followUp.priorDqlArtifact.sourcePath,
1071
+ source: truncateTraceText(followUp.priorDqlArtifact.source, 4000),
1072
+ metrics: followUp.priorDqlArtifact.metrics,
1073
+ dimensions: followUp.priorDqlArtifact.dimensions,
1074
+ filters: followUp.priorDqlArtifact.filters,
1075
+ timeDimension: followUp.priorDqlArtifact.timeDimension,
1076
+ orderBy: followUp.priorDqlArtifact.orderBy,
1077
+ limit: followUp.priorDqlArtifact.limit,
1078
+ }
1079
+ : undefined;
1080
+ return {
1081
+ kind: followUp.kind,
1082
+ sourceTurnId: followUp.sourceTurnId,
1083
+ sourceBlockName: followUp.sourceBlockName,
1084
+ sourceQuestion: followUp.sourceQuestion,
1085
+ filters: followUp.filters,
1086
+ dimensions: followUp.dimensions,
1087
+ priorResultColumns: followUp.priorResultColumns,
1088
+ priorResultValues: followUp.priorResultValues,
1089
+ priorResultRef,
1090
+ priorDqlArtifact,
1091
+ priorLimit: followUp.priorLimit,
1092
+ priorMeasures: followUp.priorMeasures,
1093
+ resolvedReferences: followUp.resolvedReferences,
1094
+ unresolvedReferences: followUp.unresolvedReferences,
1095
+ };
1096
+ }
1097
+ function truncateTraceText(value, maxLength) {
1098
+ if (!value || value.length <= maxLength)
1099
+ return value;
1100
+ return `${value.slice(0, maxLength)}...`;
1101
+ }
509
1102
  function ratio(numerator, denominator) {
510
1103
  if (denominator === 0)
511
1104
  return null;
@@ -516,6 +1109,17 @@ function average(values) {
516
1109
  return 0;
517
1110
  return Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(2));
518
1111
  }
1112
+ function summarizeResultColumns(columns) {
1113
+ return (columns ?? []).map((column) => {
1114
+ if (typeof column === 'string')
1115
+ return column;
1116
+ if (column && typeof column === 'object' && 'name' in column) {
1117
+ const name = column.name;
1118
+ return typeof name === 'string' ? name : JSON.stringify(column);
1119
+ }
1120
+ return String(column);
1121
+ });
1122
+ }
519
1123
  function formatRate(value) {
520
1124
  return value === null ? 'n/a' : `${Math.round(value * 1000) / 10}%`;
521
1125
  }
@@ -588,6 +1192,10 @@ async function executeRuntimeCell(runtimeBase, cell) {
588
1192
  return payload;
589
1193
  }
590
1194
  export const __test__ = {
1195
+ agentEvalThresholdsPass,
1196
+ buildEvalTrace,
1197
+ cliAnalysisDepth,
1198
+ cliReasoningEffort,
591
1199
  computeEvalMetrics,
592
1200
  evaluateCase,
593
1201
  };