@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
@@ -7,7 +7,8 @@ import { selectCascadeRunRoute } from "./cascade/route-policy.js";
7
7
  import { canUseEngineEscalation, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, recordEngineEscalation, recordLaneRepair, } from "./cascade/budgets.js";
8
8
  import { buildAnalysisQuestionPlan } from "./metadata/analysis-planner.js";
9
9
  import { conversationHistoryFromContext, isLikelyClarificationReply, } from "./conversation/snapshot.js";
10
- import { buildCoverageGap, classifyProviderFailure, } from './analytical-orchestration.js';
10
+ import { buildCoverageGap, classifyProviderFailure, normalizeCanonicalQueryResult, } from './analytical-orchestration.js';
11
+ import { createAskToolKernelV2, createAskV2ExecutionCapabilityV1, isAskV2ExecutionReceiptAuthorizedV1, } from './ask-runtime/ask-agent-runtime-v2.js';
11
12
  import { evaluateAnalyticalRequestPolicy } from './analytical-request-policy.js';
12
13
  import { frozenRequiredOutputBindingProofsForPlan } from './generated-analytical-proposal.js';
13
14
  import { attachAskTraceObserverV1, askTraceObserverForV1, finalizeAgentRunTraceV1, noOpAskTraceObserverV1, recordAuthoritativeRouterDecisionV1, recordAuthoritativePlanFreezeV1, recordEngineTraceEventV1, recordExecutionAttemptSummaryV1, } from './ask-observability/index.js';
