@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.
- package/dist/agent-run-engine.d.ts +60 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +333 -21
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +13 -0
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +91 -0
- package/dist/agentic/analyst-loop.d.ts.map +1 -0
- package/dist/agentic/analyst-loop.js +311 -0
- package/dist/agentic/analyst-loop.js.map +1 -0
- package/dist/agentic/assumptions.d.ts +62 -0
- package/dist/agentic/assumptions.d.ts.map +1 -0
- package/dist/agentic/assumptions.js +73 -0
- package/dist/agentic/assumptions.js.map +1 -0
- package/dist/agentic/business-explanation.d.ts +56 -0
- package/dist/agentic/business-explanation.d.ts.map +1 -0
- package/dist/agentic/business-explanation.js +130 -0
- package/dist/agentic/business-explanation.js.map +1 -0
- package/dist/agentic/identifier-ledger.d.ts +74 -0
- package/dist/agentic/identifier-ledger.d.ts.map +1 -0
- package/dist/agentic/identifier-ledger.js +151 -0
- package/dist/agentic/identifier-ledger.js.map +1 -0
- package/dist/agentic/ledger-tools.d.ts +68 -0
- package/dist/agentic/ledger-tools.d.ts.map +1 -0
- package/dist/agentic/ledger-tools.js +198 -0
- package/dist/agentic/ledger-tools.js.map +1 -0
- package/dist/agentic/orchestrator-policy.d.ts +60 -0
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -0
- package/dist/agentic/orchestrator-policy.js +73 -0
- package/dist/agentic/orchestrator-policy.js.map +1 -0
- package/dist/agentic/orchestrator.d.ts +54 -0
- package/dist/agentic/orchestrator.d.ts.map +1 -0
- package/dist/agentic/orchestrator.js +67 -0
- package/dist/agentic/orchestrator.js.map +1 -0
- package/dist/agentic/preview-tool.d.ts +36 -0
- package/dist/agentic/preview-tool.d.ts.map +1 -0
- package/dist/agentic/preview-tool.js +71 -0
- package/dist/agentic/preview-tool.js.map +1 -0
- package/dist/agentic/rerank.d.ts +50 -0
- package/dist/agentic/rerank.d.ts.map +1 -0
- package/dist/agentic/rerank.js +140 -0
- package/dist/agentic/rerank.js.map +1 -0
- package/dist/agentic/research/hypothesis.d.ts +110 -0
- package/dist/agentic/research/hypothesis.d.ts.map +1 -0
- package/dist/agentic/research/hypothesis.js +125 -0
- package/dist/agentic/research/hypothesis.js.map +1 -0
- package/dist/agentic/research/synthesis.d.ts +32 -0
- package/dist/agentic/research/synthesis.d.ts.map +1 -0
- package/dist/agentic/research/synthesis.js +94 -0
- package/dist/agentic/research/synthesis.js.map +1 -0
- package/dist/agentic/research-agent.d.ts +54 -0
- package/dist/agentic/research-agent.d.ts.map +1 -0
- package/dist/agentic/research-agent.js +176 -0
- package/dist/agentic/research-agent.js.map +1 -0
- package/dist/agentic/sql-authorization.d.ts +131 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -0
- package/dist/agentic/sql-authorization.js +232 -0
- package/dist/agentic/sql-authorization.js.map +1 -0
- package/dist/agentic/tool-loop.d.ts +29 -0
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +129 -52
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/agentic/toolset.d.ts.map +1 -1
- package/dist/agentic/toolset.js +155 -0
- package/dist/agentic/toolset.js.map +1 -1
- package/dist/agentic/turn-plan.d.ts +35 -0
- package/dist/agentic/turn-plan.d.ts.map +1 -0
- package/dist/agentic/turn-plan.js +108 -0
- package/dist/agentic/turn-plan.js.map +1 -0
- package/dist/agentic/value-probe.d.ts +89 -0
- package/dist/agentic/value-probe.d.ts.map +1 -0
- package/dist/agentic/value-probe.js +304 -0
- package/dist/agentic/value-probe.js.map +1 -0
- package/dist/analytical-orchestration.d.ts +80 -0
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +218 -6
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/analytical-request-policy.d.ts +26 -0
- package/dist/analytical-request-policy.d.ts.map +1 -0
- package/dist/analytical-request-policy.js +82 -0
- package/dist/analytical-request-policy.js.map +1 -0
- package/dist/analytical-result-facts.d.ts +13 -0
- package/dist/analytical-result-facts.d.ts.map +1 -1
- package/dist/analytical-result-facts.js +37 -1
- package/dist/analytical-result-facts.js.map +1 -1
- package/dist/answer-loop.d.ts +57 -16
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +161 -30
- package/dist/answer-loop.js.map +1 -1
- package/dist/cascade/budgets.d.ts.map +1 -1
- package/dist/cascade/budgets.js +8 -1
- package/dist/cascade/budgets.js.map +1 -1
- package/dist/cascade/packer.d.ts +88 -0
- package/dist/cascade/packer.d.ts.map +1 -0
- package/dist/cascade/packer.js +106 -0
- package/dist/cascade/packer.js.map +1 -0
- package/dist/conversation/result-ops.d.ts +18 -3
- package/dist/conversation/result-ops.d.ts.map +1 -1
- package/dist/conversation/result-ops.js +77 -5
- package/dist/conversation/result-ops.js.map +1 -1
- package/dist/conversation/rolling-summary.d.ts.map +1 -1
- package/dist/conversation/rolling-summary.js +7 -2
- package/dist/conversation/rolling-summary.js.map +1 -1
- package/dist/conversation/session-store.d.ts +3 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +8 -4
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +10 -1
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/hints/store.d.ts.map +1 -1
- package/dist/hints/store.js +15 -5
- package/dist/hints/store.js.map +1 -1
- package/dist/index.d.ts +27 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -5
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +8 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +65 -0
- package/dist/intent-controller.js.map +1 -1
- package/dist/metadata/block-fit.d.ts +18 -0
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +41 -1
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts +20 -23
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +136 -11
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +15 -7
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/metadata/metric-match.d.ts +32 -0
- package/dist/metadata/metric-match.d.ts.map +1 -1
- package/dist/metadata/metric-match.js +120 -9
- package/dist/metadata/metric-match.js.map +1 -1
- package/dist/research-loop.d.ts +33 -2
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +44 -4
- package/dist/research-loop.js.map +1 -1
- package/dist/router.d.ts +28 -16
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +250 -17
- package/dist/router.js.map +1 -1
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
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()
|