@duckcodeailabs/dql-agent 1.13.4 → 1.13.5

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 (120) hide show
  1. package/dist/agent-run-engine.d.ts +66 -3
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +441 -28
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/agentic/tool-loop.d.ts.map +1 -1
  6. package/dist/agentic/tool-loop.js +100 -32
  7. package/dist/agentic/tool-loop.js.map +1 -1
  8. package/dist/aggregation-safety-proof.d.ts +41 -0
  9. package/dist/aggregation-safety-proof.d.ts.map +1 -0
  10. package/dist/aggregation-safety-proof.js +511 -0
  11. package/dist/aggregation-safety-proof.js.map +1 -0
  12. package/dist/analytical-compatibility.d.ts.map +1 -1
  13. package/dist/analytical-compatibility.js +42 -25
  14. package/dist/analytical-compatibility.js.map +1 -1
  15. package/dist/analytical-execution-graph.d.ts +4 -21
  16. package/dist/analytical-execution-graph.d.ts.map +1 -1
  17. package/dist/analytical-execution-graph.js +27 -0
  18. package/dist/analytical-execution-graph.js.map +1 -1
  19. package/dist/analytical-frame.d.ts +12 -1
  20. package/dist/analytical-frame.d.ts.map +1 -1
  21. package/dist/analytical-frame.js +102 -2
  22. package/dist/analytical-frame.js.map +1 -1
  23. package/dist/analytical-result-facts.d.ts +53 -0
  24. package/dist/analytical-result-facts.d.ts.map +1 -1
  25. package/dist/analytical-result-facts.js +144 -0
  26. package/dist/analytical-result-facts.js.map +1 -1
  27. package/dist/answer-loop.d.ts +40 -3
  28. package/dist/answer-loop.d.ts.map +1 -1
  29. package/dist/answer-loop.js +681 -37
  30. package/dist/answer-loop.js.map +1 -1
  31. package/dist/cascade/budgets.js +1 -1
  32. package/dist/cascade/route-policy.d.ts +1 -1
  33. package/dist/cascade/route-policy.d.ts.map +1 -1
  34. package/dist/cascade/route-policy.js +2 -0
  35. package/dist/cascade/route-policy.js.map +1 -1
  36. package/dist/generated-analytical-proposal.d.ts +37 -0
  37. package/dist/generated-analytical-proposal.d.ts.map +1 -0
  38. package/dist/generated-analytical-proposal.js +219 -0
  39. package/dist/generated-analytical-proposal.js.map +1 -0
  40. package/dist/index.d.ts +9 -5
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +6 -3
  43. package/dist/index.js.map +1 -1
  44. package/dist/intent-controller.d.ts +8 -1
  45. package/dist/intent-controller.d.ts.map +1 -1
  46. package/dist/intent-controller.js.map +1 -1
  47. package/dist/kg/build.js +102 -6
  48. package/dist/kg/build.js.map +1 -1
  49. package/dist/meaning-resolution.d.ts +23 -0
  50. package/dist/meaning-resolution.d.ts.map +1 -1
  51. package/dist/meaning-resolution.js +61 -8
  52. package/dist/meaning-resolution.js.map +1 -1
  53. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  54. package/dist/metadata/analysis-planner.js +46 -2
  55. package/dist/metadata/analysis-planner.js.map +1 -1
  56. package/dist/metadata/block-fit.d.ts.map +1 -1
  57. package/dist/metadata/block-fit.js +55 -2
  58. package/dist/metadata/block-fit.js.map +1 -1
  59. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  60. package/dist/metadata/meaning-evidence.js +323 -36
  61. package/dist/metadata/meaning-evidence.js.map +1 -1
  62. package/dist/metadata/sql-context-validation.d.ts +3 -0
  63. package/dist/metadata/sql-context-validation.d.ts.map +1 -1
  64. package/dist/metadata/sql-context-validation.js +16 -5
  65. package/dist/metadata/sql-context-validation.js.map +1 -1
  66. package/dist/plan-execution-adapter.d.ts +9 -1
  67. package/dist/plan-execution-adapter.d.ts.map +1 -1
  68. package/dist/plan-execution-adapter.js +81 -32
  69. package/dist/plan-execution-adapter.js.map +1 -1
  70. package/dist/provider-egress.d.ts +122 -0
  71. package/dist/provider-egress.d.ts.map +1 -0
  72. package/dist/provider-egress.js +504 -0
  73. package/dist/provider-egress.js.map +1 -0
  74. package/dist/providers/claude.d.ts.map +1 -1
  75. package/dist/providers/claude.js +76 -33
  76. package/dist/providers/claude.js.map +1 -1
  77. package/dist/providers/dispatch.d.ts +15 -0
  78. package/dist/providers/dispatch.d.ts.map +1 -0
  79. package/dist/providers/dispatch.js +25 -0
  80. package/dist/providers/dispatch.js.map +1 -0
  81. package/dist/providers/gemini.d.ts.map +1 -1
  82. package/dist/providers/gemini.js +16 -8
  83. package/dist/providers/gemini.js.map +1 -1
  84. package/dist/providers/index.d.ts +2 -1
  85. package/dist/providers/index.d.ts.map +1 -1
  86. package/dist/providers/index.js +1 -0
  87. package/dist/providers/index.js.map +1 -1
  88. package/dist/providers/ollama.d.ts.map +1 -1
  89. package/dist/providers/ollama.js +29 -10
  90. package/dist/providers/ollama.js.map +1 -1
  91. package/dist/providers/openai.d.ts.map +1 -1
  92. package/dist/providers/openai.js +92 -36
  93. package/dist/providers/openai.js.map +1 -1
  94. package/dist/providers/types.d.ts +43 -0
  95. package/dist/providers/types.d.ts.map +1 -1
  96. package/dist/providers/types.js.map +1 -1
  97. package/dist/relationship-proof.d.ts +19 -0
  98. package/dist/relationship-proof.d.ts.map +1 -0
  99. package/dist/relationship-proof.js +75 -0
  100. package/dist/relationship-proof.js.map +1 -0
  101. package/dist/research-loop.d.ts.map +1 -1
  102. package/dist/research-loop.js +17 -11
  103. package/dist/research-loop.js.map +1 -1
  104. package/dist/resolved-analytical-plan.d.ts +25 -3
  105. package/dist/resolved-analytical-plan.d.ts.map +1 -1
  106. package/dist/resolved-analytical-plan.js +369 -27
  107. package/dist/resolved-analytical-plan.js.map +1 -1
  108. package/dist/router.d.ts +30 -1
  109. package/dist/router.d.ts.map +1 -1
  110. package/dist/router.js +705 -44
  111. package/dist/router.js.map +1 -1
  112. package/dist/semantic-identities.d.ts +18 -0
  113. package/dist/semantic-identities.d.ts.map +1 -0
  114. package/dist/semantic-identities.js +60 -0
  115. package/dist/semantic-identities.js.map +1 -0
  116. package/dist/semantic-proof-authority.d.ts +29 -0
  117. package/dist/semantic-proof-authority.d.ts.map +1 -0
  118. package/dist/semantic-proof-authority.js +34 -0
  119. package/dist/semantic-proof-authority.js.map +1 -0
  120. package/package.json +4 -4
