@duckcodeailabs/dql-agent 1.14.3-rc.1 → 1.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-run-engine.d.ts +177 -3
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +2061 -57
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +51 -3
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +544 -9
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +2 -0
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +41 -4
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/ledger-tools.d.ts.map +1 -1
- package/dist/agentic/ledger-tools.js +8 -1
- package/dist/agentic/ledger-tools.js.map +1 -1
- package/dist/agentic/orchestrator-policy.d.ts +3 -1
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
- package/dist/agentic/orchestrator-policy.js +1 -1
- package/dist/agentic/orchestrator-policy.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts +13 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +441 -32
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/analytical-frame.d.ts +19 -0
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +170 -12
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +949 -10
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +499 -56
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +166 -2
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +654 -18
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +27 -0
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +55 -0
- package/dist/answer-shape.js.map +1 -1
- package/dist/ask-observability/instrumentation.d.ts.map +1 -1
- package/dist/ask-observability/instrumentation.js +52 -1
- package/dist/ask-observability/instrumentation.js.map +1 -1
- package/dist/ask-observability/portable.js +562 -0
- package/dist/ask-observability/portable.js.map +1 -1
- package/dist/ask-observability/types.d.ts +26 -3
- package/dist/ask-observability/types.d.ts.map +1 -1
- package/dist/ask-observability/types.js.map +1 -1
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
- package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
- package/dist/conversation/session-store.d.ts +31 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +38 -0
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts +4 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +85 -3
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +2 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -0
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +18 -0
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/generated-analytical-proposal.js +37 -2
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +77 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +32 -2
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +129 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +108 -15
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.js +57 -6
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +14 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +28 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +19 -0
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +113 -6
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/provider-egress.d.ts +4 -1
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +50 -5
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts +2 -2
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +191 -28
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +191 -21
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +79 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/requirement-clauses.d.ts +23 -0
- package/dist/requirement-clauses.d.ts.map +1 -0
- package/dist/requirement-clauses.js +70 -0
- package/dist/requirement-clauses.js.map +1 -0
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +201 -15
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +109 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +753 -76
- package/dist/router.js.map +1 -1
- package/package.json +4 -4
package/dist/agent-run-engine.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1135
|
+
? compareResolvedPlanShadow(taskRouteDecision, route)
|
|
959
1136
|
: undefined;
|
|
960
1137
|
emit({
|
|
961
1138
|
type: "route.decided",
|
|
962
1139
|
message: stepCount === 1
|
|
963
|
-
?
|
|
1140
|
+
? taskRouteDecision.reason
|
|
964
1141
|
: `Routed step ${stepCount} to ${route.replaceAll("_", " ")}.`,
|
|
965
1142
|
route,
|
|
966
1143
|
payload: stepCount === 1
|
|
967
1144
|
? {
|
|
968
|
-
...
|
|
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 =
|
|
1018
|
-
|
|
1019
|
-
|
|
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 &&
|
|
1024
|
-
recordAuthoritativePlanFreezeV1(traceObserver,
|
|
1274
|
+
if (!planWasFrozen && taskRouteDecision.analyticalCascadeDecision?.planFrozen) {
|
|
1275
|
+
recordAuthoritativePlanFreezeV1(traceObserver, taskRouteDecision.analyticalCascadeDecision);
|
|
1025
1276
|
}
|
|
1026
|
-
result = preserveFrozenAnalyticalRoute(route,
|
|
1027
|
-
result = consumeRepeatedClarificationSelection(
|
|
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
|
-
|
|
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 ||
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2837
|
-
if (
|
|
4676
|
+
const failureRecord = failure;
|
|
4677
|
+
if (failureRecord.code !== 'RESULT_CONTRACT_MISMATCH' || failureRecord.phase !== 'result_validation')
|
|
2838
4678
|
continue;
|
|
2839
4679
|
return {
|
|
2840
|
-
safeAction: terminalIncidentSafeAction(
|
|
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
|
|
2989
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
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) {
|