@@ -544,12 +545,22 @@ export class AgentRunEngine {
544
545
  */
545
546
  async decideRoute(request) {
546
547
  const requestedAction = requestedModeToAction(request.requestedMode);
548
+ // An explicitly selected authoritative-V2 Research turn still has to
549
+ // enter the V2 router once. The old forced-mode shortcut predates the V2
550
+ // tool kernel and returned the legacy `investigate` decision before the
551
+ // host could attach the immutable retrieval workspace, so the V2 Research
552
+ // planner/handler was never reached. This is deliberately narrow: legacy
553
+ // and shadow Research retain their existing forced-mode behavior, and no
554
+ // browser-provided value can set this host-owned runtime mode.
555
+ const authoritativeV2Research = request.askAgentRuntimeMode === 'authoritative_v2'
556
+ && request.requestedMode === 'research';
547
557
  // `ask` constrains the eventual analytical action to a direct answer, but it
548
558
  // still needs retrieval-first meaning resolution. Treating it like the SQL,
549
559
  // block, or app authoring modes used to bypass the evidence router entirely
550
560
  // on the primary Ask surface.
551
- if (requestedAction && request.requestedMode !== "ask")
561
+ if (requestedAction && request.requestedMode !== "ask" && !authoritativeV2Research) {
552
562
  return buildIntentDecision(request);
563
+ }
553
564
  if (this.router) {
554
565
  try {
555
566
  const routed = await this.router.decide(request);
@@ -604,7 +615,12 @@ export class AgentRunEngine {
604
615
  catch {
605
616
  traceObserver = noOpAskTraceObserverV1;
606
617
  }
607
- request = attachAskTraceObserverV1({ ...request }, traceObserver);
618
+ // The engine-owned ID is available before retrieval/routing. Bind it to
619
+ // this internal request object now so request-scoped host capabilities
620
+ // (for example one cold-literal probe) can never be minted against an
621
+ // anonymous or browser-supplied identity. Public ingress still strips any
622
+ // caller-provided `runId` before the engine chooses this value.
623
+ request = attachAskTraceObserverV1({ ...request, runId }, traceObserver);
608
624
  // Continuity is relationship evidence, not new routing input. Keep only
609
625
  // stable run IDs and one-way fingerprints so a trace can explain why this
610
626
  // turn reused a clarification/result/derived plan without persisting chat
@@ -694,6 +710,23 @@ export class AgentRunEngine {
694
710
  artifacts: [...progress.artifacts],
695
711
  evaluations: [...progress.evaluations],
696
712
  events: [...progress.events],
713
+ ...(progress.analyticalTaskOutcomes
714
+ ? { analyticalTaskOutcomes: progress.analyticalTaskOutcomes.map((outcome) => ({
715
+ ...outcome,
716
+ ...(outcome.dependencyTaskIds ? { dependencyTaskIds: [...outcome.dependencyTaskIds] } : {}),
717
+ })) }
718
+ : {}),
719
+ ...(progress.analyticalTaskOutcomeSummary
720
+ ? {
721
+ analyticalTaskOutcomeSummary: {
722
+ ...progress.analyticalTaskOutcomeSummary,
723
+ successfulTaskIds: [...progress.analyticalTaskOutcomeSummary.successfulTaskIds],
724
+ failedTaskIds: [...progress.analyticalTaskOutcomeSummary.failedTaskIds],
725
+ dependencyBlockedTaskIds: [...progress.analyticalTaskOutcomeSummary.dependencyBlockedTaskIds],
726
+ },
727
+ }
728
+ : {}),
729
+ ...(progress.askAnalystState ? { askAnalystState: progress.askAnalystState } : {}),
697
730
  };
698
731
  checkpointQueue = checkpointQueue.then(async () => {
699
732
  try {
@@ -760,6 +793,23 @@ export class AgentRunEngine {
760
793
  ...(clarificationContinuation ? { clarificationResolved: true } : {}),
761
794
  },
762
795
  });
796
+ // AskAnalystRuntimeV1 emits typed state checkpoints after framing and each
797
+ // bounded tool action. They are local/restart material only and are never
798
+ // exported through the content-free trace projection.
799
+ // Checkpointing adds a callback after the observer was attached above.
800
+ // Preserve the non-enumerable trace observer through this immutable update:
801
+ // otherwise the authoritative runtime can record its router state while
802
+ // the later provider/compiler/execution adapter sees a no-op observer.
803
+ // That made a successfully executed deterministic physical program appear
804
+ // to have no SQL generation, validation, authorization, or execution in
805
+ // the same trace.
806
+ request = attachAskTraceObserverV1({
807
+ ...request,
808
+ askAnalystCheckpoint: (state) => {
809
+ progress.askAnalystState = state;
810
+ persistProgress();
811
+ },
812
+ }, traceObserver);
763
813
  // This check is intentionally before route selection. A restricted direct
764
814
  // disclosure must not be embedded, retrieved, sent to a provider, value
765
815
  // probed, or compiled merely to explain why it cannot be answered.
@@ -871,7 +921,8 @@ export class AgentRunEngine {
871
921
  run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
872
922
  run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
873
923
  run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
874
- run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
924
+ attachAskAnalystRuntimeReceipt(run, request.askAgentRuntimeMode);
925
+ run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4, run.diagnosticReceiptV5, run.diagnosticReceiptV6, run.diagnosticReceiptV7, run.diagnosticReceiptV8);
875
926
  // Observability is deliberately finalized only after the authoritative
876
927
  // receipt exists, and before the ordinary run store persists its compact
877
928
  // reference. A local trace write failure never changes this outcome.
@@ -900,6 +951,31 @@ export class AgentRunEngine {
900
951
  source: "heuristic",
901
952
  }
902
953
  : await awaitWithAbort(this.decideRoute(request), request.signal);
954
+ // V2 owns interpretation and pre-freeze route progression in its bounded
955
+ // tool runtime. Carry only its host-created state to the executor; no
956
+ // public request path can manufacture this handoff. The engine remains
957
+ // the owner of policy, plan freeze, execution and persistence.
958
+ if (routeDecision.askAgentV2Decision) {
959
+ // `ASK_TRACE_OBSERVER_V1` is deliberately non-enumerable. This V2
960
+ // carrier update is the first immutable request replacement after
961
+ // routing, so a plain spread would detach the physical provider
962
+ // preflight from the root trace precisely on authoritative V2 turns.
963
+ // Keep the observer with the server-owned state; no client value can
964
+ // attach it.
965
+ const v2ExecutionCapability = routeDecision.askAgentV2Decision.mode === 'authoritative_v2'
966
+ ? createAskV2ExecutionCapabilityV1({
967
+ id: randomUUID(),
968
+ runId,
969
+ state: routeDecision.askAgentV2Decision.state,
970
+ })
971
+ : undefined;
972
+ request = attachAskTraceObserverV1({
973
+ ...request,
974
+ askAgentRuntimeMode: routeDecision.askAgentV2Decision.mode,
975
+ askAgentV2State: routeDecision.askAgentV2Decision.state,
976
+ ...(v2ExecutionCapability ? { askAgentV2ExecutionCapability: v2ExecutionCapability } : {}),
977
+ }, traceObserver);
978
+ }
903
979
  routeDecision = enforceOrdinaryAnalyticalPlanBoundary(request, routeDecision);
904
980
  traceObserver.finishSpan(classifySpan, { outcome: 'ok', reasonCode: 'route_selected' });
905
981
  // Router/cascade evidence is captured after its authoritative decision
@@ -909,11 +985,18 @@ export class AgentRunEngine {
909
985
  const defaultRoute = answerAnywayRoute(constrainRouteForAudience(selectRoute(request, routeDecision), audience), request, audience, routeDecision);
910
986
  const authoritativeAsk = routeDecision.resolvedAnalyticalPlan?.mode === 'authoritative'
911
987
  && requestedMode !== 'research';
912
- const activePlanner = authoritativeAsk
988
+ // AskAnalystRuntimeV1 owns the single immutable task program. The
989
+ // engine may still use the legacy deterministic/LLM planners for every
990
+ // other surface, but it must never replace a runtime-frozen Ask task
991
+ // with a new generic one-step interpretation.
992
+ const runtimeFrozenPlan = authoritativeAsk
993
+ ? routeDecision.askAnalystDecision?.frozenPlan
994
+ : undefined;
995
+ const activePlanner = authoritativeAsk && !runtimeFrozenPlan
913
996
  ? createDeterministicAgentRunPlanner()
914
997
  : this.planner;
915
998
  const planningSignal = request.runBudget?.hardSignal ?? request.signal;
916
- plan = await awaitWithAbort(Promise.resolve(activePlanner.plan({
999
+ plan = runtimeFrozenPlan ?? await awaitWithAbort(Promise.resolve(activePlanner.plan({
917
1000
  request,
918
1001
  routeDecision,
919
1002
  defaultRoute,
@@ -933,6 +1016,38 @@ export class AgentRunEngine {
933
1016
  ...step,
934
1017
  route: answerAnywayRoute(constrainRouteForAudience(step.route, audience), request, audience, routeDecision),
935
1018
  }));
1019
+ // A multi-task authoritative Ask has one frozen task queue. V2 task
1020
+ // outcome receipts may retain an independent sibling when another task
1021
+ // fails; persisted pre-V2 decisions deliberately retain the historical
1022
+ // all-or-nothing aggregate below. Neither mode may silently omit a
1023
+ // task or substitute a freshly parsed child graph.
1024
+ const authoritativeTaskExecutions = authoritativeAsk
1025
+ ? routeDecision.askAnalystDecision?.taskExecutions ?? []
1026
+ : [];
1027
+ const initialTaskOutcomes = authoritativeAsk
1028
+ ? routeDecision.askAnalystDecision?.taskOutcomes ?? []
1029
+ : [];
1030
+ const initialTaskOutcomeSummary = authoritativeAsk
1031
+ ? routeDecision.askAnalystDecision?.taskOutcomeSummary
1032
+ : undefined;
1033
+ const authoritativePartialOutcomeMode = Boolean(initialTaskOutcomeSummary
1034
+ && initialTaskOutcomeSummary.taskCount > 1);
1035
+ const authoritativeCompoundAsk = authoritativeTaskExecutions.length > 1 || authoritativePartialOutcomeMode;
1036
+ if (initialTaskOutcomes.length > 0)
1037
+ progress.analyticalTaskOutcomes = initialTaskOutcomes;
1038
+ if (initialTaskOutcomeSummary)
1039
+ progress.analyticalTaskOutcomeSummary = initialTaskOutcomeSummary;
1040
+ const authoritativeTaskIds = new Set(authoritativeTaskExecutions.map((task) => task.taskId));
1041
+ const authoritativeQueueIds = queue
1042
+ .map((step) => step.askAnalystTaskId)
1043
+ .filter((taskId) => Boolean(taskId));
1044
+ const authoritativeQueueValid = !authoritativeCompoundAsk || (queue.length === authoritativeTaskExecutions.length
1045
+ && authoritativeQueueIds.length === authoritativeTaskExecutions.length
1046
+ && new Set(authoritativeQueueIds).size === authoritativeQueueIds.length
1047
+ && authoritativeQueueIds.every((taskId) => authoritativeTaskIds.has(taskId)));
1048
+ if (!authoritativeQueueValid) {
1049
+ throw Object.assign(new Error('The authoritative Ask runtime did not supply one frozen execution step for every executable task.'), { code: 'ASK_ANALYST_TASK_PLAN_MISMATCH' });
1050
+ }
936
1051
  const budgets = createCascadeBudgetState(this.budgetModel);
937
1052
  let stepCount = 0;
938
1053
  let finalStep;
@@ -943,11 +1058,73 @@ export class AgentRunEngine {
943
1058
  // non-answer step (e.g. a research/draft step that emits only an artifact)
944
1059
  // must not drop the data answer an earlier step already computed.
945
1060
  let bestAnswerResult;
946
- while (queue.length > 0 && stepCount < this.maxSteps) {
1061
+ /** Dependencies skipped after a parent execution failure. */
1062
+ const runtimeDependencyBlockedTaskIds = new Map();
1063
+ // Compiler receipts describe what can be attempted, never what has
1064
+ // executed. Persist an execution-only aggregate before task 1 starts,
1065
+ // and again after every task reaches a terminal step. That makes an
1066
+ // interrupted local run recoverable without advertising compiled tasks
1067
+ // as completed work.
1068
+ const checkpointAuthoritativeTaskOutcomes = () => {
1069
+ if (!authoritativePartialOutcomeMode)
1070
+ return;
1071
+ const aggregate = aggregateAuthoritativeTaskOutcomes({
1072
+ initialTaskOutcomes,
1073
+ taskExecutions: authoritativeTaskExecutions,
1074
+ steps: executedSteps,
1075
+ dependencyBlockedTaskIds: runtimeDependencyBlockedTaskIds,
1076
+ taskCount: initialTaskOutcomeSummary?.taskCount,
1077
+ finalized: false,
1078
+ });
1079
+ progress.analyticalTaskOutcomes = aggregate.outcomes;
1080
+ progress.analyticalTaskOutcomeSummary = aggregate.summary;
1081
+ progress.steps = [...executedSteps];
1082
+ persistProgress();
1083
+ };
1084
+ checkpointAuthoritativeTaskOutcomes();
1085
+ // The runtime bounds ordinary Ask to three accepted tasks. Once it
1086
+ // accepts a compound mission, every frozen child must receive one
1087
+ // execution attempt even when the generic engine's normal plan cap is
1088
+ // smaller. Do not turn the cap into a silent partial answer.
1089
+ const executionStepLimit = authoritativeCompoundAsk ? queue.length : this.maxSteps;
1090
+ while (queue.length > 0 && stepCount < executionStepLimit) {
947
1091
  const planned = queue.shift();
948
1092
  stepCount += 1;
949
1093
  const route = planned.route;
950
1094
  const stepId = `${runId}:step:${stepCount}`;
1095
+ // A compound authoritative Ask carries one immutable compiler handoff
1096
+ // per accepted task. Swap it in at the execution boundary rather than
1097
+ // letting task-2 inherit task-1's frame, candidates, or cascade.
1098
+ const taskExecution = authoritativeAsk && planned.askAnalystTaskId
1099
+ ? routeDecision.askAnalystDecision?.taskExecutions?.find((task) => task.taskId === planned.askAnalystTaskId)
1100
+ : undefined;
1101
+ const taskQuestion = taskExecution
1102
+ ? taskExecution.state.mission.tasks.find((task) => task.id === taskExecution.taskId)?.question
1103
+ ?? planned.goal
1104
+ : undefined;
1105
+ const taskRequest = taskExecution && taskQuestion
1106
+ ? attachAskTraceObserverV1({
1107
+ ...request,
1108
+ // Every compiler and executor sees only the frozen child
1109
+ // question. The submitted parent remains captured by the root
1110
+ // trace observer and is restored onto the persisted run below.
1111
+ question: taskQuestion,
1112
+ askAnalystTaskChild: {
1113
+ version: 1,
1114
+ taskId: taskExecution.taskId,
1115
+ question: taskQuestion,
1116
+ instructions: [...planned.successCriteria],
1117
+ },
1118
+ askAnalystState: taskExecution.state,
1119
+ askAnalystProgram: taskExecution.program,
1120
+ askAnalystMeaningResolution: taskExecution.meaningResolution,
1121
+ askAnalystTierReadiness: taskExecution.tierReadiness,
1122
+ hostRequirementSeed: taskExecution.requirementSeed,
1123
+ }, traceObserver)
1124
+ : request;
1125
+ let taskRouteDecision = taskExecution
1126
+ ? taskScopedRouteDecision(routeDecision, taskExecution)
1127
+ : routeDecision;
951
1128
  emit({
952
1129
  type: "step.started",
953
1130
  message: `Step ${stepCount}: ${planned.goal}`,
@@ -955,23 +1132,83 @@ export class AgentRunEngine {
955
1132
  payload: { stepId, index: stepCount, goal: planned.goal, successCriteria: planned.successCriteria },
956
1133
  });
957
1134
  const resolvedPlanShadow = stepCount === 1
958
- ? compareResolvedPlanShadow(routeDecision, route)
1135
+ ? compareResolvedPlanShadow(taskRouteDecision, route)
959
1136
  : undefined;
960
1137
  emit({
961
1138
  type: "route.decided",
962
1139
  message: stepCount === 1
963
- ? routeDecision.reason
1140
+ ? taskRouteDecision.reason
964
1141
  : `Routed step ${stepCount} to ${route.replaceAll("_", " ")}.`,
965
1142
  route,
966
1143
  payload: stepCount === 1
967
1144
  ? {
968
- ...routeDecision,
1145
+ ...taskRouteDecision,
969
1146
  ...(resolvedPlanShadow
970
1147
  ? { resolvedPlanShadow }
971
1148
  : {}),
972
1149
  }
973
1150
  : { route, goal: planned.goal },
974
1151
  });
1152
+ // V2 compound Ask treats a task dependency as an execution boundary,
1153
+ // not a reason to reinterpret or replan the child. A dependent child
1154
+ // may use its predecessor only after that predecessor produced an
1155
+ // accepted result. Independent siblings continue to their own frozen
1156
+ // programs after a failure; this branch is deliberately restricted to
1157
+ // the additive V2 receipt so pre-V2 persisted compound runs preserve
1158
+ // their historical all-or-nothing behavior.
1159
+ const dependencyTaskIds = taskExecution
1160
+ ? taskExecution.dependencyTaskIds
1161
+ ?? taskExecution.state.mission.tasks.find((task) => task.id === taskExecution.taskId)?.dependencies
1162
+ ?? []
1163
+ : [];
1164
+ const unmetDependencyIds = authoritativePartialOutcomeMode && taskExecution
1165
+ ? dependencyTaskIds.filter((dependencyTaskId) => !hasAcceptedAuthoritativeTaskResult(executedSteps.find((step) => step.askAnalystTaskId === dependencyTaskId)))
1166
+ : [];
1167
+ if (taskExecution && unmetDependencyIds.length > 0) {
1168
+ runtimeDependencyBlockedTaskIds.set(taskExecution.taskId, unmetDependencyIds);
1169
+ const summary = 'This task was not executed because a required task did not complete successfully.';
1170
+ const dependencyStep = {
1171
+ id: stepId,
1172
+ index: stepCount,
1173
+ route,
1174
+ ...(planned.askAnalystTaskId ? { askAnalystTaskId: planned.askAnalystTaskId } : {}),
1175
+ goal: planned.goal,
1176
+ successCriteria: planned.successCriteria,
1177
+ status: 'blocked',
1178
+ attempts: 0,
1179
+ summary,
1180
+ evaluations: [{
1181
+ id: `task-dependency:${taskExecution.taskId}`,
1182
+ label: 'Task dependency',
1183
+ passed: false,
1184
+ severity: 'blocking',
1185
+ message: summary,
1186
+ evidence: { dependencyTaskIds: unmetDependencyIds },
1187
+ }],
1188
+ artifacts: [],
1189
+ };
1190
+ executedSteps.push(dependencyStep);
1191
+ const dependencyOutcome = {
1192
+ status: 'blocked',
1193
+ trustState: 'blocked',
1194
+ artifacts: [],
1195
+ stopReason: 'blocked',
1196
+ summary,
1197
+ };
1198
+ finalStep = dependencyStep;
1199
+ finalResult = { status: 'blocked', trustState: 'blocked', summary };
1200
+ finalOutcome = dependencyOutcome;
1201
+ checkpointAuthoritativeTaskOutcomes();
1202
+ emit({
1203
+ type: 'step.completed',
1204
+ message: `Step ${stepCount} dependency blocked.`,
1205
+ route,
1206
+ status: 'blocked',
1207
+ trustState: 'blocked',
1208
+ payload: { stepId, status: 'blocked', dependencyTaskIds: unmetDependencyIds },
1209
+ });
1210
+ continue;
1211
+ }
975
1212
  let attempt = 0;
976
1213
  let repairHint;
977
1214
  let priorEvaluations;
@@ -996,9 +1233,9 @@ export class AgentRunEngine {
996
1233
  // preflight before any executor result exists.
997
1234
  result = await this.executeRoute({
998
1235
  runId,
999
- request,
1236
+ request: taskRequest,
1000
1237
  route,
1001
- routeDecision,
1238
+ routeDecision: taskRouteDecision,
1002
1239
  maxRepairAttempts: budgets.limits.lane.execution,
1003
1240
  attempt,
1004
1241
  stepGoal: planned.goal,
@@ -1014,23 +1251,61 @@ export class AgentRunEngine {
1014
1251
  // semantic plan into generated work (or vice versa) after execution
1015
1252
  // has started. Keep this guard in the engine as well as host adapters
1016
1253
  // so an injected/legacy executor cannot redefine durable provenance.
1017
- const planWasFrozen = routeDecision.analyticalCascadeDecision?.planFrozen === true;
1018
- routeDecision = applyExploratoryExecutionFreeze(routeDecision, result.analyticalExecutionFreeze);
1019
- routeDecision = applyExploratoryExecutionFreeze(routeDecision, result.analyticalExecutionRepairFreeze);
1254
+ const planWasFrozen = taskRouteDecision.analyticalCascadeDecision?.planFrozen === true;
1255
+ taskRouteDecision = applyExploratoryExecutionFreeze(taskRouteDecision, result.analyticalExecutionFreeze);
1256
+ taskRouteDecision = applyExploratoryExecutionFreeze(taskRouteDecision, result.analyticalExecutionRepairFreeze);
1257
+ // Legacy/non-authoritative execution has no task-local durable
1258
+ // decision. Promote the validated host authorization receipts back
1259
+ // to the run-level decision so the persisted run and trace retain
1260
+ // the same immutable exploratory handoff that the executor used.
1261
+ // Authoritative compound Ask keeps its outer decision as a turn
1262
+ // summary and records each task-local handoff independently.
1263
+ // A single authoritative task is still the whole Ask answer. Its
1264
+ // host-issued exploratory execution freeze must be promoted to the
1265
+ // outer decision so V3/V6 persistence and the trace retain the same
1266
+ // capability receipt that authorized SQL. Compound Ask keeps each
1267
+ // frozen child isolated under its task execution receipts.
1268
+ if (!taskExecution || (routeDecision.askAnalystDecision?.taskExecutions?.length ?? 0) === 1) {
1269
+ routeDecision = taskRouteDecision;
1270
+ }
1020
1271
  // The router froze the exploratory plan before SQL generation. The
1021
1272
  // host receipt below only authorizes this exact SQL/target against
1022
1273
  // that immutable plan; it never creates a second freeze transition.
1023
- if (!planWasFrozen && routeDecision.analyticalCascadeDecision?.planFrozen) {
1024
- recordAuthoritativePlanFreezeV1(traceObserver, routeDecision.analyticalCascadeDecision);
1274
+ if (!planWasFrozen && taskRouteDecision.analyticalCascadeDecision?.planFrozen) {
1275
+ recordAuthoritativePlanFreezeV1(traceObserver, taskRouteDecision.analyticalCascadeDecision);
1025
1276
  }
1026
- result = preserveFrozenAnalyticalRoute(route, routeDecision, result);
1027
- result = consumeRepeatedClarificationSelection(request, routeDecision, result);
1277
+ result = preserveFrozenAnalyticalRoute(route, taskRouteDecision, result);
1278
+ result = consumeRepeatedClarificationSelection(taskRequest, taskRouteDecision, result);
1028
1279
  if (result.analyticalTurnPlan)
1029
1280
  progress.analyticalTurnPlan = result.analyticalTurnPlan;
1030
1281
  if (result.analyticalTaskOutcomes)
1031
1282
  progress.analyticalTaskOutcomes = result.analyticalTaskOutcomes;
1032
1283
  persistProgress();
1033
- evaluations = this.evaluate({ route, request, routeDecision, result, attempt });
1284
+ // A terminal V2 `finish_answer` is accepted only after the host has
1285
+ // frozen one snapshot-bound plan and recorded an actual execution
1286
+ // result. The generic evaluator predates that runtime and can
1287
+ // otherwise request a legacy replan of the very same certified
1288
+ // artifact. That second invocation is correctly refused by the
1289
+ // V2 kernel as `POST_FREEZE_REPAIR_REQUIRED`, but it also discards
1290
+ // the valid result which already ran. Preserve the successful V2
1291
+ // boundary here; terminal V2 errors still flow through the ordinary
1292
+ // evaluation and blocked-outcome path below.
1293
+ const acceptedV2TerminalState = acceptedAskAgentV2TerminalState(taskRequest, taskRouteDecision, result, runId);
1294
+ // Run the generic gates for a successful V2 terminal too — but with
1295
+ // their REPAIRS disarmed. Replacing the whole array with the single
1296
+ // terminal receipt meant the answer-shape gate never inspected a V2
1297
+ // result: a run whose executed columns did not match the question's
1298
+ // required outputs shipped as "1 check passed". The repair hooks
1299
+ // stay stripped for the reason documented above — the legacy replan
1300
+ // would discard the validated result — so a failed gate REPORTS
1301
+ // (status/summary see it) without ever re-planning the frozen plan.
1302
+ evaluations = acceptedV2TerminalState
1303
+ ? [
1304
+ ...this.evaluate({ route, request: taskRequest, routeDecision: taskRouteDecision, result, attempt })
1305
+ .map(disarmRepairForV2Terminal),
1306
+ acceptedAskAgentV2TerminalEvaluation(acceptedV2TerminalState),
1307
+ ]
1308
+ : this.evaluate({ route, request: taskRequest, routeDecision: taskRouteDecision, result, attempt });
1034
1309
  for (const evaluation of evaluations) {
1035
1310
  emit({
1036
1311
  type: "evaluation.recorded",
@@ -1052,7 +1327,7 @@ export class AgentRunEngine {
1052
1327
  // A frozen analytical plan has one route and no downstream planner,
1053
1328
  // rematch, route escalation, or whole-answer regeneration authority.
1054
1329
  // Typed server-issued repair is a separate derived run.
1055
- if (authoritativeAsk || routeDecision.analyticalCascadeDecision?.planFrozen === true) {
1330
+ if (authoritativeAsk || taskRouteDecision.analyticalCascadeDecision?.planFrozen === true) {
1056
1331
  stepStatus = 'needs_review';
1057
1332
  break;
1058
1333
  }
@@ -1069,7 +1344,7 @@ export class AgentRunEngine {
1069
1344
  artifacts: result.artifacts ?? [],
1070
1345
  };
1071
1346
  const decision = await activePlanner.replan({
1072
- request,
1347
+ request: taskRequest,
1073
1348
  plan,
1074
1349
  currentStep,
1075
1350
  remainingSteps: queue,
@@ -1151,6 +1426,7 @@ export class AgentRunEngine {
1151
1426
  id: stepId,
1152
1427
  index: stepCount,
1153
1428
  route,
1429
+ ...(planned.askAnalystTaskId ? { askAnalystTaskId: planned.askAnalystTaskId } : {}),
1154
1430
  goal: planned.goal,
1155
1431
  successCriteria: planned.successCriteria,
1156
1432
  status: "escalated",
@@ -1159,6 +1435,7 @@ export class AgentRunEngine {
1159
1435
  evaluations,
1160
1436
  artifacts: [],
1161
1437
  });
1438
+ checkpointAuthoritativeTaskOutcomes();
1162
1439
  emit({
1163
1440
  type: "step.completed",
1164
1441
  message: `Step ${stepCount} escalated to ${escalation.route.replaceAll("_", " ")}.`,
@@ -1173,11 +1450,12 @@ export class AgentRunEngine {
1173
1450
  });
1174
1451
  continue;
1175
1452
  }
1176
- const outcome = computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion, routeDecision.terminalOutcome?.message);
1453
+ const outcome = computeStepOutcome(route, result, evaluations, taskRequest, isClarify, clarifyQuestion, taskRouteDecision.terminalOutcome?.message);
1177
1454
  const step = {
1178
1455
  id: stepId,
1179
1456
  index: stepCount,
1180
1457
  route,
1458
+ ...(planned.askAnalystTaskId ? { askAnalystTaskId: planned.askAnalystTaskId } : {}),
1181
1459
  resolvedRoute: result.resolvedRoute,
1182
1460
  goal: planned.goal,
1183
1461
  successCriteria: planned.successCriteria,
@@ -1197,6 +1475,11 @@ export class AgentRunEngine {
1197
1475
  payload: artifact,
1198
1476
  });
1199
1477
  }
1478
+ // Artifact events above are the immutable result proof. Checkpoint the
1479
+ // task only after that proof has joined persisted progress; a restart
1480
+ // between task siblings can then retain a completed independent result
1481
+ // and mark only its dependents as blocked.
1482
+ checkpointAuthoritativeTaskOutcomes();
1200
1483
  emit({
1201
1484
  type: "step.completed",
1202
1485
  message: `Step ${stepCount} ${step.status}.`,
@@ -1210,7 +1493,14 @@ export class AgentRunEngine {
1210
1493
  finalStep = step;
1211
1494
  finalResult = result;
1212
1495
  finalOutcome = outcome;
1213
- break;
1496
+ // Every accepted authoritative child was frozen before the first
1497
+ // execution. A blocked/clarify outcome for task-1 must not prevent
1498
+ // task-2 from receiving its independently scoped attempt and receipt.
1499
+ // Finalization below aggregates these outcomes as all-or-nothing, so
1500
+ // no partial answer can escape.
1501
+ if (!authoritativeCompoundAsk)
1502
+ break;
1503
+ continue;
1214
1504
  }
1215
1505
  finalStep = step;
1216
1506
  finalResult = result;
@@ -1218,14 +1508,36 @@ export class AgentRunEngine {
1218
1508
  if (outcome.status !== "blocked" && typeof result.answer === "string" && result.answer.trim().length > 0) {
1219
1509
  bestAnswerResult = result;
1220
1510
  }
1221
- if (outcome.status === "blocked")
1511
+ if (outcome.status === "blocked" && !authoritativeCompoundAsk)
1222
1512
  break;
1223
- if (outcome.status === "needs_clarification")
1513
+ if (outcome.status === "needs_clarification" && !authoritativeCompoundAsk)
1224
1514
  break;
1225
- if (isTerminalSuccess(route, outcome))
1515
+ // A successful task is terminal only for a single-task Ask. Multi-task
1516
+ // authoritative plans were all frozen before execution and therefore
1517
+ // continue to their own task-local result receipt.
1518
+ const hasMoreAuthoritativeTasks = authoritativeAsk
1519
+ && (routeDecision.askAnalystDecision?.taskExecutions?.length ?? 0) > 1;
1520
+ if (isTerminalSuccess(route, outcome) && !hasMoreAuthoritativeTasks)
1226
1521
  break;
1227
1522
  // Otherwise continue to the next planned step (if any remain).
1228
1523
  }
1524
+ const authoritativeTaskOutcomeAggregate = authoritativePartialOutcomeMode
1525
+ ? aggregateAuthoritativeTaskOutcomes({
1526
+ initialTaskOutcomes,
1527
+ taskExecutions: authoritativeTaskExecutions,
1528
+ steps: executedSteps,
1529
+ dependencyBlockedTaskIds: runtimeDependencyBlockedTaskIds,
1530
+ taskCount: initialTaskOutcomeSummary?.taskCount,
1531
+ finalized: true,
1532
+ })
1533
+ : undefined;
1534
+ const authoritativeCompoundFailure = authoritativeCompoundAsk && !authoritativePartialOutcomeMode
1535
+ ? compoundAskFailureForFrozenTasks({
1536
+ expectedTaskIds: [...authoritativeTaskIds],
1537
+ plan,
1538
+ steps: executedSteps,
1539
+ })
1540
+ : undefined;
1229
1541
  const run = this.finalizeRun({
1230
1542
  runId,
1231
1543
  request,
@@ -1239,6 +1551,8 @@ export class AgentRunEngine {
1239
1551
  finalOutcome,
1240
1552
  clarifyOutcome,
1241
1553
  bestAnswerResult,
1554
+ ...(authoritativeTaskOutcomeAggregate ? { authoritativeTaskOutcomeAggregate } : {}),
1555
+ ...(authoritativeCompoundFailure ? { authoritativeCompoundFailure } : {}),
1242
1556
  budgetUsage: cascadeBudgetTrace(budgets),
1243
1557
  events,
1244
1558
  });
@@ -1259,7 +1573,8 @@ export class AgentRunEngine {
1259
1573
  run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
1260
1574
  run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
1261
1575
  run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
1262
- run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
1576
+ attachAskAnalystRuntimeReceipt(run, request.askAgentRuntimeMode);
1577
+ run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4, run.diagnosticReceiptV5, run.diagnosticReceiptV6, run.diagnosticReceiptV7, run.diagnosticReceiptV8);
1263
1578
  finalizeAgentRunTraceV1(traceObserver, run);
1264
1579
  await checkpointQueue;
1265
1580
  await this.store?.save(run);
@@ -1349,6 +1664,7 @@ export class AgentRunEngine {
1349
1664
  run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
1350
1665
  run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
1351
1666
  run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
1667
+ attachAskAnalystRuntimeReceipt(run, request.askAgentRuntimeMode);
1352
1668
  finalizeAgentRunTraceV1(traceObserver, run);
1353
1669
  await checkpointQueue;
1354
1670
  await this.store?.save(run);
@@ -1438,7 +1754,8 @@ export class AgentRunEngine {
1438
1754
  run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
1439
1755
  run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
1440
1756
  run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
1441
- run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
1757
+ attachAskAnalystRuntimeReceipt(run, request.askAgentRuntimeMode);
1758
+ run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4, run.diagnosticReceiptV5, run.diagnosticReceiptV6, run.diagnosticReceiptV7, run.diagnosticReceiptV8);
1442
1759
  finalizeAgentRunTraceV1(traceObserver, run);
1443
1760
  await checkpointQueue;
1444
1761
  await this.store?.save(run);
@@ -1450,6 +1767,113 @@ export class AgentRunEngine {
1450
1767
  const repairAttempts = input.budgetUsage.usage.laneExecutionAttemptsUsed;
1451
1768
  const escalationAttempts = input.budgetUsage.usage.engineEscalationsUsed;
1452
1769
  const completedAt = this.timestamp();
1770
+ if (input.authoritativeTaskOutcomeAggregate) {
1771
+ const aggregate = input.authoritativeTaskOutcomeAggregate;
1772
+ const successfulSteps = input.steps.filter((step) => {
1773
+ const taskId = step.askAnalystTaskId;
1774
+ return taskId
1775
+ ? aggregate.summary.successfulTaskIds.includes(taskId)
1776
+ : false;
1777
+ });
1778
+ const lastSuccessfulStep = [...successfulSteps].reverse()[0];
1779
+ const route = lastSuccessfulStep?.resolvedRoute ?? lastSuccessfulStep?.route ?? 'blocked';
1780
+ const hasSuccessfulTask = aggregate.summary.successfulTaskIds.length > 0;
1781
+ const status = !hasSuccessfulTask
1782
+ ? 'blocked'
1783
+ : aggregate.summary.trustState === 'review_required'
1784
+ ? 'needs_review'
1785
+ : 'completed';
1786
+ const artifacts = input.steps.flatMap((step) => step.artifacts);
1787
+ const evaluations = input.steps.flatMap((step) => step.evaluations);
1788
+ const partialSummary = taskOutcomeAggregateSummaryText(aggregate.summary);
1789
+ return {
1790
+ id: input.runId,
1791
+ question: input.request.question,
1792
+ requestedMode: input.requestedMode,
1793
+ conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
1794
+ route,
1795
+ status,
1796
+ trustState: aggregate.summary.trustState,
1797
+ stopReason: status === 'blocked'
1798
+ ? 'blocked'
1799
+ : aggregate.summary.trustState === 'review_required'
1800
+ ? 'generated_review_required'
1801
+ : 'governed_compound_answer',
1802
+ startedAt: input.startedAt,
1803
+ completedAt,
1804
+ selectedObject: input.request.selectedObject,
1805
+ executionTarget: input.request.executionTarget,
1806
+ routeDecision: input.routeDecision,
1807
+ plan: input.plan,
1808
+ steps: input.steps,
1809
+ summary: partialSummary,
1810
+ answer: partialSummary,
1811
+ answerKind: 'governed',
1812
+ artifacts,
1813
+ evaluations,
1814
+ events: input.events,
1815
+ nextActions: applyAudienceToNextActions(defaultNextActions(route, status), resolveAudience(input.request), status),
1816
+ repairAttempts,
1817
+ escalationAttempts,
1818
+ budgetUsage: input.budgetUsage,
1819
+ analyticalTaskOutcomes: aggregate.outcomes,
1820
+ analyticalTaskOutcomeSummary: aggregate.summary,
1821
+ ...authoringDerivationFromRequest(input.request),
1822
+ };
1823
+ }
1824
+ if (input.authoritativeCompoundFailure) {
1825
+ const failure = input.authoritativeCompoundFailure;
1826
+ const failedSteps = input.steps.filter((step) => step.askAnalystTaskId && failure.failedTaskIds.includes(step.askAnalystTaskId));
1827
+ // The per-task steps remain on the run and trace as evidence, but a
1828
+ // compound Ask never adopts an earlier data artifact/answer after one
1829
+ // accepted frozen task failed. Returning only the failed artifacts
1830
+ // avoids presenting a partial result as the response to the whole ask.
1831
+ const artifacts = failedSteps.flatMap((step) => step.artifacts);
1832
+ const evaluations = [
1833
+ ...input.steps.flatMap((step) => step.evaluations),
1834
+ {
1835
+ id: 'authoritative-compound-all-or-nothing',
1836
+ label: 'Frozen Ask task completion',
1837
+ passed: false,
1838
+ severity: 'blocking',
1839
+ message: failure.message,
1840
+ evidence: {
1841
+ expectedTaskIds: failure.expectedTaskIds,
1842
+ completedTaskIds: failure.completedTaskIds,
1843
+ failedTaskIds: failure.failedTaskIds,
1844
+ missingTaskIds: failure.missingTaskIds,
1845
+ },
1846
+ },
1847
+ ];
1848
+ return {
1849
+ id: input.runId,
1850
+ question: input.request.question,
1851
+ requestedMode: input.requestedMode,
1852
+ conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
1853
+ route: 'blocked',
1854
+ status: 'blocked',
1855
+ trustState: 'blocked',
1856
+ stopReason: 'blocked',
1857
+ startedAt: input.startedAt,
1858
+ completedAt,
1859
+ selectedObject: input.request.selectedObject,
1860
+ executionTarget: input.request.executionTarget,
1861
+ routeDecision: input.routeDecision,
1862
+ plan: input.plan,
1863
+ steps: input.steps,
1864
+ summary: failure.message,
1865
+ answer: failure.message,
1866
+ answerKind: 'governed',
1867
+ artifacts,
1868
+ evaluations,
1869
+ events: input.events,
1870
+ nextActions: applyAudienceToNextActions(defaultNextActions('blocked', 'blocked'), resolveAudience(input.request), 'blocked'),
1871
+ repairAttempts,
1872
+ escalationAttempts,
1873
+ budgetUsage: input.budgetUsage,
1874
+ ...authoringDerivationFromRequest(input.request),
1875
+ };
1876
+ }
1453
1877
  if (!finalStep || !finalResult || !finalOutcome) {
1454
1878
  // No step produced a usable result (e.g. an empty plan). Treat as blocked.
1455
1879
  return {
@@ -1486,6 +1910,30 @@ export class AgentRunEngine {
1486
1910
  };
1487
1911
  }
1488
1912
  const route = finalResult.resolvedRoute ?? finalStep.resolvedRoute ?? finalStep.route;
1913
+ // Provider diagnostics are produced at the physical runner boundary.
1914
+ // Merge only that typed, redacted observation; executor prose never gets
1915
+ // to rewrite routing, trust, or cascade authority.
1916
+ // A V2 provider boundary can be observed by the tool runner after its
1917
+ // legacy-shaped no-answer envelope has already crossed the local executor
1918
+ // adapter. The immutable V2 state is shared with this finalizer and is
1919
+ // the durable source of truth at that point. Project its terminal
1920
+ // provider observation here, at the persisted route-decision boundary,
1921
+ // rather than trying to infer it from user-facing error prose upstream.
1922
+ const providerFailure = finalResult.providerFailure
1923
+ // A frozen child receives the same server-owned V2 state as the root
1924
+ // request. Its scoped compiler decision deliberately replaces the
1925
+ // business-plan fields, so use the request carrier as the first-class
1926
+ // persistence fallback rather than losing a physical provider
1927
+ // observation simply because the child route was rehydrated.
1928
+ ?? providerFailureFromAskAgentV2State(input.request.askAgentV2State ?? input.routeDecision.askAgentV2Decision?.state)
1929
+ // Planner/preflight failures can be terminal before a V2 executor emits
1930
+ // an answer envelope. The typed decision is already the authoritative
1931
+ // boundary in that case; retain it for older receipt readers without
1932
+ // classifying user-facing prose.
1933
+ ?? input.routeDecision.providerFailure;
1934
+ const finalRouteDecision = providerFailure
1935
+ ? { ...input.routeDecision, providerFailure }
1936
+ : input.routeDecision;
1489
1937
  // Aggregate artifacts across every accepted step so a multi-step plan
1490
1938
  // (e.g. research → block draft) surfaces all of its durable work, while the
1491
1939
  // status/trust/answer reflect the final step.
@@ -1512,7 +1960,7 @@ export class AgentRunEngine {
1512
1960
  completedAt,
1513
1961
  selectedObject: input.request.selectedObject,
1514
1962
  executionTarget: input.request.executionTarget,
1515
- routeDecision: input.routeDecision,
1963
+ routeDecision: finalRouteDecision,
1516
1964
  plan: input.plan,
1517
1965
  steps: input.steps,
1518
1966
  summary: finalOutcome.summary,
@@ -1536,6 +1984,8 @@ export class AgentRunEngine {
1536
1984
  ...(finalResult.narrationIntegrityReceipt ? {
1537
1985
  narrationIntegrityReceipt: finalResult.narrationIntegrityReceipt,
1538
1986
  } : {}),
1987
+ ...(finalResult.askAnalystState ? { askAnalystState: finalResult.askAnalystState } : {}),
1988
+ ...(finalResult.businessAnswer ? { businessAnswer: finalResult.businessAnswer } : {}),
1539
1989
  escalationAttempts,
1540
1990
  budgetUsage: input.budgetUsage,
1541
1991
  ...(finalResult.analyticalTurnPlan ? { analyticalTurnPlan: finalResult.analyticalTurnPlan } : {}),
@@ -1591,6 +2041,247 @@ export class AgentRunEngine {
1591
2041
  return this.now().toISOString();
1592
2042
  }
1593
2043
  }
2044
+ function canonicalTaskResultArtifactForStep(step) {
2045
+ if (!step)
2046
+ return undefined;
2047
+ for (const artifact of step.artifacts) {
2048
+ if (artifact.kind !== 'answer' || artifact.trustState === 'blocked')
2049
+ continue;
2050
+ const payload = objectRecordForResultFacts(artifact.payload);
2051
+ const result = objectRecordForResultFacts(payload?.result);
2052
+ const fingerprint = stringForResultFacts(result?.resultFingerprint);
2053
+ if (!result || !fingerprint)
2054
+ continue;
2055
+ const canonical = canonicalResultForFactProjection(result);
2056
+ if (!canonical || canonical.columns.length === 0 || canonical.resultFingerprint !== fingerprint)
2057
+ continue;
2058
+ return { resultFingerprint: fingerprint };
2059
+ }
2060
+ return undefined;
2061
+ }
2062
+ function hasAcceptedAuthoritativeTaskResult(step) {
2063
+ if (step?.status !== 'passed' && step?.status !== 'repaired' && step?.status !== 'needs_review')
2064
+ return false;
2065
+ // A generated/review-required response becomes an accepted independent task
2066
+ // only once the immutable canonical result artifact is present. Narrative
2067
+ // text alone is not evidence that a query executed.
2068
+ return Boolean(canonicalTaskResultArtifactForStep(step));
2069
+ }
2070
+ function aggregateAuthoritativeTaskOutcomes(input) {
2071
+ const outcomeByTaskId = new Map();
2072
+ for (const outcome of input.initialTaskOutcomes) {
2073
+ // Each compiler task is authoritative exactly once. Preserve a planning
2074
+ // gap/dependency receipt while permitting a matching executable task to
2075
+ // replace only its own provisional status after it actually runs.
2076
+ outcomeByTaskId.set(outcome.taskId, {
2077
+ ...outcome,
2078
+ ...(outcome.dependencyTaskIds ? { dependencyTaskIds: [...outcome.dependencyTaskIds] } : {}),
2079
+ });
2080
+ }
2081
+ const orderedTaskIds = [
2082
+ ...input.taskExecutions.map((task) => task.taskId),
2083
+ ...input.initialTaskOutcomes.map((outcome) => outcome.taskId),
2084
+ ].filter((taskId, index, all) => all.indexOf(taskId) === index);
2085
+ for (const taskExecution of input.taskExecutions) {
2086
+ const taskId = taskExecution.taskId;
2087
+ const dependencyTaskIds = input.dependencyBlockedTaskIds.get(taskId);
2088
+ if (dependencyTaskIds?.length) {
2089
+ outcomeByTaskId.set(taskId, {
2090
+ version: 1,
2091
+ taskId,
2092
+ status: 'dependency_blocked',
2093
+ trustState: 'blocked',
2094
+ summary: 'This task was not executed because a required task did not complete successfully.',
2095
+ failure: {
2096
+ version: 1,
2097
+ code: 'DEPENDENCY_BLOCKED',
2098
+ message: 'A prerequisite task did not complete successfully.',
2099
+ phase: 'dependency',
2100
+ },
2101
+ dependencyTaskIds: [...dependencyTaskIds],
2102
+ });
2103
+ continue;
2104
+ }
2105
+ const step = input.steps.find((candidate) => candidate.askAnalystTaskId === taskId);
2106
+ if (!step) {
2107
+ if (!input.finalized)
2108
+ continue;
2109
+ outcomeByTaskId.set(taskId, {
2110
+ version: 1,
2111
+ taskId,
2112
+ status: 'blocked',
2113
+ trustState: 'blocked',
2114
+ summary: 'This task did not receive its required frozen execution attempt.',
2115
+ failure: {
2116
+ version: 1,
2117
+ code: 'TASK_EXECUTION_MISSING',
2118
+ message: 'This task did not receive its required frozen execution attempt.',
2119
+ phase: 'execution',
2120
+ },
2121
+ });
2122
+ continue;
2123
+ }
2124
+ if (hasAcceptedAuthoritativeTaskResult(step)) {
2125
+ const trustState = taskOutcomeTrustForExecutedStep(step, taskExecution);
2126
+ const resultFingerprint = taskResultFingerprintForStep(step);
2127
+ outcomeByTaskId.set(taskId, {
2128
+ version: 1,
2129
+ taskId,
2130
+ status: 'completed',
2131
+ trustState,
2132
+ summary: step.summary,
2133
+ ...(resultFingerprint ? { resultFingerprint } : {}),
2134
+ });
2135
+ continue;
2136
+ }
2137
+ const isClarification = step.status === 'clarify';
2138
+ const acceptedWithoutCanonicalResult = step.status === 'passed'
2139
+ || step.status === 'repaired'
2140
+ || step.status === 'needs_review';
2141
+ const message = acceptedWithoutCanonicalResult
2142
+ ? 'This task did not produce an immutable canonical result artifact.'
2143
+ : step.summary ?? (isClarification
2144
+ ? 'This task requires a business clarification before it can run.'
2145
+ : 'This task did not complete its frozen execution.');
2146
+ outcomeByTaskId.set(taskId, {
2147
+ version: 1,
2148
+ taskId,
2149
+ status: isClarification ? 'gap' : 'blocked',
2150
+ trustState: 'blocked',
2151
+ summary: message,
2152
+ failure: {
2153
+ version: 1,
2154
+ code: acceptedWithoutCanonicalResult
2155
+ ? 'TASK_EXECUTION_RESULT_MISSING'
2156
+ : isClarification ? 'TASK_REQUIRES_CLARIFICATION' : 'TASK_EXECUTION_FAILED',
2157
+ message,
2158
+ phase: 'execution',
2159
+ },
2160
+ });
2161
+ }
2162
+ const outcomes = orderedTaskIds
2163
+ .map((taskId) => outcomeByTaskId.get(taskId))
2164
+ .filter((outcome) => Boolean(outcome));
2165
+ const successfulTaskIds = outcomes
2166
+ .filter((outcome) => outcome.status === 'completed' || outcome.status === 'partial')
2167
+ .map((outcome) => outcome.taskId);
2168
+ const failedTaskIds = outcomes
2169
+ .filter((outcome) => outcome.status !== 'completed' && outcome.status !== 'partial' && outcome.status !== 'dependency_blocked')
2170
+ .map((outcome) => outcome.taskId);
2171
+ const dependencyBlockedTaskIds = outcomes
2172
+ .filter((outcome) => outcome.status === 'dependency_blocked')
2173
+ .map((outcome) => outcome.taskId);
2174
+ const successfulTrustStates = outcomes
2175
+ .filter((outcome) => outcome.status === 'completed' || outcome.status === 'partial')
2176
+ .map((outcome) => outcome.trustState ?? 'blocked');
2177
+ const taskCount = Math.max(input.taskCount ?? 0, outcomes.length);
2178
+ return {
2179
+ outcomes,
2180
+ summary: {
2181
+ version: 1,
2182
+ status: successfulTaskIds.length === 0
2183
+ ? 'blocked'
2184
+ : failedTaskIds.length || dependencyBlockedTaskIds.length || successfulTaskIds.length < taskCount
2185
+ ? 'partial'
2186
+ : 'completed',
2187
+ trustState: leastTrustedExecutedTaskOutcomeState(successfulTrustStates),
2188
+ taskCount,
2189
+ successfulTaskIds,
2190
+ failedTaskIds,
2191
+ dependencyBlockedTaskIds,
2192
+ },
2193
+ };
2194
+ }
2195
+ function taskOutcomeTrustForExecutedStep(step, taskExecution) {
2196
+ const artifactStates = step.artifacts
2197
+ .map((artifact) => normalizeTaskOutcomeTrustState(artifact.trustState))
2198
+ .filter((state) => Boolean(state));
2199
+ const compiledState = taskExecution.compiledTrustState
2200
+ ?? taskOutcomeTrustStateForCompiler(taskExecution.resolvedPlan.compiler);
2201
+ // A review-required compiler is never elevated merely because an adapter
2202
+ // artifact used the older `governed` label.
2203
+ if (step.status === 'needs_review' || compiledState === 'review_required')
2204
+ return 'review_required';
2205
+ return leastTrustedExecutedTaskOutcomeState([...artifactStates, compiledState]);
2206
+ }
2207
+ function normalizeTaskOutcomeTrustState(trustState) {
2208
+ if (trustState === 'grounded')
2209
+ return 'governed';
2210
+ return trustState === 'certified'
2211
+ || trustState === 'governed'
2212
+ || trustState === 'review_required'
2213
+ || trustState === 'blocked'
2214
+ || trustState === 'not_applicable'
2215
+ ? trustState
2216
+ : undefined;
2217
+ }
2218
+ function taskOutcomeTrustStateForCompiler(compiler) {
2219
+ if (compiler === 'certified')
2220
+ return 'certified';
2221
+ if (compiler === 'metricflow' || compiler === 'governed_relational')
2222
+ return 'governed';
2223
+ if (compiler === 'exploratory_sql')
2224
+ return 'review_required';
2225
+ return 'blocked';
2226
+ }
2227
+ function leastTrustedExecutedTaskOutcomeState(states) {
2228
+ if (states.length === 0)
2229
+ return 'blocked';
2230
+ const score = {
2231
+ certified: 4,
2232
+ governed: 3,
2233
+ review_required: 2,
2234
+ not_applicable: 1,
2235
+ blocked: 0,
2236
+ };
2237
+ return states.reduce((least, candidate) => score[candidate] < score[least] ? candidate : least);
2238
+ }
2239
+ function taskResultFingerprintForStep(step) {
2240
+ return canonicalTaskResultArtifactForStep(step)?.resultFingerprint;
2241
+ }
2242
+ function taskOutcomeAggregateSummaryText(summary) {
2243
+ if (summary.status === 'completed') {
2244
+ return `All ${summary.taskCount} independent analytical task${summary.taskCount === 1 ? '' : 's'} completed.`;
2245
+ }
2246
+ if (summary.status === 'partial') {
2247
+ return `${summary.successfulTaskIds.length} of ${summary.taskCount} independent analytical task${summary.taskCount === 1 ? '' : 's'} completed. The remaining task receipts explain what needs attention.`;
2248
+ }
2249
+ return 'No independently executable analytical task completed. Review the task receipts for the recorded gaps or dependency blocks.';
2250
+ }
2251
+ /**
2252
+ * A compound authoritative Ask is accepted only after every task has its own
2253
+ * immutable program. Preserve a receipt for every attempted child, then make
2254
+ * the parent terminal when any child blocked/clarified/escalated or was never
2255
+ * attempted. This is intentionally evaluated after the queue drains so a
2256
+ * first failure cannot hide a later frozen task from the trace.
2257
+ */
2258
+ function compoundAskFailureForFrozenTasks(input) {
2259
+ const expectedTaskIds = [...input.expectedTaskIds];
2260
+ const expected = new Set(expectedTaskIds);
2261
+ const taskSteps = input.steps.filter((step) => step.askAnalystTaskId && expected.has(step.askAnalystTaskId));
2262
+ const completedTaskIds = [...new Set(taskSteps
2263
+ .filter((step) => step.status === 'passed' || step.status === 'repaired' || step.status === 'needs_review')
2264
+ .map((step) => step.askAnalystTaskId))];
2265
+ const failedTaskIds = [...new Set(taskSteps
2266
+ .filter((step) => step.status === 'blocked' || step.status === 'clarify' || step.status === 'escalated')
2267
+ .map((step) => step.askAnalystTaskId))];
2268
+ const attempted = new Set(taskSteps.map((step) => step.askAnalystTaskId));
2269
+ const missingTaskIds = expectedTaskIds.filter((taskId) => !attempted.has(taskId));
2270
+ if (failedTaskIds.length === 0 && missingTaskIds.length === 0 && completedTaskIds.length === expectedTaskIds.length) {
2271
+ return undefined;
2272
+ }
2273
+ const failureParts = [
2274
+ failedTaskIds.length ? `${failedTaskIds.length} frozen task${failedTaskIds.length === 1 ? '' : 's'} failed` : '',
2275
+ missingTaskIds.length ? `${missingTaskIds.length} frozen task${missingTaskIds.length === 1 ? '' : 's'} did not receive an execution attempt` : '',
2276
+ ].filter(Boolean);
2277
+ return {
2278
+ expectedTaskIds,
2279
+ completedTaskIds,
2280
+ failedTaskIds,
2281
+ missingTaskIds,
2282
+ message: `The Ask plan ran every available frozen task, but ${failureParts.join(' and ') || 'the task receipts were incomplete'}. No partial result was accepted.`,
2283
+ };
2284
+ }
1594
2285
  function traceLinkFingerprint(value) {
1595
2286
  return `sha256:${createHash('sha256').update(value).digest('hex')}`;
1596
2287
  }
@@ -1975,6 +2666,26 @@ function clarificationAlreadyAsked(clarifyingQuestion, history) {
1975
2666
  return lastAsk > 0;
1976
2667
  }
1977
2668
  function enforceOrdinaryAnalyticalPlanBoundary(request, decision) {
2669
+ // V2 deliberately has no deterministic business-meaning terminal at this
2670
+ // seam. Its candidate workspace is a bounded agent input, and pre-freeze
2671
+ // ineligible/unavailable/ambiguous outcomes are returned to that same tool
2672
+ // loop. Do not let V1's rescue/reinterpretation policy create a second
2673
+ // authority before the tool runtime can try the next safe tier.
2674
+ if (decision.askAgentV2Decision?.mode === 'authoritative_v2'
2675
+ || request.askAgentRuntimeMode === 'authoritative_v2') {
2676
+ return decision;
2677
+ }
2678
+ // AskAnalystRuntimeV1 has already retrieved, planned, verified and (when
2679
+ // possible) frozen this ordinary Ask turn. The engine is a dispatcher at
2680
+ // this boundary, not a second cascade owner. In particular, do not let the
2681
+ // legacy modelling-gap rescue reinterpret a pre-freeze canonical decision:
2682
+ // that used to turn one immutable cascade into two competing routes.
2683
+ // Post-freeze executor/warehouse safety checks remain below the engine
2684
+ // boundary and are intentionally unchanged.
2685
+ if (decision.askAnalystDecision?.mode === 'authoritative'
2686
+ || request.askAnalystState?.mode === 'authoritative') {
2687
+ return decision;
2688
+ }
1978
2689
  const ordinaryAsk = request.requestedMode === undefined
1979
2690
  || request.requestedMode === 'auto'
1980
2691
  || request.requestedMode === 'ask';
@@ -2398,14 +3109,15 @@ function diagnosticReceiptV3ForRun(run) {
2398
3109
  .map((payload) => payload.providerFailure)
2399
3110
  .find((failure) => Boolean(failure) && typeof failure === 'object' && !Array.isArray(failure));
2400
3111
  const persistedProviderDiagnostic = artifactProviderDiagnostic?.diagnostic;
2401
- const provider = persistedProviderDiagnostic && typeof persistedProviderDiagnostic === 'object'
2402
- ? persistedProviderDiagnostic
2403
- : (() => {
2404
- const failure = run.diagnosticReceipt?.failure;
2405
- return failure && (failure.code === 'AI_PROVIDER_FAILURE' || /provider/i.test(failure.code))
2406
- ? classifyProviderFailure({ message: failure.message, code: failure.code, phase: 'generation' })
2407
- : undefined;
2408
- })();
3112
+ const provider = run.routeDecision?.providerFailure
3113
+ ?? (persistedProviderDiagnostic && typeof persistedProviderDiagnostic === 'object'
3114
+ ? persistedProviderDiagnostic
3115
+ : (() => {
3116
+ const failure = run.diagnosticReceipt?.failure;
3117
+ return failure && (failure.code === 'AI_PROVIDER_FAILURE' || /provider/i.test(failure.code))
3118
+ ? classifyProviderFailure({ message: failure.message, code: failure.code, phase: 'generation' })
3119
+ : undefined;
3120
+ })());
2409
3121
  return {
2410
3122
  version: 3,
2411
3123
  runId: run.id,
@@ -2436,6 +3148,8 @@ function diagnosticReceiptV4ForRun(run) {
2436
3148
  roleCounts.set(evidence.role, (roleCounts.get(evidence.role) ?? 0) + evidence.candidateCount);
2437
3149
  }
2438
3150
  const terminalIncident = terminalIncidentForRun(run, cascade?.stopReason);
3151
+ const runtimeReviewRequired = run.askAnalystState?.resolvedPlan?.reviewRequired === true
3152
+ || run.routeDecision?.askAnalystDecision?.state.resolvedPlan?.reviewRequired === true;
2439
3153
  const summaryInput = {
2440
3154
  version: 1,
2441
3155
  understoodRequest: {
@@ -2464,7 +3178,7 @@ function diagnosticReceiptV4ForRun(run) {
2464
3178
  selectedPlan: {
2465
3179
  tier: cascade.selectedTier,
2466
3180
  planFrozen: cascade.planFrozen,
2467
- reviewRequired: cascade.selectedTier === 'exploratory_sql',
3181
+ reviewRequired: cascade.selectedTier === 'exploratory_sql' || runtimeReviewRequired,
2468
3182
  },
2469
3183
  }
2470
3184
  : {}),
@@ -2487,6 +3201,1045 @@ function diagnosticReceiptV4ForRun(run) {
2487
3201
  finalStopReason: run.stopReason,
2488
3202
  };
2489
3203
  }
3204
+ /**
3205
+ * Attach the V1.15 runtime state after the executor settles. The state was
3206
+ * created before the compiler broker ran; this final projection adds only
3207
+ * outcome counters and never asks a legacy layer to reinterpret the question.
3208
+ */
3209
+ function attachAskAnalystRuntimeReceipt(run, runtimeMode) {
3210
+ if (runtimeMode)
3211
+ run.askAgentRuntimeMode = runtimeMode;
3212
+ const initial = run.askAnalystState ?? run.routeDecision?.askAnalystDecision?.state;
3213
+ if (!initial) {
3214
+ attachAskAgentV2RuntimeReceipt(run);
3215
+ return;
3216
+ }
3217
+ const phase = run.status === 'needs_clarification'
3218
+ ? 'clarify'
3219
+ : run.status === 'blocked' || run.status === 'cancelled'
3220
+ ? 'blocked'
3221
+ : 'executed';
3222
+ const executionAttempts = analyticalExecutionAttemptCount(run);
3223
+ const workspaceTools = runtimeWorkspaceToolsForRun(initial, run, executionAttempts);
3224
+ const state = finalizeAskAnalystState(initial, {
3225
+ phase,
3226
+ workspaceTools,
3227
+ ...(run.routeDecision?.askAnalystDecision?.resolvedPlan
3228
+ ? { resolvedPlan: run.routeDecision.askAnalystDecision.resolvedPlan }
3229
+ : {}),
3230
+ toolCalls: workspaceTools.length,
3231
+ executionAttempts,
3232
+ repairAttempts: run.repairAttempts,
3233
+ });
3234
+ run.askAnalystState = state;
3235
+ // Ordinary Ask intentionally does not send result rows to a narration
3236
+ // provider. Every successful compiler still owes the reader a useful,
3237
+ // fact-bound answer, though. Project the canonical local result into a
3238
+ // bounded set of deterministic facts before accepting the BusinessAnswer;
3239
+ // this is the common path for certified blocks, MetricFlow, and safe
3240
+ // physical execution that did not already produce graph-native facts.
3241
+ attachDeterministicResultFacts(run);
3242
+ // The persisted/runtime envelope is the only reader-facing answer authority
3243
+ // for an authoritative Ask. Never let an executor's arbitrary `answer`
3244
+ // bypass the fact/narrative validation below after the runtime has frozen a
3245
+ // program.
3246
+ run.businessAnswer = businessAnswerForRun(run);
3247
+ run.answer = run.businessAnswer.answer;
3248
+ run.diagnosticReceiptV5 = diagnosticReceiptV5ForRun(run, state, run.businessAnswer);
3249
+ run.diagnosticReceiptV6 = diagnosticReceiptV6ForRun(run, state, run.diagnosticReceiptV5);
3250
+ run.diagnosticReceiptV7 = diagnosticReceiptV7ForRun(run, state, run.diagnosticReceiptV6);
3251
+ }
3252
+ /** V2's compact receipt is additive and deliberately does not alter V1-V7. */
3253
+ function attachAskAgentV2RuntimeReceipt(run) {
3254
+ const state = run.routeDecision?.askAgentV2Decision?.state;
3255
+ if (!state)
3256
+ return;
3257
+ run.askAgentRuntimeMode ??= state.mode;
3258
+ // The V2 tool runtime may already have recorded the exact terminal boundary
3259
+ // (for example provider versus execution failure). Do not overwrite it
3260
+ // with the engine's broad status during persistence.
3261
+ if (!state.terminalOutcome) {
3262
+ state.terminal = run.status === 'needs_clarification'
3263
+ ? 'clarification'
3264
+ : run.status === 'blocked' || run.status === 'cancelled'
3265
+ ? 'error'
3266
+ : 'completed';
3267
+ }
3268
+ // V8 reports only V2 tool/execution evidence. A route step or an inspected
3269
+ // candidate is not a warehouse connection, and a failed validation is not a
3270
+ // result. Deriving these fields from the actual canonical tool receipts
3271
+ // keeps a terminal tool error blocked instead of making it look like a
3272
+ // review-required generated result.
3273
+ const executionTools = new Set([
3274
+ 'run_certified',
3275
+ 'compile_and_run_semantic',
3276
+ 'compile_and_run_dql',
3277
+ 'validate_and_run_sql',
3278
+ ]);
3279
+ const executionObservations = state.observations.filter((observation) => executionTools.has(observation.tool)
3280
+ && (observation.outcome === 'executed' || observation.outcome === 'error')
3281
+ && observation.origin === 'execution');
3282
+ const executionAttempts = executionObservations.length;
3283
+ const hasExecutedResult = executionObservations.some((observation) => observation.outcome === 'executed');
3284
+ // V2 deliberately has no V1 `resolvedAnalyticalPlan`. Once its immutable
3285
+ // tool receipt proves a frozen execution result, project the same bounded
3286
+ // deterministic facts used by the older authoritative runtime. This is
3287
+ // presentation only: it neither reroutes the question nor grants a new
3288
+ // execution capability.
3289
+ if (hasExecutedResult) {
3290
+ attachDeterministicResultFacts(run);
3291
+ run.businessAnswer = businessAnswerForRun(run);
3292
+ run.answer = run.businessAnswer.answer;
3293
+ }
3294
+ // The V2 receipt has no row/prompt payload. It may nevertheless state the
3295
+ // count of accepted fact identities only after an actual result boundary.
3296
+ const businessAnswer = run.businessAnswer ?? businessAnswerForRun(run);
3297
+ run.diagnosticReceiptV8 = createAskToolKernelV2(state).diagnosticReceipt(run.stopReason, {
3298
+ connectionAttempted: executionAttempts > 0,
3299
+ executionAttempts,
3300
+ factCount: hasExecutedResult ? businessAnswer.factIds.length : 0,
3301
+ narration: hasExecutedResult && businessAnswer.mode === 'facts_only'
3302
+ ? 'fact_bound'
3303
+ : run.status === 'needs_clarification'
3304
+ ? 'not_applicable'
3305
+ : 'deterministic_fallback',
3306
+ }, {
3307
+ // These are physical egress receipts owned by the server wrapper. A
3308
+ // provider planning observation alone never increments the user-visible
3309
+ // dispatch count.
3310
+ providerDispatches: run.providerEgressReceipts?.length ?? 0,
3311
+ toolCalls: state.observations.filter((observation) => !observation.executionAuthorized).length,
3312
+ executionAttempts,
3313
+ repairs: state.observations.filter((observation) => observation.executionAuthorized && observation.samePlanRepair).length,
3314
+ });
3315
+ }
3316
+ /** Preserve either persisted state version while adding executor-owned facts. */
3317
+ function finalizeAskAnalystState(initial, input) {
3318
+ return {
3319
+ ...initial,
3320
+ phase: input.phase,
3321
+ workspace: {
3322
+ ...initial.workspace,
3323
+ tools: input.workspaceTools,
3324
+ },
3325
+ ...(input.resolvedPlan ? { resolvedPlan: input.resolvedPlan } : {}),
3326
+ toolCalls: input.toolCalls,
3327
+ executionAttempts: input.executionAttempts,
3328
+ repairAttempts: input.repairAttempts,
3329
+ };
3330
+ }
3331
+ function analyticalExecutionAttemptCount(run) {
3332
+ const executableRoutes = new Set([
3333
+ 'certified_answer', 'semantic_answer', 'generated_answer', 'research',
3334
+ ]);
3335
+ return run.steps
3336
+ .filter((step) => executableRoutes.has(step.resolvedRoute ?? step.route))
3337
+ .reduce((total, step) => total + step.attempts, 0);
3338
+ }
3339
+ /**
3340
+ * Keep the default runtime story short and useful: snapshot, interpretation
3341
+ * when a provider actually ran, compiler, execution, and the one permitted
3342
+ * repair. Detailed nested spans remain in Advanced trace instead of flooding
3343
+ * the first diagnostic view with every retrieval lane.
3344
+ */
3345
+ function runtimeWorkspaceToolsForRun(initial, run, executionAttempts) {
3346
+ const tools = [...initial.workspace.tools];
3347
+ const has = (kind) => tools.some((tool) => tool.kind === kind);
3348
+ const candidateIds = initial.workspace.admittedCandidateIds.slice(0, 32);
3349
+ // Provider meaning receipts are written by AskAnalystRuntimeV1 at the actual
3350
+ // call boundary. Do not synthesize one from aggregate telemetry here.
3351
+ if (executionAttempts > 0 && !has('execute')) {
3352
+ tools.push({
3353
+ version: 1,
3354
+ id: 'tool:execute',
3355
+ kind: 'execute',
3356
+ status: run.status === 'blocked' || run.status === 'cancelled' ? 'failed' : 'completed',
3357
+ candidateIds,
3358
+ reasonCode: run.status === 'blocked' || run.status === 'cancelled' ? 'execution_failed' : 'execution_completed',
3359
+ });
3360
+ }
3361
+ if (run.repairAttempts > 0 && !has('repair')) {
3362
+ tools.push({
3363
+ version: 1,
3364
+ id: 'tool:repair',
3365
+ kind: 'repair',
3366
+ status: run.status === 'blocked' ? 'failed' : 'completed',
3367
+ candidateIds,
3368
+ reasonCode: run.status === 'blocked' ? 'repair_exhausted' : 'repair_completed',
3369
+ });
3370
+ }
3371
+ return tools.slice(0, 12);
3372
+ }
3373
+ const RESULT_FACT_MAX_ROWS = 10;
3374
+ const RESULT_FACT_MAX_COLUMNS = 12;
3375
+ const RESULT_FACT_MAX_VALUE_CHARS = 1_024;
3376
+ const RESULT_FACT_NARRATIVE_ROWS = 5;
3377
+ /**
3378
+ * Turn a successful canonical result into local, result-fingerprint-bound
3379
+ * facts when a compiler did not emit the stricter analytical graph fact set.
3380
+ * This never calls a provider and never trusts an executor's prose. It is
3381
+ * deliberately bounded by rows, columns, and scalar size so a result cannot
3382
+ * turn an Ask receipt into an unbounded secondary data store.
3383
+ */
3384
+ function attachDeterministicResultFacts(run) {
3385
+ if (run.status !== 'completed' && run.status !== 'needs_review')
3386
+ return;
3387
+ // Do not let a prior SQL cell, a draft, or an unrelated answer artifact
3388
+ // become reader-facing fact authority. A fact projection belongs only to the
3389
+ // executed answer artifact from the final authoritative frozen plan.
3390
+ const authoritative = authoritativeExecutedAnswerArtifactsForRun(run);
3391
+ const authoritativeIds = new Set(authoritative.map(({ artifact }) => artifact.id));
3392
+ if (authoritativeIds.size === 0)
3393
+ return;
3394
+ run.artifacts = run.artifacts.map((artifact) => {
3395
+ if (!authoritativeIds.has(artifact.id))
3396
+ return artifact;
3397
+ const payload = objectRecordForResultFacts(artifact.payload);
3398
+ if (!payload || payload.kind === 'no_answer' || hasFactLinkedNarrative(payload))
3399
+ return artifact;
3400
+ const projection = deterministicResultFactProjection({
3401
+ artifactId: artifact.id,
3402
+ trustState: artifact.trustState,
3403
+ question: run.question,
3404
+ result: payload.result,
3405
+ answerTier: typeof payload.answerTier === 'string' ? payload.answerTier : undefined,
3406
+ });
3407
+ if (!projection)
3408
+ return artifact;
3409
+ return {
3410
+ ...artifact,
3411
+ payload: {
3412
+ ...payload,
3413
+ analyticalFacts: projection.factSet,
3414
+ analyticalNarrative: projection.narrative,
3415
+ },
3416
+ };
3417
+ });
3418
+ }
3419
+ /**
3420
+ * Facts and narrative may only come from the final execution artifact selected
3421
+ * by an authoritative frozen Ask plan. `run.artifacts` intentionally retains
3422
+ * earlier durable work for inspection, so scanning it wholesale would let a
3423
+ * stale SQL cell or previous answer supersede the result the engine actually
3424
+ * accepted. Multiple final answer artifacts are acceptable only when they
3425
+ * prove the same canonical result fingerprint; ambiguity fails closed.
3426
+ */
3427
+ function authoritativeExecutedAnswerArtifactsForRun(run) {
3428
+ // Notebook Ask submits ordinary analytical turns as `auto`; the runtime has
3429
+ // already classified and frozen the authoritative Ask plan by this point.
3430
+ // Treating only the legacy explicit `ask` mode as fact eligible discarded
3431
+ // verified result facts after a successful query and produced the generic
3432
+ // "no fact-linked narrative" message. Other modes remain closed here.
3433
+ const runtimeFrozenAuthoritative = run.askAnalystState?.mode === 'authoritative'
3434
+ && run.askAnalystState.resolvedPlan?.planFrozen === true;
3435
+ const decisionFrozenAuthoritative = run.routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative'
3436
+ && run.routeDecision.analyticalCascadeDecision?.planFrozen === true;
3437
+ // V2 freezes its typed plan in the tool kernel rather than in V1's
3438
+ // `resolvedAnalyticalPlan`. It may project local facts only when the
3439
+ // terminal state says `finish_answer` *and* a real execution observation
3440
+ // exists; a provider/general answer cannot acquire governed facts merely by
3441
+ // finishing a turn.
3442
+ const v2State = run.routeDecision?.askAgentV2Decision?.state;
3443
+ const runtimeFrozenAuthoritativeV2 = run.askAgentRuntimeMode === 'authoritative_v2'
3444
+ && v2State?.resolvedPlan?.frozen === true
3445
+ && v2State.terminalOutcome?.kind === 'finish_answer'
3446
+ && v2State.observations.some((observation) => (observation.outcome === 'executed'
3447
+ && observation.origin === 'execution'
3448
+ && (observation.tool === 'run_certified'
3449
+ || observation.tool === 'compile_and_run_semantic'
3450
+ || observation.tool === 'compile_and_run_dql'
3451
+ || observation.tool === 'validate_and_run_sql')));
3452
+ if ((run.requestedMode !== 'ask' && run.requestedMode !== 'auto')
3453
+ || run.status === 'blocked'
3454
+ || run.status === 'cancelled'
3455
+ || (!runtimeFrozenAuthoritative && !decisionFrozenAuthoritative && !runtimeFrozenAuthoritativeV2)) {
3456
+ return [];
3457
+ }
3458
+ const finalStep = [...run.steps].reverse().find((step) => (step.resolvedRoute ?? step.route) === run.route
3459
+ && step.status !== 'blocked'
3460
+ && step.status !== 'clarify');
3461
+ const finalAnswerIds = new Set(finalStep?.artifacts
3462
+ .filter((artifact) => artifact.kind === 'answer' && artifact.trustState !== 'blocked')
3463
+ .map((artifact) => artifact.id));
3464
+ // A V2 host result can reach the engine through its terminal executor
3465
+ // envelope after the step was created. Its aggregate artifacts retain the
3466
+ // frozen result even when the step-local artifact list is empty. This
3467
+ // fallback is deliberately limited to a frozen V2 terminal execution; the
3468
+ // canonical fingerprint check below still rejects ambiguity.
3469
+ const acceptedArtifactIds = runtimeFrozenAuthoritativeV2
3470
+ ? new Set(run.artifacts
3471
+ .filter((artifact) => artifact.kind === 'answer' && artifact.trustState !== 'blocked')
3472
+ .map((artifact) => artifact.id))
3473
+ : finalAnswerIds.size > 0
3474
+ ? finalAnswerIds
3475
+ : undefined;
3476
+ if (!acceptedArtifactIds?.size)
3477
+ return [];
3478
+ const candidates = run.artifacts.flatMap((artifact) => {
3479
+ if (!acceptedArtifactIds.has(artifact.id) || artifact.kind !== 'answer' || artifact.trustState === 'blocked')
3480
+ return [];
3481
+ const payload = objectRecordForResultFacts(artifact.payload);
3482
+ const rawResult = payload && objectRecordForResultFacts(payload.result);
3483
+ const canonical = rawResult ? canonicalResultForFactProjection(rawResult) : undefined;
3484
+ if (!payload || !canonical)
3485
+ return [];
3486
+ return [{ artifact, payload, resultFingerprint: canonical.resultFingerprint }];
3487
+ });
3488
+ const resultFingerprints = new Set(candidates.map((candidate) => candidate.resultFingerprint));
3489
+ return resultFingerprints.size === 1 ? candidates : [];
3490
+ }
3491
+ function deterministicResultFactProjection(input) {
3492
+ const rawResult = objectRecordForResultFacts(input.result);
3493
+ const canonical = rawResult ? canonicalResultForFactProjection(rawResult) : undefined;
3494
+ if (!rawResult || !canonical)
3495
+ return undefined;
3496
+ const rawReceipt = objectRecordForResultFacts(rawResult.executionReceipt);
3497
+ const receiptFingerprint = stringForResultFacts(rawReceipt?.resultFingerprint);
3498
+ if (canonical.columns.length === 0)
3499
+ return undefined;
3500
+ const columns = canonical.columns.slice(0, RESULT_FACT_MAX_COLUMNS);
3501
+ const rows = canonical.rows.slice(0, RESULT_FACT_MAX_ROWS).map((row) => Object.fromEntries(columns.flatMap((column) => {
3502
+ const value = boundedResultFactValue(row[column]);
3503
+ return value === undefined ? [] : [[column, value]];
3504
+ })));
3505
+ const provenance = {
3506
+ artifactId: input.artifactId,
3507
+ trustState: input.trustState,
3508
+ ...(input.answerTier ?? canonical.answerTier ? { answerTier: input.answerTier ?? canonical.answerTier } : {}),
3509
+ ...(receiptFingerprint ? { executionReceiptFingerprint: receiptFingerprint } : {}),
3510
+ };
3511
+ const scopeDetails = {
3512
+ rowCount: canonical.rowCount,
3513
+ returnedRowCount: canonical.rows.length,
3514
+ columns,
3515
+ ...(canonical.truncated ? { truncated: true } : {}),
3516
+ };
3517
+ // The applied window is host-computed at the execution boundary and rides
3518
+ // on the result payload itself — never provider prose. Without it, a
3519
+ // truthful zero-row window answer is indistinguishable from a failure.
3520
+ const rawWindow = objectRecordForResultFacts(rawResult.appliedTimeWindow);
3521
+ const appliedTimeWindow = rawWindow
3522
+ && stringForResultFacts(rawWindow.expression)
3523
+ && stringForResultFacts(rawWindow.startInclusive)
3524
+ && stringForResultFacts(rawWindow.endExclusive)
3525
+ ? {
3526
+ expression: stringForResultFacts(rawWindow.expression),
3527
+ startInclusive: stringForResultFacts(rawWindow.startInclusive),
3528
+ endExclusive: stringForResultFacts(rawWindow.endExclusive),
3529
+ }
3530
+ : undefined;
3531
+ const facts = [
3532
+ {
3533
+ factId: deterministicResultFactId(canonical.resultFingerprint, 'scope', scopeDetails),
3534
+ kind: 'result_scope',
3535
+ resultFingerprint: canonical.resultFingerprint,
3536
+ details: scopeDetails,
3537
+ provenance,
3538
+ },
3539
+ ...(appliedTimeWindow ? [{
3540
+ factId: deterministicResultFactId(canonical.resultFingerprint, 'window', appliedTimeWindow),
3541
+ kind: 'result_window',
3542
+ resultFingerprint: canonical.resultFingerprint,
3543
+ details: appliedTimeWindow,
3544
+ provenance,
3545
+ }] : []),
3546
+ ...rows.map((values, rowIndex) => ({
3547
+ factId: deterministicResultFactId(canonical.resultFingerprint, `row:${rowIndex}`, values),
3548
+ kind: 'result_row',
3549
+ resultFingerprint: canonical.resultFingerprint,
3550
+ rowIndex,
3551
+ values,
3552
+ provenance,
3553
+ })),
3554
+ ];
3555
+ const factSetPayload = {
3556
+ version: 1,
3557
+ resultFingerprint: canonical.resultFingerprint,
3558
+ facts,
3559
+ };
3560
+ const factSet = {
3561
+ ...factSetPayload,
3562
+ factSetId: `result-facts:${deterministicResultHash(factSetPayload).slice(0, 24)}`,
3563
+ };
3564
+ const narrative = deterministicResultNarrative({
3565
+ question: input.question,
3566
+ factSet,
3567
+ rowCount: canonical.rowCount,
3568
+ returnedRowCount: canonical.rows.length,
3569
+ truncated: canonical.truncated === true,
3570
+ columns,
3571
+ });
3572
+ return { factSet, narrative };
3573
+ }
3574
+ function deterministicResultNarrative(input) {
3575
+ const scope = input.factSet.facts[0];
3576
+ const windowFact = input.factSet.facts.find((fact) => fact.kind === 'result_window');
3577
+ const windowText = windowFact
3578
+ ? `${String(windowFact.details?.expression)} (${String(windowFact.details?.startInclusive)} through ${String(windowFact.details?.endExclusive)}, end exclusive)`
3579
+ : undefined;
3580
+ const claims = [{
3581
+ claimId: 'claim:result_scope',
3582
+ factIds: [scope.factId],
3583
+ text: `The query returned ${input.rowCount.toLocaleString()} row${input.rowCount === 1 ? '' : 's'} across ${input.columns.length.toLocaleString()} column${input.columns.length === 1 ? '' : 's'}${input.truncated ? '; the returned rows are truncated.' : '.'}`,
3584
+ }];
3585
+ if (windowFact && windowText && input.returnedRowCount > 0) {
3586
+ claims.push({
3587
+ claimId: 'claim:result_window',
3588
+ factIds: [windowFact.factId],
3589
+ text: `Rows are filtered to ${windowText}.`,
3590
+ });
3591
+ }
3592
+ const rowFacts = input.factSet.facts
3593
+ .filter((fact) => fact.kind === 'result_row' && fact.rowIndex !== undefined && Boolean(fact.values))
3594
+ .slice(0, RESULT_FACT_NARRATIVE_ROWS);
3595
+ const rankedQuestion = /\b(?:top|highest|most|least|lowest)\b/i.test(input.question);
3596
+ for (const fact of rowFacts) {
3597
+ const values = fact.values;
3598
+ const labelColumn = input.columns.find((column) => /(?:customer|account|client|user|name)(?:_|$)/i.test(column) && values[column] != null)
3599
+ ?? input.columns.find((column) => values[column] != null);
3600
+ const label = labelColumn ? deterministicResultDisplayValue(values[labelColumn]) : undefined;
3601
+ const details = input.columns
3602
+ .filter((column) => column !== labelColumn && values[column] !== undefined)
3603
+ .map((column) => `${humanizeResultColumn(column)}: ${deterministicResultDisplayValue(values[column])}`);
3604
+ const text = label
3605
+ ? `${rankedQuestion ? 'Returned result' : 'Result'} ${fact.rowIndex + 1}: ${label}${details.length > 0 ? ` — ${details.join('; ')}` : ''}.`
3606
+ : `Returned result ${fact.rowIndex + 1}${details.length > 0 ? `: ${details.join('; ')}` : '.'}`;
3607
+ claims.push({
3608
+ claimId: `claim:result_row:${fact.rowIndex}`,
3609
+ factIds: [fact.factId],
3610
+ text,
3611
+ });
3612
+ }
3613
+ if (rowFacts.length === 0 && input.returnedRowCount === 0) {
3614
+ // A LIMIT truncates surplus rows; returning zero therefore proves the
3615
+ // window itself matched nothing. Naming the exact dates turns "failure"
3616
+ // into "true and actionable": the reader can see at once whether their
3617
+ // data simply ends before the requested period.
3618
+ claims.push({
3619
+ claimId: 'claim:no_returned_rows',
3620
+ factIds: windowFact ? [scope.factId, windowFact.factId] : [scope.factId],
3621
+ text: windowFact && windowText
3622
+ ? `The query returned no rows for the requested window ${windowText} — the governed source holds no matching rows in that period.`
3623
+ : 'The query completed with zero returned rows.',
3624
+ });
3625
+ }
3626
+ return {
3627
+ version: 1,
3628
+ factSetId: input.factSet.factSetId,
3629
+ text: claims.map((claim) => claim.text).join(' '),
3630
+ claims,
3631
+ };
3632
+ }
3633
+ function hasFactLinkedNarrative(payload) {
3634
+ const factSet = objectRecordForResultFacts(payload.analyticalFacts);
3635
+ const narrative = objectRecordForResultFacts(payload.analyticalNarrative);
3636
+ if (!factSet || !narrative || typeof factSet.factSetId !== 'string' || narrative.factSetId !== factSet.factSetId)
3637
+ return false;
3638
+ // Facts may only narrate the exact canonical result that the artifact
3639
+ // persists. A graph-native fact set produced before a normalization or
3640
+ // execution-receipt change is useful diagnostics, but it is not authority
3641
+ // for the reader-facing answer.
3642
+ const rawResult = objectRecordForResultFacts(payload.result);
3643
+ const canonical = rawResult ? canonicalResultForFactProjection(rawResult) : undefined;
3644
+ if (rawResult && (!canonical || factSet.resultFingerprint !== canonical.resultFingerprint))
3645
+ return false;
3646
+ const factIds = new Set(Array.isArray(factSet.facts)
3647
+ ? factSet.facts.flatMap((fact) => {
3648
+ const record = objectRecordForResultFacts(fact);
3649
+ return typeof record?.factId === 'string' ? [record.factId] : [];
3650
+ })
3651
+ : []);
3652
+ if (factIds.size === 0 || !Array.isArray(narrative.claims))
3653
+ return false;
3654
+ const claims = narrative.claims.flatMap((claim) => {
3655
+ const record = objectRecordForResultFacts(claim);
3656
+ const ids = Array.isArray(record?.factIds)
3657
+ ? record.factIds.filter((id) => typeof id === 'string')
3658
+ : [];
3659
+ return ids.length > 0 ? [ids] : [];
3660
+ });
3661
+ return claims.length > 0 && claims.every((ids) => ids.every((id) => factIds.has(id)));
3662
+ }
3663
+ function canonicalResultForFactProjection(rawResult) {
3664
+ const rawReceipt = objectRecordForResultFacts(rawResult.executionReceipt);
3665
+ const suppliedFingerprint = stringForResultFacts(rawResult.resultFingerprint);
3666
+ const receiptFingerprint = stringForResultFacts(rawReceipt?.resultFingerprint);
3667
+ // The persisted result fingerprint identifies the exact rendered row set.
3668
+ // A nested receipt can identify an earlier graph/adapter boundary instead,
3669
+ // so retain it separately as provenance but never let it replace the reader
3670
+ // result identity. The host has already admitted this internal result at the
3671
+ // execution boundary; this projector never accepts public row input.
3672
+ return normalizeCanonicalQueryResult({
3673
+ columns: rawResult.columns,
3674
+ rows: rawResult.rows,
3675
+ rowCount: rawResult.rowCount,
3676
+ executionTime: rawResult.executionTime,
3677
+ resultFingerprint: suppliedFingerprint ?? receiptFingerprint,
3678
+ executionReceipt: rawResult.executionReceipt,
3679
+ trustState: rawResult.trustState,
3680
+ answerTier: rawResult.answerTier,
3681
+ });
3682
+ }
3683
+ function objectRecordForResultFacts(value) {
3684
+ return value && typeof value === 'object' && !Array.isArray(value)
3685
+ ? value
3686
+ : undefined;
3687
+ }
3688
+ function stringForResultFacts(value) {
3689
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
3690
+ }
3691
+ function boundedResultFactValue(value) {
3692
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') {
3693
+ return typeof value === 'string' && value.length > RESULT_FACT_MAX_VALUE_CHARS ? undefined : value;
3694
+ }
3695
+ if (typeof value === 'number')
3696
+ return Number.isFinite(value) ? value : undefined;
3697
+ if (typeof value === 'bigint')
3698
+ return value.toString();
3699
+ try {
3700
+ const serialized = JSON.stringify(value);
3701
+ return serialized && serialized.length <= RESULT_FACT_MAX_VALUE_CHARS ? serialized : undefined;
3702
+ }
3703
+ catch {
3704
+ return undefined;
3705
+ }
3706
+ }
3707
+ function deterministicResultFactId(resultFingerprint, kind, payload) {
3708
+ return `result-fact:${deterministicResultHash({ resultFingerprint, kind, payload }).slice(0, 24)}`;
3709
+ }
3710
+ function deterministicResultHash(value) {
3711
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
3712
+ }
3713
+ function deterministicResultDisplayValue(value) {
3714
+ if (value === null)
3715
+ return 'null';
3716
+ if (typeof value === 'string')
3717
+ return value;
3718
+ if (typeof value === 'number')
3719
+ return Number.isFinite(value) ? String(value) : 'not-a-number';
3720
+ if (typeof value === 'boolean')
3721
+ return value ? 'true' : 'false';
3722
+ return typeof value === 'undefined' ? 'undefined' : String(value);
3723
+ }
3724
+ function humanizeResultColumn(column) {
3725
+ return column.replace(/[_-]+/g, ' ').replace(/\b\w/g, (character) => character.toUpperCase());
3726
+ }
3727
+ function businessAnswerForRun(run) {
3728
+ // An accepted compound Ask is all-or-nothing. The engine deliberately
3729
+ // continues through later frozen children after an earlier one fails so the
3730
+ // trace has a receipt for each task; it must not then replace the aggregate
3731
+ // terminal message with a generic connection incident or a later child
3732
+ // result. The blocking evaluation is server-generated at finalization, not
3733
+ // executor prose, so it is a safe deterministic answer authority here.
3734
+ const compoundFailure = run.evaluations.find((evaluation) => evaluation.id === 'authoritative-compound-all-or-nothing'
3735
+ && evaluation.severity === 'blocking'
3736
+ && evaluation.passed === false
3737
+ && typeof evaluation.message === 'string'
3738
+ && evaluation.message.trim().length > 0);
3739
+ if (compoundFailure) {
3740
+ return {
3741
+ version: 1,
3742
+ mode: 'deterministic_fallback',
3743
+ trustState: 'blocked',
3744
+ factIds: [],
3745
+ answer: compoundFailure.message,
3746
+ limitations: ['No partial result was accepted because one or more frozen Ask tasks did not complete.'],
3747
+ };
3748
+ }
3749
+ const authoritative = authoritativeExecutedAnswerArtifactsForRun(run);
3750
+ const factIds = new Set();
3751
+ const resultFingerprint = authoritative[0]?.resultFingerprint;
3752
+ const factSetIds = new Set();
3753
+ const narratives = [];
3754
+ for (const { payload: record } of authoritative) {
3755
+ const factSet = record.analyticalFacts;
3756
+ if (factSet && typeof factSet === 'object' && !Array.isArray(factSet)) {
3757
+ const factRecord = factSet;
3758
+ if (factRecord.resultFingerprint !== resultFingerprint)
3759
+ continue;
3760
+ if (typeof factRecord.factSetId === 'string')
3761
+ factSetIds.add(factRecord.factSetId);
3762
+ if (Array.isArray(factRecord.facts)) {
3763
+ for (const fact of factRecord.facts) {
3764
+ if (fact && typeof fact === 'object' && typeof fact.factId === 'string') {
3765
+ factIds.add(fact.factId);
3766
+ }
3767
+ }
3768
+ }
3769
+ }
3770
+ const narrative = record.analyticalNarrative;
3771
+ if (narrative && typeof narrative === 'object' && !Array.isArray(narrative)) {
3772
+ const narrativeRecord = narrative;
3773
+ const text = typeof narrativeRecord.text === 'string' ? narrativeRecord.text.trim() : '';
3774
+ const factSetId = typeof narrativeRecord.factSetId === 'string' ? narrativeRecord.factSetId : '';
3775
+ const claims = Array.isArray(narrativeRecord.claims)
3776
+ ? narrativeRecord.claims.flatMap((claim) => {
3777
+ if (!claim || typeof claim !== 'object' || Array.isArray(claim))
3778
+ return [];
3779
+ const claimRecord = claim;
3780
+ const ids = Array.isArray(claimRecord.factIds)
3781
+ ? claimRecord.factIds.filter((id) => typeof id === 'string')
3782
+ : [];
3783
+ return [{ factIds: ids }];
3784
+ })
3785
+ : [];
3786
+ if (text && factSetId)
3787
+ narratives.push({ text, factSetId, claims });
3788
+ }
3789
+ }
3790
+ const acceptedNarrative = narratives.find((narrative) => factSetIds.has(narrative.factSetId)
3791
+ && narrative.claims.length > 0
3792
+ && narrative.claims.every((claim) => claim.factIds.length > 0 && claim.factIds.every((id) => factIds.has(id))));
3793
+ const factsOnly = Boolean(acceptedNarrative);
3794
+ const deterministicAnswer = run.status === 'blocked' || run.status === 'cancelled'
3795
+ ? deterministicTerminalAnswerForRun(run)
3796
+ : run.status === 'needs_clarification'
3797
+ ? 'One business choice is required before DQL can run this question.'
3798
+ : run.analyticalTaskOutcomeSummary?.status === 'partial'
3799
+ ? taskOutcomeAggregateSummaryText(run.analyticalTaskOutcomeSummary)
3800
+ : 'The query completed, but no fact-linked narrative was retained. Open the result to review the validated data.';
3801
+ return {
3802
+ version: 1,
3803
+ mode: factsOnly ? 'facts_only' : 'deterministic_fallback',
3804
+ trustState: run.trustState === 'grounded' ? 'governed' : run.trustState,
3805
+ factIds: [...factIds].sort(),
3806
+ ...(resultFingerprint ? { resultFingerprint } : {}),
3807
+ ...(run.analyticalTaskOutcomeSummary ? { taskOutcomeSummary: run.analyticalTaskOutcomeSummary } : {}),
3808
+ answer: acceptedNarrative?.text ?? deterministicAnswer,
3809
+ limitations: run.status === 'blocked'
3810
+ ? ['No executable result was accepted.']
3811
+ : run.status === 'needs_clarification'
3812
+ ? ['A materially different executable business meaning requires a choice.']
3813
+ : run.analyticalTaskOutcomeSummary?.status === 'partial'
3814
+ ? ['One or more independent analytical tasks did not complete; inspect the retained task receipts.']
3815
+ : factsOnly
3816
+ ? []
3817
+ : ['Narrative is deterministic because no validated analytical fact set was retained.'],
3818
+ };
3819
+ }
3820
+ /**
3821
+ * A blocked Ask still needs a useful, content-safe explanation. Derive this
3822
+ * only from the typed terminal incident already persisted for the run: never
3823
+ * surface a raw connector, provider, SQL, or model error through the answer
3824
+ * field. The trace retains the redacted diagnostic receipt for operators.
3825
+ */
3826
+ /** Words that carry no business meaning when matching a question to a field. */
3827
+ const UNMODELED_STOP_WORDS = new Set([
3828
+ 'what', 'which', 'who', 'whom', 'whose', 'where', 'when', 'why', 'how', 'the', 'a', 'an',
3829
+ 'is', 'are', 'was', 'were', 'be', 'been', 'do', 'does', 'did', 'has', 'have', 'had',
3830
+ 'for', 'from', 'with', 'and', 'or', 'of', 'in', 'on', 'at', 'to', 'by', 'per', 'each',
3831
+ 'me', 'my', 'our', 'his', 'her', 'their', 'them', 'they', 'he', 'she', 'it', 'that', 'this',
3832
+ 'show', 'list', 'give', 'find', 'tell', 'belongs', 'belong', 'get', 'top', 'most', 'many',
3833
+ ]);
3834
+ /** The identifier leaves the snapshot actually admitted, as plain labels. */
3835
+ function modeledFieldLabels(run) {
3836
+ const state = run.askAnalystState ?? run.routeDecision?.askAnalystDecision?.state;
3837
+ const ids = [
3838
+ ...(state?.workspace?.workspaceCandidateIds ?? []),
3839
+ ...(state?.workspace?.admittedCandidateIds ?? []),
3840
+ ];
3841
+ const labels = ids.map((id) => {
3842
+ const leaf = id.split(':').pop() ?? id;
3843
+ return (leaf.split('.').pop() ?? leaf).replace(/_/g, ' ').trim().toLowerCase();
3844
+ }).filter((label) => label.length > 2);
3845
+ return [...new Set(labels)];
3846
+ }
3847
+ /**
3848
+ * Say what is missing, and what exists instead.
3849
+ *
3850
+ * "DQL could not prove one safe analytical path" is true and useless: it does
3851
+ * not say which part of the question could not be served, so the reader cannot
3852
+ * tell a modeling gap from a bug and has nothing to try next. When a term in
3853
+ * the question matches nothing the snapshot admitted — asking for "region"
3854
+ * where only locations are modeled — naming that term and the nearest governed
3855
+ * fields turns a dead end into a next step.
3856
+ */
3857
+ function unmodeledRequestAnswer(run) {
3858
+ const question = typeof run.question === 'string' ? run.question : '';
3859
+ if (!question.trim())
3860
+ return undefined;
3861
+ const labels = modeledFieldLabels(run);
3862
+ if (labels.length === 0)
3863
+ return undefined;
3864
+ const haystack = labels.join(' ');
3865
+ const unmodeled = question
3866
+ .toLowerCase()
3867
+ .replace(/[^a-z0-9\s]+/g, ' ')
3868
+ .split(/\s+/)
3869
+ .filter((word) => word.length > 3 && !UNMODELED_STOP_WORDS.has(word))
3870
+ // A term the admitted snapshot never mentions, in any field, anywhere.
3871
+ .find((word) => !haystack.includes(word) && !haystack.includes(word.replace(/s$/, '')));
3872
+ if (!unmodeled)
3873
+ return undefined;
3874
+ const alternatives = labels.filter((label) => !/^\d/.test(label)).slice(0, 5);
3875
+ return `"${unmodeled}" is not modeled in this project, so no governed query can answer it.`
3876
+ + (alternatives.length
3877
+ ? ` The fields that are modeled here include ${alternatives.join(', ')}.`
3878
+ + ' Ask again using one of those, or tell me which should stand in for'
3879
+ + ` "${unmodeled}".`
3880
+ : '');
3881
+ }
3882
+ function deterministicTerminalAnswerForRun(run) {
3883
+ const incident = terminalIncidentForRun(run, run.routeDecision?.analyticalCascadeDecision?.stopReason);
3884
+ switch (incident?.code) {
3885
+ case 'CONNECTION_NOT_CONFIGURED':
3886
+ return 'No database connection is configured yet. Add an approved connection, then retry this question.';
3887
+ case 'PROVIDER_FAILURE':
3888
+ return 'The AI provider could not complete this Ask step. Check provider readiness, then retry.';
3889
+ case 'COMPILATION_FAILED':
3890
+ return 'DQL selected a governed plan but could not compile it for the current target. Review the semantic target, then retry.';
3891
+ case 'RESULT_CONTRACT_MISMATCH':
3892
+ return 'The query ran, but its result did not match the frozen plan. Review the result contract and trace, then retry.';
3893
+ case 'ANALYTICAL_COVERAGE_GAP':
3894
+ return unmodeledRequestAnswer(run)
3895
+ ?? 'DQL could not prove one safe analytical path from the current metadata snapshot. Review the available modeled fields, then retry.';
3896
+ case 'ANALYTICAL_EXECUTION_FAILED':
3897
+ return 'The selected governed query did not complete on the current connection. Review the connection and trace, then retry.';
3898
+ case 'CANCELLED':
3899
+ return 'This Ask run was cancelled before it completed.';
3900
+ default:
3901
+ return 'No executable data answer was accepted for this Ask run.';
3902
+ }
3903
+ }
3904
+ function diagnosticReceiptV5ForRun(run, state, businessAnswer) {
3905
+ const legacy = run.diagnosticReceiptV4?.summary;
3906
+ const selectedCompiler = state.resolvedPlan?.compiler;
3907
+ const ordinaryRoleInferenceAmbiguity = run.status === 'needs_clarification'
3908
+ && (state.version === 2 || state.version === 3)
3909
+ && state.planningReceipt?.verification?.reasonCode === 'ordinary_role_inference_ambiguous';
3910
+ const whatHappened = run.status === 'blocked'
3911
+ ? 'The Ask runtime did not complete an executable analytical answer.'
3912
+ : run.status === 'needs_clarification'
3913
+ ? ordinaryRoleInferenceAmbiguity
3914
+ ? 'The Ask runtime paused because inferred candidate fields need one business choice.'
3915
+ : 'The Ask runtime paused because validated executable meanings materially differ.'
3916
+ : selectedCompiler
3917
+ ? `The Ask runtime compiled one route-neutral program with the ${selectedCompiler} compiler.`
3918
+ : 'The Ask runtime completed without selecting an analytical compiler.';
3919
+ const why = run.status === 'blocked'
3920
+ ? legacy?.terminalIncident
3921
+ ? `The recorded terminal incident was ${legacy.terminalIncident.code}.`
3922
+ : 'No safe executable compiler plan was accepted from the current evidence snapshot.'
3923
+ : run.status === 'needs_clarification'
3924
+ ? ordinaryRoleInferenceAmbiguity
3925
+ ? 'The snapshot retained multiple safe inferred fields for one requested role, so DQL did not choose or execute a query.'
3926
+ : 'The selected meanings would change the result, so DQL did not guess.'
3927
+ : state.resolvedPlan?.reviewRequired
3928
+ ? 'The selected plan required review before generated SQL execution.'
3929
+ : 'The selected plan passed the current compiler and trust boundary.';
3930
+ const impact = run.status === 'blocked'
3931
+ ? 'No executable data answer was completed for this run.'
3932
+ : run.status === 'needs_clarification'
3933
+ ? 'The query is waiting for one business choice; no query was executed.'
3934
+ : businessAnswer.mode === 'facts_only'
3935
+ ? 'The displayed answer is bound to validated result facts.'
3936
+ : 'The displayed answer uses the deterministic narration fallback.';
3937
+ const summaryInput = {
3938
+ version: 2,
3939
+ runtimeMode: state.mode,
3940
+ whatHappened,
3941
+ why,
3942
+ impact,
3943
+ nextAction: legacy?.safeNextAction ?? 'none',
3944
+ ...(selectedCompiler ? { selectedCompiler } : {}),
3945
+ programTaskCount: state.program.taskIds.length,
3946
+ admittedCandidateCount: state.workspace.admittedCandidateIds.length,
3947
+ toolCallCount: state.toolCalls,
3948
+ executionAttempts: state.executionAttempts,
3949
+ };
3950
+ const provider = providerFailureForRun(run);
3951
+ return {
3952
+ version: 5,
3953
+ runId: run.id,
3954
+ state: diagnosticAskAnalystState(state),
3955
+ summary: {
3956
+ ...summaryInput,
3957
+ summaryFingerprint: receiptFingerprint(summaryInput),
3958
+ },
3959
+ businessAnswer: diagnosticBusinessAnswer(businessAnswer),
3960
+ ...(provider ? { provider } : {}),
3961
+ finalStopReason: run.stopReason,
3962
+ };
3963
+ }
3964
+ /**
3965
+ * V6 is the concise, receipt-only Ask story shown by default. It is built
3966
+ * from already durable state at finalization, so it cannot create a second
3967
+ * routing authority or infer a connection/SQL attempt before one occurred.
3968
+ */
3969
+ function diagnosticReceiptV6ForRun(run, state, receipt) {
3970
+ const tools = state.workspace.tools;
3971
+ const tool = (kind) => tools.find((item) => item.kind === kind);
3972
+ const planner = tool('provider_meaning');
3973
+ const extension = tool('candidate_extension');
3974
+ const cascade = run.routeDecision?.analyticalCascadeDecision;
3975
+ const planFrozen = state.resolvedPlan?.planFrozen === true || cascade?.planFrozen === true;
3976
+ const executionAttempts = state.executionAttempts;
3977
+ const connectionAttempted = planFrozen && (executionAttempts > 0 || terminalConnectionSetupFailureForRun(run));
3978
+ const persistedPlanning = state.version === 2 || state.version === 3 ? state.planningReceipt : undefined;
3979
+ // A failed dispatch is still a planner call. Older persisted V2 state could
3980
+ // be checkpointed before its receipt incremented, while the tool boundary
3981
+ // was already durable. Prefer that boundary to avoid a V6/UI story which
3982
+ // says "planner skipped" even though diagnostics show a provider attempt.
3983
+ const plannerAttempted = planner?.status === 'completed' || planner?.status === 'failed';
3984
+ const plannerCalls = Math.max(persistedPlanning?.plannerCalls ?? 0, plannerAttempted ? Math.max(1, state.planningContinuations) : 0);
3985
+ const revisionCalls = persistedPlanning?.revisionCalls
3986
+ ?? (extension?.status === 'completed' && plannerCalls > 1 ? 1 : 0);
3987
+ const plannerMode = persistedPlanning?.mode ?? (plannerCalls === 0
3988
+ ? (state.workspace.admittedCandidateIds.length ? 'deterministic_binding' : 'exact_fast_path')
3989
+ : revisionCalls > 0 ? 'targeted_revision' : 'initial_planner');
3990
+ // V2 workspace admission captures count-only business role coverage after
3991
+ // exact pins, atomic relationship paths, and the 16-card cap. Prefer it to
3992
+ // raw retrieval metadata: the latter answers "what was found", while this
3993
+ // receipt must explain "what the planner could actually use". V1 remains
3994
+ // readable through the legacy trace-metadata fallback.
3995
+ const recordedRoleCoverage = state.workspace.version === 2
3996
+ ? state.workspace.roleCoverage
3997
+ : undefined;
3998
+ const roleCounts = new Map();
3999
+ for (const candidate of run.routeDecision?.retrievalEvidence?.candidateTraceMetadata ?? []) {
4000
+ roleCounts.set(candidate.role, (roleCounts.get(candidate.role) ?? 0) + 1);
4001
+ }
4002
+ const roleCoverage = recordedRoleCoverage?.length
4003
+ ? recordedRoleCoverage
4004
+ .filter((entry) => Number.isFinite(entry.candidateCount) && entry.candidateCount >= 0)
4005
+ .map((entry) => ({
4006
+ role: entry.role,
4007
+ candidateCount: entry.candidateCount,
4008
+ ...(entry.state === 'alternatives' || entry.state === 'proven' ? { state: entry.state } : {}),
4009
+ }))
4010
+ .sort((left, right) => left.role.localeCompare(right.role))
4011
+ : [...roleCounts.entries()]
4012
+ .map(([role, candidateCount]) => ({ role, candidateCount }))
4013
+ .sort((left, right) => left.role.localeCompare(right.role));
4014
+ const terminalIncident = run.diagnosticReceiptV4?.terminalIncident;
4015
+ const verification = persistedPlanning?.verification ?? (state.phase === 'blocked'
4016
+ ? { version: 1, status: 'invalid', missingRoles: [], candidateIds: [], reasonCode: 'pre_freeze_verification_blocked' }
4017
+ : state.phase === 'clarify'
4018
+ ? { version: 1, status: 'ambiguous', missingRoles: [], candidateIds: [], reasonCode: 'validated_meaning_ambiguity' }
4019
+ : { version: 1, status: 'valid', missingRoles: [], candidateIds: state.program.candidateIds.slice(0, 16), reasonCode: 'immutable_program_verified' });
4020
+ const story = [
4021
+ { stage: 'retrieval', status: tool('retrieve_snapshot')?.status === 'failed' ? 'unavailable' : 'completed', reasonCode: tool('retrieve_snapshot')?.reasonCode ?? 'snapshot_not_recorded' },
4022
+ { stage: 'role_coverage', status: state.workspace.workspaceCandidateIds?.length || state.workspace.admittedCandidateIds.length ? 'completed' : 'unavailable', reasonCode: state.workspace.workspaceCandidateIds?.length ? 'bounded_workspace_qualified' : 'no_qualified_workspace' },
4023
+ {
4024
+ stage: 'planner',
4025
+ status: planner?.status === 'failed' ? 'blocked' : plannerCalls > 0 ? 'completed' : 'skipped',
4026
+ reasonCode: planner?.reasonCode ?? plannerMode,
4027
+ },
4028
+ { stage: 'verification', status: verification.status === 'valid' ? 'completed' : verification.status === 'ambiguous' ? 'blocked' : 'blocked', reasonCode: verification.reasonCode },
4029
+ { stage: 'targeted_recovery', status: extension?.status === 'completed' ? 'completed' : 'skipped', reasonCode: extension?.reasonCode ?? 'not_required' },
4030
+ { stage: 'cascade', status: cascade?.planFrozen ? 'completed' : state.phase === 'blocked' ? 'blocked' : 'unavailable', reasonCode: cascade?.stopReason ?? (state.phase === 'blocked' ? 'pre_freeze_blocked' : 'cascade_not_selected') },
4031
+ { stage: 'freeze', status: planFrozen ? 'completed' : 'skipped', reasonCode: planFrozen ? 'immutable_plan_frozen' : 'no_plan_frozen' },
4032
+ { stage: 'connection', status: connectionAttempted ? 'completed' : 'skipped', reasonCode: connectionAttempted ? 'connection_boundary_attempted_after_freeze' : 'connection_not_attempted' },
4033
+ { stage: 'execution', status: executionAttempts > 0 ? 'completed' : 'skipped', reasonCode: executionAttempts > 0 ? 'executor_attempt_recorded' : 'execution_not_attempted' },
4034
+ { stage: 'facts', status: receipt.businessAnswer?.factIds.length ? 'completed' : 'skipped', reasonCode: receipt.businessAnswer?.factIds.length ? 'fact_bound_business_answer' : 'no_accepted_result_facts' },
4035
+ ];
4036
+ return {
4037
+ ...receipt,
4038
+ version: 6,
4039
+ planning: {
4040
+ version: 1,
4041
+ mode: plannerMode,
4042
+ plannerCalls,
4043
+ revisionCalls,
4044
+ verification,
4045
+ },
4046
+ roleCoverage,
4047
+ cascade: {
4048
+ attempts: (cascade?.attempts ?? []).map((attempt) => ({
4049
+ tier: attempt.tier,
4050
+ outcome: attempt.outcome,
4051
+ planFrozen: attempt.planFrozen,
4052
+ })),
4053
+ ...(cascade?.selectedTier ? { selectedTier: cascade.selectedTier } : {}),
4054
+ ...(cascade?.stopReason ? { stopReason: cascade.stopReason } : {}),
4055
+ planFrozen,
4056
+ },
4057
+ ...(terminalIncident ? {
4058
+ origin: {
4059
+ boundary: terminalIncident.boundary,
4060
+ origin: terminalIncident.origin,
4061
+ impact: terminalIncident.impact,
4062
+ },
4063
+ } : {}),
4064
+ connection: { attempted: connectionAttempted },
4065
+ execution: { attempts: executionAttempts },
4066
+ facts: {
4067
+ factCount: receipt.businessAnswer?.factIds.length ?? 0,
4068
+ ...(receipt.businessAnswer?.resultFingerprint ? { resultFingerprint: receipt.businessAnswer.resultFingerprint } : {}),
4069
+ },
4070
+ safeNextAction: receipt.summary.nextAction,
4071
+ story,
4072
+ };
4073
+ }
4074
+ /**
4075
+ * V7 is the compact first-read inspector. It is a pure projection of the
4076
+ * finalized V6 receipt and frozen runtime state, so browser presentation
4077
+ * cannot become a second planner, router, or trust authority.
4078
+ */
4079
+ function diagnosticReceiptV7ForRun(run, state, receipt) {
4080
+ const requirements = state.frame.requirements;
4081
+ const planning = receipt.planning;
4082
+ const resolvedPlan = state.resolvedPlan ?? run.routeDecision?.askAnalystDecision?.resolvedPlan;
4083
+ const factCount = receipt.facts.factCount;
4084
+ const narration = factCount > 0
4085
+ ? 'fact_bound'
4086
+ : run.status === 'completed' || run.status === 'needs_review'
4087
+ ? 'result_without_facts'
4088
+ : 'not_applicable';
4089
+ return {
4090
+ ...receipt,
4091
+ version: 7,
4092
+ inspector: {
4093
+ understood: {
4094
+ questionKind: state.frame.kind,
4095
+ conversationBinding: state.frame.conversation.binding,
4096
+ measureCount: requirements.measures.length,
4097
+ dimensionCount: requirements.dimensions.length + requirements.entityDisplayTerms.length + (requirements.outputTerms?.length ?? 0),
4098
+ entityRequested: requirements.entityTerms.length > 0,
4099
+ hasBoundFilter: state.program.filters.length > 0,
4100
+ },
4101
+ evidence: {
4102
+ admittedCandidateCount: state.workspace.admittedCandidateIds.length,
4103
+ roleCount: receipt.roleCoverage.length,
4104
+ recoveryAttempted: receipt.story.some((step) => step.stage === 'targeted_recovery' && step.status === 'completed'),
4105
+ },
4106
+ planning: {
4107
+ mode: planning?.mode ?? 'deterministic_binding',
4108
+ plannerCalls: planning?.plannerCalls ?? 0,
4109
+ verification: planning?.verification.status ?? (state.phase === 'clarify' ? 'ambiguous' : state.phase === 'blocked' ? 'invalid' : 'valid'),
4110
+ },
4111
+ route: {
4112
+ ...(receipt.cascade.selectedTier ? { selectedTier: receipt.cascade.selectedTier } : {}),
4113
+ tierAttemptCount: receipt.cascade.attempts.length,
4114
+ planFrozen: receipt.cascade.planFrozen,
4115
+ reviewRequired: resolvedPlan?.reviewRequired === true,
4116
+ },
4117
+ outcome: {
4118
+ connectionAttempted: receipt.connection.attempted,
4119
+ executionAttempts: receipt.execution.attempts,
4120
+ factCount,
4121
+ narration,
4122
+ },
4123
+ },
4124
+ };
4125
+ }
4126
+ function providerFailureForRun(run) {
4127
+ if (run.routeDecision?.providerFailure)
4128
+ return run.routeDecision.providerFailure;
4129
+ if (run.diagnosticReceiptV3?.provider)
4130
+ return run.diagnosticReceiptV3.provider;
4131
+ for (const artifact of run.artifacts) {
4132
+ const payload = artifact.payload;
4133
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
4134
+ continue;
4135
+ const failure = payload.providerFailure;
4136
+ if (!failure || typeof failure !== 'object' || Array.isArray(failure))
4137
+ continue;
4138
+ const diagnostic = failure.diagnostic;
4139
+ if (diagnostic && typeof diagnostic === 'object' && !Array.isArray(diagnostic)) {
4140
+ return diagnostic;
4141
+ }
4142
+ }
4143
+ return undefined;
4144
+ }
4145
+ /**
4146
+ * Project the V2 tool-kernel's terminal provider observation for V1/V3
4147
+ * receipt readers. This runs only at the final persistence boundary, after
4148
+ * the provider/tool runner has settled its shared state. It intentionally
4149
+ * does not classify free-form error text or synthesize a failure for a
4150
+ * non-terminal provider observation.
4151
+ */
4152
+ function providerFailureFromAskAgentV2State(state) {
4153
+ if (state?.terminalOutcome?.kind !== 'provider_failure')
4154
+ return undefined;
4155
+ const provider = [...state.observations]
4156
+ .reverse()
4157
+ .find((observation) => observation.provider)?.provider;
4158
+ if (!provider)
4159
+ return undefined;
4160
+ const phase = provider.phase === 'agent_control'
4161
+ || provider.phase === 'tool_followup'
4162
+ ? 'generation'
4163
+ : provider.phase;
4164
+ const safeAction = provider.safeAction === 'retry_same_provider'
4165
+ || provider.safeAction === 'fix_provider_configuration'
4166
+ || provider.safeAction === 'wait_and_retry'
4167
+ || provider.safeAction === 'inspect_run'
4168
+ || provider.safeAction === 'none'
4169
+ ? provider.safeAction
4170
+ : 'inspect_run';
4171
+ return {
4172
+ version: 1,
4173
+ cause: provider.cause,
4174
+ phase,
4175
+ retryable: provider.retryable,
4176
+ safeAction,
4177
+ };
4178
+ }
4179
+ /** Content-free export boundary for V5 inspector/full-trace receipts. */
4180
+ function diagnosticAskAnalystState(state) {
4181
+ return {
4182
+ version: 1,
4183
+ mode: state.mode,
4184
+ phase: state.phase,
4185
+ questionFingerprint: state.frame.questionFingerprint,
4186
+ kind: state.frame.kind,
4187
+ requirementCounts: {
4188
+ measures: state.frame.requirements.measures.length,
4189
+ dimensions: state.frame.requirements.dimensions.length,
4190
+ entityTerms: state.frame.requirements.entityTerms.length + state.frame.requirements.entityDisplayTerms.length,
4191
+ members: state.frame.requirements.memberTerms.length,
4192
+ filters: state.program.filters?.length ?? 0,
4193
+ },
4194
+ mission: {
4195
+ mode: state.mission.mode,
4196
+ taskCount: state.mission.tasks.length,
4197
+ deferredTaskCount: state.mission.deferredTasks?.length ?? 0,
4198
+ hypothesisCount: state.mission.hypotheses.length,
4199
+ },
4200
+ workspace: {
4201
+ ...(state.workspace.snapshotId ? { snapshotId: state.workspace.snapshotId } : {}),
4202
+ ...(state.workspace.sourceFingerprint ? { sourceFingerprint: state.workspace.sourceFingerprint } : {}),
4203
+ admittedCandidateCount: state.workspace.admittedCandidateIds.length,
4204
+ excludedCandidateCount: state.workspace.excludedCandidates.length,
4205
+ sourceCoverage: state.workspace.sourceCoverage.map((coverage) => ({
4206
+ source: coverage.source,
4207
+ status: coverage.status,
4208
+ candidateCount: coverage.candidateIds.length,
4209
+ })),
4210
+ tools: state.workspace.tools.map((tool) => ({
4211
+ id: tool.id,
4212
+ kind: tool.kind,
4213
+ status: tool.status,
4214
+ reasonCode: tool.reasonCode,
4215
+ })),
4216
+ },
4217
+ program: {
4218
+ id: state.program.id,
4219
+ taskCount: state.program.taskIds.length,
4220
+ candidateCount: state.program.candidateIds.length,
4221
+ requiredRoles: [...state.program.requiredRoles],
4222
+ outputAssertionCount: state.program.outputs.assertions?.length ?? 0,
4223
+ },
4224
+ ...(state.resolvedPlan ? { resolvedPlan: state.resolvedPlan } : {}),
4225
+ counters: {
4226
+ planningContinuations: state.planningContinuations,
4227
+ toolCalls: state.toolCalls,
4228
+ executionAttempts: state.executionAttempts,
4229
+ repairAttempts: state.repairAttempts,
4230
+ },
4231
+ };
4232
+ }
4233
+ function diagnosticBusinessAnswer(answer) {
4234
+ return {
4235
+ version: 1,
4236
+ mode: answer.mode,
4237
+ trustState: answer.trustState,
4238
+ factIds: [...answer.factIds],
4239
+ ...(answer.resultFingerprint ? { resultFingerprint: answer.resultFingerprint } : {}),
4240
+ limitationCount: answer.limitations.length,
4241
+ };
4242
+ }
2490
4243
  const ASK_SUMMARY_EVIDENCE_ROLES = [
2491
4244
  'metric',
2492
4245
  'entity_key',
@@ -2698,8 +4451,17 @@ function isAskResearchChildTier(value) {
2698
4451
  return Boolean(value) && ASK_RESEARCH_CHILD_TIERS.includes(value);
2699
4452
  }
2700
4453
  function terminalIncidentForRun(run, cascadeStopReason) {
4454
+ const runtimeState = run.askAnalystState ?? run.routeDecision?.askAnalystDecision?.state;
4455
+ const planFrozen = runtimeState?.resolvedPlan?.planFrozen === true
4456
+ || run.routeDecision?.analyticalCascadeDecision?.planFrozen === true;
4457
+ const executionRecorded = analyticalExecutionAttemptCount(run) > 0
4458
+ || (run.telemetry?.sqlExecutions ?? 0) > 0;
2701
4459
  const executionSetupFailure = terminalConnectionSetupFailureForRun(run);
2702
- if (executionSetupFailure) {
4460
+ // Connection wording is legal only after an immutable plan crossed its
4461
+ // actual connector boundary. A planner/verification failure is never a
4462
+ // "current connection" incident merely because an older adapter used a
4463
+ // broad blocked status.
4464
+ if (executionSetupFailure && planFrozen) {
2703
4465
  return {
2704
4466
  version: 1,
2705
4467
  code: 'CONNECTION_NOT_CONFIGURED',
@@ -2749,7 +4511,7 @@ function terminalIncidentForRun(run, cascadeStopReason) {
2749
4511
  if (failureCode === 'RUN_CANCELLED' || run.status === 'cancelled') {
2750
4512
  return { version: 1, code: 'CANCELLED', boundary: 'run', origin: 'unknown', impact: 'run_cancelled', safeAction: 'none' };
2751
4513
  }
2752
- if (failureCode === 'CONNECTION_NOT_CONFIGURED') {
4514
+ if (failureCode === 'CONNECTION_NOT_CONFIGURED' && planFrozen) {
2753
4515
  return { version: 1, code: 'CONNECTION_NOT_CONFIGURED', boundary: 'sql.execute', origin: 'governance_gate', impact: 'execution_not_attempted', safeAction: 'configure_connection' };
2754
4516
  }
2755
4517
  // A frozen semantic/analytical plan may fail while the compiler is resolving
@@ -2769,6 +4531,21 @@ function terminalIncidentForRun(run, cascadeStopReason) {
2769
4531
  safeAction: compilationFailure.safeAction,
2770
4532
  };
2771
4533
  }
4534
+ // Result validation is a distinct post-execution boundary. The statement
4535
+ // may have run successfully, but its rows were deliberately rejected
4536
+ // against the immutable plan; do not rewrite that evidence as a connection
4537
+ // or SQL execution failure merely because both happen after plan freeze.
4538
+ const resultValidationFailure = terminalResultValidationFailureForRun(run);
4539
+ if (resultValidationFailure) {
4540
+ return {
4541
+ version: 1,
4542
+ code: 'RESULT_CONTRACT_MISMATCH',
4543
+ boundary: 'result.validate',
4544
+ origin: 'result_validator',
4545
+ impact: 'answer_not_produced',
4546
+ safeAction: resultValidationFailure.safeAction,
4547
+ };
4548
+ }
2772
4549
  const warehouseFailure = terminalWarehouseFailureForRun(run);
2773
4550
  if (warehouseFailure) {
2774
4551
  return {
@@ -2791,9 +4568,28 @@ function terminalIncidentForRun(run, cascadeStopReason) {
2791
4568
  if (cascadeStopReason === 'coverage_gap' || cascadeStopReason === 'ambiguous' || cascadeStopReason === 'denied') {
2792
4569
  return { version: 1, code: 'ANALYTICAL_COVERAGE_GAP', boundary: 'cascade', origin: 'governance_gate', impact: 'answer_not_produced', safeAction: 'inspect_failure' };
2793
4570
  }
2794
- if (run.status === 'blocked') {
4571
+ // Planning/meaning/reference validation has no connection or SQL boundary.
4572
+ // Preserve that truth even when an older adapter gives the terminal run a
4573
+ // broad `blocked` status without a cascade stop reason.
4574
+ if (run.status === 'blocked'
4575
+ && runtimeState?.phase === 'blocked'
4576
+ && !planFrozen
4577
+ && !executionRecorded) {
4578
+ return {
4579
+ version: 1,
4580
+ code: 'ANALYTICAL_COVERAGE_GAP',
4581
+ boundary: 'cascade',
4582
+ origin: 'governance_gate',
4583
+ impact: 'answer_not_produced',
4584
+ safeAction: 'inspect_failure',
4585
+ };
4586
+ }
4587
+ if (run.status === 'blocked' && planFrozen && executionRecorded) {
2795
4588
  return { version: 1, code: 'ANALYTICAL_EXECUTION_FAILED', boundary: 'sql.execute', origin: 'unknown', impact: 'execution_failed', safeAction: 'inspect_failure' };
2796
4589
  }
4590
+ if (run.status === 'blocked') {
4591
+ return { version: 1, code: 'ANALYTICAL_COVERAGE_GAP', boundary: 'cascade', origin: 'governance_gate', impact: 'answer_not_produced', safeAction: 'inspect_failure' };
4592
+ }
2797
4593
  return undefined;
2798
4594
  }
2799
4595
  /**
@@ -2826,6 +4622,50 @@ function terminalConnectionSetupFailureForRun(run) {
2826
4622
  function terminalCompilationFailureForRun(run) {
2827
4623
  if ((run.telemetry?.sqlExecutions ?? 0) > 0)
2828
4624
  return undefined;
4625
+ for (const artifact of run.artifacts) {
4626
+ const payload = artifact.payload;
4627
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
4628
+ continue;
4629
+ const record = payload;
4630
+ const failure = record.analyticalFailure;
4631
+ if (failure && typeof failure === 'object' && !Array.isArray(failure)) {
4632
+ const failureRecord = failure;
4633
+ if (failureRecord.code === 'COMPILATION_FAILED' && failureRecord.phase === 'compilation') {
4634
+ return {
4635
+ safeAction: terminalIncidentSafeAction(failureRecord.safeActions) ?? 'inspect_failure',
4636
+ };
4637
+ }
4638
+ }
4639
+ // Semantic adapters retain their own compiler receipt. Some historical
4640
+ // answer-loop paths lose the outer analytical-failure wrapper while
4641
+ // serializing a failed provider tool result; the typed semantic receipt is
4642
+ // still a pre-SQL compiler fact and must never fall through to the generic
4643
+ // "current connection" incident. It is only considered before a durable
4644
+ // SQL execution counter exists, so a real warehouse failure keeps its
4645
+ // execution classification.
4646
+ const semanticTrace = record.semanticExecutionTrace;
4647
+ if (!semanticTrace || typeof semanticTrace !== 'object' || Array.isArray(semanticTrace))
4648
+ continue;
4649
+ const traceFailure = semanticTrace.failure;
4650
+ if (!traceFailure || typeof traceFailure !== 'object' || Array.isArray(traceFailure))
4651
+ continue;
4652
+ const traceFailureRecord = traceFailure;
4653
+ if (traceFailureRecord.phase === 'compilation'
4654
+ && (traceFailureRecord.code === 'SEMANTIC_COMPILATION_FAILED'
4655
+ || traceFailureRecord.code === 'COMPILATION_FAILED')) {
4656
+ return {
4657
+ safeAction: terminalIncidentSafeAction(traceFailureRecord.safeActions) ?? 'inspect_failure',
4658
+ };
4659
+ }
4660
+ }
4661
+ return undefined;
4662
+ }
4663
+ /**
4664
+ * A validated result-contract rejection is neither a compiler failure nor a
4665
+ * warehouse failure. Read only the producer-owned typed failure so malformed
4666
+ * or legacy error text cannot manufacture this incident.
4667
+ */
4668
+ function terminalResultValidationFailureForRun(run) {
2829
4669
  for (const artifact of run.artifacts) {
2830
4670
  const payload = artifact.payload;
2831
4671
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
@@ -2833,11 +4673,11 @@ function terminalCompilationFailureForRun(run) {
2833
4673
  const failure = payload.analyticalFailure;
2834
4674
  if (!failure || typeof failure !== 'object' || Array.isArray(failure))
2835
4675
  continue;
2836
- const record = failure;
2837
- if (record.code !== 'COMPILATION_FAILED' || record.phase !== 'compilation')
4676
+ const failureRecord = failure;
4677
+ if (failureRecord.code !== 'RESULT_CONTRACT_MISMATCH' || failureRecord.phase !== 'result_validation')
2838
4678
  continue;
2839
4679
  return {
2840
- safeAction: terminalIncidentSafeAction(record.safeActions) ?? 'inspect_failure',
4680
+ safeAction: terminalIncidentSafeAction(failureRecord.safeActions) ?? 'inspect_failure',
2841
4681
  };
2842
4682
  }
2843
4683
  return undefined;
@@ -2943,7 +4783,7 @@ function durationBetweenMs(startedAt, completedAt) {
2943
4783
  function receiptFingerprint(value) {
2944
4784
  return createHash('sha256').update(JSON.stringify(value)).digest('hex');
2945
4785
  }
2946
- function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3, receiptV4) {
4786
+ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3, receiptV4, receiptV5, receiptV6, receiptV7, receiptV8) {
2947
4787
  if (artifacts.length === 0) {
2948
4788
  if (!receipt.failure)
2949
4789
  return artifacts;
@@ -2952,7 +4792,7 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3, recei
2952
4792
  kind: "answer",
2953
4793
  title: "Agent run diagnostics",
2954
4794
  trustState: "blocked",
2955
- payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}), ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}), ...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}) },
4795
+ payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}), ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}), ...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}), ...(receiptV5 ? { diagnosticReceiptV5: receiptV5 } : {}), ...(receiptV6 ? { diagnosticReceiptV6: receiptV6 } : {}), ...(receiptV7 ? { diagnosticReceiptV7: receiptV7 } : {}), ...(receiptV8 ? { diagnosticReceiptV8: receiptV8 } : {}) },
2956
4796
  }];
2957
4797
  }
2958
4798
  const preferredIndex = Math.max(0, artifacts.findIndex((artifact) => artifact.kind === "answer"));
@@ -2970,10 +4810,72 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3, recei
2970
4810
  ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}),
2971
4811
  ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}),
2972
4812
  ...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}),
4813
+ ...(receiptV5 ? { diagnosticReceiptV5: receiptV5 } : {}),
4814
+ ...(receiptV6 ? { diagnosticReceiptV6: receiptV6 } : {}),
4815
+ ...(receiptV7 ? { diagnosticReceiptV7: receiptV7 } : {}),
4816
+ ...(receiptV8 ? { diagnosticReceiptV8: receiptV8 } : {}),
2973
4817
  },
2974
4818
  };
2975
4819
  });
2976
4820
  }
4821
+ function acceptedAskAgentV2TerminalState(request, decision, result, runId) {
4822
+ // A scoped execution can retain an earlier immutable request snapshot while
4823
+ // the provider advances its cloned V2 state. That is why the execution
4824
+ // carrier is an explicit runner return value. It is *not* enough for an
4825
+ // executor to return receipt-shaped JSON: the engine verifies the
4826
+ // process-local server attestation, current run, immutable snapshot closure,
4827
+ // frozen plan identity, and canonical result fingerprint below.
4828
+ const states = [
4829
+ request.askAgentV2State,
4830
+ decision.askAgentV2Decision?.state,
4831
+ ].filter((state) => Boolean(state));
4832
+ if (result.status === 'blocked')
4833
+ return undefined;
4834
+ const receipt = result.askAgentV2ExecutionReceipt;
4835
+ const terminal = result.askAgentV2Outcome;
4836
+ const state = states.find((candidate) => isAskV2ExecutionReceiptAuthorizedV1({
4837
+ receipt,
4838
+ capability: request.askAgentV2ExecutionCapability,
4839
+ state: candidate,
4840
+ result: result.result,
4841
+ runId,
4842
+ }));
4843
+ if (state
4844
+ && receipt
4845
+ && terminal?.kind === 'finish_answer'
4846
+ && terminal.origin === 'execution') {
4847
+ return { tier: receipt.tier, planId: receipt.planId };
4848
+ }
4849
+ return undefined;
4850
+ }
4851
+ /**
4852
+ * Keep a gate's VERDICT while removing its authority to re-plan.
4853
+ *
4854
+ * After an accepted V2 terminal, the frozen plan already executed; the legacy
4855
+ * repair machinery would replan the same artifact, be refused as
4856
+ * POST_FREEZE_REPAIR_REQUIRED, and discard the validated result. The failure
4857
+ * itself must still be visible — that is the entire point of running the
4858
+ * gates — so only the repair hooks are stripped.
4859
+ */
4860
+ function disarmRepairForV2Terminal(evaluation) {
4861
+ if (evaluation.passed)
4862
+ return evaluation;
4863
+ const { suggestedRepair: _repair, repairAction: _action, ...reported } = evaluation;
4864
+ return reported;
4865
+ }
4866
+ function acceptedAskAgentV2TerminalEvaluation(boundary) {
4867
+ return {
4868
+ id: 'ask-v2-terminal-result',
4869
+ label: 'Authoritative V2 result validation',
4870
+ passed: true,
4871
+ severity: 'info',
4872
+ message: 'The snapshot-bound V2 plan executed and its result was validated before the terminal answer was accepted.',
4873
+ evidence: {
4874
+ tier: boundary.tier,
4875
+ planId: boundary.planId,
4876
+ },
4877
+ };
4878
+ }
2977
4879
  function computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion, terminalOutcomeMessage) {
2978
4880
  const fallback = defaultOutcome(route);
2979
4881
  if (isClarify) {
@@ -2985,16 +4887,32 @@ function computeStepOutcome(route, result, evaluations, request, isClarify, clar
2985
4887
  summary: result.summary ?? clarifyQuestion ?? fallback.summary,
2986
4888
  };
2987
4889
  }
2988
- const status = result.status ?? statusFromEvaluations(route, evaluations, fallback.status);
2989
- const trustState = result.trustState ?? trustStateFromEvaluations(route, evaluations, fallback.trustState);
4890
+ const rawStatus = result.status ?? statusFromEvaluations(route, evaluations, fallback.status);
4891
+ const rawTrustState = result.trustState ?? trustStateFromEvaluations(route, evaluations, fallback.trustState);
4892
+ // A unique semantic grouping may be inferred only under the runtime's
4893
+ // explicit review contract. Preserve that contract through the generic
4894
+ // engine adapter instead of allowing a successful MetricFlow executor to
4895
+ // silently re-label it as governed.
4896
+ const runtimeReviewRequired = request.askAnalystState?.resolvedPlan?.reviewRequired === true;
4897
+ const status = runtimeReviewRequired && rawStatus === 'completed'
4898
+ ? 'needs_review'
4899
+ : rawStatus;
4900
+ const trustState = runtimeReviewRequired && status !== 'blocked' && status !== 'needs_clarification'
4901
+ ? 'review_required'
4902
+ : rawTrustState;
2990
4903
  // API-007 / AGT-019: a blocked analytical run may carry an intentionally
2991
4904
  // redacted failure envelope needed for inspection and immutable repair. Keep
2992
4905
  // only artifacts that the executor explicitly marked blocked; never retain a
2993
4906
  // governed/reviewable artifact merely because it happened to accompany a
2994
4907
  // terminal failure.
2995
- const artifacts = status === "blocked"
4908
+ const rawArtifacts = status === "blocked"
2996
4909
  ? (result.artifacts ?? []).filter((artifact) => artifact.trustState === "blocked")
2997
4910
  : result.artifacts ?? defaultArtifacts(route, result, request);
4911
+ const artifacts = runtimeReviewRequired && status !== 'blocked'
4912
+ ? rawArtifacts.map((artifact) => artifact.trustState === 'blocked'
4913
+ ? artifact
4914
+ : { ...artifact, trustState: 'review_required' })
4915
+ : rawArtifacts;
2998
4916
  const stopReason = result.stopReason ?? stopReasonFor(route, status, trustState, artifacts);
2999
4917
  return {
3000
4918
  status,
@@ -3002,19 +4920,45 @@ function computeStepOutcome(route, result, evaluations, request, isClarify, clar
3002
4920
  artifacts,
3003
4921
  stopReason,
3004
4922
  summary: status === "blocked"
3005
- ? terminalOutcomeMessage ?? blockingOutcomeSummary(evaluations, fallback.summary)
4923
+ ? terminalOutcomeMessage ?? blockingOutcomeSummary(evaluations, result, fallback.summary)
3006
4924
  : result.summary ?? fallback.summary,
3007
4925
  ...(result.answerTier ? { terminalTier: result.answerTier } : {}),
3008
4926
  };
3009
4927
  }
3010
- function blockingOutcomeSummary(evaluations, fallback) {
4928
+ /**
4929
+ * The sentence a typed refusal code deserves. These are the honest,
4930
+ * user-actionable readings; the coarse code is still what machines branch on.
4931
+ */
4932
+ function refusalCodeSummary(code) {
4933
+ switch (code) {
4934
+ case 'grounding_gap': return 'DQL could not ground every part of this question in the current metadata snapshot, so no query was accepted.';
4935
+ case 'modeling_gap': return 'Part of this question is not modeled in this project yet, so no governed query can answer it as asked.';
4936
+ case 'ambiguous': return 'One business choice is required before DQL can run this question.';
4937
+ case 'provider_error': return 'The AI provider could not complete this Ask step.';
4938
+ case 'orchestration_budget_exhausted': return 'DQL stopped this run at its own orchestration budget before the question was settled.';
4939
+ case 'policy_blocked': return 'A governance policy blocked this request before execution.';
4940
+ case 'execution_error': return 'The selected governed query did not complete on the current connection.';
4941
+ default: return undefined;
4942
+ }
4943
+ }
4944
+ function blockingOutcomeSummary(evaluations, result, fallback) {
3011
4945
  const messages = evaluations
3012
4946
  .filter((evaluation) => !evaluation.passed && evaluation.severity === 'blocking')
3013
4947
  .map((evaluation) => evaluation.message.trim())
3014
4948
  .filter(Boolean);
3015
- return messages[0] ?? (fallback.includes('Answered')
3016
- ? 'The analytical result did not pass its required validation and was not accepted.'
3017
- : fallback);
4949
+ if (messages[0])
4950
+ return messages[0];
4951
+ // A typed refusal is the truth this run actually recorded. The old fallback
4952
+ // fabricated "did not pass its required validation" whenever no evaluation
4953
+ // was blocking and the route's default summary contained "Answered" — it
4954
+ // asserted a validation that never ran, and masked deadline/budget/gap
4955
+ // terminals behind an invented one.
4956
+ const typed = refusalCodeSummary(result.answerRefusalCode);
4957
+ if (typed)
4958
+ return typed;
4959
+ return fallback.includes('Answered')
4960
+ ? 'The run stopped before an answer was accepted. Open the trace for the exact boundary.'
4961
+ : fallback;
3018
4962
  }
3019
4963
  function consumeRepeatedClarificationSelection(request, routeDecision, result) {
3020
4964
  const selectedEvidenceId = request.selectedEvidenceId;
@@ -3035,6 +4979,48 @@ function consumeRepeatedClarificationSelection(request, routeDecision, result) {
3035
4979
  answer: message,
3036
4980
  };
3037
4981
  }
4982
+ /**
4983
+ * Rehydrate the server-owned compiler decision for one frozen Ask task. The
4984
+ * outer decision remains the durable turn summary, while this scoped view is
4985
+ * the sole authority passed to the executor/evaluator for the current step.
4986
+ */
4987
+ function taskScopedRouteDecision(outer, task) {
4988
+ return {
4989
+ ...task.compilerDecision,
4990
+ // The task compiler consumes its canonical local semantic execution ID
4991
+ // (for example `semantic:account_revenue:revenue`), while the persisted
4992
+ // meaning receipt must retain the exact qualified candidate selected by
4993
+ // the immutable planner (`semantic:metric:account_revenue.revenue`).
4994
+ // Preserve that reader-facing identity without changing the frozen
4995
+ // compiler plan or allowing a task to reinterpret meaning.
4996
+ meaningResolution: {
4997
+ ...task.meaningResolution,
4998
+ ...(outer.meaningResolution?.recommendedExecutionId
4999
+ ? { recommendedExecutionId: outer.meaningResolution.recommendedExecutionId }
5000
+ : {}),
5001
+ },
5002
+ // Preserve the root V2 runtime carrier across the frozen-task scope. It
5003
+ // contains only server-owned snapshot/tool observations and is the source
5004
+ // of a physical provider preflight outcome; dropping it here caused a
5005
+ // child request to persist a generic blocked result instead of its typed
5006
+ // provider diagnostic.
5007
+ ...(outer.askAgentV2Decision ? { askAgentV2Decision: outer.askAgentV2Decision } : {}),
5008
+ ...(outer.providerFailure ? { providerFailure: outer.providerFailure } : {}),
5009
+ // `compileVerifiedAskTasks` accepts a task only after the compiler broker
5010
+ // supplied its complete resolved-plan authority. Do not manufacture a
5011
+ // legacy resolved plan here from a V2 receipt.
5012
+ ...(task.compilerDecision.resolvedAnalyticalPlan
5013
+ ? { resolvedAnalyticalPlan: task.compilerDecision.resolvedAnalyticalPlan }
5014
+ : {}),
5015
+ askAnalystDecision: {
5016
+ version: 1,
5017
+ mode: outer.askAnalystDecision?.mode ?? 'authoritative',
5018
+ state: task.state,
5019
+ resolvedPlan: task.resolvedPlan,
5020
+ ...(outer.askAnalystDecision?.frozenPlan ? { frozenPlan: outer.askAnalystDecision.frozenPlan } : {}),
5021
+ },
5022
+ };
5023
+ }
3038
5024
  function isTerminalSuccess(route, outcome) {
3039
5025
  // A completed certified answer is the terminal success — no further steps add trust.
3040
5026
  // A conversational reply is likewise terminal (there is no data work to chain).
@@ -3195,6 +5181,21 @@ export function selectRoute(request, decision) {
3195
5181
  return 'blocked';
3196
5182
  if (decision.action === 'block')
3197
5183
  return 'blocked';
5184
+ // A unique Tier 1 artifact may be proven complete by the authoritative V2
5185
+ // retrieval workspace before any provider turn. Treat that host-owned
5186
+ // result as a route selection, not as a V1 business interpretation or an
5187
+ // instruction for the model to rediscover the same block. The artifact is
5188
+ // still rechecked and frozen only at the V2 execution-capability boundary.
5189
+ const v2State = decision.askAgentV2Decision?.mode === 'authoritative_v2'
5190
+ ? decision.askAgentV2Decision.state
5191
+ : undefined;
5192
+ const v2ExactCertified = decision.action === 'answer'
5193
+ && decision.requiresClarification !== true
5194
+ && Boolean(v2State?.exactCertifiedCandidateId
5195
+ && v2State.tierStates?.certified?.status === 'complete'
5196
+ && v2State.tierStates.certified.candidateIds.includes(v2State.exactCertifiedCandidateId));
5197
+ if (v2ExactCertified)
5198
+ return 'certified_answer';
3198
5199
  const authoritativePlan = decision.resolvedAnalyticalPlan?.mode === 'authoritative'
3199
5200
  ? decision.resolvedAnalyticalPlan
3200
5201
  : undefined;
@@ -3444,6 +5445,11 @@ function stopReasonFor(route, status, trustState, artifacts) {
3444
5445
  return "cancelled";
3445
5446
  if (status === "blocked" || trustState === "blocked")
3446
5447
  return "blocked";
5448
+ // A semantic result may be executable yet require review because the
5449
+ // runtime used one declared inferred grouping. Other reviewable authoring
5450
+ // routes keep their existing artifact-created stop semantics.
5451
+ if (route === "semantic_answer" && (status === "needs_review" || trustState === "review_required"))
5452
+ return "human_review_required";
3447
5453
  if (route === "conversation")
3448
5454
  return "conversational_reply";
3449
5455
  if (status === "needs_clarification")
@@ -3454,8 +5460,6 @@ function stopReasonFor(route, status, trustState, artifacts) {
3454
5460
  return "governed_semantic_answer";
3455
5461
  if (artifacts.length > 0 && route !== "generated_answer")
3456
5462
  return "artifact_created";
3457
- if (status === "needs_review")
3458
- return "human_review_required";
3459
5463
  return "generated_review_required";
3460
5464
  }
3461
5465
  function defaultNextActions(route, status) {