@duckcodeailabs/dql-agent 1.14.2 → 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 +239 -4
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +2924 -85
- 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-planner.d.ts +6 -0
- package/dist/agent-run-planner.d.ts.map +1 -1
- package/dist/agent-run-planner.js +2 -0
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +591 -11
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +9 -1
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +204 -4
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/assumptions.d.ts +1 -1
- package/dist/agentic/assumptions.d.ts.map +1 -1
- package/dist/agentic/assumptions.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/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +10 -1
- package/dist/agentic/research-agent.js.map +1 -1
- package/dist/agentic/sql-authorization.d.ts +8 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -1
- package/dist/agentic/sql-authorization.js +3 -0
- package/dist/agentic/sql-authorization.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-compatibility.d.ts.map +1 -1
- package/dist/analytical-compatibility.js +8 -1
- package/dist/analytical-compatibility.js.map +1 -1
- package/dist/analytical-failure-repair.d.ts.map +1 -1
- package/dist/analytical-failure-repair.js +6 -2
- package/dist/analytical-failure-repair.js.map +1 -1
- package/dist/analytical-frame.d.ts +42 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +490 -26
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +1259 -9
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +970 -84
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +230 -10
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1182 -85
- 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/index.d.ts +11 -0
- package/dist/ask-observability/index.d.ts.map +1 -0
- package/dist/ask-observability/index.js +7 -0
- package/dist/ask-observability/index.js.map +1 -0
- package/dist/ask-observability/instrumentation.d.ts +35 -0
- package/dist/ask-observability/instrumentation.d.ts.map +1 -0
- package/dist/ask-observability/instrumentation.js +523 -0
- package/dist/ask-observability/instrumentation.js.map +1 -0
- package/dist/ask-observability/observer.d.ts +62 -0
- package/dist/ask-observability/observer.d.ts.map +1 -0
- package/dist/ask-observability/observer.js +537 -0
- package/dist/ask-observability/observer.js.map +1 -0
- package/dist/ask-observability/portable.d.ts +98 -0
- package/dist/ask-observability/portable.d.ts.map +1 -0
- package/dist/ask-observability/portable.js +1089 -0
- package/dist/ask-observability/portable.js.map +1 -0
- package/dist/ask-observability/store.d.ts +130 -0
- package/dist/ask-observability/store.d.ts.map +1 -0
- package/dist/ask-observability/store.js +1114 -0
- package/dist/ask-observability/store.js.map +1 -0
- package/dist/ask-observability/types.d.ts +380 -0
- package/dist/ask-observability/types.d.ts.map +1 -0
- package/dist/ask-observability/types.js +2 -0
- package/dist/ask-observability/types.js.map +1 -0
- package/dist/ask-observability/utils.d.ts +21 -0
- package/dist/ask-observability/utils.d.ts.map +1 -0
- package/dist/ask-observability/utils.js +102 -0
- package/dist/ask-observability/utils.js.map +1 -0
- 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.d.ts +37 -1
- package/dist/generated-analytical-proposal.d.ts.map +1 -1
- package/dist/generated-analytical-proposal.js +246 -3
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/governed-relational-compiler.d.ts +15 -0
- package/dist/governed-relational-compiler.d.ts.map +1 -1
- package/dist/governed-relational-compiler.js +99 -1
- package/dist/governed-relational-compiler.js.map +1 -1
- package/dist/index.d.ts +21 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -7
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +87 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +2 -2
- 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 +207 -6
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +439 -53
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +78 -7
- 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 +29 -2
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +28 -2
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +373 -16
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +9 -1
- package/dist/plan-execution-adapter.d.ts.map +1 -1
- package/dist/plan-execution-adapter.js +93 -13
- package/dist/plan-execution-adapter.js.map +1 -1
- package/dist/provider-egress.d.ts +28 -11
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +72 -22
- 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 +201 -37
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/dispatch.d.ts +27 -1
- package/dist/providers/dispatch.d.ts.map +1 -1
- package/dist/providers/dispatch.js +63 -1
- package/dist/providers/dispatch.js.map +1 -1
- package/dist/providers/gemini.js +10 -10
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.js +15 -15
- package/dist/providers/ollama.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 +213 -43
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +131 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/relationship-proof.d.ts +81 -0
- package/dist/relationship-proof.d.ts.map +1 -1
- package/dist/relationship-proof.js +228 -4
- package/dist/relationship-proof.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 +18 -4
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +953 -50
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +118 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +2481 -232
- package/dist/router.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +23 -4
- package/dist/semantic-bridge/compose.js.map +1 -1
- package/package.json +4 -4
package/dist/agent-run-engine.js
CHANGED
|
@@ -7,8 +7,11 @@ 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';
|
|
13
|
+
import { frozenRequiredOutputBindingProofsForPlan } from './generated-analytical-proposal.js';
|
|
14
|
+
import { attachAskTraceObserverV1, askTraceObserverForV1, finalizeAgentRunTraceV1, noOpAskTraceObserverV1, recordAuthoritativeRouterDecisionV1, recordAuthoritativePlanFreezeV1, recordEngineTraceEventV1, recordExecutionAttemptSummaryV1, } from './ask-observability/index.js';
|
|
12
15
|
/** Routes a stakeholder may never land on (analyst authoring lives in the Notebook). */
|
|
13
16
|
const ANALYST_ONLY_ROUTES = new Set(["sql_cell", "dql_block_draft", "modeling_draft", "skill_draft"]);
|
|
14
17
|
/**
|
|
@@ -199,21 +202,30 @@ export function resolveClarificationContinuation(request) {
|
|
|
199
202
|
if (!reply || (!structuredSelection && !isLikelyClarificationReply(reply)))
|
|
200
203
|
return undefined;
|
|
201
204
|
const fromServer = latestClarificationFromConversationContext(request.conversationContext);
|
|
205
|
+
const serverIssuedStructuredSelection = structuredSelection
|
|
206
|
+
? serverIssuedStructuredClarification(request)
|
|
207
|
+
: undefined;
|
|
202
208
|
const fromHistory = latestClarificationFromHistory(request.history);
|
|
203
209
|
// A UI selection is bound to the exact run that rendered the options. Carry
|
|
204
210
|
// that run's source question explicitly so the continuation still works when
|
|
205
211
|
// the optional conversation store is unavailable, after a reload, or when
|
|
206
212
|
// the user selects an option on an older visible answer. Server history still
|
|
207
213
|
// supplies the original clarifying prose when it is available.
|
|
208
|
-
const pending =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
const pending = serverIssuedStructuredSelection
|
|
215
|
+
?? (explicitSourceQuestion
|
|
216
|
+
? {
|
|
217
|
+
sourceQuestion: explicitSourceQuestion,
|
|
218
|
+
clarifyingQuestion: fromServer?.clarifyingQuestion
|
|
219
|
+
?? fromHistory?.clarifyingQuestion
|
|
220
|
+
?? 'Which governed meaning should be used?',
|
|
221
|
+
}
|
|
222
|
+
: fromServer ?? fromHistory);
|
|
223
|
+
// A structured option intentionally submits the original question together
|
|
224
|
+
// with a stable, server-issued identifier. Treating that exact text as a
|
|
225
|
+
// fresh question drops the persisted typed frame and makes the router ask
|
|
226
|
+
// the same clarification again. The equality guard remains important for
|
|
227
|
+
// free-text replies, where an unchanged question carries no new meaning.
|
|
228
|
+
if (!pending || (!structuredSelection && pending.sourceQuestion.trim().toLowerCase() === reply.toLowerCase()))
|
|
217
229
|
return undefined;
|
|
218
230
|
return {
|
|
219
231
|
...pending,
|
|
@@ -226,6 +238,44 @@ export function resolveClarificationContinuation(request) {
|
|
|
226
238
|
].join('\n\n'),
|
|
227
239
|
};
|
|
228
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Use the original server snapshot for an identifier-bound continuation when
|
|
243
|
+
* it carries the host-only authority record that local runtime reconstructs
|
|
244
|
+
* from its persisted thread. This prevents a browser-provided label (or a
|
|
245
|
+
* stale client source question) from replacing the typed analytical frame
|
|
246
|
+
* before router validation. The router still performs the final snapshot and
|
|
247
|
+
* option-ID validation before any plan can freeze.
|
|
248
|
+
*/
|
|
249
|
+
function serverIssuedStructuredClarification(request) {
|
|
250
|
+
if (!request.threadId)
|
|
251
|
+
return undefined;
|
|
252
|
+
const context = clarificationRecord(request.conversationContext);
|
|
253
|
+
const authority = clarificationRecord(context?.serverIssuedClarificationSelection);
|
|
254
|
+
if (authority?.version !== 1
|
|
255
|
+
|| clarificationString(authority.threadId) !== request.threadId)
|
|
256
|
+
return undefined;
|
|
257
|
+
const authoritySourceTurnId = clarificationString(authority.sourceTurnId);
|
|
258
|
+
const authoritySnapshotId = clarificationString(authority.snapshotId);
|
|
259
|
+
if (!authoritySourceTurnId || !authoritySnapshotId)
|
|
260
|
+
return undefined;
|
|
261
|
+
for (const source of [
|
|
262
|
+
clarificationRecord(context?.conversationEnvelope),
|
|
263
|
+
clarificationRecord(context?.serverSnapshot),
|
|
264
|
+
]) {
|
|
265
|
+
if (clarificationString(source?.threadId) !== request.threadId)
|
|
266
|
+
continue;
|
|
267
|
+
const pending = clarificationRecord(source?.pendingClarification);
|
|
268
|
+
const selection = clarificationRecord(pending?.selection);
|
|
269
|
+
if (clarificationString(pending?.sourceTurnId) !== authoritySourceTurnId
|
|
270
|
+
|| clarificationString(selection?.snapshotId) !== authoritySnapshotId)
|
|
271
|
+
continue;
|
|
272
|
+
const sourceQuestion = clarificationString(pending?.sourceQuestion);
|
|
273
|
+
const clarifyingQuestion = clarificationString(pending?.question);
|
|
274
|
+
if (sourceQuestion && clarifyingQuestion)
|
|
275
|
+
return { sourceQuestion, clarifyingQuestion };
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
229
279
|
function latestClarificationFromHistory(history) {
|
|
230
280
|
if (!history?.length)
|
|
231
281
|
return undefined;
|
|
@@ -467,6 +517,7 @@ export class AgentRunEngine {
|
|
|
467
517
|
budgetModel;
|
|
468
518
|
maxSteps;
|
|
469
519
|
routeTimeoutSignal;
|
|
520
|
+
traceObserverFactory;
|
|
470
521
|
constructor(options = {}) {
|
|
471
522
|
this.executors = options.executors ?? {};
|
|
472
523
|
this.gates = options.gates ?? {};
|
|
@@ -485,6 +536,7 @@ export class AgentRunEngine {
|
|
|
485
536
|
};
|
|
486
537
|
this.maxSteps = Math.max(1, options.maxSteps ?? DEFAULT_MAX_STEPS);
|
|
487
538
|
this.routeTimeoutSignal = options.routeTimeoutSignal ?? ((durationMs) => AbortSignal.timeout(durationMs));
|
|
539
|
+
this.traceObserverFactory = options.traceObserverFactory;
|
|
488
540
|
}
|
|
489
541
|
/**
|
|
490
542
|
* Decide the high-level action. A forced `requestedMode` bypasses routing.
|
|
@@ -493,12 +545,22 @@ export class AgentRunEngine {
|
|
|
493
545
|
*/
|
|
494
546
|
async decideRoute(request) {
|
|
495
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';
|
|
496
557
|
// `ask` constrains the eventual analytical action to a direct answer, but it
|
|
497
558
|
// still needs retrieval-first meaning resolution. Treating it like the SQL,
|
|
498
559
|
// block, or app authoring modes used to bypass the evidence router entirely
|
|
499
560
|
// on the primary Ask surface.
|
|
500
|
-
if (requestedAction && request.requestedMode !== "ask")
|
|
561
|
+
if (requestedAction && request.requestedMode !== "ask" && !authoritativeV2Research) {
|
|
501
562
|
return buildIntentDecision(request);
|
|
563
|
+
}
|
|
502
564
|
if (this.router) {
|
|
503
565
|
try {
|
|
504
566
|
const routed = await this.router.decide(request);
|
|
@@ -542,6 +604,64 @@ export class AgentRunEngine {
|
|
|
542
604
|
const startedAt = this.timestamp();
|
|
543
605
|
const runStartedAtMs = Date.parse(startedAt);
|
|
544
606
|
const requestedMode = request.requestedMode ?? "auto";
|
|
607
|
+
// OBS-001/OBS-002: valid engine requests receive a server-owned trace after
|
|
608
|
+
// their run ID is known. The observer is explicitly non-authoritative and
|
|
609
|
+
// a factory/store failure becomes a no-op, never an Ask failure.
|
|
610
|
+
let traceObserver = noOpAskTraceObserverV1;
|
|
611
|
+
try {
|
|
612
|
+
traceObserver = this.traceObserverFactory?.({ runId, request, startedAt, requestedMode })
|
|
613
|
+
?? noOpAskTraceObserverV1;
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
traceObserver = noOpAskTraceObserverV1;
|
|
617
|
+
}
|
|
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);
|
|
624
|
+
// Continuity is relationship evidence, not new routing input. Keep only
|
|
625
|
+
// stable run IDs and one-way fingerprints so a trace can explain why this
|
|
626
|
+
// turn reused a clarification/result/derived plan without persisting chat
|
|
627
|
+
// text, values, SQL, or an invented parent trace.
|
|
628
|
+
if (clarificationContinuation) {
|
|
629
|
+
traceObserver.recordLink({
|
|
630
|
+
kind: 'clarification_continuation',
|
|
631
|
+
choiceFingerprint: traceLinkFingerprint(request.selectedEvidenceId ?? clarificationContinuation.sourceQuestion),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (request.selectedResultBinding) {
|
|
635
|
+
traceObserver.recordLink({
|
|
636
|
+
kind: 'prior_result',
|
|
637
|
+
targetRunId: request.selectedResultBinding.sourceRunId,
|
|
638
|
+
choiceFingerprint: traceLinkFingerprint([
|
|
639
|
+
request.selectedResultBinding.sourceArtifactId,
|
|
640
|
+
request.selectedResultBinding.canonicalColumn,
|
|
641
|
+
request.selectedResultBinding.rowFingerprint,
|
|
642
|
+
request.selectedResultBinding.resultFingerprint,
|
|
643
|
+
].join('\u0000')),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
const derivedSourceRunId = traceDerivedSourceRunId(request.workspaceContext);
|
|
647
|
+
if (derivedSourceRunId) {
|
|
648
|
+
traceObserver.recordLink({
|
|
649
|
+
kind: 'derived_repair',
|
|
650
|
+
targetRunId: derivedSourceRunId,
|
|
651
|
+
choiceFingerprint: traceLinkFingerprint(derivedSourceRunId),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const conversationBinding = traceConversationBinding(request, clarificationContinuation);
|
|
655
|
+
const conversationTrace = traceObserver.startSpan({
|
|
656
|
+
name: 'conversation.hydrate',
|
|
657
|
+
stage: 'conversation',
|
|
658
|
+
payload: {
|
|
659
|
+
kind: 'conversation',
|
|
660
|
+
continuation: conversationBinding !== 'none',
|
|
661
|
+
binding: conversationBinding,
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
traceObserver.finishSpan(conversationTrace, { outcome: 'ok', reasonCode: 'completed' });
|
|
545
665
|
const runBudget = request.runBudget ?? createAgentRunBudget({
|
|
546
666
|
requestedMode,
|
|
547
667
|
startedAtMs: runStartedAtMs,
|
|
@@ -549,7 +669,11 @@ export class AgentRunEngine {
|
|
|
549
669
|
timeoutSignal: this.routeTimeoutSignal,
|
|
550
670
|
nowMs: () => this.now().getTime(),
|
|
551
671
|
});
|
|
552
|
-
|
|
672
|
+
// Preserve the non-enumerable observer across the immutable request update.
|
|
673
|
+
// A plain spread drops symbol properties, which previously made the router
|
|
674
|
+
// lose candidate/cascade/freeze evidence even though the engine still
|
|
675
|
+
// emitted its own outer spans.
|
|
676
|
+
request = attachAskTraceObserverV1({ ...request, runBudget, signal: runBudget.hardSignal }, traceObserver);
|
|
553
677
|
const events = [];
|
|
554
678
|
let plan;
|
|
555
679
|
const executedSteps = [];
|
|
@@ -573,6 +697,7 @@ export class AgentRunEngine {
|
|
|
573
697
|
startedAt,
|
|
574
698
|
updatedAt: startedAt,
|
|
575
699
|
},
|
|
700
|
+
...(traceObserver.reference() ? { traceReference: traceObserver.reference() } : {}),
|
|
576
701
|
};
|
|
577
702
|
let checkpointQueue = Promise.resolve();
|
|
578
703
|
const persistProgress = () => {
|
|
@@ -585,6 +710,23 @@ export class AgentRunEngine {
|
|
|
585
710
|
artifacts: [...progress.artifacts],
|
|
586
711
|
evaluations: [...progress.evaluations],
|
|
587
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 } : {}),
|
|
588
730
|
};
|
|
589
731
|
checkpointQueue = checkpointQueue.then(async () => {
|
|
590
732
|
try {
|
|
@@ -632,6 +774,12 @@ export class AgentRunEngine {
|
|
|
632
774
|
}
|
|
633
775
|
if (full.type === "step.completed")
|
|
634
776
|
progress.steps = [...executedSteps];
|
|
777
|
+
// Event payloads are intentionally not copied: they may contain raw
|
|
778
|
+
// answer/tool data. The typed mapping records only stage identity.
|
|
779
|
+
recordEngineTraceEventV1(traceObserver, full);
|
|
780
|
+
const traceReference = traceObserver.reference();
|
|
781
|
+
if (traceReference)
|
|
782
|
+
progress.traceReference = traceReference;
|
|
635
783
|
persistProgress();
|
|
636
784
|
onEvent?.(full);
|
|
637
785
|
};
|
|
@@ -645,6 +793,23 @@ export class AgentRunEngine {
|
|
|
645
793
|
...(clarificationContinuation ? { clarificationResolved: true } : {}),
|
|
646
794
|
},
|
|
647
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);
|
|
648
813
|
// This check is intentionally before route selection. A restricted direct
|
|
649
814
|
// disclosure must not be embedded, retrieved, sent to a provider, value
|
|
650
815
|
// probed, or compiled merely to explain why it cannot be answered.
|
|
@@ -728,6 +893,7 @@ export class AgentRunEngine {
|
|
|
728
893
|
id: runId,
|
|
729
894
|
question: submittedQuestion,
|
|
730
895
|
requestedMode,
|
|
896
|
+
conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
|
|
731
897
|
route: 'blocked',
|
|
732
898
|
status: 'blocked',
|
|
733
899
|
trustState: 'blocked',
|
|
@@ -754,7 +920,13 @@ export class AgentRunEngine {
|
|
|
754
920
|
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
755
921
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
756
922
|
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
757
|
-
run.
|
|
923
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
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);
|
|
926
|
+
// Observability is deliberately finalized only after the authoritative
|
|
927
|
+
// receipt exists, and before the ordinary run store persists its compact
|
|
928
|
+
// reference. A local trace write failure never changes this outcome.
|
|
929
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
758
930
|
await checkpointQueue;
|
|
759
931
|
await this.store?.save(run);
|
|
760
932
|
return run;
|
|
@@ -765,6 +937,11 @@ export class AgentRunEngine {
|
|
|
765
937
|
// pre-try await escaped the engine and left active UI runs looking endless.
|
|
766
938
|
let routeDecision = buildIntentDecision(request);
|
|
767
939
|
try {
|
|
940
|
+
const classifySpan = traceObserver.startSpan({
|
|
941
|
+
name: 'request.classify',
|
|
942
|
+
stage: 'request',
|
|
943
|
+
payload: { kind: 'stage', requestedMode },
|
|
944
|
+
});
|
|
768
945
|
routeDecision = clarificationContinuation && !request.selectedEvidenceId
|
|
769
946
|
? {
|
|
770
947
|
action: "answer",
|
|
@@ -774,15 +951,52 @@ export class AgentRunEngine {
|
|
|
774
951
|
source: "heuristic",
|
|
775
952
|
}
|
|
776
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
|
+
}
|
|
777
979
|
routeDecision = enforceOrdinaryAnalyticalPlanBoundary(request, routeDecision);
|
|
980
|
+
traceObserver.finishSpan(classifySpan, { outcome: 'ok', reasonCode: 'route_selected' });
|
|
981
|
+
// Router/cascade evidence is captured after its authoritative decision
|
|
982
|
+
// is sealed. The trace adapter only projects IDs, counters, and typed
|
|
983
|
+
// receipts; it never participates in route selection.
|
|
984
|
+
recordAuthoritativeRouterDecisionV1(traceObserver, routeDecision);
|
|
778
985
|
const defaultRoute = answerAnywayRoute(constrainRouteForAudience(selectRoute(request, routeDecision), audience), request, audience, routeDecision);
|
|
779
986
|
const authoritativeAsk = routeDecision.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
780
987
|
&& requestedMode !== 'research';
|
|
781
|
-
|
|
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
|
|
782
996
|
? createDeterministicAgentRunPlanner()
|
|
783
997
|
: this.planner;
|
|
784
998
|
const planningSignal = request.runBudget?.hardSignal ?? request.signal;
|
|
785
|
-
plan = await awaitWithAbort(Promise.resolve(activePlanner.plan({
|
|
999
|
+
plan = runtimeFrozenPlan ?? await awaitWithAbort(Promise.resolve(activePlanner.plan({
|
|
786
1000
|
request,
|
|
787
1001
|
routeDecision,
|
|
788
1002
|
defaultRoute,
|
|
@@ -802,6 +1016,38 @@ export class AgentRunEngine {
|
|
|
802
1016
|
...step,
|
|
803
1017
|
route: answerAnywayRoute(constrainRouteForAudience(step.route, audience), request, audience, routeDecision),
|
|
804
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
|
+
}
|
|
805
1051
|
const budgets = createCascadeBudgetState(this.budgetModel);
|
|
806
1052
|
let stepCount = 0;
|
|
807
1053
|
let finalStep;
|
|
@@ -812,11 +1058,73 @@ export class AgentRunEngine {
|
|
|
812
1058
|
// non-answer step (e.g. a research/draft step that emits only an artifact)
|
|
813
1059
|
// must not drop the data answer an earlier step already computed.
|
|
814
1060
|
let bestAnswerResult;
|
|
815
|
-
|
|
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) {
|
|
816
1091
|
const planned = queue.shift();
|
|
817
1092
|
stepCount += 1;
|
|
818
1093
|
const route = planned.route;
|
|
819
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;
|
|
820
1128
|
emit({
|
|
821
1129
|
type: "step.started",
|
|
822
1130
|
message: `Step ${stepCount}: ${planned.goal}`,
|
|
@@ -824,23 +1132,83 @@ export class AgentRunEngine {
|
|
|
824
1132
|
payload: { stepId, index: stepCount, goal: planned.goal, successCriteria: planned.successCriteria },
|
|
825
1133
|
});
|
|
826
1134
|
const resolvedPlanShadow = stepCount === 1
|
|
827
|
-
? compareResolvedPlanShadow(
|
|
1135
|
+
? compareResolvedPlanShadow(taskRouteDecision, route)
|
|
828
1136
|
: undefined;
|
|
829
1137
|
emit({
|
|
830
1138
|
type: "route.decided",
|
|
831
1139
|
message: stepCount === 1
|
|
832
|
-
?
|
|
1140
|
+
? taskRouteDecision.reason
|
|
833
1141
|
: `Routed step ${stepCount} to ${route.replaceAll("_", " ")}.`,
|
|
834
1142
|
route,
|
|
835
1143
|
payload: stepCount === 1
|
|
836
1144
|
? {
|
|
837
|
-
...
|
|
1145
|
+
...taskRouteDecision,
|
|
838
1146
|
...(resolvedPlanShadow
|
|
839
1147
|
? { resolvedPlanShadow }
|
|
840
1148
|
: {}),
|
|
841
1149
|
}
|
|
842
1150
|
: { route, goal: planned.goal },
|
|
843
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
|
+
}
|
|
844
1212
|
let attempt = 0;
|
|
845
1213
|
let repairHint;
|
|
846
1214
|
let priorEvaluations;
|
|
@@ -859,11 +1227,15 @@ export class AgentRunEngine {
|
|
|
859
1227
|
: `Re-running ${route.replaceAll("_", " ")} executor (repair attempt ${attempt}).`,
|
|
860
1228
|
route,
|
|
861
1229
|
});
|
|
1230
|
+
// Provider readiness belongs at the provider boundary. The engine
|
|
1231
|
+
// cannot infer it from an executor return value: a deterministic
|
|
1232
|
+
// route may be provider-free and a provider route can fail during
|
|
1233
|
+
// preflight before any executor result exists.
|
|
862
1234
|
result = await this.executeRoute({
|
|
863
1235
|
runId,
|
|
864
|
-
request,
|
|
1236
|
+
request: taskRequest,
|
|
865
1237
|
route,
|
|
866
|
-
routeDecision,
|
|
1238
|
+
routeDecision: taskRouteDecision,
|
|
867
1239
|
maxRepairAttempts: budgets.limits.lane.execution,
|
|
868
1240
|
attempt,
|
|
869
1241
|
stepGoal: planned.goal,
|
|
@@ -873,20 +1245,67 @@ export class AgentRunEngine {
|
|
|
873
1245
|
emit,
|
|
874
1246
|
emitAnswerDelta: onAnswerDelta,
|
|
875
1247
|
});
|
|
1248
|
+
recordExecutionAttemptSummaryV1(traceObserver, result);
|
|
876
1249
|
// The router owns a frozen analytical tier. An executor may report a
|
|
877
1250
|
// same-tier execution failure, but it cannot turn a certified or
|
|
878
1251
|
// semantic plan into generated work (or vice versa) after execution
|
|
879
1252
|
// has started. Keep this guard in the engine as well as host adapters
|
|
880
1253
|
// so an injected/legacy executor cannot redefine durable provenance.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
+
}
|
|
1271
|
+
// The router froze the exploratory plan before SQL generation. The
|
|
1272
|
+
// host receipt below only authorizes this exact SQL/target against
|
|
1273
|
+
// that immutable plan; it never creates a second freeze transition.
|
|
1274
|
+
if (!planWasFrozen && taskRouteDecision.analyticalCascadeDecision?.planFrozen) {
|
|
1275
|
+
recordAuthoritativePlanFreezeV1(traceObserver, taskRouteDecision.analyticalCascadeDecision);
|
|
1276
|
+
}
|
|
1277
|
+
result = preserveFrozenAnalyticalRoute(route, taskRouteDecision, result);
|
|
1278
|
+
result = consumeRepeatedClarificationSelection(taskRequest, taskRouteDecision, result);
|
|
884
1279
|
if (result.analyticalTurnPlan)
|
|
885
1280
|
progress.analyticalTurnPlan = result.analyticalTurnPlan;
|
|
886
1281
|
if (result.analyticalTaskOutcomes)
|
|
887
1282
|
progress.analyticalTaskOutcomes = result.analyticalTaskOutcomes;
|
|
888
1283
|
persistProgress();
|
|
889
|
-
|
|
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 });
|
|
890
1309
|
for (const evaluation of evaluations) {
|
|
891
1310
|
emit({
|
|
892
1311
|
type: "evaluation.recorded",
|
|
@@ -908,7 +1327,7 @@ export class AgentRunEngine {
|
|
|
908
1327
|
// A frozen analytical plan has one route and no downstream planner,
|
|
909
1328
|
// rematch, route escalation, or whole-answer regeneration authority.
|
|
910
1329
|
// Typed server-issued repair is a separate derived run.
|
|
911
|
-
if (authoritativeAsk ||
|
|
1330
|
+
if (authoritativeAsk || taskRouteDecision.analyticalCascadeDecision?.planFrozen === true) {
|
|
912
1331
|
stepStatus = 'needs_review';
|
|
913
1332
|
break;
|
|
914
1333
|
}
|
|
@@ -925,7 +1344,7 @@ export class AgentRunEngine {
|
|
|
925
1344
|
artifacts: result.artifacts ?? [],
|
|
926
1345
|
};
|
|
927
1346
|
const decision = await activePlanner.replan({
|
|
928
|
-
request,
|
|
1347
|
+
request: taskRequest,
|
|
929
1348
|
plan,
|
|
930
1349
|
currentStep,
|
|
931
1350
|
remainingSteps: queue,
|
|
@@ -1007,6 +1426,7 @@ export class AgentRunEngine {
|
|
|
1007
1426
|
id: stepId,
|
|
1008
1427
|
index: stepCount,
|
|
1009
1428
|
route,
|
|
1429
|
+
...(planned.askAnalystTaskId ? { askAnalystTaskId: planned.askAnalystTaskId } : {}),
|
|
1010
1430
|
goal: planned.goal,
|
|
1011
1431
|
successCriteria: planned.successCriteria,
|
|
1012
1432
|
status: "escalated",
|
|
@@ -1015,6 +1435,7 @@ export class AgentRunEngine {
|
|
|
1015
1435
|
evaluations,
|
|
1016
1436
|
artifacts: [],
|
|
1017
1437
|
});
|
|
1438
|
+
checkpointAuthoritativeTaskOutcomes();
|
|
1018
1439
|
emit({
|
|
1019
1440
|
type: "step.completed",
|
|
1020
1441
|
message: `Step ${stepCount} escalated to ${escalation.route.replaceAll("_", " ")}.`,
|
|
@@ -1029,11 +1450,12 @@ export class AgentRunEngine {
|
|
|
1029
1450
|
});
|
|
1030
1451
|
continue;
|
|
1031
1452
|
}
|
|
1032
|
-
const outcome = computeStepOutcome(route, result, evaluations,
|
|
1453
|
+
const outcome = computeStepOutcome(route, result, evaluations, taskRequest, isClarify, clarifyQuestion, taskRouteDecision.terminalOutcome?.message);
|
|
1033
1454
|
const step = {
|
|
1034
1455
|
id: stepId,
|
|
1035
1456
|
index: stepCount,
|
|
1036
1457
|
route,
|
|
1458
|
+
...(planned.askAnalystTaskId ? { askAnalystTaskId: planned.askAnalystTaskId } : {}),
|
|
1037
1459
|
resolvedRoute: result.resolvedRoute,
|
|
1038
1460
|
goal: planned.goal,
|
|
1039
1461
|
successCriteria: planned.successCriteria,
|
|
@@ -1053,6 +1475,11 @@ export class AgentRunEngine {
|
|
|
1053
1475
|
payload: artifact,
|
|
1054
1476
|
});
|
|
1055
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();
|
|
1056
1483
|
emit({
|
|
1057
1484
|
type: "step.completed",
|
|
1058
1485
|
message: `Step ${stepCount} ${step.status}.`,
|
|
@@ -1066,7 +1493,14 @@ export class AgentRunEngine {
|
|
|
1066
1493
|
finalStep = step;
|
|
1067
1494
|
finalResult = result;
|
|
1068
1495
|
finalOutcome = outcome;
|
|
1069
|
-
|
|
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;
|
|
1070
1504
|
}
|
|
1071
1505
|
finalStep = step;
|
|
1072
1506
|
finalResult = result;
|
|
@@ -1074,14 +1508,36 @@ export class AgentRunEngine {
|
|
|
1074
1508
|
if (outcome.status !== "blocked" && typeof result.answer === "string" && result.answer.trim().length > 0) {
|
|
1075
1509
|
bestAnswerResult = result;
|
|
1076
1510
|
}
|
|
1077
|
-
if (outcome.status === "blocked")
|
|
1511
|
+
if (outcome.status === "blocked" && !authoritativeCompoundAsk)
|
|
1078
1512
|
break;
|
|
1079
|
-
if (outcome.status === "needs_clarification")
|
|
1513
|
+
if (outcome.status === "needs_clarification" && !authoritativeCompoundAsk)
|
|
1080
1514
|
break;
|
|
1081
|
-
|
|
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)
|
|
1082
1521
|
break;
|
|
1083
1522
|
// Otherwise continue to the next planned step (if any remain).
|
|
1084
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;
|
|
1085
1541
|
const run = this.finalizeRun({
|
|
1086
1542
|
runId,
|
|
1087
1543
|
request,
|
|
@@ -1095,6 +1551,8 @@ export class AgentRunEngine {
|
|
|
1095
1551
|
finalOutcome,
|
|
1096
1552
|
clarifyOutcome,
|
|
1097
1553
|
bestAnswerResult,
|
|
1554
|
+
...(authoritativeTaskOutcomeAggregate ? { authoritativeTaskOutcomeAggregate } : {}),
|
|
1555
|
+
...(authoritativeCompoundFailure ? { authoritativeCompoundFailure } : {}),
|
|
1098
1556
|
budgetUsage: cascadeBudgetTrace(budgets),
|
|
1099
1557
|
events,
|
|
1100
1558
|
});
|
|
@@ -1114,7 +1572,10 @@ export class AgentRunEngine {
|
|
|
1114
1572
|
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
1115
1573
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1116
1574
|
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1117
|
-
run.
|
|
1575
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
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);
|
|
1578
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
1118
1579
|
await checkpointQueue;
|
|
1119
1580
|
await this.store?.save(run);
|
|
1120
1581
|
return run;
|
|
@@ -1166,6 +1627,7 @@ export class AgentRunEngine {
|
|
|
1166
1627
|
id: runId,
|
|
1167
1628
|
question: submittedQuestion,
|
|
1168
1629
|
requestedMode,
|
|
1630
|
+
conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
|
|
1169
1631
|
route: cancelledRoute,
|
|
1170
1632
|
status: "cancelled",
|
|
1171
1633
|
trustState: "not_applicable",
|
|
@@ -1201,6 +1663,9 @@ export class AgentRunEngine {
|
|
|
1201
1663
|
};
|
|
1202
1664
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1203
1665
|
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1666
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
1667
|
+
attachAskAnalystRuntimeReceipt(run, request.askAgentRuntimeMode);
|
|
1668
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
1204
1669
|
await checkpointQueue;
|
|
1205
1670
|
await this.store?.save(run);
|
|
1206
1671
|
return run;
|
|
@@ -1208,7 +1673,9 @@ export class AgentRunEngine {
|
|
|
1208
1673
|
const message = isOrchestrationBudgetExhausted(err)
|
|
1209
1674
|
? 'Ask could not complete within its bounded orchestration. Nothing was executed; narrow the metric or dimension and retry.'
|
|
1210
1675
|
: err instanceof Error && err.name === "TimeoutError"
|
|
1211
|
-
?
|
|
1676
|
+
? requestedMode === 'research'
|
|
1677
|
+
? 'This Research run reached its bounded deadline before finalization. Review the recorded branch receipts and trace, then narrow the investigation and retry. No result was accepted.'
|
|
1678
|
+
: "This analytical run reached its time limit before it finished. A timeout alone does not prove a cross-model join or semantic-modeling problem. Open Trust & Steps to see the last recorded phase; retry the same bounded question or use Research for a longer budget. No result was accepted."
|
|
1212
1679
|
: err instanceof Error ? err.message : String(err);
|
|
1213
1680
|
const failedRoute = progress.route;
|
|
1214
1681
|
const failedPhase = progress.lifecycle.phase;
|
|
@@ -1220,7 +1687,7 @@ export class AgentRunEngine {
|
|
|
1220
1687
|
trustState: "blocked",
|
|
1221
1688
|
});
|
|
1222
1689
|
const completedAt = this.timestamp();
|
|
1223
|
-
const failure = diagnosticFailureFromError(err, failedPhase);
|
|
1690
|
+
const failure = diagnosticFailureFromError(err, failedPhase, requestedMode);
|
|
1224
1691
|
const evaluations = [
|
|
1225
1692
|
...progress.evaluations,
|
|
1226
1693
|
{
|
|
@@ -1248,6 +1715,7 @@ export class AgentRunEngine {
|
|
|
1248
1715
|
id: runId,
|
|
1249
1716
|
question: submittedQuestion,
|
|
1250
1717
|
requestedMode,
|
|
1718
|
+
conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
|
|
1251
1719
|
route: "blocked",
|
|
1252
1720
|
status: "blocked",
|
|
1253
1721
|
trustState: "blocked",
|
|
@@ -1285,7 +1753,10 @@ export class AgentRunEngine {
|
|
|
1285
1753
|
};
|
|
1286
1754
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1287
1755
|
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1288
|
-
run.
|
|
1756
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
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);
|
|
1759
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
1289
1760
|
await checkpointQueue;
|
|
1290
1761
|
await this.store?.save(run);
|
|
1291
1762
|
return run;
|
|
@@ -1296,12 +1767,120 @@ export class AgentRunEngine {
|
|
|
1296
1767
|
const repairAttempts = input.budgetUsage.usage.laneExecutionAttemptsUsed;
|
|
1297
1768
|
const escalationAttempts = input.budgetUsage.usage.engineEscalationsUsed;
|
|
1298
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
|
+
}
|
|
1299
1877
|
if (!finalStep || !finalResult || !finalOutcome) {
|
|
1300
1878
|
// No step produced a usable result (e.g. an empty plan). Treat as blocked.
|
|
1301
1879
|
return {
|
|
1302
1880
|
id: input.runId,
|
|
1303
1881
|
question: input.request.question,
|
|
1304
1882
|
requestedMode: input.requestedMode,
|
|
1883
|
+
conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
|
|
1305
1884
|
route: "blocked",
|
|
1306
1885
|
status: "blocked",
|
|
1307
1886
|
trustState: "blocked",
|
|
@@ -1331,6 +1910,30 @@ export class AgentRunEngine {
|
|
|
1331
1910
|
};
|
|
1332
1911
|
}
|
|
1333
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;
|
|
1334
1937
|
// Aggregate artifacts across every accepted step so a multi-step plan
|
|
1335
1938
|
// (e.g. research → block draft) surfaces all of its durable work, while the
|
|
1336
1939
|
// status/trust/answer reflect the final step.
|
|
@@ -1348,6 +1951,7 @@ export class AgentRunEngine {
|
|
|
1348
1951
|
id: input.runId,
|
|
1349
1952
|
question: input.request.question,
|
|
1350
1953
|
requestedMode: input.requestedMode,
|
|
1954
|
+
conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
|
|
1351
1955
|
route,
|
|
1352
1956
|
status: finalOutcome.status,
|
|
1353
1957
|
trustState: finalOutcome.trustState,
|
|
@@ -1356,7 +1960,7 @@ export class AgentRunEngine {
|
|
|
1356
1960
|
completedAt,
|
|
1357
1961
|
selectedObject: input.request.selectedObject,
|
|
1358
1962
|
executionTarget: input.request.executionTarget,
|
|
1359
|
-
routeDecision:
|
|
1963
|
+
routeDecision: finalRouteDecision,
|
|
1360
1964
|
plan: input.plan,
|
|
1361
1965
|
steps: input.steps,
|
|
1362
1966
|
summary: finalOutcome.summary,
|
|
@@ -1380,6 +1984,8 @@ export class AgentRunEngine {
|
|
|
1380
1984
|
...(finalResult.narrationIntegrityReceipt ? {
|
|
1381
1985
|
narrationIntegrityReceipt: finalResult.narrationIntegrityReceipt,
|
|
1382
1986
|
} : {}),
|
|
1987
|
+
...(finalResult.askAnalystState ? { askAnalystState: finalResult.askAnalystState } : {}),
|
|
1988
|
+
...(finalResult.businessAnswer ? { businessAnswer: finalResult.businessAnswer } : {}),
|
|
1383
1989
|
escalationAttempts,
|
|
1384
1990
|
budgetUsage: input.budgetUsage,
|
|
1385
1991
|
...(finalResult.analyticalTurnPlan ? { analyticalTurnPlan: finalResult.analyticalTurnPlan } : {}),
|
|
@@ -1411,9 +2017,13 @@ export class AgentRunEngine {
|
|
|
1411
2017
|
const signal = context.request.runBudget?.hardSignal ?? context.request.signal;
|
|
1412
2018
|
if (signal?.aborted)
|
|
1413
2019
|
throw signal.reason ?? routeTimeoutError();
|
|
2020
|
+
// Route executors own physical provider/tool/SQL boundaries. Preserve the
|
|
2021
|
+
// non-enumerable observer when adding the run signal; a normal object
|
|
2022
|
+
// spread would otherwise leave canonical routing evidence intact while
|
|
2023
|
+
// silently dropping every physical execution span.
|
|
1414
2024
|
const execution = Promise.resolve(executor({
|
|
1415
2025
|
...context,
|
|
1416
|
-
request: { ...context.request, ...(signal ? { signal } : {}) },
|
|
2026
|
+
request: attachAskTraceObserverV1({ ...context.request, ...(signal ? { signal } : {}) }, askTraceObserverForV1(context.request)),
|
|
1417
2027
|
}));
|
|
1418
2028
|
return awaitWithAbort(execution, signal);
|
|
1419
2029
|
}
|
|
@@ -1431,6 +2041,285 @@ export class AgentRunEngine {
|
|
|
1431
2041
|
return this.now().toISOString();
|
|
1432
2042
|
}
|
|
1433
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
|
+
}
|
|
2285
|
+
function traceLinkFingerprint(value) {
|
|
2286
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
2287
|
+
}
|
|
2288
|
+
/** Only a host-produced repair derivation is linkable; ordinary authoring context is not. */
|
|
2289
|
+
function traceDerivedSourceRunId(workspaceContext) {
|
|
2290
|
+
if (!workspaceContext || typeof workspaceContext !== 'object' || Array.isArray(workspaceContext))
|
|
2291
|
+
return undefined;
|
|
2292
|
+
const record = workspaceContext;
|
|
2293
|
+
const derivation = record.traceDerivation;
|
|
2294
|
+
if (derivation !== 'analytical_repair' && derivation !== 'derived_repair')
|
|
2295
|
+
return undefined;
|
|
2296
|
+
return typeof record.sourceRunId === 'string' && record.sourceRunId.trim()
|
|
2297
|
+
? record.sourceRunId
|
|
2298
|
+
: undefined;
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Record only why the conversation boundary was available, never the member,
|
|
2302
|
+
* question, row, or free-text selection that it carried. The local runtime
|
|
2303
|
+
* resolves its typed follow-up before retrieval; this trace label lets an
|
|
2304
|
+
* office reproduction distinguish a missing binding from a later retrieval or
|
|
2305
|
+
* execution failure without turning traces into chat persistence.
|
|
2306
|
+
*/
|
|
2307
|
+
function traceConversationBinding(request, clarification) {
|
|
2308
|
+
if (clarification || request.selectedEvidenceId)
|
|
2309
|
+
return 'structured_clarification';
|
|
2310
|
+
if (request.conversationBinding)
|
|
2311
|
+
return request.conversationBinding;
|
|
2312
|
+
if (request.selectedResultBinding)
|
|
2313
|
+
return 'prior_result';
|
|
2314
|
+
const context = request.conversationContext;
|
|
2315
|
+
if (!context || Object.keys(context).length === 0)
|
|
2316
|
+
return 'none';
|
|
2317
|
+
if ('analyticalTaskDependencyBinding' in context)
|
|
2318
|
+
return 'task_dependency';
|
|
2319
|
+
// A snapshot merely makes a prior binding *available*. It is not evidence
|
|
2320
|
+
// that this self-contained question selected it.
|
|
2321
|
+
return 'none';
|
|
2322
|
+
}
|
|
1434
2323
|
/**
|
|
1435
2324
|
* Ordinary analytical Ask cannot delegate meaning to the legacy answer
|
|
1436
2325
|
* generator. Retrieval/meaning must first produce the immutable RAP consumed by
|
|
@@ -1595,14 +2484,19 @@ function applyExploratoryExecutionFreeze(decision, freeze) {
|
|
|
1595
2484
|
return decision;
|
|
1596
2485
|
const cascade = decision.analyticalCascadeDecision;
|
|
1597
2486
|
const attempt = cascade?.attempts.find((candidate) => candidate.tier === 'exploratory_sql');
|
|
2487
|
+
const selectedPlan = decision.resolvedAnalyticalPlan;
|
|
2488
|
+
const existing = cascade?.exploratoryExecutionFreeze;
|
|
2489
|
+
const existingRepair = cascade?.exploratoryRepairExecutionFreeze;
|
|
2490
|
+
const authorizationAttempt = normalizedExploratoryAuthorizationAttempt(freeze);
|
|
1598
2491
|
const sameCandidates = Boolean(attempt
|
|
1599
2492
|
&& attempt.candidateIds.length === freeze.candidateIds.length
|
|
1600
2493
|
&& attempt.candidateIds.every((candidate, index) => candidate === freeze.candidateIds[index]));
|
|
1601
2494
|
const retrievalSnapshotId = decision.retrievalEvidence?.snapshotId;
|
|
1602
|
-
const
|
|
2495
|
+
const validBaseReceipt = Boolean(cascade
|
|
1603
2496
|
&& cascade.selectedTier === 'exploratory_sql'
|
|
1604
|
-
&& cascade.planFrozen ===
|
|
2497
|
+
&& cascade.planFrozen === true
|
|
1605
2498
|
&& attempt?.outcome === 'executable'
|
|
2499
|
+
&& attempt.planFrozen === true
|
|
1606
2500
|
&& sameCandidates
|
|
1607
2501
|
&& freeze.version === 1
|
|
1608
2502
|
&& freeze.selectedTier === 'exploratory_sql'
|
|
@@ -1612,28 +2506,132 @@ function applyExploratoryExecutionFreeze(decision, freeze) {
|
|
|
1612
2506
|
&& freeze.snapshotId.trim()
|
|
1613
2507
|
&& freeze.targetFingerprint.trim()
|
|
1614
2508
|
&& freeze.sqlFingerprint.trim()
|
|
2509
|
+
&& selectedPlan?.capability === 'bounded_exploration'
|
|
2510
|
+
&& selectedPlan.planId === freeze.planId
|
|
2511
|
+
&& selectedPlan.fingerprint === freeze.planFingerprint
|
|
2512
|
+
&& selectedPlan.snapshotId === freeze.snapshotId
|
|
2513
|
+
&& freezeCarriesRequiredOutputBindings(selectedPlan, freeze)
|
|
1615
2514
|
&& (!retrievalSnapshotId || retrievalSnapshotId === freeze.snapshotId));
|
|
1616
|
-
if (!
|
|
1617
|
-
throw
|
|
1618
|
-
|
|
1619
|
-
|
|
2515
|
+
if (!validBaseReceipt) {
|
|
2516
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
2517
|
+
}
|
|
2518
|
+
// A replay of one exact host handoff is harmless. A repair is a fresh,
|
|
2519
|
+
// separately-minted capability, but its receipt must name the initial SQL
|
|
2520
|
+
// authorization and keep every immutable plan binding identical.
|
|
2521
|
+
if (authorizationAttempt.index === 0) {
|
|
2522
|
+
if (existing) {
|
|
2523
|
+
if (sameExploratoryAuthorizationReceipt(existing, freeze))
|
|
2524
|
+
return decision;
|
|
2525
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
2526
|
+
}
|
|
2527
|
+
if (existingRepair)
|
|
2528
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
2529
|
+
return withExploratoryAuthorizationReceipt(decision, freeze, 'initial');
|
|
2530
|
+
}
|
|
2531
|
+
if (authorizationAttempt.index !== 1
|
|
2532
|
+
|| !authorizationAttempt.parentSqlFingerprint
|
|
2533
|
+
|| !existing
|
|
2534
|
+
|| existingRepair
|
|
2535
|
+
|| authorizationAttempt.parentSqlFingerprint !== existing.sqlFingerprint
|
|
2536
|
+
|| !sameExploratoryPlanBindings(existing, freeze)) {
|
|
2537
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
1620
2538
|
}
|
|
2539
|
+
return withExploratoryAuthorizationReceipt(decision, freeze, 'repair');
|
|
2540
|
+
}
|
|
2541
|
+
function withExploratoryAuthorizationReceipt(decision, freeze, kind) {
|
|
2542
|
+
const cascade = decision.analyticalCascadeDecision;
|
|
1621
2543
|
return {
|
|
1622
2544
|
...decision,
|
|
1623
2545
|
analyticalCascadeDecision: {
|
|
1624
2546
|
...cascade,
|
|
1625
|
-
|
|
1626
|
-
|
|
2547
|
+
...(kind === 'initial'
|
|
2548
|
+
? { exploratoryExecutionFreeze: freeze }
|
|
2549
|
+
: { exploratoryRepairExecutionFreeze: freeze }),
|
|
1627
2550
|
attempts: cascade.attempts.map((candidate) => candidate.tier === 'exploratory_sql'
|
|
1628
2551
|
? {
|
|
1629
2552
|
...candidate,
|
|
1630
|
-
|
|
1631
|
-
|
|
2553
|
+
// The router froze the plan before SQL generation. The host only
|
|
2554
|
+
// binds exact SQL/target bytes to that immutable plan. A repair
|
|
2555
|
+
// cannot choose another tier or mutate the analytical frame.
|
|
2556
|
+
reason: kind === 'repair'
|
|
2557
|
+
? `${candidate.reason} Host authorized one same-plan SQL repair against frozen plan ${freeze.planId}.`
|
|
2558
|
+
: `${candidate.reason} Host authorized SQL execution against frozen plan ${freeze.planId}.`,
|
|
1632
2559
|
}
|
|
1633
2560
|
: candidate),
|
|
1634
2561
|
},
|
|
1635
2562
|
};
|
|
1636
2563
|
}
|
|
2564
|
+
function normalizedExploratoryAuthorizationAttempt(freeze) {
|
|
2565
|
+
const attempt = freeze.authorizationAttempt;
|
|
2566
|
+
// V1/V3 persisted receipts predate explicit authorization-attempt evidence.
|
|
2567
|
+
// They are compatible only as the original handoff, never as a repair.
|
|
2568
|
+
if (!attempt)
|
|
2569
|
+
return { index: 0 };
|
|
2570
|
+
if (attempt.version !== 1 || (attempt.index !== 0 && attempt.index !== 1)) {
|
|
2571
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
2572
|
+
}
|
|
2573
|
+
if (attempt.index === 0) {
|
|
2574
|
+
if ('parentSqlFingerprint' in attempt && attempt.parentSqlFingerprint) {
|
|
2575
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
2576
|
+
}
|
|
2577
|
+
return { index: 0 };
|
|
2578
|
+
}
|
|
2579
|
+
if (!attempt.parentSqlFingerprint?.trim())
|
|
2580
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
2581
|
+
return { index: 1, parentSqlFingerprint: attempt.parentSqlFingerprint };
|
|
2582
|
+
}
|
|
2583
|
+
function sameExploratoryPlanBindings(left, right) {
|
|
2584
|
+
return left.version === right.version
|
|
2585
|
+
&& left.selectedTier === right.selectedTier
|
|
2586
|
+
&& left.planId === right.planId
|
|
2587
|
+
&& left.planFingerprint === right.planFingerprint
|
|
2588
|
+
&& left.snapshotId === right.snapshotId
|
|
2589
|
+
&& left.targetFingerprint === right.targetFingerprint
|
|
2590
|
+
&& left.authorization === right.authorization
|
|
2591
|
+
&& sameFrozenRequiredOutputBindings(left.requiredOutputBindings, right.requiredOutputBindings)
|
|
2592
|
+
&& left.candidateIds.length === right.candidateIds.length
|
|
2593
|
+
&& left.candidateIds.every((candidate, index) => candidate === right.candidateIds[index]);
|
|
2594
|
+
}
|
|
2595
|
+
function freezeCarriesRequiredOutputBindings(plan, freeze) {
|
|
2596
|
+
if (!plan)
|
|
2597
|
+
return false;
|
|
2598
|
+
// Pre-V4 persisted plans did not carry an output contract. They remain
|
|
2599
|
+
// readable, but newly frozen plans with explicit outputs must carry the
|
|
2600
|
+
// exact physical binding proofs below.
|
|
2601
|
+
const required = plan.outputContract?.requiredOutputs ?? [];
|
|
2602
|
+
if (required.length === 0)
|
|
2603
|
+
return true;
|
|
2604
|
+
const expected = frozenRequiredOutputBindingProofsForPlan(plan);
|
|
2605
|
+
const actual = freeze.requiredOutputBindings;
|
|
2606
|
+
return expected.length === required.length
|
|
2607
|
+
&& Array.isArray(actual)
|
|
2608
|
+
&& sameFrozenRequiredOutputBindings(actual, expected);
|
|
2609
|
+
}
|
|
2610
|
+
function sameFrozenRequiredOutputBindings(left, right) {
|
|
2611
|
+
const normalize = (bindings) => (bindings ?? []).map((binding) => [
|
|
2612
|
+
binding.version,
|
|
2613
|
+
binding.outputName.toLowerCase().replace(/["`\[\]]/g, ''),
|
|
2614
|
+
binding.qualifiedId,
|
|
2615
|
+
binding.relation.toLowerCase().replace(/["`\[\]]/g, '').replace(/\s*\.\s*/g, '.'),
|
|
2616
|
+
binding.column.toLowerCase().replace(/["`\[\]]/g, ''),
|
|
2617
|
+
].join('|')).sort();
|
|
2618
|
+
const leftBindings = normalize(left);
|
|
2619
|
+
const rightBindings = normalize(right);
|
|
2620
|
+
return leftBindings.length === rightBindings.length
|
|
2621
|
+
&& leftBindings.every((binding, index) => binding === rightBindings[index]);
|
|
2622
|
+
}
|
|
2623
|
+
function sameExploratoryAuthorizationReceipt(left, right) {
|
|
2624
|
+
const leftAttempt = normalizedExploratoryAuthorizationAttempt(left);
|
|
2625
|
+
const rightAttempt = normalizedExploratoryAuthorizationAttempt(right);
|
|
2626
|
+
return sameExploratoryPlanBindings(left, right)
|
|
2627
|
+
&& left.sqlFingerprint === right.sqlFingerprint
|
|
2628
|
+
&& left.authorization === right.authorization
|
|
2629
|
+
&& leftAttempt.index === rightAttempt.index
|
|
2630
|
+
&& leftAttempt.parentSqlFingerprint === rightAttempt.parentSqlFingerprint;
|
|
2631
|
+
}
|
|
2632
|
+
function exploratoryAuthorizationStateMismatch() {
|
|
2633
|
+
return Object.assign(new Error('The exploratory SQL authorization receipt did not match the already-frozen analytical plan. Execution was not attempted.'), { code: 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH' });
|
|
2634
|
+
}
|
|
1637
2635
|
/**
|
|
1638
2636
|
* Has this exact clarification already been asked in this thread?
|
|
1639
2637
|
*
|
|
@@ -1668,6 +2666,26 @@ function clarificationAlreadyAsked(clarifyingQuestion, history) {
|
|
|
1668
2666
|
return lastAsk > 0;
|
|
1669
2667
|
}
|
|
1670
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
|
+
}
|
|
1671
2689
|
const ordinaryAsk = request.requestedMode === undefined
|
|
1672
2690
|
|| request.requestedMode === 'auto'
|
|
1673
2691
|
|| request.requestedMode === 'ask';
|
|
@@ -1946,10 +2964,21 @@ function terminalLifecycle(prior, phase, completedAt, eventCursor) {
|
|
|
1946
2964
|
completedAt,
|
|
1947
2965
|
};
|
|
1948
2966
|
}
|
|
1949
|
-
function diagnosticFailureFromError(error, phase) {
|
|
2967
|
+
function diagnosticFailureFromError(error, phase, requestedMode) {
|
|
1950
2968
|
const name = error instanceof Error ? error.name : "";
|
|
1951
2969
|
const message = error instanceof Error ? error.message : String(error);
|
|
1952
2970
|
const lower = `${name} ${message}`.toLowerCase();
|
|
2971
|
+
if (error
|
|
2972
|
+
&& typeof error === 'object'
|
|
2973
|
+
&& error.code === 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH') {
|
|
2974
|
+
return {
|
|
2975
|
+
code: 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH',
|
|
2976
|
+
phase: 'sql.authorize',
|
|
2977
|
+
message: 'The frozen exploratory plan did not match the SQL authorization receipt. Execution was not attempted.',
|
|
2978
|
+
recoverable: false,
|
|
2979
|
+
safeActions: ['export_redacted_trace'],
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
1953
2982
|
if (isOrchestrationBudgetExhausted(error)) {
|
|
1954
2983
|
return {
|
|
1955
2984
|
code: 'orchestration_budget_exhausted',
|
|
@@ -1960,6 +2989,15 @@ function diagnosticFailureFromError(error, phase) {
|
|
|
1960
2989
|
};
|
|
1961
2990
|
}
|
|
1962
2991
|
if (name === "TimeoutError" || lower.includes("time limit") || lower.includes("timeout")) {
|
|
2992
|
+
if (requestedMode === 'research') {
|
|
2993
|
+
return {
|
|
2994
|
+
code: 'RESEARCH_RUN_DEADLINE',
|
|
2995
|
+
phase: 'research.run',
|
|
2996
|
+
message: 'Research reached its bounded run deadline before finalization.',
|
|
2997
|
+
recoverable: true,
|
|
2998
|
+
safeActions: ['inspect_failure'],
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
1963
3001
|
return {
|
|
1964
3002
|
code: "TIMEOUT",
|
|
1965
3003
|
phase,
|
|
@@ -2058,6 +3096,11 @@ function diagnosticReceiptV3ForRun(run) {
|
|
|
2058
3096
|
// route names or identifier text here: that erased stale/error lane states
|
|
2059
3097
|
// and falsely reported governed-relational success for pure exploration.
|
|
2060
3098
|
const cascade = run.routeDecision?.analyticalCascadeDecision;
|
|
3099
|
+
// The router may retain a broader terminal witness for presentation, while
|
|
3100
|
+
// the cascade carries the only persistable, enumerated relationship-proof
|
|
3101
|
+
// receipt. Prefer that immutable cascade value and do not infer a gap from a
|
|
3102
|
+
// failure message or route label here.
|
|
3103
|
+
const terminalGap = cascade?.terminalGap;
|
|
2061
3104
|
const sourceCoverage = cascade?.sourceCoverage ?? [];
|
|
2062
3105
|
const planFrozen = cascade?.planFrozen ?? false;
|
|
2063
3106
|
const artifactProviderDiagnostic = run.artifacts
|
|
@@ -2066,50 +3109,1681 @@ function diagnosticReceiptV3ForRun(run) {
|
|
|
2066
3109
|
.map((payload) => payload.providerFailure)
|
|
2067
3110
|
.find((failure) => Boolean(failure) && typeof failure === 'object' && !Array.isArray(failure));
|
|
2068
3111
|
const persistedProviderDiagnostic = artifactProviderDiagnostic?.diagnostic;
|
|
2069
|
-
const provider =
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
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
|
+
})());
|
|
2077
3121
|
return {
|
|
2078
3122
|
version: 3,
|
|
2079
3123
|
runId: run.id,
|
|
2080
3124
|
sourceCoverage,
|
|
2081
3125
|
...(cascade ? { cascade } : {}),
|
|
3126
|
+
...(terminalGap ? { terminalGap } : {}),
|
|
2082
3127
|
planFrozen,
|
|
2083
3128
|
...(provider ? { provider } : {}),
|
|
2084
3129
|
finalStopReason: run.stopReason,
|
|
2085
3130
|
};
|
|
2086
3131
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
3132
|
+
/**
|
|
3133
|
+
* Build the one canonical, content-safe Ask story. This is produced from the
|
|
3134
|
+
* authoritative run receipt once, then joined by both the inspector and the
|
|
3135
|
+
* full local trace. Neither surface is allowed to reconstruct an incident from
|
|
3136
|
+
* spans or a generic error string.
|
|
3137
|
+
*/
|
|
3138
|
+
function diagnosticReceiptV4ForRun(run) {
|
|
3139
|
+
const cascade = run.routeDecision?.analyticalCascadeDecision;
|
|
3140
|
+
const requirements = cascade?.requirements;
|
|
3141
|
+
const candidates = run.routeDecision?.retrievalEvidence?.candidateTraceMetadata ?? [];
|
|
3142
|
+
const roleCounts = new Map();
|
|
3143
|
+
for (const candidate of candidates) {
|
|
3144
|
+
roleCounts.set(candidate.role, (roleCounts.get(candidate.role) ?? 0) + 1);
|
|
3145
|
+
}
|
|
3146
|
+
const researchBranchObservability = researchBranchObservabilityForRun(run);
|
|
3147
|
+
for (const evidence of researchBranchObservability.evidenceByRole) {
|
|
3148
|
+
roleCounts.set(evidence.role, (roleCounts.get(evidence.role) ?? 0) + evidence.candidateCount);
|
|
3149
|
+
}
|
|
3150
|
+
const terminalIncident = terminalIncidentForRun(run, cascade?.stopReason);
|
|
3151
|
+
const runtimeReviewRequired = run.askAnalystState?.resolvedPlan?.reviewRequired === true
|
|
3152
|
+
|| run.routeDecision?.askAnalystDecision?.state.resolvedPlan?.reviewRequired === true;
|
|
3153
|
+
const summaryInput = {
|
|
2089
3154
|
version: 1,
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
3155
|
+
understoodRequest: {
|
|
3156
|
+
measures: requirements?.measures.length ?? 0,
|
|
3157
|
+
dimensions: requirements?.dimensions.length ?? 0,
|
|
3158
|
+
entityRequested: Boolean((requirements?.entityTerms.length ?? 0) || (requirements?.entityDisplayTerms.length ?? 0)),
|
|
3159
|
+
outputCount: requirements?.outputTerms?.length ?? 0,
|
|
3160
|
+
...(requirements?.ranking
|
|
3161
|
+
? { ranking: { ...requirements.ranking } }
|
|
3162
|
+
: {}),
|
|
3163
|
+
// This comes from the server-owned request admission, not a generic
|
|
3164
|
+
// `followsUp` heuristic. A complete question with thread history is
|
|
3165
|
+
// still `none` unless it explicitly selected a valid binding.
|
|
3166
|
+
conversationBinding: run.conversationBinding ?? 'none',
|
|
3167
|
+
},
|
|
3168
|
+
evidenceByRole: [...roleCounts.entries()]
|
|
3169
|
+
.map(([role, candidateCount]) => ({ role, candidateCount }))
|
|
3170
|
+
.sort((left, right) => left.role.localeCompare(right.role)),
|
|
3171
|
+
tierDecisions: (cascade?.attempts ?? []).map((attempt) => ({
|
|
3172
|
+
tier: attempt.tier,
|
|
3173
|
+
outcome: attempt.outcome,
|
|
3174
|
+
planFrozen: attempt.planFrozen,
|
|
3175
|
+
})),
|
|
3176
|
+
...(cascade?.selectedTier
|
|
3177
|
+
? {
|
|
3178
|
+
selectedPlan: {
|
|
3179
|
+
tier: cascade.selectedTier,
|
|
3180
|
+
planFrozen: cascade.planFrozen,
|
|
3181
|
+
reviewRequired: cascade.selectedTier === 'exploratory_sql' || runtimeReviewRequired,
|
|
3182
|
+
},
|
|
3183
|
+
}
|
|
3184
|
+
: {}),
|
|
3185
|
+
...(terminalIncident ? { terminalIncident } : {}),
|
|
3186
|
+
...(researchBranchObservability.summary ? { researchBranchSummary: researchBranchObservability.summary } : {}),
|
|
3187
|
+
safeNextAction: terminalIncident?.safeAction
|
|
3188
|
+
?? (researchBranchObservability.summary?.partialSuccess
|
|
3189
|
+
? researchBranchObservability.summary.safeAction
|
|
3190
|
+
: 'none'),
|
|
3191
|
+
};
|
|
3192
|
+
const summary = {
|
|
3193
|
+
...summaryInput,
|
|
3194
|
+
summaryFingerprint: receiptFingerprint(summaryInput),
|
|
2097
3195
|
};
|
|
2098
|
-
}
|
|
2099
|
-
function withTotalDuration(telemetry, total) {
|
|
2100
3196
|
return {
|
|
2101
|
-
|
|
2102
|
-
|
|
3197
|
+
version: 4,
|
|
3198
|
+
runId: run.id,
|
|
3199
|
+
summary,
|
|
3200
|
+
...(terminalIncident ? { terminalIncident } : {}),
|
|
3201
|
+
finalStopReason: run.stopReason,
|
|
2103
3202
|
};
|
|
2104
3203
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
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
|
+
}
|
|
4243
|
+
const ASK_SUMMARY_EVIDENCE_ROLES = [
|
|
4244
|
+
'metric',
|
|
4245
|
+
'entity_key',
|
|
4246
|
+
'entity_label',
|
|
4247
|
+
'categorical_dimension',
|
|
4248
|
+
'time_dimension',
|
|
4249
|
+
'member',
|
|
4250
|
+
'relationship',
|
|
4251
|
+
'context',
|
|
4252
|
+
];
|
|
4253
|
+
const ASK_RESEARCH_BRANCH_FAILURE_CODES = [
|
|
4254
|
+
'execution_failed',
|
|
4255
|
+
'research_branch_timeout',
|
|
4256
|
+
'budget_exhausted',
|
|
4257
|
+
'run_deadline',
|
|
4258
|
+
'cancelled',
|
|
4259
|
+
];
|
|
4260
|
+
const ASK_RESEARCH_CHILD_TIERS = [
|
|
4261
|
+
'certified',
|
|
4262
|
+
'semantic',
|
|
4263
|
+
'governed_relational',
|
|
4264
|
+
'exploratory_sql',
|
|
4265
|
+
];
|
|
4266
|
+
/**
|
|
4267
|
+
* Project only persisted, typed Research child evidence into V4. The root
|
|
4268
|
+
* result remains authoritative: this helper never promotes a failed branch
|
|
4269
|
+
* into a root incident or infers a missing plan from spans.
|
|
4270
|
+
*/
|
|
4271
|
+
function researchBranchObservabilityForRun(run) {
|
|
4272
|
+
// A persisted V4 receipt can be reprojected after request normalization by
|
|
4273
|
+
// a host. The root route is therefore the durable authority as well as the
|
|
4274
|
+
// original requested mode: an explicit Research run must not lose its
|
|
4275
|
+
// child-story merely because an older host omitted `requestedMode` while
|
|
4276
|
+
// preserving the authoritative `research` route and research artifact.
|
|
4277
|
+
if (run.requestedMode !== 'research' && run.route !== 'research') {
|
|
4278
|
+
return { evidenceByRole: [] };
|
|
4279
|
+
}
|
|
4280
|
+
const payload = persistedResearchArtifactPayloadForRun(run);
|
|
4281
|
+
if (!payload)
|
|
4282
|
+
return { evidenceByRole: [] };
|
|
4283
|
+
const rawReceipts = Array.isArray(payload.researchBranchReceipts)
|
|
4284
|
+
? payload.researchBranchReceipts
|
|
4285
|
+
: [];
|
|
4286
|
+
const receipts = new Map();
|
|
4287
|
+
for (const value of rawReceipts) {
|
|
4288
|
+
const receipt = clarificationRecord(value);
|
|
4289
|
+
const childRunId = clarificationString(receipt?.childRunId);
|
|
4290
|
+
const branchId = clarificationString(receipt?.branchId);
|
|
4291
|
+
const state = clarificationString(receipt?.state);
|
|
4292
|
+
const stopReason = clarificationString(receipt?.stopReason);
|
|
4293
|
+
// This field is producer-owned. A malformed imported receipt must not
|
|
4294
|
+
// become an apparently successful Research story.
|
|
4295
|
+
if (!receipt || !childRunId || !branchId || !state || !stopReason)
|
|
4296
|
+
continue;
|
|
4297
|
+
const key = `${childRunId}:${branchId}`;
|
|
4298
|
+
if (!receipts.has(key))
|
|
4299
|
+
receipts.set(key, receipt);
|
|
4300
|
+
}
|
|
4301
|
+
if (receipts.size === 0)
|
|
4302
|
+
return { evidenceByRole: [] };
|
|
4303
|
+
const childRuns = persistedResearchChildRuns(payload);
|
|
4304
|
+
const evidenceByRole = persistedResearchChildEvidenceByRole(childRuns);
|
|
4305
|
+
const receiptBackedChildIds = persistedReceiptBackedResearchChildIds(payload);
|
|
4306
|
+
let completedBranches = 0;
|
|
4307
|
+
let failedBranches = 0;
|
|
4308
|
+
let timedOutBranches = 0;
|
|
4309
|
+
let skippedBranches = 0;
|
|
4310
|
+
const failureReasons = new Map();
|
|
4311
|
+
const linkedChildRunIds = new Set();
|
|
4312
|
+
for (const receipt of receipts.values()) {
|
|
4313
|
+
const childRunId = clarificationString(receipt.childRunId);
|
|
4314
|
+
linkedChildRunIds.add(childRunId);
|
|
4315
|
+
const state = clarificationString(receipt.state);
|
|
4316
|
+
const stopReason = clarificationString(receipt.stopReason);
|
|
4317
|
+
if (state === 'completed' && stopReason === 'completed') {
|
|
4318
|
+
completedBranches += 1;
|
|
4319
|
+
continue;
|
|
4320
|
+
}
|
|
4321
|
+
if (state === 'timed_out')
|
|
4322
|
+
timedOutBranches += 1;
|
|
4323
|
+
else if (state === 'skipped')
|
|
4324
|
+
skippedBranches += 1;
|
|
4325
|
+
else
|
|
4326
|
+
failedBranches += 1;
|
|
4327
|
+
if (isAskResearchBranchFailureCode(stopReason)) {
|
|
4328
|
+
failureReasons.set(stopReason, (failureReasons.get(stopReason) ?? 0) + 1);
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
const receiptBackedBranches = [...receipts.values()]
|
|
4332
|
+
.filter((receipt) => clarificationString(receipt.state) === 'completed'
|
|
4333
|
+
&& clarificationString(receipt.stopReason) === 'completed'
|
|
4334
|
+
&& receiptBackedChildIds.has(clarificationString(receipt.childRunId)))
|
|
4335
|
+
.length;
|
|
4336
|
+
const incompleteBranches = failedBranches + timedOutBranches + skippedBranches;
|
|
4337
|
+
const summary = {
|
|
4338
|
+
version: 1,
|
|
4339
|
+
totalBranches: receipts.size,
|
|
4340
|
+
completedBranches,
|
|
4341
|
+
receiptBackedBranches,
|
|
4342
|
+
failedBranches,
|
|
4343
|
+
timedOutBranches,
|
|
4344
|
+
skippedBranches,
|
|
4345
|
+
partialSuccess: receiptBackedBranches > 0 && incompleteBranches > 0,
|
|
4346
|
+
failureReasons: [...failureReasons.entries()]
|
|
4347
|
+
.map(([code, branchCount]) => ({ code, branchCount }))
|
|
4348
|
+
.sort((left, right) => left.code.localeCompare(right.code)),
|
|
4349
|
+
availableChildPlans: persistedResearchChildPlans(childRuns),
|
|
4350
|
+
linkedChildRunCount: linkedChildRunIds.size,
|
|
4351
|
+
safeAction: 'inspect_research_failures',
|
|
4352
|
+
};
|
|
4353
|
+
return { summary, evidenceByRole };
|
|
4354
|
+
}
|
|
4355
|
+
/** Use only the durable root research artifact with branch receipts. */
|
|
4356
|
+
function persistedResearchArtifactPayloadForRun(run) {
|
|
4357
|
+
let selected;
|
|
4358
|
+
let selectedCount = -1;
|
|
4359
|
+
for (const artifact of run.artifacts) {
|
|
4360
|
+
if (artifact.kind !== 'research_run')
|
|
4361
|
+
continue;
|
|
4362
|
+
const payload = clarificationRecord(artifact.payload);
|
|
4363
|
+
const count = Array.isArray(payload?.researchBranchReceipts) ? payload.researchBranchReceipts.length : 0;
|
|
4364
|
+
if (payload && count > selectedCount) {
|
|
4365
|
+
selected = payload;
|
|
4366
|
+
selectedCount = count;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
return selected;
|
|
4370
|
+
}
|
|
4371
|
+
function persistedResearchChildRuns(payload) {
|
|
4372
|
+
const candidates = [
|
|
4373
|
+
...(Array.isArray(payload.researchRuns) ? payload.researchRuns : []),
|
|
4374
|
+
payload.researchRun,
|
|
4375
|
+
];
|
|
4376
|
+
const byId = new Map();
|
|
4377
|
+
for (const value of candidates) {
|
|
4378
|
+
const child = clarificationRecord(value);
|
|
4379
|
+
const id = clarificationString(child?.id);
|
|
4380
|
+
if (child && id && !byId.has(id))
|
|
4381
|
+
byId.set(id, child);
|
|
4382
|
+
}
|
|
4383
|
+
return [...byId.values()];
|
|
4384
|
+
}
|
|
4385
|
+
function persistedResearchChildEvidenceByRole(childRuns) {
|
|
4386
|
+
const counts = new Map();
|
|
4387
|
+
for (const child of childRuns) {
|
|
4388
|
+
const routeDecision = clarificationRecord(child.routeDecision);
|
|
4389
|
+
const retrieval = clarificationRecord(routeDecision?.retrievalEvidence);
|
|
4390
|
+
const candidates = Array.isArray(retrieval?.candidateTraceMetadata)
|
|
4391
|
+
? retrieval.candidateTraceMetadata
|
|
4392
|
+
: [];
|
|
4393
|
+
for (const value of candidates) {
|
|
4394
|
+
const candidate = clarificationRecord(value);
|
|
4395
|
+
const role = clarificationString(candidate?.role);
|
|
4396
|
+
if (!isAskSummaryEvidenceRole(role))
|
|
4397
|
+
continue;
|
|
4398
|
+
counts.set(role, (counts.get(role) ?? 0) + 1);
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
return [...counts.entries()]
|
|
4402
|
+
.map(([role, candidateCount]) => ({ role, candidateCount }))
|
|
4403
|
+
.sort((left, right) => left.role.localeCompare(right.role));
|
|
4404
|
+
}
|
|
4405
|
+
function persistedReceiptBackedResearchChildIds(payload) {
|
|
4406
|
+
const ledger = clarificationRecord(payload.researchLedgerV2);
|
|
4407
|
+
const entries = Array.isArray(ledger?.entries) ? ledger.entries : [];
|
|
4408
|
+
const ids = new Set();
|
|
4409
|
+
for (const value of entries) {
|
|
4410
|
+
const entry = clarificationRecord(value);
|
|
4411
|
+
const id = clarificationString(entry?.id);
|
|
4412
|
+
const receipts = Array.isArray(entry?.receipts) ? entry.receipts : [];
|
|
4413
|
+
if (entry?.status === 'observed' && id && receipts.some((receipt) => clarificationString(receipt))) {
|
|
4414
|
+
ids.add(id);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
return ids;
|
|
4418
|
+
}
|
|
4419
|
+
function persistedResearchChildPlans(childRuns) {
|
|
4420
|
+
const plansByTier = new Map();
|
|
4421
|
+
for (const child of childRuns) {
|
|
4422
|
+
const childRunId = clarificationString(child.id);
|
|
4423
|
+
const context = clarificationRecord(child.context);
|
|
4424
|
+
const authority = clarificationRecord(context?.branchAuthority);
|
|
4425
|
+
const tier = clarificationString(authority?.selectedTier);
|
|
4426
|
+
const planId = clarificationString(authority?.planId);
|
|
4427
|
+
const planFingerprint = clarificationString(authority?.planFingerprint);
|
|
4428
|
+
if (!childRunId || authority?.planFrozen !== true || !isAskResearchChildTier(tier) || !planId || !planFingerprint)
|
|
4429
|
+
continue;
|
|
4430
|
+
const entry = plansByTier.get(tier) ?? { planKeys: new Set(), childRunIds: new Set() };
|
|
4431
|
+
entry.planKeys.add(`${planId}:${planFingerprint}`);
|
|
4432
|
+
entry.childRunIds.add(childRunId);
|
|
4433
|
+
plansByTier.set(tier, entry);
|
|
4434
|
+
}
|
|
4435
|
+
return [...plansByTier.entries()]
|
|
4436
|
+
.map(([tier, value]) => ({
|
|
4437
|
+
tier,
|
|
4438
|
+
frozenPlanCount: value.planKeys.size,
|
|
4439
|
+
branchCount: value.childRunIds.size,
|
|
4440
|
+
reviewRequired: tier === 'exploratory_sql',
|
|
4441
|
+
}))
|
|
4442
|
+
.sort((left, right) => left.tier.localeCompare(right.tier));
|
|
4443
|
+
}
|
|
4444
|
+
function isAskSummaryEvidenceRole(value) {
|
|
4445
|
+
return Boolean(value) && ASK_SUMMARY_EVIDENCE_ROLES.includes(value);
|
|
4446
|
+
}
|
|
4447
|
+
function isAskResearchBranchFailureCode(value) {
|
|
4448
|
+
return Boolean(value) && ASK_RESEARCH_BRANCH_FAILURE_CODES.includes(value);
|
|
4449
|
+
}
|
|
4450
|
+
function isAskResearchChildTier(value) {
|
|
4451
|
+
return Boolean(value) && ASK_RESEARCH_CHILD_TIERS.includes(value);
|
|
4452
|
+
}
|
|
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;
|
|
4459
|
+
const executionSetupFailure = terminalConnectionSetupFailureForRun(run);
|
|
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) {
|
|
4465
|
+
return {
|
|
4466
|
+
version: 1,
|
|
4467
|
+
code: 'CONNECTION_NOT_CONFIGURED',
|
|
4468
|
+
boundary: 'sql.execute',
|
|
4469
|
+
origin: 'governance_gate',
|
|
4470
|
+
impact: 'execution_not_attempted',
|
|
4471
|
+
safeAction: 'configure_connection',
|
|
4472
|
+
};
|
|
4473
|
+
}
|
|
4474
|
+
const failureCode = run.diagnosticReceipt?.failure?.code;
|
|
4475
|
+
if (failureCode === 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH') {
|
|
4476
|
+
return {
|
|
4477
|
+
version: 1,
|
|
4478
|
+
code: 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH',
|
|
4479
|
+
boundary: 'sql.authorize',
|
|
4480
|
+
origin: 'internal_invariant',
|
|
4481
|
+
impact: 'execution_not_attempted',
|
|
4482
|
+
safeAction: 'export_redacted_trace',
|
|
4483
|
+
};
|
|
4484
|
+
}
|
|
4485
|
+
if (failureCode === 'RESEARCH_RUN_DEADLINE') {
|
|
4486
|
+
return {
|
|
4487
|
+
version: 1,
|
|
4488
|
+
code: 'RESEARCH_RUN_DEADLINE',
|
|
4489
|
+
boundary: 'run',
|
|
4490
|
+
origin: 'governance_gate',
|
|
4491
|
+
impact: 'answer_not_produced',
|
|
4492
|
+
safeAction: 'inspect_failure',
|
|
4493
|
+
};
|
|
4494
|
+
}
|
|
4495
|
+
// A completed root can still be materially limited when every admitted
|
|
4496
|
+
// Research child exhausted its bounded window. This is producer-owned
|
|
4497
|
+
// receipt evidence, not an incident reconstructed from trace timing. It
|
|
4498
|
+
// must be visible in the same V4 summary used by the inspector and full
|
|
4499
|
+
// trace so the user is never told there was no incident after a zero-finding
|
|
4500
|
+
// investigation.
|
|
4501
|
+
if (terminalResearchBranchTimeoutForRun(run)) {
|
|
4502
|
+
return {
|
|
4503
|
+
version: 1,
|
|
4504
|
+
code: 'RESEARCH_BRANCH_TIMEOUT',
|
|
4505
|
+
boundary: 'run',
|
|
4506
|
+
origin: 'governance_gate',
|
|
4507
|
+
impact: 'answer_not_produced',
|
|
4508
|
+
safeAction: 'inspect_research_failures',
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
if (failureCode === 'RUN_CANCELLED' || run.status === 'cancelled') {
|
|
4512
|
+
return { version: 1, code: 'CANCELLED', boundary: 'run', origin: 'unknown', impact: 'run_cancelled', safeAction: 'none' };
|
|
4513
|
+
}
|
|
4514
|
+
if (failureCode === 'CONNECTION_NOT_CONFIGURED' && planFrozen) {
|
|
4515
|
+
return { version: 1, code: 'CONNECTION_NOT_CONFIGURED', boundary: 'sql.execute', origin: 'governance_gate', impact: 'execution_not_attempted', safeAction: 'configure_connection' };
|
|
4516
|
+
}
|
|
4517
|
+
// A frozen semantic/analytical plan may fail while the compiler is resolving
|
|
4518
|
+
// its already-proven identifiers. That is categorically different from a
|
|
4519
|
+
// warehouse failure: no statement was authorized or executed. Preserve the
|
|
4520
|
+
// producer's typed `COMPILATION_FAILED` cause before consulting connector
|
|
4521
|
+
// evidence so both Ask surfaces tell the same pre-SQL story.
|
|
4522
|
+
const compilationFailure = terminalCompilationFailureForRun(run);
|
|
4523
|
+
if (compilationFailure) {
|
|
4524
|
+
const semantic = isSemanticCompilationForRun(run);
|
|
4525
|
+
return {
|
|
4526
|
+
version: 1,
|
|
4527
|
+
code: 'COMPILATION_FAILED',
|
|
4528
|
+
boundary: semantic ? 'semantic.compile' : 'plan.compile',
|
|
4529
|
+
origin: semantic ? 'semantic_compiler' : 'plan_compiler',
|
|
4530
|
+
impact: 'execution_not_attempted',
|
|
4531
|
+
safeAction: compilationFailure.safeAction,
|
|
4532
|
+
};
|
|
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
|
+
}
|
|
4549
|
+
const warehouseFailure = terminalWarehouseFailureForRun(run);
|
|
4550
|
+
if (warehouseFailure) {
|
|
4551
|
+
return {
|
|
4552
|
+
version: 1,
|
|
4553
|
+
code: 'ANALYTICAL_EXECUTION_FAILED',
|
|
4554
|
+
boundary: 'sql.execute',
|
|
4555
|
+
origin: 'warehouse',
|
|
4556
|
+
impact: 'execution_failed',
|
|
4557
|
+
// A typed missing relation after a frozen plan reached the connector is
|
|
4558
|
+
// not a generic retry. The target may be an empty local database or a
|
|
4559
|
+
// different approved warehouse, so direct the operator to that target.
|
|
4560
|
+
safeAction: warehouseFailure.category === 'unknown_relation'
|
|
4561
|
+
? 'change_authorized_connection'
|
|
4562
|
+
: 'inspect_failure',
|
|
4563
|
+
};
|
|
4564
|
+
}
|
|
4565
|
+
if (run.diagnosticReceiptV3?.provider) {
|
|
4566
|
+
return { version: 1, code: 'PROVIDER_FAILURE', boundary: 'provider', origin: 'provider', impact: 'answer_not_produced', safeAction: 'inspect_failure' };
|
|
4567
|
+
}
|
|
4568
|
+
if (cascadeStopReason === 'coverage_gap' || cascadeStopReason === 'ambiguous' || cascadeStopReason === 'denied') {
|
|
4569
|
+
return { version: 1, code: 'ANALYTICAL_COVERAGE_GAP', boundary: 'cascade', origin: 'governance_gate', impact: 'answer_not_produced', safeAction: 'inspect_failure' };
|
|
4570
|
+
}
|
|
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) {
|
|
4588
|
+
return { version: 1, code: 'ANALYTICAL_EXECUTION_FAILED', boundary: 'sql.execute', origin: 'unknown', impact: 'execution_failed', safeAction: 'inspect_failure' };
|
|
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
|
+
}
|
|
4593
|
+
return undefined;
|
|
4594
|
+
}
|
|
4595
|
+
/**
|
|
4596
|
+
* Read only the narrow host setup receipt emitted before a connector receives
|
|
4597
|
+
* SQL. This must win over the broad analytical failure payload because a
|
|
4598
|
+
* semantic graph can catch the host error after its compiler work completed.
|
|
4599
|
+
*/
|
|
4600
|
+
function terminalConnectionSetupFailureForRun(run) {
|
|
4601
|
+
return run.artifacts.some((artifact) => {
|
|
4602
|
+
const payload = artifact.payload;
|
|
4603
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
4604
|
+
return false;
|
|
4605
|
+
const setup = payload.observabilityExecutionFailure;
|
|
4606
|
+
if (!setup || typeof setup !== 'object' || Array.isArray(setup))
|
|
4607
|
+
return false;
|
|
4608
|
+
const record = setup;
|
|
4609
|
+
return record.version === 1
|
|
4610
|
+
&& record.phase === 'execution'
|
|
4611
|
+
&& record.cause === 'connection_not_configured'
|
|
4612
|
+
&& record.safeAction === 'configure_connection';
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4615
|
+
/**
|
|
4616
|
+
* Read only a producer-owned analytical failure. A compiler failure may have
|
|
4617
|
+
* prepared SQL text, but it is still pre-execution until the durable telemetry
|
|
4618
|
+
* records a SQL call. This guard keeps a real warehouse failure from being
|
|
4619
|
+
* relabeled as semantic/planning just because a legacy adapter reused a broad
|
|
4620
|
+
* failure code in a later stage.
|
|
4621
|
+
*/
|
|
4622
|
+
function terminalCompilationFailureForRun(run) {
|
|
4623
|
+
if ((run.telemetry?.sqlExecutions ?? 0) > 0)
|
|
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) {
|
|
4669
|
+
for (const artifact of run.artifacts) {
|
|
4670
|
+
const payload = artifact.payload;
|
|
4671
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
4672
|
+
continue;
|
|
4673
|
+
const failure = payload.analyticalFailure;
|
|
4674
|
+
if (!failure || typeof failure !== 'object' || Array.isArray(failure))
|
|
4675
|
+
continue;
|
|
4676
|
+
const failureRecord = failure;
|
|
4677
|
+
if (failureRecord.code !== 'RESULT_CONTRACT_MISMATCH' || failureRecord.phase !== 'result_validation')
|
|
4678
|
+
continue;
|
|
4679
|
+
return {
|
|
4680
|
+
safeAction: terminalIncidentSafeAction(failureRecord.safeActions) ?? 'inspect_failure',
|
|
4681
|
+
};
|
|
4682
|
+
}
|
|
4683
|
+
return undefined;
|
|
4684
|
+
}
|
|
4685
|
+
function isSemanticCompilationForRun(run) {
|
|
4686
|
+
// Only the router-owned cascade may identify a semantic execution tier.
|
|
4687
|
+
// Direct/legacy semantic callers can still carry an immutable plan, but
|
|
4688
|
+
// without that authority their failure is accurately a generic plan compile
|
|
4689
|
+
// incident rather than a reconstructed semantic route.
|
|
4690
|
+
return run.routeDecision?.analyticalCascadeDecision?.selectedTier === 'semantic';
|
|
4691
|
+
}
|
|
4692
|
+
/** Keep V4's recovery action in the same compact vocabulary as trace spans. */
|
|
4693
|
+
function terminalIncidentSafeAction(value) {
|
|
4694
|
+
if (!Array.isArray(value))
|
|
4695
|
+
return undefined;
|
|
4696
|
+
const actions = [
|
|
4697
|
+
'export_redacted_trace',
|
|
4698
|
+
'configure_connection',
|
|
4699
|
+
'change_authorized_connection',
|
|
4700
|
+
'inspect_failure',
|
|
4701
|
+
'retry_same_plan',
|
|
4702
|
+
'refresh_snapshot',
|
|
4703
|
+
'edit_dql',
|
|
4704
|
+
'open_sql_notebook',
|
|
4705
|
+
'request_access',
|
|
4706
|
+
'reapply_semantic_runtime',
|
|
4707
|
+
'review_analytical_failure',
|
|
4708
|
+
'inspect_research_failures',
|
|
4709
|
+
'none',
|
|
4710
|
+
];
|
|
4711
|
+
return value.find((action) => typeof action === 'string' && actions.includes(action));
|
|
4712
|
+
}
|
|
4713
|
+
/**
|
|
4714
|
+
* A Research root is deliberately allowed to complete its receipt-bound
|
|
4715
|
+
* synthesis after child deadlines. Surface a terminal incident only when no
|
|
4716
|
+
* child completed an observation and all admitted children were bounded out;
|
|
4717
|
+
* a partially successful investigation remains a review-required answer with
|
|
4718
|
+
* a limited-scope note rather than a false failure.
|
|
4719
|
+
*/
|
|
4720
|
+
function terminalResearchBranchTimeoutForRun(run) {
|
|
4721
|
+
for (const artifact of run.artifacts) {
|
|
4722
|
+
const payload = artifact.payload;
|
|
4723
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
4724
|
+
continue;
|
|
4725
|
+
const receipts = payload.researchBranchReceipts;
|
|
4726
|
+
if (!Array.isArray(receipts) || receipts.length === 0)
|
|
4727
|
+
continue;
|
|
4728
|
+
const stopReasons = receipts
|
|
4729
|
+
.map((receipt) => receipt && typeof receipt === 'object'
|
|
4730
|
+
? receipt.stopReason
|
|
4731
|
+
: undefined)
|
|
4732
|
+
.filter((reason) => typeof reason === 'string');
|
|
4733
|
+
if (stopReasons.length !== receipts.length)
|
|
4734
|
+
continue;
|
|
4735
|
+
const allBounded = stopReasons.every((reason) => reason === 'research_branch_timeout' || reason === 'budget_exhausted');
|
|
4736
|
+
if (allBounded && stopReasons.some((reason) => reason === 'research_branch_timeout'))
|
|
4737
|
+
return true;
|
|
4738
|
+
}
|
|
4739
|
+
return false;
|
|
4740
|
+
}
|
|
4741
|
+
/**
|
|
4742
|
+
* Read only enum evidence emitted at the real connector boundary. SQL text and
|
|
4743
|
+
* redacted driver diagnostics remain in the artifact inspector; they cannot
|
|
4744
|
+
* become routing or trace-summary authority.
|
|
4745
|
+
*/
|
|
4746
|
+
function terminalWarehouseFailureForRun(run) {
|
|
4747
|
+
for (const artifact of run.artifacts) {
|
|
4748
|
+
const payload = artifact.payload;
|
|
4749
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
4750
|
+
continue;
|
|
4751
|
+
const failure = payload.warehouseFailure;
|
|
4752
|
+
if (!failure || typeof failure !== 'object' || Array.isArray(failure))
|
|
4753
|
+
continue;
|
|
4754
|
+
const record = failure;
|
|
4755
|
+
if (record.version === 1 && record.origin === 'warehouse' && typeof record.category === 'string') {
|
|
4756
|
+
return { category: record.category };
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
return undefined;
|
|
4760
|
+
}
|
|
4761
|
+
function emptyRunTelemetry(total, fallbackReason) {
|
|
4762
|
+
return {
|
|
4763
|
+
version: 1,
|
|
4764
|
+
stageDurationsMs: { total },
|
|
4765
|
+
providerRoundTrips: 0,
|
|
4766
|
+
toolCalls: 0,
|
|
4767
|
+
sqlExecutions: 0,
|
|
4768
|
+
repairs: 0,
|
|
4769
|
+
egressReceipts: 0,
|
|
4770
|
+
fallbackReason,
|
|
4771
|
+
};
|
|
4772
|
+
}
|
|
4773
|
+
function withTotalDuration(telemetry, total) {
|
|
4774
|
+
return {
|
|
4775
|
+
...telemetry,
|
|
4776
|
+
stageDurationsMs: { ...telemetry.stageDurationsMs, total },
|
|
4777
|
+
};
|
|
4778
|
+
}
|
|
4779
|
+
function durationBetweenMs(startedAt, completedAt) {
|
|
4780
|
+
const duration = Date.parse(completedAt) - Date.parse(startedAt);
|
|
4781
|
+
return Number.isFinite(duration) && duration >= 0 ? Math.min(86_400_000, duration) : 0;
|
|
2108
4782
|
}
|
|
2109
4783
|
function receiptFingerprint(value) {
|
|
2110
4784
|
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
2111
4785
|
}
|
|
2112
|
-
function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
|
|
4786
|
+
function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3, receiptV4, receiptV5, receiptV6, receiptV7, receiptV8) {
|
|
2113
4787
|
if (artifacts.length === 0) {
|
|
2114
4788
|
if (!receipt.failure)
|
|
2115
4789
|
return artifacts;
|
|
@@ -2118,7 +4792,7 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
|
|
|
2118
4792
|
kind: "answer",
|
|
2119
4793
|
title: "Agent run diagnostics",
|
|
2120
4794
|
trustState: "blocked",
|
|
2121
|
-
payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}), ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}) },
|
|
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 } : {}) },
|
|
2122
4796
|
}];
|
|
2123
4797
|
}
|
|
2124
4798
|
const preferredIndex = Math.max(0, artifacts.findIndex((artifact) => artifact.kind === "answer"));
|
|
@@ -2135,10 +4809,73 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
|
|
|
2135
4809
|
diagnosticReceipt: receipt,
|
|
2136
4810
|
...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}),
|
|
2137
4811
|
...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}),
|
|
4812
|
+
...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}),
|
|
4813
|
+
...(receiptV5 ? { diagnosticReceiptV5: receiptV5 } : {}),
|
|
4814
|
+
...(receiptV6 ? { diagnosticReceiptV6: receiptV6 } : {}),
|
|
4815
|
+
...(receiptV7 ? { diagnosticReceiptV7: receiptV7 } : {}),
|
|
4816
|
+
...(receiptV8 ? { diagnosticReceiptV8: receiptV8 } : {}),
|
|
2138
4817
|
},
|
|
2139
4818
|
};
|
|
2140
4819
|
});
|
|
2141
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
|
+
}
|
|
2142
4879
|
function computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion, terminalOutcomeMessage) {
|
|
2143
4880
|
const fallback = defaultOutcome(route);
|
|
2144
4881
|
if (isClarify) {
|
|
@@ -2150,16 +4887,32 @@ function computeStepOutcome(route, result, evaluations, request, isClarify, clar
|
|
|
2150
4887
|
summary: result.summary ?? clarifyQuestion ?? fallback.summary,
|
|
2151
4888
|
};
|
|
2152
4889
|
}
|
|
2153
|
-
const
|
|
2154
|
-
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;
|
|
2155
4903
|
// API-007 / AGT-019: a blocked analytical run may carry an intentionally
|
|
2156
4904
|
// redacted failure envelope needed for inspection and immutable repair. Keep
|
|
2157
4905
|
// only artifacts that the executor explicitly marked blocked; never retain a
|
|
2158
4906
|
// governed/reviewable artifact merely because it happened to accompany a
|
|
2159
4907
|
// terminal failure.
|
|
2160
|
-
const
|
|
4908
|
+
const rawArtifacts = status === "blocked"
|
|
2161
4909
|
? (result.artifacts ?? []).filter((artifact) => artifact.trustState === "blocked")
|
|
2162
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;
|
|
2163
4916
|
const stopReason = result.stopReason ?? stopReasonFor(route, status, trustState, artifacts);
|
|
2164
4917
|
return {
|
|
2165
4918
|
status,
|
|
@@ -2167,19 +4920,45 @@ function computeStepOutcome(route, result, evaluations, request, isClarify, clar
|
|
|
2167
4920
|
artifacts,
|
|
2168
4921
|
stopReason,
|
|
2169
4922
|
summary: status === "blocked"
|
|
2170
|
-
? terminalOutcomeMessage ?? blockingOutcomeSummary(evaluations, fallback.summary)
|
|
4923
|
+
? terminalOutcomeMessage ?? blockingOutcomeSummary(evaluations, result, fallback.summary)
|
|
2171
4924
|
: result.summary ?? fallback.summary,
|
|
2172
4925
|
...(result.answerTier ? { terminalTier: result.answerTier } : {}),
|
|
2173
4926
|
};
|
|
2174
4927
|
}
|
|
2175
|
-
|
|
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) {
|
|
2176
4945
|
const messages = evaluations
|
|
2177
4946
|
.filter((evaluation) => !evaluation.passed && evaluation.severity === 'blocking')
|
|
2178
4947
|
.map((evaluation) => evaluation.message.trim())
|
|
2179
4948
|
.filter(Boolean);
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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;
|
|
2183
4962
|
}
|
|
2184
4963
|
function consumeRepeatedClarificationSelection(request, routeDecision, result) {
|
|
2185
4964
|
const selectedEvidenceId = request.selectedEvidenceId;
|
|
@@ -2200,6 +4979,48 @@ function consumeRepeatedClarificationSelection(request, routeDecision, result) {
|
|
|
2200
4979
|
answer: message,
|
|
2201
4980
|
};
|
|
2202
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
|
+
}
|
|
2203
5024
|
function isTerminalSuccess(route, outcome) {
|
|
2204
5025
|
// A completed certified answer is the terminal success — no further steps add trust.
|
|
2205
5026
|
// A conversational reply is likewise terminal (there is no data work to chain).
|
|
@@ -2360,6 +5181,21 @@ export function selectRoute(request, decision) {
|
|
|
2360
5181
|
return 'blocked';
|
|
2361
5182
|
if (decision.action === 'block')
|
|
2362
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';
|
|
2363
5199
|
const authoritativePlan = decision.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
2364
5200
|
? decision.resolvedAnalyticalPlan
|
|
2365
5201
|
: undefined;
|
|
@@ -2609,6 +5445,11 @@ function stopReasonFor(route, status, trustState, artifacts) {
|
|
|
2609
5445
|
return "cancelled";
|
|
2610
5446
|
if (status === "blocked" || trustState === "blocked")
|
|
2611
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";
|
|
2612
5453
|
if (route === "conversation")
|
|
2613
5454
|
return "conversational_reply";
|
|
2614
5455
|
if (status === "needs_clarification")
|
|
@@ -2619,8 +5460,6 @@ function stopReasonFor(route, status, trustState, artifacts) {
|
|
|
2619
5460
|
return "governed_semantic_answer";
|
|
2620
5461
|
if (artifacts.length > 0 && route !== "generated_answer")
|
|
2621
5462
|
return "artifact_created";
|
|
2622
|
-
if (status === "needs_review")
|
|
2623
|
-
return "human_review_required";
|
|
2624
5463
|
return "generated_review_required";
|
|
2625
5464
|
}
|
|
2626
5465
|
function defaultNextActions(route, status) {
|