@duckcodeailabs/dql-agent 1.14.0 → 1.14.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 (146) hide show
  1. package/dist/agent-run-engine.d.ts +60 -2
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +333 -21
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agent-run-gates.d.ts.map +1 -1
  6. package/dist/agent-run-gates.js +13 -0
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agentic/analyst-loop.d.ts +91 -0
  9. package/dist/agentic/analyst-loop.d.ts.map +1 -0
  10. package/dist/agentic/analyst-loop.js +311 -0
  11. package/dist/agentic/analyst-loop.js.map +1 -0
  12. package/dist/agentic/assumptions.d.ts +62 -0
  13. package/dist/agentic/assumptions.d.ts.map +1 -0
  14. package/dist/agentic/assumptions.js +73 -0
  15. package/dist/agentic/assumptions.js.map +1 -0
  16. package/dist/agentic/business-explanation.d.ts +56 -0
  17. package/dist/agentic/business-explanation.d.ts.map +1 -0
  18. package/dist/agentic/business-explanation.js +130 -0
  19. package/dist/agentic/business-explanation.js.map +1 -0
  20. package/dist/agentic/identifier-ledger.d.ts +74 -0
  21. package/dist/agentic/identifier-ledger.d.ts.map +1 -0
  22. package/dist/agentic/identifier-ledger.js +151 -0
  23. package/dist/agentic/identifier-ledger.js.map +1 -0
  24. package/dist/agentic/ledger-tools.d.ts +68 -0
  25. package/dist/agentic/ledger-tools.d.ts.map +1 -0
  26. package/dist/agentic/ledger-tools.js +198 -0
  27. package/dist/agentic/ledger-tools.js.map +1 -0
  28. package/dist/agentic/orchestrator-policy.d.ts +60 -0
  29. package/dist/agentic/orchestrator-policy.d.ts.map +1 -0
  30. package/dist/agentic/orchestrator-policy.js +73 -0
  31. package/dist/agentic/orchestrator-policy.js.map +1 -0
  32. package/dist/agentic/orchestrator.d.ts +54 -0
  33. package/dist/agentic/orchestrator.d.ts.map +1 -0
  34. package/dist/agentic/orchestrator.js +67 -0
  35. package/dist/agentic/orchestrator.js.map +1 -0
  36. package/dist/agentic/preview-tool.d.ts +36 -0
  37. package/dist/agentic/preview-tool.d.ts.map +1 -0
  38. package/dist/agentic/preview-tool.js +71 -0
  39. package/dist/agentic/preview-tool.js.map +1 -0
  40. package/dist/agentic/rerank.d.ts +50 -0
  41. package/dist/agentic/rerank.d.ts.map +1 -0
  42. package/dist/agentic/rerank.js +140 -0
  43. package/dist/agentic/rerank.js.map +1 -0
  44. package/dist/agentic/research/hypothesis.d.ts +110 -0
  45. package/dist/agentic/research/hypothesis.d.ts.map +1 -0
  46. package/dist/agentic/research/hypothesis.js +125 -0
  47. package/dist/agentic/research/hypothesis.js.map +1 -0
  48. package/dist/agentic/research/synthesis.d.ts +32 -0
  49. package/dist/agentic/research/synthesis.d.ts.map +1 -0
  50. package/dist/agentic/research/synthesis.js +94 -0
  51. package/dist/agentic/research/synthesis.js.map +1 -0
  52. package/dist/agentic/research-agent.d.ts +54 -0
  53. package/dist/agentic/research-agent.d.ts.map +1 -0
  54. package/dist/agentic/research-agent.js +176 -0
  55. package/dist/agentic/research-agent.js.map +1 -0
  56. package/dist/agentic/sql-authorization.d.ts +131 -0
  57. package/dist/agentic/sql-authorization.d.ts.map +1 -0
  58. package/dist/agentic/sql-authorization.js +232 -0
  59. package/dist/agentic/sql-authorization.js.map +1 -0
  60. package/dist/agentic/tool-loop.d.ts +29 -0
  61. package/dist/agentic/tool-loop.d.ts.map +1 -1
  62. package/dist/agentic/tool-loop.js +129 -52
  63. package/dist/agentic/tool-loop.js.map +1 -1
  64. package/dist/agentic/toolset.d.ts.map +1 -1
  65. package/dist/agentic/toolset.js +155 -0
  66. package/dist/agentic/toolset.js.map +1 -1
  67. package/dist/agentic/turn-plan.d.ts +35 -0
  68. package/dist/agentic/turn-plan.d.ts.map +1 -0
  69. package/dist/agentic/turn-plan.js +108 -0
  70. package/dist/agentic/turn-plan.js.map +1 -0
  71. package/dist/agentic/value-probe.d.ts +89 -0
  72. package/dist/agentic/value-probe.d.ts.map +1 -0
  73. package/dist/agentic/value-probe.js +304 -0
  74. package/dist/agentic/value-probe.js.map +1 -0
  75. package/dist/analytical-orchestration.d.ts +80 -0
  76. package/dist/analytical-orchestration.d.ts.map +1 -1
  77. package/dist/analytical-orchestration.js +218 -6
  78. package/dist/analytical-orchestration.js.map +1 -1
  79. package/dist/analytical-request-policy.d.ts +26 -0
  80. package/dist/analytical-request-policy.d.ts.map +1 -0
  81. package/dist/analytical-request-policy.js +82 -0
  82. package/dist/analytical-request-policy.js.map +1 -0
  83. package/dist/analytical-result-facts.d.ts +13 -0
  84. package/dist/analytical-result-facts.d.ts.map +1 -1
  85. package/dist/analytical-result-facts.js +37 -1
  86. package/dist/analytical-result-facts.js.map +1 -1
  87. package/dist/answer-loop.d.ts +57 -16
  88. package/dist/answer-loop.d.ts.map +1 -1
  89. package/dist/answer-loop.js +161 -30
  90. package/dist/answer-loop.js.map +1 -1
  91. package/dist/cascade/budgets.d.ts.map +1 -1
  92. package/dist/cascade/budgets.js +8 -1
  93. package/dist/cascade/budgets.js.map +1 -1
  94. package/dist/cascade/packer.d.ts +88 -0
  95. package/dist/cascade/packer.d.ts.map +1 -0
  96. package/dist/cascade/packer.js +106 -0
  97. package/dist/cascade/packer.js.map +1 -0
  98. package/dist/conversation/result-ops.d.ts +18 -3
  99. package/dist/conversation/result-ops.d.ts.map +1 -1
  100. package/dist/conversation/result-ops.js +77 -5
  101. package/dist/conversation/result-ops.js.map +1 -1
  102. package/dist/conversation/rolling-summary.d.ts.map +1 -1
  103. package/dist/conversation/rolling-summary.js +7 -2
  104. package/dist/conversation/rolling-summary.js.map +1 -1
  105. package/dist/conversation/session-store.d.ts +3 -0
  106. package/dist/conversation/session-store.d.ts.map +1 -1
  107. package/dist/conversation/session-store.js +8 -4
  108. package/dist/conversation/session-store.js.map +1 -1
  109. package/dist/conversation/snapshot.d.ts.map +1 -1
  110. package/dist/conversation/snapshot.js +10 -1
  111. package/dist/conversation/snapshot.js.map +1 -1
  112. package/dist/hints/store.d.ts.map +1 -1
  113. package/dist/hints/store.js +15 -5
  114. package/dist/hints/store.js.map +1 -1
  115. package/dist/index.d.ts +27 -7
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +28 -5
  118. package/dist/index.js.map +1 -1
  119. package/dist/intent-controller.d.ts +8 -0
  120. package/dist/intent-controller.d.ts.map +1 -1
  121. package/dist/intent-controller.js +65 -0
  122. package/dist/intent-controller.js.map +1 -1
  123. package/dist/metadata/block-fit.d.ts +18 -0
  124. package/dist/metadata/block-fit.d.ts.map +1 -1
  125. package/dist/metadata/block-fit.js +41 -1
  126. package/dist/metadata/block-fit.js.map +1 -1
  127. package/dist/metadata/catalog.d.ts +20 -23
  128. package/dist/metadata/catalog.d.ts.map +1 -1
  129. package/dist/metadata/catalog.js +136 -11
  130. package/dist/metadata/catalog.js.map +1 -1
  131. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  132. package/dist/metadata/meaning-evidence.js +15 -7
  133. package/dist/metadata/meaning-evidence.js.map +1 -1
  134. package/dist/metadata/metric-match.d.ts +32 -0
  135. package/dist/metadata/metric-match.d.ts.map +1 -1
  136. package/dist/metadata/metric-match.js +120 -9
  137. package/dist/metadata/metric-match.js.map +1 -1
  138. package/dist/research-loop.d.ts +33 -2
  139. package/dist/research-loop.d.ts.map +1 -1
  140. package/dist/research-loop.js +44 -4
  141. package/dist/research-loop.js.map +1 -1
  142. package/dist/router.d.ts +28 -16
  143. package/dist/router.d.ts.map +1 -1
  144. package/dist/router.js +250 -17
  145. package/dist/router.js.map +1 -1
  146. package/package.json +4 -4
