@duckcodeailabs/dql-cli 1.13.4 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/args.d.ts +2 -0
  2. package/dist/args.d.ts.map +1 -1
  3. package/dist/args.js +5 -0
  4. package/dist/args.js.map +1 -1
  5. package/dist/assets/dql-notebook/assets/{AgentLogPage-DYmHO6_i.js → AgentLogPage-Ch7VK20X.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AiBuildDialog-BfhaOd1h.js → AiBuildDialog-DBr5TmyM.js} +1 -1
  7. package/dist/assets/dql-notebook/assets/{AiBuildResult-DLKFAe6R.js → AiBuildResult-jLPzQO7O.js} +1 -1
  8. package/dist/assets/dql-notebook/assets/{AiSidePanel-C6Z3URGr.js → AiSidePanel-CdlGsiVC.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{AnalyticsHome-uG3xCP_s.js → AnalyticsHome-C0DXbOwY.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/{AppsView-DfieNjkh.js → AppsView-CcpwjApv.js} +4 -4
  11. package/dist/assets/dql-notebook/assets/{BlockStudio-90J_wvWL.js → BlockStudio-CFYxafw-.js} +1 -1
  12. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-_zSMae5q.js → BusinessArtifactView-C0kLYg2p.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-DoXiPssu.js → DbtFirstModelingPage-CMwElXD_.js} +1 -1
  14. package/dist/assets/dql-notebook/assets/{GitPage-CUfAhS67.js → GitPage-JjhRDeWY.js} +1 -1
  15. package/dist/assets/dql-notebook/assets/{GlobalAiRail-ovtL7nQu.js → GlobalAiRail-DakE4NdR.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{GovernedContextPage-CaMiKAzA.js → GovernedContextPage-BokDqG6a.js} +1 -1
  17. package/dist/assets/dql-notebook/assets/{HelpDocsPage-CEGUx28b.js → HelpDocsPage-CjOv6_gz.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{HomePage-2v3SRf8V.js → HomePage-nGaNcwdw.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{LineageDAG-AaqLEiBN.js → LineageDAG-CO6CFJRg.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{LineageDetailView-CFc8pm91.js → LineageDetailView-BnGb7OF7.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{LineageDrawer-DOkFjPq-.js → LineageDrawer-C5Y0Ht0b.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-BrDwCuMs.js → LineagePathBreadcrumb-Cgh3GR3F.js} +1 -1
  23. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-CiHtQs3y.js → MiniLineageGraph-Kla9PYuj.js} +1 -1
  24. package/dist/assets/dql-notebook/assets/{NewBlockModal-DAgJZbws.js → NewBlockModal-BR2SnmPT.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{NewNotebookModal-xjG7nZ3O.js → NewNotebookModal-BaCHMYeB.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{NotebookEditor-CXZaKfYT.js → NotebookEditor-CBLqY8cE.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/{ReadinessPage-MuJBFwwg.js → ReadinessPage-CiN0IWSS.js} +1 -1
  28. package/dist/assets/dql-notebook/assets/{SetupOnboarding-vKFtKtjh.js → SetupOnboarding-BhiCsYF-.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{SkillsPage-DCFVOV5f.js → SkillsPage-CCSf8VMm.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{TrustBadge-TcwXFFHK.js → TrustBadge-BgQmFe_x.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Bw5AXNDB.js +88 -0
  32. package/dist/assets/dql-notebook/assets/{answer-to-notebook-DCFWlHB7.js → answer-to-notebook-AeDYUDla.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{arrow-left-BLnlJAtp.js → arrow-left-C55x2hq_.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{arrow-right-BmUAb1aW.js → arrow-right-C1cJrhOm.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{book-open-text-bdSChSag.js → book-open-text-CQf_sdv2.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{circle-x-Cc6dEaf4.js → circle-x-X8-Z2yLY.js} +1 -1
  37. package/dist/assets/dql-notebook/assets/{dagre.esm-CPebQKQ9.js → dagre.esm-BjjNYKyY.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{external-link-rmXvubf3.js → external-link-C2zrz5DH.js} +1 -1
  39. package/dist/assets/dql-notebook/assets/{grip-vertical-Sg4zd5-Q.js → grip-vertical-qGV_PYGU.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/{index-mQd_pC-P.js → index-ByTDPDaH.js} +127 -127
  41. package/dist/assets/dql-notebook/assets/{link-2-BLaPoFxW.js → link-2-VpyOxQXG.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{list-tree-j0s4asFK.js → list-tree-CH2Jhwms.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{minimize-2-Cl4pef4s.js → minimize-2-B2TZJ8BT.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{panel-right-open-Clz-FtWi.js → panel-right-open-BunF88lt.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{play-DFdazMg7.js → play-DAFVF4_G.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{rotate-ccw-C3tTaR7s.js → rotate-ccw-DGCrrqtY.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{semantic-fields-DIAhTlfy.js → semantic-fields-Ci-9QL9F.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{sliders-horizontal-BUiu2tfY.js → sliders-horizontal-BOAlXXbn.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{star--Yq77aH4.js → star-CkksZXHt.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{triangle-alert-D5Ng-xZO.js → triangle-alert-D3mjyJZE.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{upload-L8BKQAxP.js → upload-CTNOAVEO.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DBu97YaM.js → usePersistedAgentThreadId-DiQjc7x-.js} +4 -4
  53. package/dist/assets/dql-notebook/assets/{user-round-BK5N1cJW.js → user-round-BWd5tQRg.js} +1 -1
  54. package/dist/assets/dql-notebook/assets/{wand-sparkles-D8XDQD9z.js → wand-sparkles-CqsAv8P-.js} +1 -1
  55. package/dist/assets/dql-notebook/assets/{workflow-Bl9zwG0q.js → workflow-CSqsj-sC.js} +1 -1
  56. package/dist/assets/dql-notebook/assets/{wrench-C-zLCMbW.js → wrench-DWqzqlX8.js} +1 -1
  57. package/dist/assets/dql-notebook/assets/{x-stiJ-sP7.js → x-65M5rLCE.js} +1 -1
  58. package/dist/assets/dql-notebook/index.html +1 -1
  59. package/dist/commands/eval.d.ts +15 -1
  60. package/dist/commands/eval.d.ts.map +1 -1
  61. package/dist/commands/eval.js +32 -3
  62. package/dist/commands/eval.js.map +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/llm/answer-loop-tools.d.ts +3 -1
  65. package/dist/llm/answer-loop-tools.d.ts.map +1 -1
  66. package/dist/llm/answer-loop-tools.js +31 -23
  67. package/dist/llm/answer-loop-tools.js.map +1 -1
  68. package/dist/llm/providers/dql-agent-provider.d.ts +3 -1
  69. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  70. package/dist/llm/providers/dql-agent-provider.js +315 -22
  71. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  72. package/dist/llm/types.d.ts +29 -2
  73. package/dist/llm/types.d.ts.map +1 -1
  74. package/dist/local-runtime.d.ts +200 -5
  75. package/dist/local-runtime.d.ts.map +1 -1
  76. package/dist/local-runtime.js +2280 -457
  77. package/dist/local-runtime.js.map +1 -1
  78. package/dist/package.json +10 -10
  79. package/dist/providers/oauth/claude-oauth.d.ts +1 -1
  80. package/dist/providers/oauth/claude-oauth.d.ts.map +1 -1
  81. package/dist/providers/oauth/claude-oauth.js +18 -11
  82. package/dist/providers/oauth/claude-oauth.js.map +1 -1
  83. package/dist/providers/oauth/codex-oauth.d.ts +1 -1
  84. package/dist/providers/oauth/codex-oauth.d.ts.map +1 -1
  85. package/dist/providers/oauth/codex-oauth.js +9 -1
  86. package/dist/providers/oauth/codex-oauth.js.map +1 -1
  87. package/dist/providers/subscription-cli.d.ts +1 -1
  88. package/dist/providers/subscription-cli.d.ts.map +1 -1
  89. package/dist/providers/subscription-cli.js +33 -4
  90. package/dist/providers/subscription-cli.js.map +1 -1
  91. package/package.json +10 -10
  92. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Cu-YgIe4.js +0 -88
@@ -1,8 +1,7 @@
1
- import { ClaudeProvider, KGStore, MemoryStore, defaultKgPath, defaultMemoryPath, GeminiProvider, loadAgentSemanticLayer, OllamaProvider, OpenAIProvider, answer, buildAnalysisQuestionPlan, buildLocalContextPack, contextRetrievalBudgetForQuestion, ensureAgentProjectReady, isLikelyClarificationReply, isTrustedConversationTurn, } from '@duckcodeailabs/dql-agent';
1
+ import { ClaudeProvider, KGStore, MemoryStore, defaultKgPath, defaultMemoryPath, GeminiProvider, loadAgentSemanticLayer, OllamaProvider, OpenAIProvider, answer, buildAnalysisQuestionPlan, buildLocalContextPack, contextRetrievalBudgetForQuestion, ensureAgentProjectReady, isLikelyClarificationReply, isTrustedConversationTurn, createProviderDispatchEgressReceipt, createProviderEgressReceipt, assertProviderPayloadAllowed, prepareProviderContextForDispatch, prepareProviderWireEnvelopeForDispatch, prepareServerOwnedProviderSchemaContext, } from '@duckcodeailabs/dql-agent';
2
2
  import { buildManifest, normalizeDqlArtifactReference, resolveDbtManifestPath } from '@duckcodeailabs/dql-core';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { buildAnswerLoopTools, createGroundingContextExpander } from '../answer-loop-tools.js';
5
- import { rethrowIfCancelled } from '../cancellation.js';
6
5
  import { getSemanticRuntimeStatus } from '../../semantic-runtime.js';
7
6
  import { blockProposalDqlMetadata } from '../proposal-metadata.js';
8
7
  import { getEffectiveProviderConfig } from '../../settings/provider-settings.js';
@@ -258,11 +257,126 @@ function emitProposalFromText(text, emit) {
258
257
  // Ignore malformed proposal text. The visible assistant response still streams as text.
259
258
  }
260
259
  }
261
- export function createDqlAgentProviderRunner(id) {
260
+ export function createDqlAgentProviderRunner(id, providerOverride) {
262
261
  return {
263
262
  async run(req, emit, signal) {
264
263
  const spec = SPECS[id];
265
- const provider = spec.create(req.projectRoot);
264
+ const rawProvider = providerOverride ?? spec.create(req.projectRoot);
265
+ const isResearch = req.analysisDepth === 'deep';
266
+ const researchRowsOptIn = isResearch && req.researchResultRowsOptIn === true;
267
+ const sharedDispatchEvidence = req.providerDispatchEvidenceSink;
268
+ let providerRoundTrips = 0;
269
+ let sqlExecutions = 0;
270
+ let pendingResultRowCount = 0;
271
+ let pendingColumnCount = 0;
272
+ let pendingCumulativeResultRowCount = 0;
273
+ let pendingResearchPurpose = 'research_narration';
274
+ const providerEgressReceipts = [];
275
+ const dispatchEvidence = (fallbackReason) => {
276
+ const shared = sharedDispatchEvidence?.snapshot(fallbackReason);
277
+ return shared
278
+ ? { ...shared, sqlExecutions, fallbackReason }
279
+ : {
280
+ providerEgressReceipts: [...providerEgressReceipts],
281
+ providerRoundTrips,
282
+ toolCalls: 0,
283
+ sqlExecutions,
284
+ repairs: 0,
285
+ fallbackReason,
286
+ };
287
+ };
288
+ signal.addEventListener('abort', () => {
289
+ const reason = signal.reason;
290
+ if (reason && typeof reason === 'object') {
291
+ Object.assign(reason, { providerDispatchEvidence: dispatchEvidence('cancelled') });
292
+ }
293
+ }, { once: true });
294
+ const withPhysicalDispatchObserver = (options) => ({
295
+ ...(options ?? {}),
296
+ // A tool-calling loop capped at two physical sends can make exactly one
297
+ // tool call before it must answer, which is not enough to look something
298
+ // up and then use it. The run-scoped ledger is the real guardrail.
299
+ maxProviderDispatches: isResearch ? 8 : 4,
300
+ ...(sharedDispatchEvidence?.mayStartToolCall
301
+ ? { mayStartToolCall: () => sharedDispatchEvidence.mayStartToolCall() }
302
+ : {}),
303
+ onProviderDispatch: (event) => {
304
+ const purpose = isResearch ? pendingResearchPurpose : 'answer_generation';
305
+ if (sharedDispatchEvidence) {
306
+ const envelope = sharedDispatchEvidence.observe(event, {
307
+ purpose,
308
+ dispatchPhase: 'generation',
309
+ optIn: pendingResultRowCount > 0 && researchRowsOptIn,
310
+ serializedResultShape: {
311
+ resultRowCount: pendingResultRowCount,
312
+ columnCount: pendingColumnCount,
313
+ },
314
+ ...(pendingCumulativeResultRowCount > 0
315
+ ? { cumulativeResultRowCount: pendingCumulativeResultRowCount }
316
+ : {}),
317
+ });
318
+ pendingResultRowCount = 0;
319
+ pendingColumnCount = 0;
320
+ pendingCumulativeResultRowCount = 0;
321
+ pendingResearchPurpose = 'research_narration';
322
+ return envelope;
323
+ }
324
+ // Fallback path only (CLI/MCP-direct runs carry no shared ledger).
325
+ // Kept in step with `maxProviderDispatches` above so the same question
326
+ // does not get a different budget depending on which surface asked it.
327
+ if (!isResearch && providerRoundTrips >= 4) {
328
+ throw Object.assign(new Error('Provider dispatch budget exhausted after four ordinary generation attempts.'), {
329
+ code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED',
330
+ });
331
+ }
332
+ const envelope = prepareProviderWireEnvelopeForDispatch(rawProvider.name, event.envelope);
333
+ assertProviderPayloadAllowed(envelope, {
334
+ allowResultRows: false,
335
+ maxResultRows: 0,
336
+ purpose,
337
+ });
338
+ providerRoundTrips += 1;
339
+ providerEgressReceipts.push(createProviderDispatchEgressReceipt({
340
+ purpose,
341
+ dispatchPhase: 'generation',
342
+ provider: rawProvider.name,
343
+ permittedCategories: pendingResultRowCount > 0
344
+ ? ['instructions', 'question', 'schema_metadata', 'governed_context', 'result_rows']
345
+ : ['instructions', 'question', 'schema_metadata', 'governed_context'],
346
+ optIn: pendingResultRowCount > 0 && researchRowsOptIn,
347
+ envelope,
348
+ serializedResultShape: {
349
+ resultRowCount: pendingResultRowCount,
350
+ columnCount: pendingColumnCount,
351
+ },
352
+ ...(pendingCumulativeResultRowCount > 0
353
+ ? { cumulativeResultRowCount: pendingCumulativeResultRowCount }
354
+ : {}),
355
+ }));
356
+ pendingResultRowCount = 0;
357
+ pendingColumnCount = 0;
358
+ pendingCumulativeResultRowCount = 0;
359
+ pendingResearchPurpose = 'research_narration';
360
+ return envelope;
361
+ },
362
+ });
363
+ const provider = {
364
+ name: rawProvider.name,
365
+ available: () => rawProvider.available(),
366
+ generate: (...args) => {
367
+ return rawProvider.generate(args[0], withPhysicalDispatchObserver(args[1]));
368
+ },
369
+ ...(rawProvider.generateWithTools ? {
370
+ generateWithTools: (...args) => {
371
+ return rawProvider.generateWithTools(args[0], args[1], withPhysicalDispatchObserver(args[2]));
372
+ },
373
+ } : {}),
374
+ ...(rawProvider.generateStream ? {
375
+ generateStream: (...args) => {
376
+ return rawProvider.generateStream(args[0], withPhysicalDispatchObserver(args[1]), args[2]);
377
+ },
378
+ } : {}),
379
+ };
266
380
  const available = await provider.available().catch(() => false);
267
381
  if (!available) {
268
382
  emit({ kind: 'error', message: `${spec.label} is not configured or reachable. ${spec.setup}` });
@@ -362,7 +476,9 @@ export function createDqlAgentProviderRunner(id) {
362
476
  sourcePath: skill.sourcePath ?? '',
363
477
  }));
364
478
  const contextDurationMs = Date.now() - contextStartedAt;
365
- const answerLoopTools = buildAnswerLoopTools(req.projectRoot);
479
+ const answerLoopTools = buildAnswerLoopTools(req.projectRoot, {
480
+ researchResultRowsOptIn: researchRowsOptIn,
481
+ });
366
482
  const sourceSearchTool = answerLoopTools.find((tool) => tool.name === 'search_project_files');
367
483
  const sourceSearchStartedAt = Date.now();
368
484
  const earlySourceSearch = sourceSearchTool && shouldSearchProjectFiles(contextPack)
@@ -404,6 +520,19 @@ export function createDqlAgentProviderRunner(id) {
404
520
  if (req.projectSnapshot)
405
521
  req.assertProjectSnapshot?.(req.projectSnapshot.snapshotId);
406
522
  };
523
+ assertProviderPayloadAllowed(prepareProviderContextForDispatch({
524
+ question,
525
+ ...(conversationSnapshot ? { conversationSnapshot } : {}),
526
+ ...(memoryContext ? { memoryContext } : {}),
527
+ schemaContext: prepareServerOwnedProviderSchemaContext(schemaContext),
528
+ ...(contextPack ? { contextPack } : {}),
529
+ skills,
530
+ ...(followUp ? { followUp } : {}),
531
+ }), {
532
+ allowResultRows: false,
533
+ maxResultRows: 0,
534
+ purpose: 'answer_generation',
535
+ });
407
536
  const result = await answer({
408
537
  question,
409
538
  ...(req.resolvedAnalyticalPlan
@@ -442,22 +571,55 @@ export function createDqlAgentProviderRunner(id) {
442
571
  signal,
443
572
  reasoningEffort: req.reasoningEffort,
444
573
  analysisDepth: contextBudget.analysisDepth,
574
+ allowProviderSemanticMemberSelection: req.allowProviderSemanticMemberSelection === true,
445
575
  ...(req.semanticDriver ? { semanticDriver: req.semanticDriver } : {}),
446
576
  ...(req.semanticTableMapping ? { semanticTableMapping: req.semanticTableMapping } : {}),
447
577
  ...(req.semanticQueryCompiler ? { semanticQueryCompiler: req.semanticQueryCompiler } : {}),
448
578
  ...(req.preferredEvidenceIds?.length ? { preferredEvidenceIds: req.preferredEvidenceIds } : {}),
449
579
  ...(req.preferredExecutionId ? { preferredExecutionId: req.preferredExecutionId } : {}),
450
580
  executeCertifiedBlock: req.executeCertifiedBlock
451
- ? async (...args) => { guardSnapshot(); return req.executeCertifiedBlock(...args); }
581
+ ? async (...args) => { guardSnapshot(); sqlExecutions += 1; return req.executeCertifiedBlock(...args); }
452
582
  : undefined,
453
583
  executeGeneratedSql: req.executeGeneratedSql
454
- ? async (...args) => { guardSnapshot(); return req.executeGeneratedSql(...args); }
584
+ ? async (...args) => { guardSnapshot(); sqlExecutions += 1; return req.executeGeneratedSql(...args); }
455
585
  : undefined,
456
586
  executeDqlArtifact: req.executeDqlArtifact
457
- ? async (...args) => { guardSnapshot(); return req.executeDqlArtifact(...args); }
587
+ ? async (...args) => { guardSnapshot(); sqlExecutions += 1; return req.executeDqlArtifact(...args); }
458
588
  : undefined,
459
589
  expandGroundingContext: createGroundingContextExpander(req.projectRoot, req.probeNamedRelations),
460
590
  answerLoopTools,
591
+ providerPayloadGuard: {
592
+ purpose: 'research_tool',
593
+ allowedResultRowTools: researchRowsOptIn
594
+ ? { sample_notebook_dataset: 20, execute_local_analysis: 200 }
595
+ : {},
596
+ resultRowBudgetGroupByTool: {
597
+ sample_notebook_dataset: 'research_sample',
598
+ execute_local_analysis: 'research_local_analysis',
599
+ },
600
+ cumulativeResultRowBudgets: {
601
+ research_sample: 20,
602
+ research_local_analysis: 200,
603
+ },
604
+ onPayload: ({ toolName, output, resultRowCount, columnCount, cumulativeResultRowCount }) => {
605
+ pendingResearchPurpose = researchDispatchPurposeForTool(toolName);
606
+ pendingResultRowCount += resultRowCount;
607
+ pendingColumnCount = Math.max(pendingColumnCount, columnCount);
608
+ pendingCumulativeResultRowCount = Math.max(pendingCumulativeResultRowCount, cumulativeResultRowCount);
609
+ if (resultRowCount === 0)
610
+ return;
611
+ providerEgressReceipts.push(createProviderEgressReceipt({
612
+ purpose: researchDispatchPurposeForTool(toolName),
613
+ provider: provider.name,
614
+ permittedCategories: ['question', 'schema_metadata', 'result_rows'],
615
+ optIn: researchRowsOptIn,
616
+ payload: { toolName, output },
617
+ resultRowCount,
618
+ columnCount,
619
+ cumulativeResultRowCount,
620
+ }));
621
+ },
622
+ },
461
623
  // NOTE: no captureGeneratedDraft here — a plain answer/research question must NOT
462
624
  // auto-write a draft into the blocks space. A draft is created only when the user
463
625
  // explicitly acts (the "Create DQL draft" action → the dql_block_draft route).
@@ -483,6 +645,27 @@ export function createDqlAgentProviderRunner(id) {
483
645
  { phase: 'answer_resolution', durationMs: answerDurationMs, detail: result.route?.tier ?? result.kind },
484
646
  { phase: 'total', durationMs: Date.now() - requestStartedAt },
485
647
  ];
648
+ const terminalDispatchEvidence = dispatchEvidence('none');
649
+ result.evidence.runtimeCounters = {
650
+ providerRoundTrips: terminalDispatchEvidence.providerRoundTrips,
651
+ toolCalls: result.evidence.toolCalls?.length ?? 0,
652
+ sqlExecutions,
653
+ repairs: result.analysisPlan?.repairAttempts ?? 0,
654
+ };
655
+ if (req.analysisDepth === 'deep' && !providerEgressReceipts.some((receipt) => receipt.purpose === 'research_tool')) {
656
+ providerEgressReceipts.push(createProviderEgressReceipt({
657
+ purpose: 'research_tool',
658
+ provider: provider.name,
659
+ permittedCategories: ['instructions', 'question', 'schema_metadata', 'governed_context'],
660
+ optIn: researchRowsOptIn,
661
+ payload: { resultRows: 0, enabled: researchRowsOptIn },
662
+ resultRowCount: 0,
663
+ columnCount: 0,
664
+ }));
665
+ }
666
+ result.providerEgressReceipts = sharedDispatchEvidence
667
+ ? terminalDispatchEvidence.providerEgressReceipts
668
+ : providerEgressReceipts;
486
669
  emit({ kind: 'tool_result', id: 'governed_answer', output: result });
487
670
  emit({ kind: 'text', text: formatAgentAnswer(result) });
488
671
  if (result.proposedSql) {
@@ -507,10 +690,41 @@ export function createDqlAgentProviderRunner(id) {
507
690
  // A deadline/cancellation is NOT a provider failure. Rethrow it intact so
508
691
  // the engine renders its graceful bounded-deadline message instead of a
509
692
  // raw "<provider> failed: The operation was aborted due to timeout".
510
- rethrowIfCancelled(err, signal);
693
+ if (signal.aborted) {
694
+ const reason = signal.reason ?? err;
695
+ if (reason && typeof reason === 'object') {
696
+ Object.assign(reason, { providerDispatchEvidence: dispatchEvidence('cancelled') });
697
+ }
698
+ throw reason;
699
+ }
511
700
  const message = err instanceof Error ? err.message : String(err);
701
+ // Both codes are DQL's OWN budget refusing to start another dispatch.
702
+ // Only the count-based one was recognized, so a time-based refusal was
703
+ // reported as "<provider> failed: The 30-second soft target elapsed" —
704
+ // blaming the user's AI subscription for DQL's timer and sending them
705
+ // to debug the wrong system entirely.
706
+ const budgetCode = err && typeof err === 'object'
707
+ ? String(err.code)
708
+ : '';
709
+ const orchestrationBudgetExhausted = budgetCode === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED'
710
+ || budgetCode === 'RUN_SOFT_TARGET_EXCEEDED'
711
+ || budgetCode === 'RUN_DEADLINE_INSUFFICIENT';
712
+ // The project catalog was rebuilt underneath a run in flight. That is
713
+ // DQL's own concurrency, and telling the user their AI subscription
714
+ // failed sends them to re-authenticate a provider that worked fine.
715
+ const snapshotDrift = budgetCode === 'PROJECT_SNAPSHOT_MISMATCH';
512
716
  const setupHint = shouldShowProviderSetupHint(message) ? ` ${spec.setup}` : '';
513
- emit({ kind: 'error', message: `${spec.label} failed: ${message}.${setupHint}` });
717
+ emit({
718
+ kind: 'error',
719
+ message: orchestrationBudgetExhausted
720
+ ? `Ask stopped at its own bounded orchestration budget before a final answer was available — the AI provider did not fail. (${message})`
721
+ : snapshotDrift
722
+ ? 'The project snapshot was rebuilt while this run was in flight, so DQL discarded the in-progress plan rather than answer from a stale catalog. The AI provider did not fail — retry the question.'
723
+ : `${spec.label} failed: ${message}.${setupHint}`,
724
+ dispatchEvidence: dispatchEvidence(orchestrationBudgetExhausted ? 'orchestration_budget_exhausted'
725
+ : snapshotDrift ? 'project_snapshot_mismatch'
726
+ : 'provider_error'),
727
+ });
514
728
  }
515
729
  },
516
730
  };
@@ -663,13 +877,20 @@ export function renderExtraContext(req, followUp) {
663
877
  // questions. Render it as prose with its trust instruction, and strip it
664
878
  // from the JSON below rather than shipping the same content twice.
665
879
  try {
666
- const envelope = JSON.parse(upstream);
667
- const appContext = envelope.workspaceContext?.appContext;
880
+ const rawEnvelope = JSON.parse(upstream);
881
+ const appContext = rawEnvelope.workspaceContext?.appContext;
668
882
  const rendered = renderAppContextForPrompt(appContext);
669
883
  if (rendered) {
670
884
  parts.push(rendered);
671
- const { appContext: _dropped, ...restWorkspace } = envelope.workspaceContext ?? {};
672
- upstream = JSON.stringify({ ...envelope, workspaceContext: restWorkspace }, null, 2);
885
+ const { appContext: _dropped, ...restWorkspace } = rawEnvelope.workspaceContext ?? {};
886
+ const envelope = prepareProviderContextForDispatch({
887
+ ...rawEnvelope,
888
+ workspaceContext: restWorkspace,
889
+ });
890
+ upstream = JSON.stringify(envelope, null, 2);
891
+ }
892
+ else {
893
+ upstream = JSON.stringify(prepareProviderContextForDispatch(rawEnvelope), null, 2);
673
894
  }
674
895
  }
675
896
  catch {
@@ -885,6 +1106,27 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
885
1106
  const priorResultValues = cleanStringRecordArray(activeResult?.dimensionValues) ?? cleanStringRecordArray(context.resultDimensionValues);
886
1107
  const priorResultColumns = mergeStrings(arrayValue(activeResult?.columns), context.resultColumns, context.outputColumns);
887
1108
  const priorResultRef = priorResultRefFromTurn(activeTurn, activeResult, priorResultColumns);
1109
+ // The prior turn's actual rows, so a follow-up that computes ACROSS what was
1110
+ // already shown ("show only the top 3", "of these, the average") is answered
1111
+ // from those rows instead of a fresh warehouse query.
1112
+ //
1113
+ // `answer-loop.ts` declared `followUp.priorResult` and `tryCrossResultAnswer`
1114
+ // read it, but NOTHING ever populated it — the cross-result path was dead in
1115
+ // the repository. The turn already persists everything it needs: `columns`,
1116
+ // the bounded `rowsSample`, `measureColumns` and the full `rowCount`.
1117
+ const priorSampleRows = Array.isArray(activeResult?.rowsSample)
1118
+ ? activeResult.rowsSample.filter((row) => Array.isArray(row))
1119
+ : [];
1120
+ const priorResultRowCount = typeof activeResult?.rowCount === 'number' ? activeResult.rowCount : undefined;
1121
+ const priorMeasureColumns = arrayValue(activeResult?.measureColumns);
1122
+ const priorResult = priorResultColumns?.length && priorSampleRows.length > 0
1123
+ ? {
1124
+ columns: priorResultColumns,
1125
+ rows: priorSampleRows,
1126
+ ...(priorMeasureColumns?.length ? { measureColumns: priorMeasureColumns } : {}),
1127
+ ...(priorResultRowCount !== undefined ? { rowCount: priorResultRowCount } : {}),
1128
+ }
1129
+ : undefined;
888
1130
  const priorDqlArtifact = cleanDqlArtifactReference(activeTurn?.dqlArtifact) ?? cleanDqlArtifactReference(context.dqlArtifact);
889
1131
  const resolvedReferences = resolveConversationReferences(question, turns, priorResultValues);
890
1132
  const focusedPriorResultValues = resolvedReferences.valuesByDimension ?? priorResultValues;
@@ -928,6 +1170,7 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
928
1170
  ? mergeStrings(hasFocusedReference ? undefined : activeTurnStringArray(activeTurn, 'requestedDimensions'), hasFocusedReference ? undefined : context.requestedDimensions, extractDrilldownDimensions(question), resolvedReferences.dimensions)
929
1171
  : undefined,
930
1172
  priorResultColumns: relativeComparison ? undefined : priorResultColumns,
1173
+ ...(priorResult && !relativeComparison ? { priorResult } : {}),
931
1174
  priorResultValues: focusedPriorResultValues,
932
1175
  priorResultRef: relativeComparison ? undefined : priorResultRef,
933
1176
  priorDqlArtifact: relativeComparison ? undefined : priorDqlArtifact,
@@ -943,6 +1186,7 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
943
1186
  })),
944
1187
  resolvedReferences: resolvedReferences.labels,
945
1188
  unresolvedReferences: resolvedReferences.unresolved,
1189
+ deicticChoices: resolvedReferences.deicticChoices,
946
1190
  // The prior turn's actual rows, so a follow-up can compute across the shown
947
1191
  // results ("of these, the average") without a fresh query. A relative
948
1192
  // comparison deliberately drops the prior contract, so skip it there too.
@@ -1161,7 +1405,18 @@ function resolveConversationReferences(question, turns, activeValues) {
1161
1405
  ];
1162
1406
  let filters = resolveDeicticFilters(question, activeValues) ?? [];
1163
1407
  const labels = [];
1164
- if (!namedValues && dimensions.length > 0 && filters.length === 0) {
1408
+ // An ambiguous singular reference is the single most common follow-up shape
1409
+ // ("what region does this customer belong to?" after a ranked list). Keep the
1410
+ // candidates so the run can offer them instead of dead-ending.
1411
+ const ambiguousSingular = !namedValues
1412
+ ? resolveSingularDeicticCandidates(question, activeValues)
1413
+ : undefined;
1414
+ const deicticChoices = ambiguousSingular && ambiguousSingular.values.length > 1
1415
+ ? ambiguousSingular
1416
+ : undefined;
1417
+ // Do NOT backfill an ambiguous singular from history: filtering on all of the
1418
+ // candidates answers a different question than the one that was asked.
1419
+ if (!namedValues && !deicticChoices && dimensions.length > 0 && filters.length === 0) {
1165
1420
  for (const turn of [...turns].reverse()) {
1166
1421
  const values = cleanStringRecordArray(cleanRecord(turn.result)?.dimensionValues);
1167
1422
  if (!values)
@@ -1200,7 +1455,9 @@ function resolveConversationReferences(question, turns, activeValues) {
1200
1455
  }]
1201
1456
  : [];
1202
1457
  const unresolved = referencesNeedValues(question) && filters.length === 0
1203
- ? ['Could not resolve the referenced prior result values from conversation state.']
1458
+ ? [deicticChoices
1459
+ ? `"${deicticChoices.dimension}" was referenced in the singular, but the previous result had ${deicticChoices.values.length} of them.`
1460
+ : 'Could not resolve the referenced prior result values from conversation state.']
1204
1461
  : undefined;
1205
1462
  return {
1206
1463
  filters: filters.length > 0 ? Array.from(new Set(filters)).slice(0, 24) : undefined,
@@ -1209,6 +1466,7 @@ function resolveConversationReferences(question, turns, activeValues) {
1209
1466
  unresolved,
1210
1467
  valuesByDimension: namedValues,
1211
1468
  memberBindings: memberBindings.length > 0 ? memberBindings : undefined,
1469
+ deicticChoices,
1212
1470
  };
1213
1471
  }
1214
1472
  /**
@@ -1301,7 +1559,7 @@ function activeTurnNumber(turn, key) {
1301
1559
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
1302
1560
  }
1303
1561
  function referencesNeedValues(question) {
1304
- return /\b(?:it|its|they|their|them|this|that|these|those|same|above|previous|prior)\b/i.test(question);
1562
+ return /\b(?:it|its|they|their|them|he|she|him|his|her|hers|this|that|these|those|same|above|previous|prior)\b/i.test(question);
1305
1563
  }
1306
1564
  function extractCertifiedBlockName(content) {
1307
1565
  const fromAnswer = content.match(/Answered by certified block \*\*([^*]+)\*\*/i)?.[1];
@@ -1353,15 +1611,34 @@ function cleanStringRecordArray(value) {
1353
1611
  }
1354
1612
  return Object.keys(out).length > 0 ? out : undefined;
1355
1613
  }
1614
+ /**
1615
+ * Values a singular deictic reference ("this customer") could be pointing at.
1616
+ *
1617
+ * This used to `.slice(0, 1)` — silently picking the FIRST row of the previous
1618
+ * answer and binding it as a required filter. After a ten-row ranking, "this
1619
+ * customer" would quietly become "Melissa Lopez" because she happened to sort
1620
+ * first, and the user was never told. An ambiguous reference must stay
1621
+ * ambiguous so the caller can ask which one was meant.
1622
+ */
1623
+ function resolveSingularDeicticCandidates(question, priorValues) {
1624
+ if (!priorValues)
1625
+ return undefined;
1626
+ const singular = resolveSingularDeicticDimension(question, priorValues);
1627
+ if (!singular)
1628
+ return undefined;
1629
+ const values = Array.from(new Set(valuesForPriorDimension(priorValues, singular))).slice(0, 24);
1630
+ return values.length > 0 ? { dimension: singular, values } : undefined;
1631
+ }
1356
1632
  function resolveDeicticFilters(question, priorValues) {
1357
1633
  if (!priorValues)
1358
1634
  return undefined;
1359
1635
  const hasExplicitPluralReference = /\b(?:these|those|same)\s+(?:customers?|products?|cat(?:egor|agor|ogor)(?:y|ies)|segments?|regions?)\b/i.test(question);
1360
1636
  if (!hasExplicitPluralReference) {
1361
- const singular = resolveSingularDeicticDimension(question, priorValues);
1637
+ const singular = resolveSingularDeicticCandidates(question, priorValues);
1362
1638
  if (singular) {
1363
- const values = valuesForPriorDimension(priorValues, singular).slice(0, 1);
1364
- return values.length > 0 ? values : undefined;
1639
+ // Exactly one candidate is unambiguous and can be bound as a filter.
1640
+ // Several candidates are a question for the user, not a coin flip.
1641
+ return singular.values.length === 1 ? singular.values : undefined;
1365
1642
  }
1366
1643
  }
1367
1644
  const dims = resolveDeicticDimensions(question, priorValues) ?? [];
@@ -1401,7 +1678,18 @@ function resolveDeicticDimensions(question, priorValues) {
1401
1678
  && /\b(?:they|their|them)\b[^.?!]{0,48}\b(?:buy|bought|purchase|purchased|order|ordered|spend|spent|use|used)\b/.test(lower)
1402
1679
  && valuesForPriorDimension(priorValues, 'customer').length > 0)
1403
1680
  dims.push('customer');
1404
- if (dims.length === 0 && /\b(?:it|its|they|their|them|this|these|those|that|same|above|previous|prior)\b/.test(lower)) {
1681
+ // Singular people pronouns are common in conversational analytical follow-ups
1682
+ // ("what region does he belong to?", "what is her segment?"). Resolve them
1683
+ // against the prior result's typed customer values, preserving the same
1684
+ // ambiguity guard used by "this customer". This is intentionally limited to
1685
+ // a person-like prior dimension; a pronoun must never make us guess a product
1686
+ // or metric member from the first row.
1687
+ if (dims.length === 0
1688
+ && /\b(?:he|she|him|his|her|hers)\b/.test(lower)
1689
+ && valuesForPriorDimension(priorValues, 'customer').length > 0) {
1690
+ dims.push('customer');
1691
+ }
1692
+ if (dims.length === 0 && /\b(?:it|its|they|their|them|he|she|him|his|her|hers|this|these|those|that|same|above|previous|prior)\b/.test(lower)) {
1405
1693
  const single = singlePriorValueDimension(priorValues);
1406
1694
  if (single)
1407
1695
  dims.push(single);
@@ -1413,6 +1701,7 @@ function resolveSingularDeicticDimension(question, priorValues) {
1413
1701
  const candidates = [
1414
1702
  [/\b(?:this|that|same|previous|prior|above)\s+product\b/, 'product'],
1415
1703
  [/\b(?:this|that|same|previous|prior|above)\s+customer\b/, 'customer'],
1704
+ [/\b(?:he|she|him|his|her|hers)\b/, 'customer'],
1416
1705
  [/\b(?:this|that|same|previous|prior|above)\s+category\b/, 'category'],
1417
1706
  [/\b(?:this|that|same|previous|prior|above)\s+segment\b/, 'segment'],
1418
1707
  [/\b(?:this|that|same|previous|prior|above)\s+region\b/, 'region'],
@@ -1499,7 +1788,11 @@ export const __test__ = {
1499
1788
  shouldLoadSchemaContext,
1500
1789
  shouldSearchProjectFiles,
1501
1790
  renderProjectSourceSearch,
1791
+ researchDispatchPurposeForTool,
1502
1792
  };
1793
+ function researchDispatchPurposeForTool(toolName) {
1794
+ return toolName === 'execute_local_analysis' ? 'research_tool' : 'research_narration';
1795
+ }
1503
1796
  const GENERIC_FOLLOW_UP_WORDS = new Set([
1504
1797
  'a', 'about', 'again', 'all', 'also', 'and', 'are', 'as', 'be', 'block', 'can', 'could', 'data', 'did', 'do', 'does',
1505
1798
  'execute', 'for', 'from', 'get', 'give', 'import', 'it', 'its', 'let', 'lets', 'me', 'metrics',
@@ -1537,7 +1830,7 @@ function isGenericFollowUp(question) {
1537
1830
  function isDrilldownFollowUp(question, priorTerms = []) {
1538
1831
  const lower = question.toLowerCase();
1539
1832
  const deicticDrilldown = /\b(?:this|that|these|those|same|above|previous|prior)\s+(?:amount|value|orders?|results?|rows?|customers?|products?|cat(?:egor|agor|ogor)(?:y|ies)|segments?|regions?)\b/.test(lower)
1540
- || /\b(?:they|their|them)\b/.test(lower);
1833
+ || /\b(?:they|their|them|he|she|him|his|her|hers)\b/.test(lower);
1541
1834
  // `break ... down` is split by its object more often than not ("break that
1542
1835
  // down", "break the revenue down"), so the verb and particle are matched with
1543
1836
  // a short gap between them rather than adjacently.