@@ -1,20 +1,22 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { normalizeProviderEgressReceiptV1, } from '@duckcodeailabs/dql-core';
2
3
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
4
  import { dirname, join } from "node:path";
4
- import { classifyConversationalTurn, decideAgentAction, } from "./intent-controller.js";
5
+ import { classifyConversationalTurn, decideAgentAction, looksLikeComposeApp, } from "./intent-controller.js";
5
6
  import { selectCascadeRunRoute } from "./cascade/route-policy.js";
6
7
  import { canUseEngineEscalation, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, recordEngineEscalation, recordLaneRepair, } from "./cascade/budgets.js";
8
+ import { buildAnalysisQuestionPlan } from "./metadata/analysis-planner.js";
7
9
  import { conversationHistoryFromContext, isLikelyClarificationReply, } from "./conversation/snapshot.js";
8
10
  /** Routes a stakeholder may never land on (analyst authoring lives in the Notebook). */
9
11
  const ANALYST_ONLY_ROUTES = new Set(["sql_cell", "dql_block_draft", "modeling_draft", "skill_draft"]);
10
12
  /**
11
- * Compare the pre-execution plan with the legacy route without changing the
12
- * route. This is the R3 shadow-mode cutover signal used before R4 enforcement.
13
+ * Compare the pre-execution plan with the selected route without changing it.
14
+ * Authoritative plans never emit or persist this migration-only diagnostic.
13
15
  * Acceptance: AGT-013, API-006.
14
16
  */
15
17
  export function compareResolvedPlanShadow(decision, actualRoute) {
16
18
  const plan = decision.resolvedAnalyticalPlan;
17
- if (!plan)
19
+ if (!plan || plan.mode !== 'shadow')
18
20
  return undefined;
19
21
  const plannedRoute = decision.action === 'investigate'
20
22
  ? 'research'
@@ -36,6 +38,58 @@ export function compareResolvedPlanShadow(decision, actualRoute) {
36
38
  };
37
39
  }
38
40
  const DEFAULT_MAX_STEPS = 4;
41
+ export function agentRouteDeadlineMs(route) {
42
+ if (route === 'certified_answer' || route === 'semantic_answer')
43
+ return 5_000;
44
+ if (route === 'clarify')
45
+ return 10_000;
46
+ // Generation may now take a genuine tool round (look something up, then use
47
+ // it) instead of a single blind shot, so the discovery window has to cover it.
48
+ // Subscription-CLI providers cost roughly 10-15s per dispatch, so a window
49
+ // under ~30s silently reduces the loop back to a single blind attempt.
50
+ if (route === 'generated_answer')
51
+ return 30_000;
52
+ if (route === 'research')
53
+ return 120_000;
54
+ return undefined;
55
+ }
56
+ /** The request envelope starts before retrieval/routing, so a stuck router can
57
+ * never evade the route-specific deadline that is selected later. */
58
+ export function agentRequestDeadlineMs(requestedMode) {
59
+ return requestedMode === 'research' ? 120_000 : 45_000;
60
+ }
61
+ /** Create the one request-ingress deadline authority used by every stage. */
62
+ export function createAgentRunBudget(input) {
63
+ const nowMs = input.nowMs ?? Date.now;
64
+ const startedAtMs = input.startedAtMs ?? nowMs();
65
+ const mode = input.requestedMode === 'research' ? 'research' : 'ask';
66
+ const hardDeadlineMs = mode === 'research' ? 120_000 : 45_000;
67
+ const timeout = (input.timeoutSignal ?? AbortSignal.timeout)(hardDeadlineMs);
68
+ const hardSignal = input.inheritedSignal
69
+ ? AbortSignal.any([input.inheritedSignal, timeout])
70
+ : timeout;
71
+ const elapsedMs = () => Math.max(0, nowMs() - startedAtMs);
72
+ const softTargetMs = (route) => {
73
+ if (mode === 'research')
74
+ return 90_000;
75
+ return agentRouteDeadlineMs(route) ?? 15_000;
76
+ };
77
+ // Narration must still be reachable after a full generation window, and must
78
+ // leave the hard deadline (45s ask / 120s research) room to land.
79
+ const narrationSoftTargetMs = () => (mode === 'research' ? 100_000 : 38_000);
80
+ return Object.freeze({
81
+ startedAtMs,
82
+ hardDeadlineMs,
83
+ hardSignal,
84
+ mode,
85
+ elapsedMs,
86
+ remainingMs: () => Math.max(0, hardDeadlineMs - elapsedMs()),
87
+ softTargetMs,
88
+ mayStartDiscovery: (route) => !hardSignal.aborted && elapsedMs() < softTargetMs(route),
89
+ narrationSoftTargetMs,
90
+ mayStartNarration: () => !hardSignal.aborted && elapsedMs() < narrationSoftTargetMs(),
91
+ });
92
+ }
39
93
  /**
40
94
  * Routes whose gate failure is better answered by switching routes than by
41
95
  * re-running the same executor (repair can't add what the route can't produce).
@@ -250,7 +304,7 @@ export function escalationRouteFor(route, audience) {
250
304
  if (!target)
251
305
  return undefined;
252
306
  if (audience === "stakeholder" && ANALYST_ONLY_ROUTES.has(target))
253
- return "research";
307
+ return "generated_answer";
254
308
  return target;
255
309
  }
256
310
  /** The handoff action shown on a stakeholder's review-required output. */