@@ -1,3 +1,4 @@
1
+ import { packContext } from './cascade/packer.js';
1
2
  /**
2
3
  * Dynamic-first governed answer loop.
3
4
  *
@@ -26,7 +27,7 @@ import { domainContextSearchDomains } from './domain-context.js';
26
27
  import { deriveGeneratedDraftSlug, renderGeneratedSqlDqlArtifact } from './metadata/drafts.js';
27
28
  import { buildAnalysisQuestionPlan } from './metadata/analysis-planner.js';
28
29
  import { certifiedFitAllowsTier1, certifiedTerminationVerdict, evaluateCertifiedBlockFit } from './metadata/block-fit.js';
29
- import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricDefinition, resolveGovernedMetricSql } from './metadata/metric-match.js';
30
+ import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricDefinition, resolveGovernedMetricSql, semanticMetricEmbeddingOptions } from './metadata/metric-match.js';
30
31
  import { decideAgentAction } from './intent-controller.js';
31
32
  import { createContextLedger } from './grounding/context-ledger.js';
32
33
  import { validateAnswerResultShape } from './answer-shape.js';
@@ -421,7 +422,35 @@ const MANIFEST_KINDS = ['dbt_model', 'dbt_source'];
421
422
  * surface shows THIS; the validator's machine message (relation ids, repair
422
423
  * tool guidance) belongs in refusalDetails/validationWarnings only.
423
424
  */
