@duckcodeailabs/dql-agent 1.14.3-rc.1 → 1.14.4
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 +177 -3
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +2061 -57
- 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 +51 -3
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +544 -9
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +2 -0
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +41 -4
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/ledger-tools.d.ts.map +1 -1
- package/dist/agentic/ledger-tools.js +8 -1
- package/dist/agentic/ledger-tools.js.map +1 -1
- package/dist/agentic/orchestrator-policy.d.ts +3 -1
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
- package/dist/agentic/orchestrator-policy.js +1 -1
- package/dist/agentic/orchestrator-policy.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts +13 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +441 -32
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/analytical-frame.d.ts +19 -0
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +170 -12
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +949 -10
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +499 -56
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +166 -2
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +654 -18
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +27 -0
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +55 -0
- package/dist/answer-shape.js.map +1 -1
- package/dist/ask-observability/instrumentation.d.ts.map +1 -1
- package/dist/ask-observability/instrumentation.js +52 -1
- package/dist/ask-observability/instrumentation.js.map +1 -1
- package/dist/ask-observability/portable.js +562 -0
- package/dist/ask-observability/portable.js.map +1 -1
- package/dist/ask-observability/types.d.ts +26 -3
- package/dist/ask-observability/types.d.ts.map +1 -1
- package/dist/ask-observability/types.js.map +1 -1
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
- package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
- package/dist/conversation/session-store.d.ts +31 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +38 -0
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts +4 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +85 -3
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +2 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -0
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +18 -0
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/generated-analytical-proposal.js +37 -2
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +77 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +32 -2
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +129 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +108 -15
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.js +57 -6
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +14 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +28 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +19 -0
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +113 -6
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/provider-egress.d.ts +4 -1
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +50 -5
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts +2 -2
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +191 -28
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +191 -21
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +79 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/requirement-clauses.d.ts +23 -0
- package/dist/requirement-clauses.d.ts.map +1 -0
- package/dist/requirement-clauses.js +70 -0
- package/dist/requirement-clauses.js.map +1 -0
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +201 -15
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +109 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +753 -76
- package/dist/router.js.map +1 -1
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -41,7 +41,7 @@ import { planAnalyticalPath, humanizeAnalyticalEntityId, analyticalPolicyUserFac
|
|
|
41
41
|
import { planCertifiedAdaptation } from './metadata/block-adapt.js';
|
|
42
42
|
import { compactSqlSnippet, extractSimpleSelectShape, selectExpressionOutputName, } from './metadata/sql-shape.js';
|
|
43
43
|
import { composeSemanticQueryForQuestion, composeSemanticQueryFromCompiledMembers, composeSemanticQueryFromMembers, renderSemanticDqlArtifact, semanticDqlArtifactName, } from './semantic-bridge/compose.js';
|
|
44
|
-
import {
|
|
44
|
+
import { runAgenticToolLoopDetailed } from './agentic/tool-loop.js';
|
|
45
45
|
import { buildSemanticStageTools } from './agentic/toolset.js';
|
|
46
46
|
import { deriveAgenticTrust } from './agentic/answer-contract.js';
|
|
47
47
|
import { selectSemanticMembersViaLlm } from './semantic-bridge/member-select.js';
|
|
@@ -384,6 +384,572 @@ function certifiedInvocationInputs(block, plan) {
|
|
|
384
384
|
}
|
|
385
385
|
return Object.keys(parameters).length > 0 ? { parameters, parameterSources } : {};
|
|
386
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Build the one typed certified-block invocation used by both the ordinary
|
|
389
|
+
* answer loop and the authoritative V2 exact-fit path. Keeping this at the
|
|
390
|
+
* DQL boundary is important: a zero-provider Tier 1 shortcut may avoid
|
|
391
|
+
* conversational planning, but it must not avoid declared parameter binding,
|
|
392
|
+
* validation, or the normal overall top-N execution bound.
|
|
393
|
+
*/
|
|
394
|
+
export function buildCertifiedBlockInvocationInput(block, plan, question) {
|
|
395
|
+
return {
|
|
396
|
+
question,
|
|
397
|
+
...certifiedInvocationInputs(block, plan),
|
|
398
|
+
...(plan.requestedShape.topN?.scope === 'per_group'
|
|
399
|
+
? {}
|
|
400
|
+
: plan.requestedShape.topN?.n
|
|
401
|
+
? { rowLimit: plan.requestedShape.topN.n }
|
|
402
|
+
: {}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Prove that an immutable certified block may satisfy a question-driven
|
|
407
|
+
* overall top-N result contract without conversational planning. A declared
|
|
408
|
+
* limit parameter alone is not enough: the authored SQL must order rows and
|
|
409
|
+
* consume that exact parameter in its LIMIT clause. This prevents a host
|
|
410
|
+
* from slicing arbitrary connector order and presenting it as certified.
|
|
411
|
+
*/
|
|
412
|
+
export function certifiedBlockProvesRequestedTopN(block, plan, options = {}) {
|
|
413
|
+
const topN = plan.requestedShape.topN;
|
|
414
|
+
if (!topN)
|
|
415
|
+
return true;
|
|
416
|
+
// The artifact is loaded from the snapshot-bound KG, but that persisted
|
|
417
|
+
// payload still crosses an older JSON schema boundary. Treat a malformed
|
|
418
|
+
// parameter contract as *not proved*, never as a reason to crash the Ask
|
|
419
|
+
// run or to infer an unordered ranking from connector row order.
|
|
420
|
+
if (topN.scope !== 'overall' || !block || block.kind !== 'block')
|
|
421
|
+
return false;
|
|
422
|
+
const limitParameters = (Array.isArray(block.parameters) ? block.parameters : []).flatMap((parameter) => {
|
|
423
|
+
if (!parameter || typeof parameter.name !== 'string' || parameter.name.trim() === '')
|
|
424
|
+
return [];
|
|
425
|
+
return parameter.name === 'top_n' || parameter.binding?.kind === 'limit'
|
|
426
|
+
? [parameter]
|
|
427
|
+
: [];
|
|
428
|
+
});
|
|
429
|
+
if (typeof block.sql !== 'string')
|
|
430
|
+
return false;
|
|
431
|
+
const outerClauses = scanOutermostTopNClauses(block.sql);
|
|
432
|
+
if (!outerClauses)
|
|
433
|
+
return false;
|
|
434
|
+
const { orderBy, limitValue } = outerClauses;
|
|
435
|
+
// There are exactly two safe ways to prove the overall row bound:
|
|
436
|
+
//
|
|
437
|
+
// 1. The immutable artifact consumes its own declared top-N parameter in
|
|
438
|
+
// the outer LIMIT clause (the ordinary certified contract); or
|
|
439
|
+
// 2. The artifact intentionally has no outer LIMIT and the local host owns
|
|
440
|
+
// a frozen, typed execution row limit at the read-only SQL boundary.
|
|
441
|
+
//
|
|
442
|
+
// We never treat a fixed, driver-style, compound, or stale outer LIMIT as
|
|
443
|
+
// equivalent to the requested value. In particular, an existing fixed
|
|
444
|
+
// LIMIT prevents the host executor from appending the frozen limit, so it
|
|
445
|
+
// cannot prove a different user-requested top N.
|
|
446
|
+
const limitUsesDeclaredParameter = limitParameters.length === 1
|
|
447
|
+
&& typeof limitValue === 'string'
|
|
448
|
+
&& outerLimitUsesDeclaredTopNParameter(limitValue, limitParameters[0].name);
|
|
449
|
+
const hostOwnsFrozenRowLimit = limitValue === undefined
|
|
450
|
+
&& Number.isInteger(options.hostEnforcedRowLimit)
|
|
451
|
+
&& options.hostEnforcedRowLimit === topN.n
|
|
452
|
+
&& options.hostEnforcedRowLimit > 0;
|
|
453
|
+
if (!limitUsesDeclaredParameter && !hostOwnsFrozenRowLimit)
|
|
454
|
+
return false;
|
|
455
|
+
// A question-driven ranking is only exact when the *primary* authored sort
|
|
456
|
+
// expression proves the requested measure. Finding `revenue DESC` later in
|
|
457
|
+
// `ORDER BY customer_name ASC, revenue DESC` is not sufficient: the result
|
|
458
|
+
// is primarily alphabetical, not a top-revenue result. Do not infer a
|
|
459
|
+
// ranking measure from row shape or candidate tags; that would turn an
|
|
460
|
+
// unproven certified artifact into a false exact answer.
|
|
461
|
+
const requestedMeasures = plan.requestedShape.measures
|
|
462
|
+
.map(normalizedTopNMetricId)
|
|
463
|
+
.filter(Boolean);
|
|
464
|
+
// An omitted measure is not normally a license to infer a ranking from a
|
|
465
|
+
// certified block. The narrow exception is server-owned evidence that this
|
|
466
|
+
// is either a direct certified question/title/alias match *or* the one
|
|
467
|
+
// complete certified tuple admitted by this immutable retrieval snapshot.
|
|
468
|
+
// In both cases the artifact's own primary non-dimension output supplies
|
|
469
|
+
// the authored default ranking contract. A provider cannot manufacture
|
|
470
|
+
// either flag from text or a card count.
|
|
471
|
+
const useAuthoredRankingDefault = requestedMeasures.length === 0
|
|
472
|
+
&& (options.exactCertifiedQuestionMatch === true
|
|
473
|
+
|| options.uniqueCompleteCertifiedFit === true);
|
|
474
|
+
if (requestedMeasures.length === 0 && !useAuthoredRankingDefault)
|
|
475
|
+
return false;
|
|
476
|
+
const firstOrderExpression = splitTopLevelSqlList(orderBy)[0];
|
|
477
|
+
if (!firstOrderExpression)
|
|
478
|
+
return false;
|
|
479
|
+
const firstOrder = parseTopNOrderExpression(firstOrderExpression);
|
|
480
|
+
if (!firstOrder)
|
|
481
|
+
return false;
|
|
482
|
+
const requiredDirection = plan.requestedShape.rankingDirection === 'bottom' ? 'asc' : 'desc';
|
|
483
|
+
if (firstOrder.direction !== requiredDirection)
|
|
484
|
+
return false;
|
|
485
|
+
const orderMetricIds = topNOrderMetricIds(firstOrder.expression, block);
|
|
486
|
+
if (requestedMeasures.length > 0) {
|
|
487
|
+
return requestedMeasures.some((measure) => orderMetricIds.has(measure));
|
|
488
|
+
}
|
|
489
|
+
return [...authoredTopNRankingMetricIds(block)].some((metric) => orderMetricIds.has(metric));
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The implicit-ranking exception still needs an authored metric, not merely a
|
|
493
|
+
* sortable dimension. Keep this proof inside the immutable block contract:
|
|
494
|
+
* declared outputs, compiler output lineage, and typed output roles are all
|
|
495
|
+
* captured with the artifact. If an older block cannot distinguish its
|
|
496
|
+
* measures from dimensions, fail closed into the Ask planner.
|
|
497
|
+
*/
|
|
498
|
+
function authoredTopNRankingMetricIds(block) {
|
|
499
|
+
const dimensionIds = new Set((block.dimensions ?? []).map(normalizedTopNMetricId).filter(Boolean));
|
|
500
|
+
const outputRoles = new Map((block.outputContract ?? [])
|
|
501
|
+
.filter((output) => typeof output?.name === 'string')
|
|
502
|
+
.map((output) => [normalizedTopNMetricId(output.name), String(output.role ?? '').toLowerCase()]));
|
|
503
|
+
const outputIds = new Set([
|
|
504
|
+
...(block.declaredOutputs ?? []),
|
|
505
|
+
...(block.outputs ?? []).map((output) => output.name),
|
|
506
|
+
...(block.outputContract ?? []).map((output) => output.name),
|
|
507
|
+
].map(normalizedTopNMetricId).filter(Boolean));
|
|
508
|
+
const metrics = new Set();
|
|
509
|
+
for (const outputId of outputIds) {
|
|
510
|
+
const role = outputRoles.get(outputId) ?? '';
|
|
511
|
+
if (dimensionIds.has(outputId) || /(?:dimension|entity|label|attribute)/.test(role))
|
|
512
|
+
continue;
|
|
513
|
+
metrics.add(outputId);
|
|
514
|
+
}
|
|
515
|
+
return metrics;
|
|
516
|
+
}
|
|
517
|
+
function outerLimitUsesDeclaredTopNParameter(limitValue, parameterName) {
|
|
518
|
+
const escapedParameter = parameterName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
519
|
+
// This proof is about an authored DQL artifact, not a connector SQL
|
|
520
|
+
// statement. Accept only the two compiler-recognized interpolation forms
|
|
521
|
+
// (`${name}` and `{name}`); driver placeholders, bare identifiers, fixed
|
|
522
|
+
// values, and compound expressions do not prove that the requested typed
|
|
523
|
+
// top-N binding controls the outer result contract.
|
|
524
|
+
return new RegExp(`^(?:\\$\\{\\s*${escapedParameter}\\s*\\}|\\{\\s*${escapedParameter}\\s*\\})$`, 'i').test(topNVisibleSql(limitValue).trim());
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Locate the outer SELECT/ORDER BY/LIMIT clauses without treating text in a
|
|
528
|
+
* CTE, subquery, quoted identifier/string, or comment as part of the answer
|
|
529
|
+
* contract. A ranking proof must be about the query that actually returns the
|
|
530
|
+
* block rows, never an unused inner query that happens to mention revenue.
|
|
531
|
+
*/
|
|
532
|
+
function scanOutermostTopNClauses(sql) {
|
|
533
|
+
const tokens = topLevelSqlTokens(sql);
|
|
534
|
+
if (!tokens)
|
|
535
|
+
return undefined;
|
|
536
|
+
let orderByIndex = -1;
|
|
537
|
+
for (let index = 0; index + 1 < tokens.length; index += 1) {
|
|
538
|
+
if (tokens[index].text === 'order' && tokens[index + 1].text === 'by')
|
|
539
|
+
orderByIndex = index;
|
|
540
|
+
}
|
|
541
|
+
if (orderByIndex < 0)
|
|
542
|
+
return undefined;
|
|
543
|
+
let selectIndex = -1;
|
|
544
|
+
for (let index = 0; index < orderByIndex; index += 1) {
|
|
545
|
+
if (tokens[index].text === 'select')
|
|
546
|
+
selectIndex = index;
|
|
547
|
+
}
|
|
548
|
+
if (selectIndex < 0)
|
|
549
|
+
return undefined;
|
|
550
|
+
let fromIndex = -1;
|
|
551
|
+
for (let index = selectIndex + 1; index < orderByIndex; index += 1) {
|
|
552
|
+
if (tokens[index].text === 'from') {
|
|
553
|
+
fromIndex = index;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (fromIndex < 0)
|
|
558
|
+
return undefined;
|
|
559
|
+
let limitIndex = -1;
|
|
560
|
+
for (let index = orderByIndex + 2; index < tokens.length; index += 1) {
|
|
561
|
+
if (tokens[index].text === 'limit') {
|
|
562
|
+
limitIndex = index;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const orderByEnd = limitIndex >= 0 ? tokens[limitIndex].start : sql.length;
|
|
567
|
+
// The outer ORDER BY must directly govern the outer LIMIT when one exists.
|
|
568
|
+
// A set operation, second SELECT, or a second ORDER BY in between is too
|
|
569
|
+
// complex for this exact shortcut and correctly falls back to the bounded
|
|
570
|
+
// Ask tool runtime. The same guard applies through end-of-query for an
|
|
571
|
+
// intentionally unbounded artifact whose host owns the frozen row limit.
|
|
572
|
+
if (tokens.slice(orderByIndex + 2, limitIndex >= 0 ? limitIndex : tokens.length).some((token) => token.text === 'select' || token.text === 'union' || token.text === 'intersect' || token.text === 'except')) {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
const afterLimit = limitIndex >= 0 ? tokens.find((token, index) => index > limitIndex && (token.text === 'offset' || token.text === 'fetch' || token.text === 'for'))?.start ?? sql.length : sql.length;
|
|
576
|
+
const selectList = sql.slice(tokens[selectIndex].end, tokens[fromIndex].start).trim();
|
|
577
|
+
const orderBy = sql.slice(tokens[orderByIndex + 1].end, orderByEnd).trim();
|
|
578
|
+
const limitValue = limitIndex >= 0
|
|
579
|
+
? sql.slice(tokens[limitIndex].end, afterLimit).replace(/;\s*$/, '').trim()
|
|
580
|
+
: undefined;
|
|
581
|
+
if (!selectList || !orderBy || (limitIndex >= 0 && !limitValue))
|
|
582
|
+
return undefined;
|
|
583
|
+
return {
|
|
584
|
+
selectList,
|
|
585
|
+
orderBy,
|
|
586
|
+
...(limitValue ? { limitValue } : {}),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Tokenize only depth-zero SQL words. The state machine deliberately removes
|
|
591
|
+
* comments and literals from consideration while retaining positional ranges
|
|
592
|
+
* against the original SQL for clause slicing.
|
|
593
|
+
*/
|
|
594
|
+
function topLevelSqlTokens(sql) {
|
|
595
|
+
const tokens = [];
|
|
596
|
+
let depth = 0;
|
|
597
|
+
let quote;
|
|
598
|
+
let lineComment = false;
|
|
599
|
+
let blockCommentDepth = 0;
|
|
600
|
+
for (let index = 0; index < sql.length; index += 1) {
|
|
601
|
+
const character = sql[index];
|
|
602
|
+
const next = sql[index + 1];
|
|
603
|
+
if (lineComment) {
|
|
604
|
+
if (character === '\n' || character === '\r')
|
|
605
|
+
lineComment = false;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if (blockCommentDepth > 0) {
|
|
609
|
+
if (character === '/' && next === '*') {
|
|
610
|
+
blockCommentDepth += 1;
|
|
611
|
+
index += 1;
|
|
612
|
+
}
|
|
613
|
+
else if (character === '*' && next === '/') {
|
|
614
|
+
blockCommentDepth -= 1;
|
|
615
|
+
index += 1;
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (quote) {
|
|
620
|
+
if (quote === ']' && character === ']') {
|
|
621
|
+
if (next === ']')
|
|
622
|
+
index += 1;
|
|
623
|
+
else
|
|
624
|
+
quote = undefined;
|
|
625
|
+
}
|
|
626
|
+
else if (quote !== ']' && character === quote) {
|
|
627
|
+
if (next === quote)
|
|
628
|
+
index += 1;
|
|
629
|
+
else
|
|
630
|
+
quote = undefined;
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (character === '-' && next === '-') {
|
|
635
|
+
lineComment = true;
|
|
636
|
+
index += 1;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
if (character === '/' && next === '*') {
|
|
640
|
+
blockCommentDepth = 1;
|
|
641
|
+
index += 1;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
645
|
+
quote = character;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (character === '[') {
|
|
649
|
+
quote = ']';
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (character === '(') {
|
|
653
|
+
depth += 1;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (character === ')') {
|
|
657
|
+
if (depth === 0)
|
|
658
|
+
return undefined;
|
|
659
|
+
depth -= 1;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (depth !== 0 || !/[A-Za-z_]/.test(character))
|
|
663
|
+
continue;
|
|
664
|
+
const start = index;
|
|
665
|
+
index += 1;
|
|
666
|
+
while (index < sql.length && /[A-Za-z0-9_$]/.test(sql[index]))
|
|
667
|
+
index += 1;
|
|
668
|
+
tokens.push({ text: sql.slice(start, index).toLowerCase(), start, end: index });
|
|
669
|
+
index -= 1;
|
|
670
|
+
}
|
|
671
|
+
// A line comment is valid through EOF. Unterminated string, bracket, block
|
|
672
|
+
// comment, or parenthesis state is not a trustworthy exact-proof input.
|
|
673
|
+
return depth === 0 && !quote && blockCommentDepth === 0 ? tokens : undefined;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Split an authored comma-separated SQL list without treating a function
|
|
677
|
+
* argument, quoted string, or quoted identifier as a second ORDER BY key.
|
|
678
|
+
* This intentionally stays small and fails closed for malformed SQL; DQL's
|
|
679
|
+
* compiler is still the authority for execution syntax.
|
|
680
|
+
*/
|
|
681
|
+
function splitTopLevelSqlList(value) {
|
|
682
|
+
const expressions = [];
|
|
683
|
+
let start = 0;
|
|
684
|
+
let depth = 0;
|
|
685
|
+
let quote;
|
|
686
|
+
let lineComment = false;
|
|
687
|
+
let blockCommentDepth = 0;
|
|
688
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
689
|
+
const character = value[index];
|
|
690
|
+
const next = value[index + 1];
|
|
691
|
+
if (lineComment) {
|
|
692
|
+
if (character === '\n' || character === '\r')
|
|
693
|
+
lineComment = false;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (blockCommentDepth > 0) {
|
|
697
|
+
if (character === '/' && next === '*') {
|
|
698
|
+
blockCommentDepth += 1;
|
|
699
|
+
index += 1;
|
|
700
|
+
}
|
|
701
|
+
else if (character === '*' && next === '/') {
|
|
702
|
+
blockCommentDepth -= 1;
|
|
703
|
+
index += 1;
|
|
704
|
+
}
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (quote) {
|
|
708
|
+
if (quote === ']' && character === ']') {
|
|
709
|
+
if (next === ']')
|
|
710
|
+
index += 1;
|
|
711
|
+
else
|
|
712
|
+
quote = undefined;
|
|
713
|
+
}
|
|
714
|
+
else if (quote !== ']' && character === quote) {
|
|
715
|
+
// SQL escapes an in-string quote by doubling it. Keep scanning inside
|
|
716
|
+
// the quoted value rather than mistaking a later comma for a list
|
|
717
|
+
// separator.
|
|
718
|
+
if (value[index + 1] === quote) {
|
|
719
|
+
index += 1;
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
quote = undefined;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (character === '-' && next === '-') {
|
|
728
|
+
lineComment = true;
|
|
729
|
+
index += 1;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (character === '/' && next === '*') {
|
|
733
|
+
blockCommentDepth = 1;
|
|
734
|
+
index += 1;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
738
|
+
quote = character;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (character === '[') {
|
|
742
|
+
quote = ']';
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (character === '(') {
|
|
746
|
+
depth += 1;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (character === ')' && depth > 0) {
|
|
750
|
+
depth -= 1;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (character === ',' && depth === 0) {
|
|
754
|
+
const expression = value.slice(start, index).trim();
|
|
755
|
+
if (!expression)
|
|
756
|
+
return [];
|
|
757
|
+
expressions.push(expression);
|
|
758
|
+
start = index + 1;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (quote || blockCommentDepth > 0 || depth !== 0)
|
|
762
|
+
return [];
|
|
763
|
+
const finalExpression = value.slice(start).trim();
|
|
764
|
+
return finalExpression ? [...expressions, finalExpression] : [];
|
|
765
|
+
}
|
|
766
|
+
function parseTopNOrderExpression(expression) {
|
|
767
|
+
const visible = topNVisibleSql(expression);
|
|
768
|
+
const match = visible.match(/^(.*?)(?:\s+(asc|desc))(?:\s+nulls\s+(?:first|last))?\s*$/i);
|
|
769
|
+
if (!match?.[1] || !match[2])
|
|
770
|
+
return undefined;
|
|
771
|
+
return {
|
|
772
|
+
expression: expression.slice(0, match[1].length).trim(),
|
|
773
|
+
direction: match[2].toLowerCase(),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** Preserve SQL positions while blanking literals/comments that cannot prove a ranking contract. */
|
|
777
|
+
function topNVisibleSql(value) {
|
|
778
|
+
let output = '';
|
|
779
|
+
let quote;
|
|
780
|
+
let lineComment = false;
|
|
781
|
+
let blockCommentDepth = 0;
|
|
782
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
783
|
+
const character = value[index];
|
|
784
|
+
const next = value[index + 1];
|
|
785
|
+
const blank = () => { output += character === '\n' || character === '\r' ? character : ' '; };
|
|
786
|
+
if (lineComment) {
|
|
787
|
+
blank();
|
|
788
|
+
if (character === '\n' || character === '\r')
|
|
789
|
+
lineComment = false;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (blockCommentDepth > 0) {
|
|
793
|
+
blank();
|
|
794
|
+
if (character === '/' && next === '*') {
|
|
795
|
+
blockCommentDepth += 1;
|
|
796
|
+
index += 1;
|
|
797
|
+
output += ' ';
|
|
798
|
+
}
|
|
799
|
+
else if (character === '*' && next === '/') {
|
|
800
|
+
blockCommentDepth -= 1;
|
|
801
|
+
index += 1;
|
|
802
|
+
output += ' ';
|
|
803
|
+
}
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (quote === "'") {
|
|
807
|
+
blank();
|
|
808
|
+
if (character === "'") {
|
|
809
|
+
if (next === "'") {
|
|
810
|
+
index += 1;
|
|
811
|
+
output += ' ';
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
quote = undefined;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (quote) {
|
|
820
|
+
// Keep quoted identifiers intact for identifier normalization while the
|
|
821
|
+
// outer-clause scanner itself ignores their contents.
|
|
822
|
+
output += character;
|
|
823
|
+
if (quote === ']' && character === ']') {
|
|
824
|
+
if (next === ']') {
|
|
825
|
+
index += 1;
|
|
826
|
+
output += next;
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
quote = undefined;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else if (quote !== ']' && character === quote) {
|
|
833
|
+
if (next === quote) {
|
|
834
|
+
index += 1;
|
|
835
|
+
output += next;
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
quote = undefined;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
if (character === '-' && next === '-') {
|
|
844
|
+
output += ' ';
|
|
845
|
+
index += 1;
|
|
846
|
+
lineComment = true;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (character === '/' && next === '*') {
|
|
850
|
+
output += ' ';
|
|
851
|
+
index += 1;
|
|
852
|
+
blockCommentDepth = 1;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (character === "'") {
|
|
856
|
+
output += ' ';
|
|
857
|
+
quote = character;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (character === '"' || character === '`') {
|
|
861
|
+
output += character;
|
|
862
|
+
quote = character;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (character === '[') {
|
|
866
|
+
output += character;
|
|
867
|
+
quote = ']';
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
output += character;
|
|
871
|
+
}
|
|
872
|
+
return output;
|
|
873
|
+
}
|
|
874
|
+
/** Normalize an identifier or business measure into its opaque-free SQL key. */
|
|
875
|
+
function normalizedTopNMetricId(value) {
|
|
876
|
+
return value
|
|
877
|
+
.trim()
|
|
878
|
+
.replace(/^['"`\[]|['"`\]]$/g, '')
|
|
879
|
+
.split('.')
|
|
880
|
+
.at(-1)
|
|
881
|
+
.trim()
|
|
882
|
+
.replace(/^['"`\[]|['"`\]]$/g, '')
|
|
883
|
+
.toLowerCase()
|
|
884
|
+
.replace(/[^a-z0-9]+/g, '');
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Resolve the primary ORDER BY expression through selected-output aliases and
|
|
888
|
+
* captured output lineage. Qualified columns (`c.revenue`), SQL aliases
|
|
889
|
+
* (`SUM(o.revenue) AS revenue` / `ORDER BY revenue`), and compiler-captured
|
|
890
|
+
* output sources all converge on the same normalized key. Unknown or
|
|
891
|
+
* ambiguous expressions deliberately yield no proof.
|
|
892
|
+
*/
|
|
893
|
+
function topNOrderMetricIds(expression, block) {
|
|
894
|
+
const keys = topNIdentifierIds(expression);
|
|
895
|
+
const selectedAliases = selectedSqlOutputAliases(block.sql);
|
|
896
|
+
const outputLineage = block.outputs ?? [];
|
|
897
|
+
for (const key of [...keys]) {
|
|
898
|
+
const selectedExpression = selectedAliases.get(key);
|
|
899
|
+
if (selectedExpression) {
|
|
900
|
+
for (const selectedKey of topNIdentifierIds(selectedExpression))
|
|
901
|
+
keys.add(selectedKey);
|
|
902
|
+
}
|
|
903
|
+
for (const output of outputLineage) {
|
|
904
|
+
if (normalizedTopNMetricId(output.name) !== key)
|
|
905
|
+
continue;
|
|
906
|
+
for (const source of output.sources ?? []) {
|
|
907
|
+
const sourceKey = normalizedTopNMetricId(source.column);
|
|
908
|
+
if (sourceKey)
|
|
909
|
+
keys.add(sourceKey);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return keys;
|
|
914
|
+
}
|
|
915
|
+
function topNIdentifierIds(expression) {
|
|
916
|
+
const keys = new Set();
|
|
917
|
+
// Quoted identifiers remain intact, while literals/comments were blanked.
|
|
918
|
+
// Therefore a string such as `'revenue'` cannot prove a ranking measure.
|
|
919
|
+
const identifierPattern = /(?:\[[^\]]+\]|"(?:""|[^"])+"|`(?:``|[^`])+`|[A-Za-z_][A-Za-z0-9_$]*)/g;
|
|
920
|
+
for (const identifier of topNVisibleSql(expression).match(identifierPattern) ?? []) {
|
|
921
|
+
const key = normalizedTopNMetricId(identifier);
|
|
922
|
+
if (key && !TOP_N_SQL_WORDS.has(key))
|
|
923
|
+
keys.add(key);
|
|
924
|
+
}
|
|
925
|
+
return keys;
|
|
926
|
+
}
|
|
927
|
+
function selectedSqlOutputAliases(sql) {
|
|
928
|
+
if (!sql)
|
|
929
|
+
return new Map();
|
|
930
|
+
const select = scanOutermostTopNClauses(sql)?.selectList;
|
|
931
|
+
if (!select)
|
|
932
|
+
return new Map();
|
|
933
|
+
const aliases = new Map();
|
|
934
|
+
for (const projection of splitTopLevelSqlList(select)) {
|
|
935
|
+
const visibleProjection = topNVisibleSql(projection);
|
|
936
|
+
const explicitAlias = visibleProjection.match(/^(.*?)\s+as\s+((?:\[[^\]]+\])|(?:"(?:""|[^"])+")|(?:`(?:``|[^`])+`)|(?:[A-Za-z_][A-Za-z0-9_$]*))\s*$/i);
|
|
937
|
+
const implicitAlias = explicitAlias
|
|
938
|
+
? undefined
|
|
939
|
+
: visibleProjection.match(/^(.*?)\s+((?:\[[^\]]+\])|(?:"(?:""|[^"])+")|(?:`(?:``|[^`])+`)|(?:[A-Za-z_][A-Za-z0-9_$]*))\s*$/i);
|
|
940
|
+
const expressionLength = (explicitAlias?.[1] ?? implicitAlias?.[1] ?? visibleProjection).length;
|
|
941
|
+
const expression = projection.slice(0, expressionLength).trim();
|
|
942
|
+
const alias = (explicitAlias?.[2] ?? implicitAlias?.[2] ?? projection).trim();
|
|
943
|
+
const aliasId = normalizedTopNMetricId(alias);
|
|
944
|
+
if (aliasId)
|
|
945
|
+
aliases.set(aliasId, expression);
|
|
946
|
+
}
|
|
947
|
+
return aliases;
|
|
948
|
+
}
|
|
949
|
+
const TOP_N_SQL_WORDS = new Set([
|
|
950
|
+
'asc', 'desc', 'nulls', 'first', 'last', 'sum', 'avg', 'average', 'count',
|
|
951
|
+
'min', 'max', 'cast', 'coalesce', 'case', 'when', 'then', 'else', 'end',
|
|
952
|
+
]);
|
|
387
953
|
/**
|
|
388
954
|
* Return only output aliases that the selected certified artifact itself
|
|
389
955
|
* declares. This is deliberately built after the router has frozen the plan:
|
|
@@ -625,6 +1191,41 @@ export function renderContextValidationRefusalForUser(code, machineError, member
|
|
|
625
1191
|
: 'I could not prepare a governed query from the retrieved metadata. Name the specific metric or table and how to break it down, and I can generate a review-required draft.';
|
|
626
1192
|
}
|
|
627
1193
|
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Did validation fail because something the USER asked for is not modeled?
|
|
1196
|
+
*
|
|
1197
|
+
* "What region does he belong to" against a warehouse with no customer→region
|
|
1198
|
+
* path fails as `unknown_column: region`. Reported as a grounding gap that
|
|
1199
|
+
* reads "Not enough context to answer safely", which is untrue and unhelpful:
|
|
1200
|
+
* no amount of extra context will produce a column the business has never
|
|
1201
|
+
* modeled, and the user is left to guess whether to rephrase, re-ask, or give
|
|
1202
|
+
* up. The truthful answer names the gap and offers the way forward.
|
|
1203
|
+
*
|
|
1204
|
+
* Deliberately narrow. A column the MODEL invented is a grounding failure and
|
|
1205
|
+
* must keep its existing code — the distinction between "you asked for
|
|
1206
|
+
* something we don't have" and "the draft referenced something that isn't
|
|
1207
|
+
* there" is the whole safety value of this classification.
|
|
1208
|
+
*/
|
|
1209
|
+
function requestedDimensionModelingGap(code, offending, requestedTerms) {
|
|
1210
|
+
if (code !== 'unknown_column' && code !== 'unknown_relation')
|
|
1211
|
+
return undefined;
|
|
1212
|
+
const identifier = offending?.column ?? offending?.relation;
|
|
1213
|
+
if (!identifier)
|
|
1214
|
+
return undefined;
|
|
1215
|
+
const canonical = (value) => value
|
|
1216
|
+
.split(/[.:]/).pop()
|
|
1217
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
1218
|
+
const target = canonical(identifier);
|
|
1219
|
+
if (!target)
|
|
1220
|
+
return undefined;
|
|
1221
|
+
const requested = requestedTerms
|
|
1222
|
+
.map(canonical)
|
|
1223
|
+
.filter((term) => term.length > 2);
|
|
1224
|
+
const matched = requested.find((term) => term === target
|
|
1225
|
+
|| target.split(' ').includes(term)
|
|
1226
|
+
|| term.split(' ').includes(target));
|
|
1227
|
+
return matched ? identifier.split(/[.:]/).pop() : undefined;
|
|
1228
|
+
}
|
|
628
1229
|
function refusalCodeForValidation(code) {
|
|
629
1230
|
if (code === 'unknown_relation' || code === 'unknown_column' || code === 'insufficient_context' || code === 'missing_baseline') {
|
|
630
1231
|
return 'grounding_gap';
|
|
@@ -1024,7 +1625,9 @@ export async function answer(input) {
|
|
|
1024
1625
|
// Cross-result computations are intentionally independent of the KG and
|
|
1025
1626
|
// warehouse. Preserve this earliest exit before building any execution
|
|
1026
1627
|
// registry so lightweight follow-up callers do not need runtime services.
|
|
1027
|
-
const earlyCrossResult =
|
|
1628
|
+
const earlyCrossResult = normalizedInput.skipCrossResultComputation
|
|
1629
|
+
? null
|
|
1630
|
+
: tryCrossResultAnswer(normalizedInput);
|
|
1028
1631
|
if (earlyCrossResult) {
|
|
1029
1632
|
return {
|
|
1030
1633
|
...earlyCrossResult,
|
|
@@ -2101,14 +2704,8 @@ async function runAnswerLoop(input) {
|
|
|
2101
2704
|
let executionFailureDetail;
|
|
2102
2705
|
if (artifactHit.node.kind === 'block' && input.executeCertifiedBlock) {
|
|
2103
2706
|
try {
|
|
2104
|
-
result = await input.executeCertifiedBlock(artifactHit.node,
|
|
2105
|
-
|
|
2106
|
-
...certifiedInvocationInputs(artifactHit.node, questionPlan),
|
|
2107
|
-
rowLimit: questionPlan.requestedShape.topN?.scope === 'per_group'
|
|
2108
|
-
? undefined
|
|
2109
|
-
: questionPlan.requestedShape.topN?.n,
|
|
2110
|
-
});
|
|
2111
|
-
result = trimResultToRequestedTopN(result, questionPlan);
|
|
2707
|
+
result = await input.executeCertifiedBlock(artifactHit.node, buildCertifiedBlockInvocationInput(artifactHit.node, questionPlan, question));
|
|
2708
|
+
result = trimCertifiedBlockResultToRequestedTopN(result, questionPlan);
|
|
2112
2709
|
}
|
|
2113
2710
|
catch (err) {
|
|
2114
2711
|
executionFailureDetail = analyticalErrorDetail(err);
|
|
@@ -2977,9 +3574,15 @@ async function runAnswerLoop(input) {
|
|
|
2977
3574
|
confidence: 0,
|
|
2978
3575
|
text,
|
|
2979
3576
|
answer: text,
|
|
2980
|
-
|
|
3577
|
+
// DQL stopping itself is not the provider failing. Reporting both as
|
|
3578
|
+
// `provider_error` put "The AI provider did not respond" above a body
|
|
3579
|
+
// that said the opposite, and sent the reader to check provider health
|
|
3580
|
+
// for a run that ended on an internal ceiling.
|
|
3581
|
+
refusalCode: admissionDeclined || orchestrationBudget ? 'orchestration_budget_exhausted' : 'provider_error',
|
|
2981
3582
|
refusalDetails: {
|
|
2982
|
-
code:
|
|
3583
|
+
code: admissionDeclined
|
|
3584
|
+
? 'RUN_DEADLINE_INSUFFICIENT'
|
|
3585
|
+
: orchestrationBudget ? 'orchestration_budget_exhausted' : 'provider_error',
|
|
2983
3586
|
message: orchestrationBudget ? `${text} (${err.message})` : text,
|
|
2984
3587
|
},
|
|
2985
3588
|
citations: [],
|
|
@@ -3707,7 +4310,13 @@ async function runAnswerLoop(input) {
|
|
|
3707
4310
|
// Business-language chat text; the validator's machine message (with
|
|
3708
4311
|
// relation ids and tool guidance for the repair prompt) stays in
|
|
3709
4312
|
// refusalDetails + validationWarnings for the Inspect surface.
|
|
3710
|
-
|
|
4313
|
+
// A dimension the user explicitly asked for that the business has not
|
|
4314
|
+
// modeled is a modeling gap, not a retrieval gap. Say so, and say what
|
|
4315
|
+
// would let the question be answered.
|
|
4316
|
+
const unmodeledRequestedDimension = requestedDimensionModelingGap(contextValidation.code, contextValidation.offending, [...questionPlan.dimensionTerms, ...(input.followUp?.dimensions ?? [])]);
|
|
4317
|
+
const text = unmodeledRequestedDimension
|
|
4318
|
+
? `"${unmodeledRequestedDimension}" is not modeled. Which governed dimension should I use instead?`
|
|
4319
|
+
: renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings, contextValidation.aggregationSafetyProof?.issueCodes);
|
|
3711
4320
|
const analysisPlan = buildAnalysisPlan({
|
|
3712
4321
|
question,
|
|
3713
4322
|
intent,
|
|
@@ -3729,7 +4338,9 @@ async function runAnswerLoop(input) {
|
|
|
3729
4338
|
reviewStatus: 'none',
|
|
3730
4339
|
confidence: 0.15,
|
|
3731
4340
|
text,
|
|
3732
|
-
refusalCode:
|
|
4341
|
+
refusalCode: unmodeledRequestedDimension
|
|
4342
|
+
? 'modeling_gap'
|
|
4343
|
+
: refusalCodeForValidation(contextValidation.code),
|
|
3733
4344
|
refusalDetails: {
|
|
3734
4345
|
code: contextValidation.code ?? 'insufficient_context',
|
|
3735
4346
|
message: contextValidation.error,
|
|
@@ -4178,11 +4789,11 @@ async function runAnswerLoop(input) {
|
|
|
4178
4789
|
// Global top-N asks must return exactly N rows even when the generated SQL
|
|
4179
4790
|
// returned more (missing/oversized LIMIT) — mirrors the certified path so a
|
|
4180
4791
|
// "top 10" question never shows 200 rows. per_group scope is left intact by
|
|
4181
|
-
//
|
|
4792
|
+
// trimCertifiedBlockResultToRequestedTopN. Domain-agnostic.
|
|
4182
4793
|
let topNTrimNote;
|
|
4183
4794
|
if (result) {
|
|
4184
4795
|
const beforeRows = Array.isArray(result.rows) ? result.rows.length : result.rowCount;
|
|
4185
|
-
result =
|
|
4796
|
+
result = trimCertifiedBlockResultToRequestedTopN(result, questionPlan);
|
|
4186
4797
|
const afterRows = Array.isArray(result.rows) ? result.rows.length : result.rowCount;
|
|
4187
4798
|
if (afterRows < beforeRows) {
|
|
4188
4799
|
topNTrimNote = `Showed the top ${questionPlan.requestedShape.topN?.n ?? afterRows} of ${beforeRows} rows the query returned.`;
|
|
@@ -4413,7 +5024,13 @@ function renderExecutionSchemaForRepair(schemaContext) {
|
|
|
4413
5024
|
? `Current execution-target columns (authoritative for this repair):\n${tables.join('\n')}`
|
|
4414
5025
|
: 'No execution-target columns were available; do not invent a replacement column.';
|
|
4415
5026
|
}
|
|
4416
|
-
|
|
5027
|
+
/**
|
|
5028
|
+
* Preserve the declared answer shape even when an authored certified block
|
|
5029
|
+
* returns a broader result set than its typed `top_n` input. The V2 exact
|
|
5030
|
+
* path uses this same normalization so a provider-free execution cannot
|
|
5031
|
+
* present a different result than the ordinary certified route.
|
|
5032
|
+
*/
|
|
5033
|
+
export function trimCertifiedBlockResultToRequestedTopN(result, plan) {
|
|
4417
5034
|
const topN = plan.requestedShape.topN;
|
|
4418
5035
|
if (!topN || topN.scope === 'per_group' || !Array.isArray(result.rows) || result.rows.length <= topN.n)
|
|
4419
5036
|
return result;
|
|
@@ -8301,7 +8918,7 @@ async function generateProposalWithOptionalTools(input) {
|
|
|
8301
8918
|
// provider implements generateWithTools (Claude/OpenAI), and an equivalent text
|
|
8302
8919
|
// protocol otherwise (subscription-CLI passthrough, Ollama). This is what gives
|
|
8303
8920
|
// every provider — not just the two API ones — a real tool-driven Stage B.
|
|
8304
|
-
|
|
8921
|
+
const result = await runAgenticToolLoopDetailed(input.provider, [...input.messages], tools, {
|
|
8305
8922
|
...options,
|
|
8306
8923
|
toolPolicy,
|
|
8307
8924
|
maxToolCalls: toolBudget.maxToolCalls,
|
|
@@ -8312,7 +8929,26 @@ async function generateProposalWithOptionalTools(input) {
|
|
|
8312
8929
|
sink.push(evidenceToolCallFromEvent(event, sink.length + 1));
|
|
8313
8930
|
},
|
|
8314
8931
|
});
|
|
8932
|
+
// The tool loop converts a budget stop into a TERMINAL RESULT rather than
|
|
8933
|
+
// rethrowing, so the reason reached this caller as an empty string and the
|
|
8934
|
+
// run reported "the model declined to propose SQL" — blaming the model for
|
|
8935
|
+
// a ceiling DQL imposed on itself. The plain-generation branch above throws
|
|
8936
|
+
// these, so rethrow here too and let one handler label them honestly.
|
|
8937
|
+
const budgetStop = PROPOSAL_BUDGET_STOP_CODES[result.stop ?? ''];
|
|
8938
|
+
if (budgetStop) {
|
|
8939
|
+
throw Object.assign(new Error(`The Ask runtime stopped before a query was settled (${result.stop}).`), { code: budgetStop });
|
|
8940
|
+
}
|
|
8941
|
+
return result.text;
|
|
8315
8942
|
}
|
|
8943
|
+
/**
|
|
8944
|
+
* Tool-loop terminal reasons that mean DQL stopped itself, mapped back to the
|
|
8945
|
+
* dispatch codes the answer loop's provider-failure handler understands.
|
|
8946
|
+
*/
|
|
8947
|
+
const PROPOSAL_BUDGET_STOP_CODES = {
|
|
8948
|
+
provider_dispatch_budget_exhausted: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED',
|
|
8949
|
+
run_soft_target_exceeded: 'RUN_SOFT_TARGET_EXCEEDED',
|
|
8950
|
+
run_deadline_insufficient: 'RUN_DEADLINE_INSUFFICIENT',
|
|
8951
|
+
};
|
|
8316
8952
|
async function selectDeepGeneratedProposalCandidate(input) {
|
|
8317
8953
|
const initial = await scoreDeepGeneratedProposalCandidate(input, {
|
|
8318
8954
|
raw: input.initial.raw,
|