@@ -369,6 +423,7 @@ export class AgentRunEngine {
369
423
  now;
370
424
  budgetModel;
371
425
  maxSteps;
426
+ routeTimeoutSignal;
372
427
  constructor(options = {}) {
373
428
  this.executors = options.executors ?? {};
374
429
  this.gates = options.gates ?? {};
@@ -386,6 +441,7 @@ export class AgentRunEngine {
386
441
  engineEscalations: options.maxEngineEscalations ?? options.budgets?.engineEscalations,
387
442
  };
388
443
  this.maxSteps = Math.max(1, options.maxSteps ?? DEFAULT_MAX_STEPS);
444
+ this.routeTimeoutSignal = options.routeTimeoutSignal ?? ((durationMs) => AbortSignal.timeout(durationMs));
389
445
  }
390
446
  /**
391
447
  * Decide the high-level action. A forced `requestedMode` bypasses routing.
@@ -406,7 +462,8 @@ export class AgentRunEngine {
406
462
  if (request.requestedMode === "ask"
407
463
  && routed.action !== "converse"
408
464
  && routed.action !== "compose_app"
409
- && routed.requiresClarification !== true) {
465
+ && routed.requiresClarification !== true
466
+ && !routed.terminalOutcome) {
410
467
  return { ...routed, action: "answer" };
411
468
  }
412
469
  return routed;
@@ -440,7 +497,16 @@ export class AgentRunEngine {
440
497
  }
441
498
  const runId = request.runId ?? this.idGenerator();
442
499
  const startedAt = this.timestamp();
500
+ const runStartedAtMs = Date.parse(startedAt);
443
501
  const requestedMode = request.requestedMode ?? "auto";
502
+ const runBudget = request.runBudget ?? createAgentRunBudget({
503
+ requestedMode,
504
+ startedAtMs: runStartedAtMs,
505
+ inheritedSignal: request.signal,
506
+ timeoutSignal: this.routeTimeoutSignal,
507
+ nowMs: () => this.now().getTime(),
508
+ });
509
+ request = { ...request, runBudget, signal: runBudget.hardSignal };
444
510
  const events = [];
445
511
  let plan;
446
512
  const executedSteps = [];
@@ -550,15 +616,22 @@ export class AgentRunEngine {
550
616
  followsUp: true,
551
617
  source: "heuristic",
552
618
  }
553
- : await this.decideRoute(request);
619
+ : await awaitWithAbort(this.decideRoute(request), request.signal);
620
+ routeDecision = enforceOrdinaryAnalyticalPlanBoundary(request, routeDecision);
554
621
  const defaultRoute = answerAnywayRoute(constrainRouteForAudience(selectRoute(request, routeDecision), audience), request, audience, routeDecision);
555
- plan = await this.planner.plan({
622
+ const authoritativeAsk = routeDecision.resolvedAnalyticalPlan?.mode === 'authoritative'
623
+ && requestedMode !== 'research';
624
+ const activePlanner = authoritativeAsk
625
+ ? createDeterministicAgentRunPlanner()
626
+ : this.planner;
627
+ const planningSignal = request.runBudget?.hardSignal ?? request.signal;
628
+ plan = await awaitWithAbort(Promise.resolve(activePlanner.plan({
556
629
  request,
557
630
  routeDecision,
558
631
  defaultRoute,
559
632
  maxSteps: this.maxSteps,
560
633
  audience,
561
- });
634
+ })), planningSignal);
562
635
  emit({
563
636
  type: "plan.created",
564
637
  message: plan.rationale,
@@ -662,6 +735,30 @@ export class AgentRunEngine {
662
735
  stepStatus = attempt > 0 ? "repaired" : "passed";
663
736
  break;
664
737
  }
738
+ // A frozen analytical plan has one route and no downstream planner,
739
+ // rematch, route escalation, or whole-answer regeneration authority.
740
+ // Typed server-issued repair is a separate derived run.
741
+ if (authoritativeAsk) {
742
+ stepStatus = 'needs_review';
743
+ break;
744
+ }
745
+ // Ordinary generated Ask freezes one analytical attempt. A failed
746
+ // attempt is terminal review/manual-action work; it cannot hand
747
+ // authority back to an injected LLM replanner for another physical
748
+ // provider send. Explicit Research and authoring modes retain their
749
+ // separately budgeted planner behavior.
750
+ const ordinaryGeneratedAsk = route === 'generated_answer'
751
+ && (requestedMode === 'ask' || requestedMode === 'auto');
752
+ if (ordinaryGeneratedAsk) {
753
+ stepStatus = 'needs_review';
754
+ emit({
755
+ type: 'replan.decided',
756
+ message: 'Ordinary Ask kept the frozen analytical attempt and stopped for review without replanning.',
757
+ route,
758
+ payload: { decision: 'accept', authority: 'deterministic_fail_closed' },
759
+ });
760
+ break;
761
+ }
665
762
  const currentStep = {
666
763
  id: stepId,
667
764
  index: stepCount,
@@ -674,7 +771,7 @@ export class AgentRunEngine {
674
771
  evaluations,
675
772
  artifacts: result.artifacts ?? [],
676
773
  };
677
- const decision = await this.planner.replan({
774
+ const decision = await activePlanner.replan({
678
775
  request,
679
776
  plan,
680
777
  currentStep,
@@ -779,7 +876,7 @@ export class AgentRunEngine {
779
876
  });
780
877
  continue;
781
878
  }
782
- const outcome = computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion);
879
+ const outcome = computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion, routeDecision.terminalOutcome?.message);
783
880
  const step = {
784
881
  id: stepId,
785
882
  index: stepCount,
@@ -862,15 +959,19 @@ export class AgentRunEngine {
862
959
  run.completedAt = this.timestamp();
863
960
  run.lifecycle = terminalLifecycle(progress.lifecycle, run.status === "blocked" ? "run.failed" : "run.completed", run.completedAt, events.length);
864
961
  run.diagnosticReceipt = diagnosticReceiptForRun(run);
865
- run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt);
962
+ run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
963
+ run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2);
866
964
  await checkpointQueue;
867
965
  await this.store?.save(run);
868
966
  return run;
869
967
  }
870
968
  catch (err) {
871
- const message = err instanceof Error && err.name === "TimeoutError"
872
- ? "This analytical run reached its time limit before it finished. A timeout alone does not prove a cross-model join or semantic-modeling problem. Open Trust & Steps to see the last recorded phase; retry the same bounded question or use Research for a longer budget. No result was accepted."
873
- : err instanceof Error ? err.message : String(err);
969
+ const dispatchEvidence = providerDispatchEvidenceFromError(err);
970
+ const message = isOrchestrationBudgetExhausted(err)
971
+ ? 'Ask could not complete within its bounded orchestration. Nothing was executed; narrow the metric or dimension and retry.'
972
+ : err instanceof Error && err.name === "TimeoutError"
973
+ ? "This analytical run reached its time limit before it finished. A timeout alone does not prove a cross-model join or semantic-modeling problem. Open Trust & Steps to see the last recorded phase; retry the same bounded question or use Research for a longer budget. No result was accepted."
974
+ : err instanceof Error ? err.message : String(err);
874
975
  const failedRoute = progress.route;
875
976
  const failedPhase = progress.lifecycle.phase;
876
977
  emit({
@@ -931,8 +1032,21 @@ export class AgentRunEngine {
931
1032
  escalationAttempts: 0,
932
1033
  budgetUsage: cascadeBudgetTrace(createCascadeBudgetState(this.budgetModel)),
933
1034
  diagnosticReceipt: receipt,
1035
+ ...(dispatchEvidence.providerEgressReceipts.length
1036
+ ? { providerEgressReceipts: dispatchEvidence.providerEgressReceipts }
1037
+ : {}),
1038
+ telemetry: {
1039
+ ...emptyRunTelemetry(durationBetweenMs(startedAt, completedAt), dispatchEvidence.fallbackReason),
1040
+ providerRoundTrips: dispatchEvidence.providerRoundTrips,
1041
+ toolCalls: dispatchEvidence.toolCalls,
1042
+ sqlExecutions: dispatchEvidence.sqlExecutions,
1043
+ repairs: dispatchEvidence.repairs,
1044
+ egressReceipts: dispatchEvidence.providerEgressReceipts.length,
1045
+ },
934
1046
  lifecycle: terminalLifecycle(progress.lifecycle, "run.failed", completedAt, events.length),
935
1047
  };
1048
+ run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
1049
+ run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2);
936
1050
  await checkpointQueue;
937
1051
  await this.store?.save(run);
938
1052
  return run;
@@ -985,8 +1099,12 @@ export class AgentRunEngine {
985
1099
  // If the final step produced no user-facing answer (e.g. it only drafted an
986
1100
  // artifact), fall back to the last step that DID answer so the run never
987
1101
  // drops a data answer an earlier step already computed.
988
- const finalHasAnswer = typeof finalResult.answer === "string" && finalResult.answer.trim().length > 0;
1102
+ const finalHasAnswer = finalOutcome.status !== "blocked"
1103
+ && typeof finalResult.answer === "string" && finalResult.answer.trim().length > 0;
989
1104
  const answerSource = finalHasAnswer ? finalResult : (input.bestAnswerResult ?? finalResult);
1105
+ const acceptedAnswer = finalOutcome.status === "blocked"
1106
+ ? finalOutcome.summary
1107
+ : input.clarifyOutcome?.question ?? answerSource.answer;
990
1108
  return {
991
1109
  id: input.runId,
992
1110
  question: input.request.question,
@@ -1003,7 +1121,7 @@ export class AgentRunEngine {
1003
1121
  plan: input.plan,
1004
1122
  steps: input.steps,
1005
1123
  summary: finalOutcome.summary,
1006
- answer: input.clarifyOutcome?.question ?? answerSource.answer,
1124
+ answer: acceptedAnswer,
1007
1125
  answerKind: answerSource.answerKind ?? "governed",
1008
1126
  artifacts,
1009
1127
  evaluations: finalStep.evaluations,
@@ -1014,6 +1132,12 @@ export class AgentRunEngine {
1014
1132
  ? { clarificationOptions: finalResult.clarificationOptions ?? input.routeDecision.clarificationOptions }
1015
1133
  : {}),
1016
1134
  repairAttempts: finalResult.repairAttempts ?? repairAttempts,
1135
+ ...(finalResult.providerEgressReceipts?.length
1136
+ ? { providerEgressReceipts: finalResult.providerEgressReceipts }
1137
+ : {}),
1138
+ ...(finalResult.telemetry ? {
1139
+ telemetry: withTotalDuration(finalResult.telemetry, durationBetweenMs(input.startedAt, completedAt)),
1140
+ } : {}),
1017
1141
  escalationAttempts,
1018
1142
  budgetUsage: input.budgetUsage,
1019
1143
  ...authoringDerivationFromRequest(input.request),
@@ -1028,14 +1152,209 @@ export class AgentRunEngine {
1028
1152
  }
1029
1153
  async executeRoute(context) {
1030
1154
  const executor = this.executors[context.route];
1031
- if (executor)
1032
- return executor(context);
1155
+ if (executor) {
1156
+ const frozenPlan = context.routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative';
1157
+ const startsAnalyticalWork = context.route === 'certified_answer'
1158
+ || context.route === 'semantic_answer'
1159
+ || context.route === 'generated_answer'
1160
+ || context.route === 'research';
1161
+ if (startsAnalyticalWork
1162
+ && !frozenPlan
1163
+ && context.request.runBudget
1164
+ && !context.request.runBudget.mayStartDiscovery(context.route)) {
1165
+ return softBoundaryResult(context.route, context.request.runBudget);
1166
+ }
1167
+ const signal = context.request.runBudget?.hardSignal ?? context.request.signal;
1168
+ if (signal?.aborted)
1169
+ throw signal.reason ?? routeTimeoutError();
1170
+ const execution = Promise.resolve(executor({
1171
+ ...context,
1172
+ request: { ...context.request, ...(signal ? { signal } : {}) },
1173
+ }));
1174
+ return awaitWithAbort(execution, signal);
1175
+ }
1033
1176
  return defaultExecutorResult(context.route, context.request, context.routeDecision);
1034
1177
  }
1178
+ absoluteDeadlineSignal(deadlineMs, runStartedAtMs, inherited) {
1179
+ const elapsedMs = Math.max(0, this.now().getTime() - runStartedAtMs);
1180
+ const remainingMs = deadlineMs - elapsedMs;
1181
+ const deadlineSignal = remainingMs <= 0
1182
+ ? alreadyAbortedSignal(routeTimeoutError())
1183
+ : this.routeTimeoutSignal(remainingMs);
1184
+ return inherited ? AbortSignal.any([inherited, deadlineSignal]) : deadlineSignal;
1185
+ }
1035
1186
  timestamp() {
1036
1187
  return this.now().toISOString();
1037
1188
  }
1038
1189
  }
1190
+ /**
1191
+ * Ordinary analytical Ask cannot delegate meaning to the legacy answer
1192
+ * generator. Retrieval/meaning must first produce the immutable RAP consumed by
1193
+ * every later compiler and executor. This host-side gate protects callers that
1194
+ * inject or deserialize an older router decision as well as the canonical
1195
+ * router producer.
1196
+ */
1197
+ function enforceOrdinaryAnalyticalPlanBoundary(request, decision) {
1198
+ const ordinaryAsk = request.requestedMode === undefined
1199
+ || request.requestedMode === 'auto'
1200
+ || request.requestedMode === 'ask';
1201
+ const terminal = decision.action === 'clarify'
1202
+ || decision.action === 'block'
1203
+ || decision.requiresClarification === true
1204
+ || Boolean(decision.terminalOutcome);
1205
+ const exactSemanticContinuation = Boolean(request.selectedEvidenceId
1206
+ && decision.meaningResolution?.recommendedRoute === 'semantic'
1207
+ && decision.meaningResolution.recommendedExecutionId === request.selectedEvidenceId);
1208
+ if (!ordinaryAsk
1209
+ || terminal
1210
+ || exactSemanticContinuation
1211
+ || decision.resolvedAnalyticalPlan?.mode === 'authoritative')
1212
+ return decision;
1213
+ const explicitAuthoring = looksLikeComposeApp(request.question)
1214
+ || /\b(sql\s+(?:notebook\s+)?cell|notebook\s+cell|write a select|generate a query|dql block|block draft|draft block|create[^.?!]*block|turn[^.?!]*into[^.?!]*block)\b/i.test(request.question);
1215
+ if (explicitAuthoring)
1216
+ return decision;
1217
+ const history = request.history?.length
1218
+ ? request.history
1219
+ : conversationHistoryFromContext(request.conversationContext);
1220
+ const questionPlan = buildAnalysisQuestionPlan(request.question);
1221
+ const conversationalKind = classifyConversationalTurn(request.question, history.length > 0 || Boolean(request.conversationContext && Object.keys(request.conversationContext).length > 0));
1222
+ const definitionOnly = questionPlan.mode === 'definition'
1223
+ && questionPlan.requestedShape.dimensions.length === 0
1224
+ && questionPlan.requestedShape.filters.length === 0
1225
+ && questionPlan.timeTerms.length === 0;
1226
+ // Router category/action are advisory and may be absent or forged. A
1227
+ // deterministic host parse owns the no-data lanes too, so a forged
1228
+ // `data_analysis` cannot turn "hi" into SQL and a forged `answer` cannot
1229
+ // turn a glossary definition into an analytical execution.
1230
+ if (ordinaryAsk && (conversationalKind || definitionOnly)) {
1231
+ return {
1232
+ ...decision,
1233
+ action: 'converse',
1234
+ category: 'conversational',
1235
+ ...(conversationalKind ? { conversationalKind } : {}),
1236
+ confidence: 1,
1237
+ reason: definitionOnly
1238
+ ? 'This requests a definition, not a warehouse result, so no analytical execution is needed.'
1239
+ : 'This is a conversational turn and does not request governed data.',
1240
+ requiresClarification: false,
1241
+ };
1242
+ }
1243
+ const analyticalIntents = new Set([
1244
+ 'exact_certified_lookup',
1245
+ 'ad_hoc_ranking',
1246
+ 'driver_breakdown',
1247
+ 'diagnose_change',
1248
+ 'segment_compare',
1249
+ 'entity_drilldown',
1250
+ 'anomaly_investigation',
1251
+ ]);
1252
+ const analyticalIntent = analyticalIntents.has(questionPlan.routeIntent)
1253
+ || Boolean(request.intent && analyticalIntents.has(request.intent));
1254
+ const analyticalShape = questionPlan.mode !== 'clarify'
1255
+ && questionPlan.mode !== 'definition'
1256
+ && (questionPlan.requestedShape.measures.length > 0
1257
+ || questionPlan.requestedShape.dimensions.length > 0
1258
+ || questionPlan.requestedShape.filters.length > 0
1259
+ || questionPlan.timeTerms.length > 0
1260
+ || questionPlan.requestedShape.rankingDirection !== undefined);
1261
+ const governedEvidence = (decision.retrievalEvidence?.candidateCount ?? 0) > 0
1262
+ || (decision.meaningResolution?.selectedConceptIds.length ?? 0) > 0;
1263
+ const analytical = analyticalIntent || analyticalShape || governedEvidence;
1264
+ if (!analytical)
1265
+ return decision;
1266
+ const message = 'DQL could not freeze an exact analytical plan for the requested metric, grain, filters, ordering, and outputs. Choose a governed identifier or model the missing capability before retrying.';
1267
+ return {
1268
+ ...decision,
1269
+ action: 'block',
1270
+ confidence: 1,
1271
+ reason: message,
1272
+ requiresClarification: false,
1273
+ terminalOutcome: {
1274
+ kind: 'modeling_gap',
1275
+ code: 'ANALYTICAL_MODELING_GAP',
1276
+ message,
1277
+ candidateIds: decision.retrievalEvidence?.candidateIds ?? [],
1278
+ },
1279
+ };
1280
+ }
1281
+ function softBoundaryResult(route, budget) {
1282
+ const seconds = Math.round(budget.softTargetMs(route) / 1_000);
1283
+ return {
1284
+ resolvedRoute: 'clarify',
1285
+ status: 'needs_clarification',
1286
+ trustState: 'blocked',
1287
+ stopReason: 'needs_clarification',
1288
+ summary: `The ${seconds}-second discovery target elapsed before an analytical plan was frozen. No new provider, tool, or retrieval branch was started.`,
1289
+ answer: budget.mode === 'research'
1290
+ ? 'Research stopped starting new branches at 90 seconds. Refine the question or retry; any already validated partial findings remain available.'
1291
+ : 'The discovery window ended before DQL could freeze an exact analytical plan. Refine the metric or grain, or retry the same bounded question.',
1292
+ artifacts: [],
1293
+ evaluations: [],
1294
+ nextActions: [{ id: 'retry-after-soft-target', label: 'Retry the same question' }],
1295
+ };
1296
+ }
1297
+ function providerDispatchEvidenceFromError(error) {
1298
+ const empty = {
1299
+ providerEgressReceipts: [],
1300
+ providerRoundTrips: 0,
1301
+ toolCalls: 0,
1302
+ sqlExecutions: 0,
1303
+ repairs: 0,
1304
+ fallbackReason: 'executor_failure',
1305
+ };
1306
+ if (!error || typeof error !== 'object')
1307
+ return empty;
1308
+ const value = error.providerDispatchEvidence;
1309
+ if (!value || typeof value !== 'object')
1310
+ return empty;
1311
+ const record = value;
1312
+ const count = (key) => Number.isInteger(record[key]) && Number(record[key]) >= 0 ? Number(record[key]) : 0;
1313
+ return {
1314
+ providerEgressReceipts: Array.isArray(record.providerEgressReceipts)
1315
+ ? record.providerEgressReceipts.flatMap((receipt) => {
1316
+ const normalized = normalizeProviderEgressReceiptV1(receipt);
1317
+ return normalized ? [normalized] : [];
1318
+ })
1319
+ : [],
1320
+ providerRoundTrips: count('providerRoundTrips'),
1321
+ toolCalls: count('toolCalls'),
1322
+ sqlExecutions: count('sqlExecutions'),
1323
+ repairs: count('repairs'),
1324
+ fallbackReason: typeof record.fallbackReason === 'string' ? record.fallbackReason : 'executor_failure',
1325
+ };
1326
+ }
1327
+ function alreadyAbortedSignal(reason) {
1328
+ const controller = new AbortController();
1329
+ controller.abort(reason);
1330
+ return controller.signal;
1331
+ }
1332
+ function routeTimeoutError() {
1333
+ return new DOMException('The absolute agent route deadline elapsed.', 'TimeoutError');
1334
+ }
1335
+ function awaitWithAbort(work, signal) {
1336
+ if (!signal)
1337
+ return work;
1338
+ if (signal.aborted) {
1339
+ // The callee may already have started before its cancellation state was
1340
+ // observed. Consume any eventual rejection while refusing its result.
1341
+ void work.catch(() => undefined);
1342
+ return Promise.reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
1343
+ }
1344
+ return new Promise((resolve, reject) => {
1345
+ let settled = false;
1346
+ const finish = (callback) => {
1347
+ if (settled)
1348
+ return;
1349
+ settled = true;
1350
+ signal.removeEventListener('abort', onAbort);
1351
+ callback();
1352
+ };
1353
+ const onAbort = () => finish(() => reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')));
1354
+ signal.addEventListener('abort', onAbort, { once: true });
1355
+ work.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
1356
+ });
1357
+ }
1039
1358
  function authoringDerivationFromRequest(request) {
1040
1359
  if (request.requestedMode !== 'modeling' && request.requestedMode !== 'skill')
1041
1360
  return {};
@@ -1073,6 +1392,15 @@ function diagnosticFailureFromError(error, phase) {
1073
1392
  const name = error instanceof Error ? error.name : "";
1074
1393
  const message = error instanceof Error ? error.message : String(error);
1075
1394
  const lower = `${name} ${message}`.toLowerCase();
1395
+ if (isOrchestrationBudgetExhausted(error)) {
1396
+ return {
1397
+ code: 'orchestration_budget_exhausted',
1398
+ phase,
1399
+ message: 'Ask exhausted its bounded orchestration before a final answer was available.',
1400
+ recoverable: false,
1401
+ safeActions: ['inspect_failure'],
1402
+ };
1403
+ }
1076
1404
  if (name === "TimeoutError" || lower.includes("time limit") || lower.includes("timeout")) {
1077
1405
  return {
1078
1406
  code: "TIMEOUT",
@@ -1099,6 +1427,14 @@ function diagnosticFailureFromError(error, phase) {
1099
1427
  safeActions: ["retry_same_request", "inspect_failure"],
1100
1428
  };
1101
1429
  }
1430
+ function isOrchestrationBudgetExhausted(error) {
1431
+ if (!error || typeof error !== 'object')
1432
+ return false;
1433
+ if (error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED')
1434
+ return true;
1435
+ const message = error instanceof Error ? error.message : String(error);
1436
+ return /(?:run-wide provider dispatch|orchestration) budget exhausted/i.test(message);
1437
+ }
1102
1438
  function diagnosticReceiptForRun(run) {
1103
1439
  const failureEvaluation = run.evaluations.find((evaluation) => !evaluation.passed && evaluation.severity === "blocking");
1104
1440
  const analyticalFailure = run.artifacts
@@ -1127,6 +1463,8 @@ function diagnosticReceiptForRun(run) {
1127
1463
  steps: run.steps,
1128
1464
  artifacts: run.artifacts,
1129
1465
  evaluations: run.evaluations,
1466
+ ...(run.repairCapability ? { repairCapability: run.repairCapability } : {}),
1467
+ ...(run.providerEgressReceipts?.length ? { providerEgressReceipts: run.providerEgressReceipts } : {}),
1130
1468
  ...(run.status === "blocked"
1131
1469
  ? {
1132
1470
  failure: {
@@ -1140,7 +1478,44 @@ function diagnosticReceiptForRun(run) {
1140
1478
  : {}),
1141
1479
  };
1142
1480
  }
1143
- function attachDiagnosticReceipt(artifacts, receipt) {
1481
+ function diagnosticReceiptV2ForRun(run) {
1482
+ const telemetry = run.telemetry ?? emptyRunTelemetry(durationBetweenMs(run.startedAt, run.completedAt), 'not_recorded');
1483
+ return {
1484
+ version: 2,
1485
+ runId: run.id,
1486
+ route: run.route,
1487
+ status: run.status,
1488
+ telemetry,
1489
+ providerEgressReceiptFingerprints: (run.providerEgressReceipts ?? []).map(receiptFingerprint),
1490
+ ...(run.repairCapability ? { repairCapabilityFingerprint: receiptFingerprint(run.repairCapability) } : {}),
1491
+ };
1492
+ }
1493
+ function emptyRunTelemetry(total, fallbackReason) {
1494
+ return {
1495
+ version: 1,
1496
+ stageDurationsMs: { total },
1497
+ providerRoundTrips: 0,
1498
+ toolCalls: 0,
1499
+ sqlExecutions: 0,
1500
+ repairs: 0,
1501
+ egressReceipts: 0,
1502
+ fallbackReason,
1503
+ };
1504
+ }
1505
+ function withTotalDuration(telemetry, total) {
1506
+ return {
1507
+ ...telemetry,
1508
+ stageDurationsMs: { ...telemetry.stageDurationsMs, total },
1509
+ };
1510
+ }
1511
+ function durationBetweenMs(startedAt, completedAt) {
1512
+ const duration = Date.parse(completedAt) - Date.parse(startedAt);
1513
+ return Number.isFinite(duration) && duration >= 0 ? Math.min(86_400_000, duration) : 0;
1514
+ }
1515
+ function receiptFingerprint(value) {
1516
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
1517
+ }
1518
+ function attachDiagnosticReceipt(artifacts, receipt, receiptV2) {
1144
1519
  if (artifacts.length === 0) {
1145
1520
  if (!receipt.failure)
1146
1521
  return artifacts;
@@ -1149,7 +1524,7 @@ function attachDiagnosticReceipt(artifacts, receipt) {
1149
1524
  kind: "answer",
1150
1525
  title: "Agent run diagnostics",
1151
1526
  trustState: "blocked",
1152
- payload: { diagnosticReceipt: receipt },
1527
+ payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}) },
1153
1528
  }];
1154
1529
  }
1155
1530
  const preferredIndex = Math.max(0, artifacts.findIndex((artifact) => artifact.kind === "answer"));
@@ -1164,11 +1539,12 @@ function attachDiagnosticReceipt(artifacts, receipt) {
1164
1539
  payload: {
1165
1540
  ...payload,
1166
1541
  diagnosticReceipt: receipt,
1542
+ ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}),
1167
1543
  },
1168
1544
  };
1169
1545
  });
1170
1546
  }
1171
- function computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion) {
1547
+ function computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion, terminalOutcomeMessage) {
1172
1548
  const fallback = defaultOutcome(route);
1173
1549
  if (isClarify) {
1174
1550
  return {
@@ -1195,10 +1571,21 @@ function computeStepOutcome(route, result, evaluations, request, isClarify, clar
1195
1571
  trustState,
1196
1572
  artifacts,
1197
1573
  stopReason,
1198
- summary: result.summary ?? fallback.summary,
1574
+ summary: status === "blocked"
1575
+ ? terminalOutcomeMessage ?? blockingOutcomeSummary(evaluations, fallback.summary)
1576
+ : result.summary ?? fallback.summary,
1199
1577
  ...(result.answerTier ? { terminalTier: result.answerTier } : {}),
1200
1578
  };
1201
1579
  }
1580
+ function blockingOutcomeSummary(evaluations, fallback) {
1581
+ const messages = evaluations
1582
+ .filter((evaluation) => !evaluation.passed && evaluation.severity === 'blocking')
1583
+ .map((evaluation) => evaluation.message.trim())
1584
+ .filter(Boolean);
1585
+ return messages[0] ?? (fallback.includes('Answered')
1586
+ ? 'The analytical result did not pass its required validation and was not accepted.'
1587
+ : fallback);
1588
+ }
1202
1589
  function consumeRepeatedClarificationSelection(request, routeDecision, result) {
1203
1590
  const selectedEvidenceId = request.selectedEvidenceId;
1204
1591
  if (!selectedEvidenceId || result.status !== 'needs_clarification')
@@ -1282,7 +1669,7 @@ export function createDeterministicAgentRunPlanner() {
1282
1669
  // Honor an explicit escalate target, else the route's default — then clamp for the audience.
1283
1670
  const rawEscalation = requested ?? AGENT_RUN_ESCALATION_MAP[currentStep.route];
1284
1671
  const escalationRoute = rawEscalation
1285
- ? (audience === "stakeholder" && ANALYST_ONLY_ROUTES.has(rawEscalation) ? "research" : rawEscalation)
1672
+ ? (audience === "stakeholder" && ANALYST_ONLY_ROUTES.has(rawEscalation) ? "generated_answer" : rawEscalation)
1286
1673
  : undefined;
1287
1674
  const hint = action?.hint ?? failing.suggestedRepair ?? "Revise and retry.";
1288
1675
  if (action?.kind === "escalate" && escalationRoute) {
@@ -1376,6 +1763,19 @@ function requestedModeToAction(mode) {
1376
1763
  export function selectRoute(request, decision) {
1377
1764
  if (decision.meaningResolutionErrorCode === 'invalid_evidence_reference')
1378
1765
  return 'blocked';
1766
+ if (decision.action === 'block')
1767
+ return 'blocked';
1768
+ const authoritativePlan = decision.resolvedAnalyticalPlan?.mode === 'authoritative'
1769
+ ? decision.resolvedAnalyticalPlan
1770
+ : undefined;
1771
+ // Producer invariant: the immutable plan is the authority. A caller cannot
1772
+ // combine an answer decision with blocked capability and suppress both the
1773
+ // typed clarification and modeling/policy diagnostic.
1774
+ if (authoritativePlan?.capability === 'blocked') {
1775
+ if (decision.requiresClarification === true)
1776
+ return 'clarify';
1777
+ return 'blocked';
1778
+ }
1379
1779
  const explicitMode = request.requestedMode;
1380
1780
  if (explicitMode === 'modeling')
1381
1781
  return 'modeling_draft';
@@ -1384,9 +1784,21 @@ export function selectRoute(request, decision) {
1384
1784
  if (explicitMode && explicitMode !== 'auto' && explicitMode !== 'ask') {
1385
1785
  return selectCascadeRunRoute(request, decision);
1386
1786
  }
1787
+ const plan = decision.resolvedAnalyticalPlan;
1788
+ // Research is an explicit request mode, never a phrase/category/depth
1789
+ // inference. Diagnosis and bounded exploration in ordinary Ask stay on the
1790
+ // review-required generated route over the same frozen plan.
1791
+ if (decision.action === 'investigate') {
1792
+ if (plan?.capability === 'certified_execution')
1793
+ return 'certified_answer';
1794
+ if (plan?.capability === 'semantic_execution')
1795
+ return 'semantic_answer';
1796
+ if (plan?.capability === 'governed_relational' || plan?.capability === 'bounded_exploration')
1797
+ return 'generated_answer';
1798
+ return decision.requiresClarification ? 'clarify' : 'blocked';
1799
+ }
1387
1800
  if (decision.action !== 'answer')
1388
1801
  return selectCascadeRunRoute(request, decision);
1389
- const plan = decision.resolvedAnalyticalPlan;
1390
1802
  if (plan?.mode === 'authoritative' && decision.requiresClarification !== true) {
1391
1803
  if (plan.capability === 'certified_execution')
1392
1804
  return 'certified_answer';
@@ -1395,7 +1807,7 @@ export function selectRoute(request, decision) {
1395
1807
  if (plan.capability === 'governed_relational')
1396
1808
  return 'generated_answer';
1397
1809
  if (plan.capability === 'bounded_exploration')
1398
- return 'research';
1810
+ return 'generated_answer';
1399
1811
  return 'blocked';
1400
1812
  }
1401
1813
  // Retrieval + meaning resolution already established a compatible execution
@@ -1412,8 +1824,9 @@ export function selectRoute(request, decision) {
1412
1824
  }
1413
1825
  function defaultExecutorResult(route, request, decision) {
1414
1826
  const fallback = defaultOutcome(route);
1827
+ const terminalMessage = route === 'blocked' ? decision?.terminalOutcome?.message : undefined;
1415
1828
  return {
1416
- summary: fallback.summary,
1829
+ summary: terminalMessage ?? fallback.summary,
1417
1830
  answer: route === "clarify" ? decision?.clarifyingQuestion : undefined,
1418
1831
  evaluations: defaultEvaluations(route, request, decision),
1419
1832
  artifacts: defaultArtifacts(route, {}, request),