424
- function renderContextValidationRefusalForUser(code, machineError, memberBindings) {
425
+ /**
426
+ * Name the ONE aggregation check that actually fired.
427
+ *
428
+ * The refusal used to list all four possible causes — "rounding too early,
429
+ * losing decimal precision, summing a non-additive value, or multiplying rows
430
+ * across a join" — because the specific one was never plumbed into the message.
431
+ * The system knows: `aggregationIntegrityIssuesForSql` classifies each issue and
432
+ * the proof carries the codes. A menu of four causes reads as "something is
433
+ * wrong somewhere", which is exactly the report a user cannot act on.
434
+ */
435
+ function aggregationRefusalReason(issueCodes = []) {
436
+ const codes = new Set(issueCodes.map((code) => code.toUpperCase()));
437
+ // Ordered by how badly each distorts the number, so a query tripping several
438
+ // leads with the one that matters most.
439
+ if (codes.has('FANOUT')) {
440
+ return 'joining those tables multiplies rows, so the total would be inflated. Aggregate at the row-level grain first, then join';
441
+ }
442
+ if (codes.has('NON_ADDITIVE_MEASURE')) {
443
+ return 'it sums a measure that is not additive (an average or a ratio cannot be added up). Ask for the governed metric, which carries the right rule';
444
+ }
445
+ if (codes.has('PREMATURE_ROUNDING')) {
446
+ return 'it rounds each value before adding them, which loses money at scale. Rounding belongs on the final total';
447
+ }
448
+ if (codes.has('LOSSY_NUMERIC_CAST')) {
449
+ return 'it converts amounts to floating point before adding them, which drifts on decimal currency';
450
+ }
451
+ return undefined;
452
+ }
453
+ export function renderContextValidationRefusalForUser(code, machineError, memberBindings, aggregationIssueCodes) {
425
454
  switch (code) {
426
455
  case 'unknown_relation':
427
456
  return 'I drafted a query, but it uses a table that was not part of the metadata retrieved for this question, so I did not run it. Ask again (retrieval usually finds it on a follow-up), name the table or metric explicitly, or re-sync the dbt metadata.';
@@ -438,8 +467,15 @@ function renderContextValidationRefusalForUser(code, machineError, memberBinding
438
467
  return 'This comparison needs a baseline period or value that the drafted query did not include. Say what to compare against (for example, the prior month) and I will run it.';
439
468
  case 'unsafe_sql':
440
469
  return 'The drafted query used a statement type that is not allowed in this governed preview, so I did not run it.';
441
- case 'unsafe_aggregation':
442
- return 'I drafted a query, but it could change the metric meaning by rounding too early, losing decimal precision, summing a non-additive value, or multiplying rows across a join. I did not run it. Use the governed semantic metric, or review the native grain and join keys before retrying.';
470
+ case 'unsafe_aggregation': {
471
+ const reason = aggregationRefusalReason(aggregationIssueCodes);
472
+ if (reason) {
473
+ return `I drafted a query but did not run it, because ${reason}. Ask for the governed metric and I will use the safe version.`;
474
+ }
475
+ // No classified code reached us — say that plainly rather than listing
476
+ // every cause as if one of them were known to apply.
477
+ return 'I drafted a query, but it would change how the metric is calculated, so I did not run it. Ask for the governed semantic metric and I will use the safe version.';
478
+ }
443
479
  case 'insufficient_context':
444
480
  if (/could not be parsed|parse error|syntax/i.test(machineError)) {
445
481
  return 'I drafted a query, but its SQL syntax did not match the connected warehouse, so I did not run it. The failed draft is available in Inspect; retrying will generate a warehouse-specific query.';
@@ -601,7 +637,10 @@ function tryCrossResultAnswer(input) {
601
637
  const prior = input.followUp?.priorResult;
602
638
  if (!prior || !Array.isArray(prior.rows) || prior.rows.length === 0)
603
639
  return null;
604
- const op = detectResultSetOperation(input.question);
640
+ // Pass the prior result's shape so a follow-up that names one of its columns
641
+ // ("what's the average lifetime_spend?") is recognised without requiring a
642
+ // demonstrative pronoun.
643
+ const op = detectResultSetOperation(input.question, prior);
605
644
  if (!op)
606
645
  return null;
607
646
  const computed = computeResultSetOperation(op, prior);
@@ -696,6 +735,10 @@ async function freezeLegacySemanticSelection(input) {
696
735
  const match = await matchSemanticMetric(input.question, metricNodes, {
697
736
  measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
698
737
  ...(semanticLayer ? { canExecute: (name) => semanticLayer.canComposeMetric(name) } : {}),
738
+ ...(snapshotVectorMetricShortlist(input.contextPack).length
739
+ ? { vectorMetricShortlist: snapshotVectorMetricShortlist(input.contextPack) }
740
+ : {}),
741
+ ...semanticMetricEmbeddingOptions(input.embeddingProvider),
699
742
  }).catch(() => null);
700
743
  if (!match)
701
744
  return undefined;
@@ -813,6 +856,26 @@ async function freezeLegacySemanticSelection(input) {
813
856
  mode: 'authoritative',
814
857
  });
815
858
  }
859
+ /**
860
+ * Reuse the immutable context pack's independent vector lane. Metric matching
861
+ * never opens the catalog or runs a second metadata retrieval pass; it only
862
+ * narrows its vector-only candidate admission to IDs already retrieved for this
863
+ * request snapshot.
864
+ */
865
+ function snapshotVectorMetricShortlist(contextPack) {
866
+ const lane = contextPack?.retrievalDiagnostics.lanes?.find((item) => item.lane === 'vector');
867
+ if (!lane?.candidates.length || !contextPack)
868
+ return [];
869
+ const objects = new Map(contextPack.objects.map((object) => [object.objectKey, object]));
870
+ const result = [];
871
+ for (const candidate of lane.candidates) {
872
+ const object = objects.get(candidate.objectKey);
873
+ if (!object || (object.objectType !== 'semantic_metric' && object.objectType !== 'semantic_measure'))
874
+ continue;
875
+ result.push(candidate.objectKey, object.name);
876
+ }
877
+ return [...new Set(result)];
878
+ }
816
879
  export async function answer(input) {
817
880
  const inheritedPlan = !input.resolvedAnalyticalPlan
818
881
  && input.followUp?.priorResolvedAnalyticalPlan
@@ -1543,6 +1606,10 @@ async function runAnswerLoop(input) {
1543
1606
  : await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
1544
1607
  measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
1545
1608
  ...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
1609
+ ...(snapshotVectorMetricShortlist(input.contextPack).length
1610
+ ? { vectorMetricShortlist: snapshotVectorMetricShortlist(input.contextPack) }
1611
+ : {}),
1612
+ ...semanticMetricEmbeddingOptions(input.embeddingProvider),
1546
1613
  }).catch(() => null);
1547
1614
  // Stage 1: certified artifact match. Blocks can be executed; dashboards,
1548
1615
  // Apps, and notebooks are returned as governed citations/navigation targets.
@@ -2317,7 +2384,18 @@ async function runAnswerLoop(input) {
2317
2384
  onCompiled: (record) => compiledSemanticRecords.push(record),
2318
2385
  }),
2319
2386
  ];
2320
- if (semanticBridgeAnswer) {
2387
+ if (input.forcedGeneratedProposal) {
2388
+ // The analyst loop already did its provider/tool work. Re-entering ordinary
2389
+ // generation here would create a second proposal whose SQL is not covered
2390
+ // by the request-scoped execution capability.
2391
+ parsed = {
2392
+ sql: input.forcedGeneratedProposal.sql,
2393
+ text: input.forcedGeneratedProposal.summary
2394
+ ?? 'Prepared from identifiers observed during this bounded analyst run. Review-required until an analyst promotes it.',
2395
+ ...(input.forcedGeneratedProposal.suggestedViz ? { viz: input.forcedGeneratedProposal.suggestedViz } : {}),
2396
+ };
2397
+ }
2398
+ else if (semanticBridgeAnswer) {
2321
2399
  governedMetricAnswer = true;
2322
2400
  parsed = {
2323
2401
  sql: semanticBridgeAnswer.sql,
@@ -2503,7 +2581,7 @@ async function runAnswerLoop(input) {
2503
2581
  let deepCandidateResult;
2504
2582
  let deepCandidateExecutionError;
2505
2583
  let deepCandidateNotes = [];
2506
- if (!governedMetricAnswer && input.analysisDepth === 'deep' && parsed.sql) {
2584
+ if (!input.forcedGeneratedProposal && !governedMetricAnswer && input.analysisDepth === 'deep' && parsed.sql) {
2507
2585
  const selection = await selectDeepGeneratedProposalCandidate({
2508
2586
  provider,
2509
2587
  messages,
@@ -2705,7 +2783,12 @@ async function runAnswerLoop(input) {
2705
2783
  };
2706
2784
  }
2707
2785
  }
2708
- const tightenedFlow = tightenSourceTargetFlowProjection(parsed.sql, question, questionPlan);
2786
+ // The analyst capability binds the exact composed bytes. Any projection
2787
+ // tightening is a new SQL proposal and therefore must wait for a fresh
2788
+ // analyst pass/capability rather than silently rewriting this one.
2789
+ const tightenedFlow = input.forcedGeneratedProposal
2790
+ ? undefined
2791
+ : tightenSourceTargetFlowProjection(parsed.sql, question, questionPlan);
2709
2792
  if (tightenedFlow && tightenedFlow.sql !== parsed.sql) {
2710
2793
  parsed.sql = tightenedFlow.sql;
2711
2794
  parsed.outputs = tightenedFlow.outputs;
@@ -2723,7 +2806,7 @@ async function runAnswerLoop(input) {
2723
2806
  // model emitted to its real warehouse relation from the runtime schema BEFORE
2724
2807
  // governance validation. Same resolver the build path uses — one grounding,
2725
2808
  // no weak path. `allowedSqlContext` relations are already qualified.
2726
- if (parsed.sql) {
2809
+ if (parsed.sql && !input.forcedGeneratedProposal) {
2727
2810
  parsed.sql = contextLedger.qualifySql(parsed.sql).sql;
2728
2811
  }
2729
2812
  // Validation gate. Governed metric SQL synthesized from the semantic layer is
@@ -2840,7 +2923,8 @@ async function runAnswerLoop(input) {
2840
2923
  // that predicate entirely, inject it deterministically before spending the
2841
2924
  // bounded AI validation repair. Misbound or ambiguous predicates are never
2842
2925
  // rewritten here; they continue through the guarded correction/refusal path.
2843
- if (!contextValidation.ok
2926
+ if (!input.forcedGeneratedProposal
2927
+ && !contextValidation.ok
2844
2928
  && contextValidation.code === 'misbound_filter'
2845
2929
  && !contextValidation.offending?.relation
2846
2930
  && !contextValidation.offending?.column
@@ -2892,7 +2976,8 @@ async function runAnswerLoop(input) {
2892
2976
  // stands instead of guessing a measure.
2893
2977
  // 2) Otherwise fall back to a CLEAN governed-metric definition (direct or exact
2894
2978
  // leaf-measure; no fuzzy family guess that could answer the wrong measure).
2895
- if (!contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && semanticMetricRoute && semanticMetricMatch && !rankedGrainGap) {
2979
+ if (!input.forcedGeneratedProposal
2980
+ && !contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && semanticMetricRoute && semanticMetricMatch && !rankedGrainGap) {
2896
2981
  const grounded = contextLedger.validateRuntimeGrounding(parsed.sql);
2897
2982
  // Runtime grounding proves that relations and columns exist; it cannot prove
2898
2983
  // that a resolved business member was bound to the right dimension. Never
@@ -2922,7 +3007,8 @@ async function runAnswerLoop(input) {
2922
3007
  // nothing, or found the table and something else rejected the SQL — which are
2923
3008
  // three different fixes.
2924
3009
  let regroundDiagnostic;
2925
- if (!contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && !governedMetricAnswer && input.expandGroundingContext && canUseLaneRepair(repairBudgetState, 'reground')) {
3010
+ if (!input.forcedGeneratedProposal
3011
+ && !contextValidation.ok && contextValidation.code !== 'unsafe_aggregation' && !governedMetricAnswer && input.expandGroundingContext && canUseLaneRepair(repairBudgetState, 'reground')) {
2926
3012
  recordLaneRepair(repairBudgetState, 'reground');
2927
3013
  try {
2928
3014
  const expansion = await input.expandGroundingContext({
@@ -3019,6 +3105,7 @@ async function runAnswerLoop(input) {
3019
3105
  && input.resolvedAnalyticalPlan?.mode !== 'authoritative'
3020
3106
  && !semanticAggregationAuthorityBlocked
3021
3107
  && !governedMetricAnswer
3108
+ && !input.forcedGeneratedProposal
3022
3109
  && canUseLaneRepair(repairBudgetState, 'validation')) {
3023
3110
  recordLaneRepair(repairBudgetState, 'validation');
3024
3111
  try {
@@ -3094,7 +3181,7 @@ async function runAnswerLoop(input) {
3094
3181
  // Business-language chat text; the validator's machine message (with
3095
3182
  // relation ids and tool guidance for the repair prompt) stays in
3096
3183
  // refusalDetails + validationWarnings for the Inspect surface.
3097
- const text = renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings);
3184
+ const text = renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings, contextValidation.aggregationSafetyProof?.issueCodes);
3098
3185
  const analysisPlan = buildAnalysisPlan({
3099
3186
  question,
3100
3187
  intent,
@@ -3283,8 +3370,6 @@ async function runAnswerLoop(input) {
3283
3370
  && semanticBridgeAnswer?.sql.trim() === parsed.sql?.trim()) {
3284
3371
  return input.executeDqlArtifact({ ...semanticArtifact, limit: requestedLimit });
3285
3372
  }
3286
- if (!input.executeGeneratedSql)
3287
- throw new Error('No generated SQL executor is configured.');
3288
3373
  const artifact = buildGeneratedSqlDqlArtifact({
3289
3374
  question,
3290
3375
  sql: parsed.sql,
@@ -3300,9 +3385,20 @@ async function runAnswerLoop(input) {
3300
3385
  validationWarnings: [...contextValidation.warnings, ...deepCandidateNotes],
3301
3386
  outputs: parsed.outputs,
3302
3387
  });
3303
- return input.executeGeneratedSql(parsed.sql, artifact ? { ...artifact, limit: requestedLimit } : undefined);
3388
+ const boundedArtifact = artifact ? { ...artifact, limit: requestedLimit } : undefined;
3389
+ if (input.forcedGeneratedProposal) {
3390
+ if (!input.agenticSqlExecutionCapability || !input.executeAgenticGeneratedSql) {
3391
+ throw Object.assign(new Error('The analyst-approved SQL has no live execution capability, so DQL did not execute it.'), {
3392
+ code: 'AGENTIC_EXECUTION_CAPABILITY_REQUIRED',
3393
+ });
3394
+ }
3395
+ return input.executeAgenticGeneratedSql(input.agenticSqlExecutionCapability, parsed.sql, boundedArtifact);
3396
+ }
3397
+ if (!input.executeGeneratedSql)
3398
+ throw new Error('No generated SQL executor is configured.');
3399
+ return input.executeGeneratedSql(parsed.sql, boundedArtifact);
3304
3400
  };
3305
- if (input.executeGeneratedSql && !result) {
3401
+ if ((input.executeGeneratedSql || (input.forcedGeneratedProposal && input.executeAgenticGeneratedSql)) && !result) {
3306
3402
  // Fanout gate for native semantic direct-joins: a duplicate join-key row on
3307
3403
  // the joined side multiplies fact rows BEFORE aggregation, so every summed
3308
3404
  // value inflates silently (seen in the field as trillions-scale "governed"
@@ -3311,6 +3407,8 @@ async function runAnswerLoop(input) {
3311
3407
  // repair, which would regenerate the same multiplying join.
3312
3408
  let fanoutContradiction = false;
3313
3409
  if (!executionError
3410
+ && !input.forcedGeneratedProposal
3411
+ && input.executeGeneratedSql
3314
3412
  && semanticBridgeAnswer?.composeResult?.fanoutProbeSql
3315
3413
  && semanticBridgeAnswer.sql.trim() === parsed.sql?.trim()) {
3316
3414
  const inflationError = await probeSemanticJoinFanout(semanticBridgeAnswer.composeResult.fanoutProbeSql, semanticBridgeAnswer.composeResult.tables, input.executeGeneratedSql);
@@ -3327,7 +3425,7 @@ async function runAnswerLoop(input) {
3327
3425
  warehouseFailure = normalizeWarehouseSqlFailure(err, input.semanticDriver);
3328
3426
  executionError = warehouseFailure.redactedMessage;
3329
3427
  }
3330
- if (executionError && !fanoutContradiction && !authoritativeSemanticBinding) {
3428
+ if (executionError && !input.forcedGeneratedProposal && !fanoutContradiction && !authoritativeSemanticBinding) {
3331
3429
  warehouseFailure ??= normalizeWarehouseSqlFailure(executionError, input.semanticDriver);
3332
3430
  if (isRetryableGeneratedSqlError(warehouseFailure)) {
3333
3431
  const localRepairSql = repairGeneratedSqlLocally(parsed.sql, executionError, schemaContext);
@@ -5251,15 +5349,26 @@ function renderContextPackForPrompt(contextPack, budget) {
5251
5349
  const warnings = contextPack.warnings.length
5252
5350
  ? `Warnings:\n${contextPack.warnings.slice(0, budget.warningLimit).map((warning) => `- ${warning}`).join('\n')}\n`
5253
5351
  : '';
5254
- const objects = contextPack.objects.slice(0, budget.contextObjectLimit).map((object) => {
5255
- const detail = [
5256
- object.objectType,
5257
- object.domain ? `domain: ${object.domain}` : '',
5258
- object.status ? `status: ${object.status}` : '',
5259
- object.description ? `description: ${object.description}` : '',
5260
- ].filter(Boolean).join('; ');
5261
- return `- ${object.objectKey} (${detail})`;
5262
- }).join('\n');
5352
+ // Pack by value per token instead of truncating at a fixed position. The
5353
+ // objects arrive already ranked, so position IS the value — but cost is not
5354
+ // uniform: one object with a long description can crowd out several cheap
5355
+ // ones that rank just below it, and the old `slice` cut by rank alone. This
5356
+ // keeps the same ordering pressure while letting an inexpensive object that
5357
+ // ranked 19th still fit. The budget is derived from the existing item limit
5358
+ // so the window stays the size the caller already tuned for.
5359
+ const objects = packContext(contextPack.objects.map((object, index) => ({
5360
+ id: object.objectKey,
5361
+ score: contextPack.objects.length - index,
5362
+ render: () => {
5363
+ const detail = [
5364
+ object.objectType,
5365
+ object.domain ? `domain: ${object.domain}` : '',
5366
+ object.status ? `status: ${object.status}` : '',
5367
+ object.description ? `description: ${object.description}` : '',
5368
+ ].filter(Boolean).join('; ');
5369
+ return `- ${object.objectKey} (${detail})`;
5370
+ },
5371
+ })), Math.max(1, budget.contextObjectLimit) * 45).text;
5263
5372
  const conflicts = contextPack.retrievalDiagnostics.candidateConflicts.length
5264
5373
  ? `\nCandidate conflicts:\n${contextPack.retrievalDiagnostics.candidateConflicts.slice(0, 4).map((conflict) => `- ${conflict.reason} ${conflict.prompt}`).join('\n')}`
5265
5374
  : '';
@@ -5785,6 +5894,28 @@ function flowFieldCovers(field, term) {
5785
5894
  function normalizeFlowField(value) {
5786
5895
  return value.toLowerCase().replace(/[_-]+/g, ' ').replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
5787
5896
  }
5897
+ /**
5898
+ * Remove the model's QUERY PLAN scaffolding from user-facing prose.
5899
+ *
5900
+ * Rule 4 of the SQL prompt tells the model to state its grain, measures, join
5901
+ * path, and join keys BEFORE writing SQL. That instruction is load-bearing —
5902
+ * it is what prevents wrong-grain answers and fan-out joins — but it is
5903
+ * reasoning, not an answer, and it was reaching the reader verbatim:
5904
+ *
5905
+ * "QUERY PLAN: grain = one row per customer, filtered to the named customer
5906
+ * (Matthew Meyer, honorific stripped) ... FROM dev.customers c, filtered on
5907
+ * c.customer_name ILIKE '%Matthew Meyer%'."
5908
+ *
5909
+ * Someone who asked why a customer tops a list should not be handed a join
5910
+ * plan. The plan stays in the artifact and the trace; it leaves the prose.
5911
+ */
5912
+ export function stripQueryPlanScaffolding(text) {
5913
+ return text
5914
+ // A QUERY PLAN section runs to the next blank line or the end of the prose.
5915
+ .replace(/(?:^|\n)\s*(?:\*\*)?QUERY PLAN(?:\*\*)?\s*[::][\s\S]*?(?=\n\s*\n|$)/gi, '')
5916
+ .replace(/\n{3,}/g, '\n\n')
5917
+ .trim();
5918
+ }
5788
5919
  /**
5789
5920
  * Public for tests. Prefer the structured W2.7 JSON proposal contract, then
5790
5921
  * fall back to the legacy prose + ```sql block + Viz line format.
@@ -5798,11 +5929,11 @@ export function parseProposal(raw) {
5798
5929
  const vizMatch = raw.match(/^Viz:\s*([a-z_]+)/im);
5799
5930
  const viz = vizMatch ? vizMatch[1].trim().toLowerCase() : undefined;
5800
5931
  // Strip the SQL block + Viz line from the prose to keep the summary clean.
5801
- const text = raw
5932
+ const text = stripQueryPlanScaffolding(raw
5802
5933
  .replace(/```json[\s\S]*?```/gi, '')
5803
5934
  .replace(/```sql[\s\S]*?```/gi, '')
5804
5935
  .replace(/^Viz:.*$/gim, '')
5805
- .trim();
5936
+ .trim());
5806
5937
  return { text, sql, viz };
5807
5938
  }
5808
5939
  function parseStructuredProposal(raw) {
@@ -5830,7 +5961,7 @@ function parsedProposalFromJson(value) {
5830
5961
  return undefined;
5831
5962
  const record = value;
5832
5963
  const sql = firstJsonString(record.sql, record.query)?.trim();
5833
- const text = firstJsonString(record.summary, record.text, record.answer, record.description)?.trim() ?? '';
5964
+ const text = stripQueryPlanScaffolding(firstJsonString(record.summary, record.text, record.answer, record.description)?.trim() ?? '');
5834
5965
  const viz = firstJsonString(record.viz, record.visualization, record.chartType)
5835
5966
  ?.trim()
5836
5967
  .toLowerCase()