@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.
Files changed (128) hide show
  1. package/dist/agent-run-engine.d.ts +177 -3
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +2061 -57
  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 +51 -3
  7. package/dist/agent-run-gates.js.map +1 -1
  8. package/dist/agent-run-store.d.ts.map +1 -1
  9. package/dist/agent-run-store.js +544 -9
  10. package/dist/agent-run-store.js.map +1 -1
  11. package/dist/agentic/analyst-loop.d.ts +2 -0
  12. package/dist/agentic/analyst-loop.d.ts.map +1 -1
  13. package/dist/agentic/analyst-loop.js +41 -4
  14. package/dist/agentic/analyst-loop.js.map +1 -1
  15. package/dist/agentic/ledger-tools.d.ts.map +1 -1
  16. package/dist/agentic/ledger-tools.js +8 -1
  17. package/dist/agentic/ledger-tools.js.map +1 -1
  18. package/dist/agentic/orchestrator-policy.d.ts +3 -1
  19. package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
  20. package/dist/agentic/orchestrator-policy.js +1 -1
  21. package/dist/agentic/orchestrator-policy.js.map +1 -1
  22. package/dist/agentic/tool-loop.d.ts +13 -1
  23. package/dist/agentic/tool-loop.d.ts.map +1 -1
  24. package/dist/agentic/tool-loop.js +441 -32
  25. package/dist/agentic/tool-loop.js.map +1 -1
  26. package/dist/analytical-frame.d.ts +19 -0
  27. package/dist/analytical-frame.d.ts.map +1 -1
  28. package/dist/analytical-frame.js +170 -12
  29. package/dist/analytical-frame.js.map +1 -1
  30. package/dist/analytical-orchestration.d.ts +949 -10
  31. package/dist/analytical-orchestration.d.ts.map +1 -1
  32. package/dist/analytical-orchestration.js +499 -56
  33. package/dist/analytical-orchestration.js.map +1 -1
  34. package/dist/answer-loop.d.ts +166 -2
  35. package/dist/answer-loop.d.ts.map +1 -1
  36. package/dist/answer-loop.js +654 -18
  37. package/dist/answer-loop.js.map +1 -1
  38. package/dist/answer-shape.d.ts +27 -0
  39. package/dist/answer-shape.d.ts.map +1 -1
  40. package/dist/answer-shape.js +55 -0
  41. package/dist/answer-shape.js.map +1 -1
  42. package/dist/ask-observability/instrumentation.d.ts.map +1 -1
  43. package/dist/ask-observability/instrumentation.js +52 -1
  44. package/dist/ask-observability/instrumentation.js.map +1 -1
  45. package/dist/ask-observability/portable.js +562 -0
  46. package/dist/ask-observability/portable.js.map +1 -1
  47. package/dist/ask-observability/types.d.ts +26 -3
  48. package/dist/ask-observability/types.d.ts.map +1 -1
  49. package/dist/ask-observability/types.js.map +1 -1
  50. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
  51. package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
  52. package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
  53. package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
  54. package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
  55. package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
  56. package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
  57. package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
  58. package/dist/conversation/session-store.d.ts +31 -0
  59. package/dist/conversation/session-store.d.ts.map +1 -1
  60. package/dist/conversation/session-store.js +38 -0
  61. package/dist/conversation/session-store.js.map +1 -1
  62. package/dist/conversation/snapshot.d.ts +4 -1
  63. package/dist/conversation/snapshot.d.ts.map +1 -1
  64. package/dist/conversation/snapshot.js +85 -3
  65. package/dist/conversation/snapshot.js.map +1 -1
  66. package/dist/conversation/turn-trust.d.ts.map +1 -1
  67. package/dist/conversation/turn-trust.js +2 -0
  68. package/dist/conversation/turn-trust.js.map +1 -1
  69. package/dist/embeddings/provider.d.ts +1 -0
  70. package/dist/embeddings/provider.d.ts.map +1 -1
  71. package/dist/embeddings/provider.js +18 -0
  72. package/dist/embeddings/provider.js.map +1 -1
  73. package/dist/generated-analytical-proposal.js +37 -2
  74. package/dist/generated-analytical-proposal.js.map +1 -1
  75. package/dist/index.d.ts +15 -8
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +9 -4
  78. package/dist/index.js.map +1 -1
  79. package/dist/intent-controller.d.ts +77 -1
  80. package/dist/intent-controller.d.ts.map +1 -1
  81. package/dist/intent-controller.js +1 -1
  82. package/dist/intent-controller.js.map +1 -1
  83. package/dist/kg/build.js +32 -2
  84. package/dist/kg/build.js.map +1 -1
  85. package/dist/meaning-resolution.d.ts +129 -0
  86. package/dist/meaning-resolution.d.ts.map +1 -1
  87. package/dist/meaning-resolution.js +108 -15
  88. package/dist/meaning-resolution.js.map +1 -1
  89. package/dist/metadata/analysis-planner.js +57 -6
  90. package/dist/metadata/analysis-planner.js.map +1 -1
  91. package/dist/metadata/catalog.d.ts +14 -0
  92. package/dist/metadata/catalog.d.ts.map +1 -1
  93. package/dist/metadata/catalog.js +28 -1
  94. package/dist/metadata/catalog.js.map +1 -1
  95. package/dist/metadata/meaning-evidence.d.ts +19 -0
  96. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  97. package/dist/metadata/meaning-evidence.js +113 -6
  98. package/dist/metadata/meaning-evidence.js.map +1 -1
  99. package/dist/provider-egress.d.ts +4 -1
  100. package/dist/provider-egress.d.ts.map +1 -1
  101. package/dist/provider-egress.js +50 -5
  102. package/dist/provider-egress.js.map +1 -1
  103. package/dist/providers/claude.d.ts +2 -2
  104. package/dist/providers/claude.d.ts.map +1 -1
  105. package/dist/providers/claude.js +191 -28
  106. package/dist/providers/claude.js.map +1 -1
  107. package/dist/providers/index.d.ts +1 -1
  108. package/dist/providers/index.d.ts.map +1 -1
  109. package/dist/providers/index.js.map +1 -1
  110. package/dist/providers/openai.d.ts +2 -2
  111. package/dist/providers/openai.d.ts.map +1 -1
  112. package/dist/providers/openai.js +191 -21
  113. package/dist/providers/openai.js.map +1 -1
  114. package/dist/providers/types.d.ts +79 -1
  115. package/dist/providers/types.d.ts.map +1 -1
  116. package/dist/providers/types.js.map +1 -1
  117. package/dist/requirement-clauses.d.ts +23 -0
  118. package/dist/requirement-clauses.d.ts.map +1 -0
  119. package/dist/requirement-clauses.js +70 -0
  120. package/dist/requirement-clauses.js.map +1 -0
  121. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  122. package/dist/resolved-analytical-plan.js +201 -15
  123. package/dist/resolved-analytical-plan.js.map +1 -1
  124. package/dist/router.d.ts +109 -2
  125. package/dist/router.d.ts.map +1 -1
  126. package/dist/router.js +753 -76
  127. package/dist/router.js.map +1 -1
  128. package/package.json +4 -4
@@ -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 { runAgenticToolLoop } from './agentic/tool-loop.js';
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 = tryCrossResultAnswer(normalizedInput);
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
- question,
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
- refusalCode: 'provider_error',
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: orchestrationBudget ? 'orchestration_budget_exhausted' : 'provider_error',
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
- const text = renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings, contextValidation.aggregationSafetyProof?.issueCodes);
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: refusalCodeForValidation(contextValidation.code),
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
- // trimResultToRequestedTopN. Domain-agnostic.
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 = trimResultToRequestedTopN(result, questionPlan);
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
- function trimResultToRequestedTopN(result, plan) {
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
- return runAgenticToolLoop(input.provider, [...input.messages], tools, {
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,