@duckcodeailabs/dql-agent 1.14.2 → 1.14.3-rc.1
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 +63 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +864 -29
- package/dist/agent-run-engine.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 +48 -3
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +7 -1
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +163 -0
- 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/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/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 +23 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +322 -16
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +314 -3
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +486 -43
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +64 -8
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +528 -67
- package/dist/answer-loop.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 +472 -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 +527 -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 +357 -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/generated-analytical-proposal.d.ts +37 -1
- package/dist/generated-analytical-proposal.d.ts.map +1 -1
- package/dist/generated-analytical-proposal.js +211 -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 +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +10 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +78 -6
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +331 -38
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +21 -1
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +1 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +9 -2
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +261 -11
- 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 +24 -10
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +22 -17
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +10 -9
- 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.js +22 -22
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +52 -0
- 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/resolved-analytical-plan.d.ts +18 -4
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +764 -47
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +10 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1785 -213
- 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
|
@@ -9,6 +9,8 @@ import { buildAnalysisQuestionPlan } from "./metadata/analysis-planner.js";
|
|
|
9
9
|
import { conversationHistoryFromContext, isLikelyClarificationReply, } from "./conversation/snapshot.js";
|
|
10
10
|
import { buildCoverageGap, classifyProviderFailure, } from './analytical-orchestration.js';
|
|
11
11
|
import { evaluateAnalyticalRequestPolicy } from './analytical-request-policy.js';
|
|
12
|
+
import { frozenRequiredOutputBindingProofsForPlan } from './generated-analytical-proposal.js';
|
|
13
|
+
import { attachAskTraceObserverV1, askTraceObserverForV1, finalizeAgentRunTraceV1, noOpAskTraceObserverV1, recordAuthoritativeRouterDecisionV1, recordAuthoritativePlanFreezeV1, recordEngineTraceEventV1, recordExecutionAttemptSummaryV1, } from './ask-observability/index.js';
|
|
12
14
|
/** Routes a stakeholder may never land on (analyst authoring lives in the Notebook). */
|
|
13
15
|
const ANALYST_ONLY_ROUTES = new Set(["sql_cell", "dql_block_draft", "modeling_draft", "skill_draft"]);
|
|
14
16
|
/**
|
|
@@ -199,21 +201,30 @@ export function resolveClarificationContinuation(request) {
|
|
|
199
201
|
if (!reply || (!structuredSelection && !isLikelyClarificationReply(reply)))
|
|
200
202
|
return undefined;
|
|
201
203
|
const fromServer = latestClarificationFromConversationContext(request.conversationContext);
|
|
204
|
+
const serverIssuedStructuredSelection = structuredSelection
|
|
205
|
+
? serverIssuedStructuredClarification(request)
|
|
206
|
+
: undefined;
|
|
202
207
|
const fromHistory = latestClarificationFromHistory(request.history);
|
|
203
208
|
// A UI selection is bound to the exact run that rendered the options. Carry
|
|
204
209
|
// that run's source question explicitly so the continuation still works when
|
|
205
210
|
// the optional conversation store is unavailable, after a reload, or when
|
|
206
211
|
// the user selects an option on an older visible answer. Server history still
|
|
207
212
|
// supplies the original clarifying prose when it is available.
|
|
208
|
-
const pending =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
const pending = serverIssuedStructuredSelection
|
|
214
|
+
?? (explicitSourceQuestion
|
|
215
|
+
? {
|
|
216
|
+
sourceQuestion: explicitSourceQuestion,
|
|
217
|
+
clarifyingQuestion: fromServer?.clarifyingQuestion
|
|
218
|
+
?? fromHistory?.clarifyingQuestion
|
|
219
|
+
?? 'Which governed meaning should be used?',
|
|
220
|
+
}
|
|
221
|
+
: fromServer ?? fromHistory);
|
|
222
|
+
// A structured option intentionally submits the original question together
|
|
223
|
+
// with a stable, server-issued identifier. Treating that exact text as a
|
|
224
|
+
// fresh question drops the persisted typed frame and makes the router ask
|
|
225
|
+
// the same clarification again. The equality guard remains important for
|
|
226
|
+
// free-text replies, where an unchanged question carries no new meaning.
|
|
227
|
+
if (!pending || (!structuredSelection && pending.sourceQuestion.trim().toLowerCase() === reply.toLowerCase()))
|
|
217
228
|
return undefined;
|
|
218
229
|
return {
|
|
219
230
|
...pending,
|
|
@@ -226,6 +237,44 @@ export function resolveClarificationContinuation(request) {
|
|
|
226
237
|
].join('\n\n'),
|
|
227
238
|
};
|
|
228
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Use the original server snapshot for an identifier-bound continuation when
|
|
242
|
+
* it carries the host-only authority record that local runtime reconstructs
|
|
243
|
+
* from its persisted thread. This prevents a browser-provided label (or a
|
|
244
|
+
* stale client source question) from replacing the typed analytical frame
|
|
245
|
+
* before router validation. The router still performs the final snapshot and
|
|
246
|
+
* option-ID validation before any plan can freeze.
|
|
247
|
+
*/
|
|
248
|
+
function serverIssuedStructuredClarification(request) {
|
|
249
|
+
if (!request.threadId)
|
|
250
|
+
return undefined;
|
|
251
|
+
const context = clarificationRecord(request.conversationContext);
|
|
252
|
+
const authority = clarificationRecord(context?.serverIssuedClarificationSelection);
|
|
253
|
+
if (authority?.version !== 1
|
|
254
|
+
|| clarificationString(authority.threadId) !== request.threadId)
|
|
255
|
+
return undefined;
|
|
256
|
+
const authoritySourceTurnId = clarificationString(authority.sourceTurnId);
|
|
257
|
+
const authoritySnapshotId = clarificationString(authority.snapshotId);
|
|
258
|
+
if (!authoritySourceTurnId || !authoritySnapshotId)
|
|
259
|
+
return undefined;
|
|
260
|
+
for (const source of [
|
|
261
|
+
clarificationRecord(context?.conversationEnvelope),
|
|
262
|
+
clarificationRecord(context?.serverSnapshot),
|
|
263
|
+
]) {
|
|
264
|
+
if (clarificationString(source?.threadId) !== request.threadId)
|
|
265
|
+
continue;
|
|
266
|
+
const pending = clarificationRecord(source?.pendingClarification);
|
|
267
|
+
const selection = clarificationRecord(pending?.selection);
|
|
268
|
+
if (clarificationString(pending?.sourceTurnId) !== authoritySourceTurnId
|
|
269
|
+
|| clarificationString(selection?.snapshotId) !== authoritySnapshotId)
|
|
270
|
+
continue;
|
|
271
|
+
const sourceQuestion = clarificationString(pending?.sourceQuestion);
|
|
272
|
+
const clarifyingQuestion = clarificationString(pending?.question);
|
|
273
|
+
if (sourceQuestion && clarifyingQuestion)
|
|
274
|
+
return { sourceQuestion, clarifyingQuestion };
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
229
278
|
function latestClarificationFromHistory(history) {
|
|
230
279
|
if (!history?.length)
|
|
231
280
|
return undefined;
|
|
@@ -467,6 +516,7 @@ export class AgentRunEngine {
|
|
|
467
516
|
budgetModel;
|
|
468
517
|
maxSteps;
|
|
469
518
|
routeTimeoutSignal;
|
|
519
|
+
traceObserverFactory;
|
|
470
520
|
constructor(options = {}) {
|
|
471
521
|
this.executors = options.executors ?? {};
|
|
472
522
|
this.gates = options.gates ?? {};
|
|
@@ -485,6 +535,7 @@ export class AgentRunEngine {
|
|
|
485
535
|
};
|
|
486
536
|
this.maxSteps = Math.max(1, options.maxSteps ?? DEFAULT_MAX_STEPS);
|
|
487
537
|
this.routeTimeoutSignal = options.routeTimeoutSignal ?? ((durationMs) => AbortSignal.timeout(durationMs));
|
|
538
|
+
this.traceObserverFactory = options.traceObserverFactory;
|
|
488
539
|
}
|
|
489
540
|
/**
|
|
490
541
|
* Decide the high-level action. A forced `requestedMode` bypasses routing.
|
|
@@ -542,6 +593,59 @@ export class AgentRunEngine {
|
|
|
542
593
|
const startedAt = this.timestamp();
|
|
543
594
|
const runStartedAtMs = Date.parse(startedAt);
|
|
544
595
|
const requestedMode = request.requestedMode ?? "auto";
|
|
596
|
+
// OBS-001/OBS-002: valid engine requests receive a server-owned trace after
|
|
597
|
+
// their run ID is known. The observer is explicitly non-authoritative and
|
|
598
|
+
// a factory/store failure becomes a no-op, never an Ask failure.
|
|
599
|
+
let traceObserver = noOpAskTraceObserverV1;
|
|
600
|
+
try {
|
|
601
|
+
traceObserver = this.traceObserverFactory?.({ runId, request, startedAt, requestedMode })
|
|
602
|
+
?? noOpAskTraceObserverV1;
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
traceObserver = noOpAskTraceObserverV1;
|
|
606
|
+
}
|
|
607
|
+
request = attachAskTraceObserverV1({ ...request }, traceObserver);
|
|
608
|
+
// Continuity is relationship evidence, not new routing input. Keep only
|
|
609
|
+
// stable run IDs and one-way fingerprints so a trace can explain why this
|
|
610
|
+
// turn reused a clarification/result/derived plan without persisting chat
|
|
611
|
+
// text, values, SQL, or an invented parent trace.
|
|
612
|
+
if (clarificationContinuation) {
|
|
613
|
+
traceObserver.recordLink({
|
|
614
|
+
kind: 'clarification_continuation',
|
|
615
|
+
choiceFingerprint: traceLinkFingerprint(request.selectedEvidenceId ?? clarificationContinuation.sourceQuestion),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (request.selectedResultBinding) {
|
|
619
|
+
traceObserver.recordLink({
|
|
620
|
+
kind: 'prior_result',
|
|
621
|
+
targetRunId: request.selectedResultBinding.sourceRunId,
|
|
622
|
+
choiceFingerprint: traceLinkFingerprint([
|
|
623
|
+
request.selectedResultBinding.sourceArtifactId,
|
|
624
|
+
request.selectedResultBinding.canonicalColumn,
|
|
625
|
+
request.selectedResultBinding.rowFingerprint,
|
|
626
|
+
request.selectedResultBinding.resultFingerprint,
|
|
627
|
+
].join('\u0000')),
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const derivedSourceRunId = traceDerivedSourceRunId(request.workspaceContext);
|
|
631
|
+
if (derivedSourceRunId) {
|
|
632
|
+
traceObserver.recordLink({
|
|
633
|
+
kind: 'derived_repair',
|
|
634
|
+
targetRunId: derivedSourceRunId,
|
|
635
|
+
choiceFingerprint: traceLinkFingerprint(derivedSourceRunId),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
const conversationBinding = traceConversationBinding(request, clarificationContinuation);
|
|
639
|
+
const conversationTrace = traceObserver.startSpan({
|
|
640
|
+
name: 'conversation.hydrate',
|
|
641
|
+
stage: 'conversation',
|
|
642
|
+
payload: {
|
|
643
|
+
kind: 'conversation',
|
|
644
|
+
continuation: conversationBinding !== 'none',
|
|
645
|
+
binding: conversationBinding,
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
traceObserver.finishSpan(conversationTrace, { outcome: 'ok', reasonCode: 'completed' });
|
|
545
649
|
const runBudget = request.runBudget ?? createAgentRunBudget({
|
|
546
650
|
requestedMode,
|
|
547
651
|
startedAtMs: runStartedAtMs,
|
|
@@ -549,7 +653,11 @@ export class AgentRunEngine {
|
|
|
549
653
|
timeoutSignal: this.routeTimeoutSignal,
|
|
550
654
|
nowMs: () => this.now().getTime(),
|
|
551
655
|
});
|
|
552
|
-
|
|
656
|
+
// Preserve the non-enumerable observer across the immutable request update.
|
|
657
|
+
// A plain spread drops symbol properties, which previously made the router
|
|
658
|
+
// lose candidate/cascade/freeze evidence even though the engine still
|
|
659
|
+
// emitted its own outer spans.
|
|
660
|
+
request = attachAskTraceObserverV1({ ...request, runBudget, signal: runBudget.hardSignal }, traceObserver);
|
|
553
661
|
const events = [];
|
|
554
662
|
let plan;
|
|
555
663
|
const executedSteps = [];
|
|
@@ -573,6 +681,7 @@ export class AgentRunEngine {
|
|
|
573
681
|
startedAt,
|
|
574
682
|
updatedAt: startedAt,
|
|
575
683
|
},
|
|
684
|
+
...(traceObserver.reference() ? { traceReference: traceObserver.reference() } : {}),
|
|
576
685
|
};
|
|
577
686
|
let checkpointQueue = Promise.resolve();
|
|
578
687
|
const persistProgress = () => {
|
|
@@ -632,6 +741,12 @@ export class AgentRunEngine {
|
|
|
632
741
|
}
|
|
633
742
|
if (full.type === "step.completed")
|
|
634
743
|
progress.steps = [...executedSteps];
|
|
744
|
+
// Event payloads are intentionally not copied: they may contain raw
|
|
745
|
+
// answer/tool data. The typed mapping records only stage identity.
|
|
746
|
+
recordEngineTraceEventV1(traceObserver, full);
|
|
747
|
+
const traceReference = traceObserver.reference();
|
|
748
|
+
if (traceReference)
|
|
749
|
+
progress.traceReference = traceReference;
|
|
635
750
|
persistProgress();
|
|
636
751
|
onEvent?.(full);
|
|
637
752
|
};
|
|
@@ -728,6 +843,7 @@ export class AgentRunEngine {
|
|
|
728
843
|
id: runId,
|
|
729
844
|
question: submittedQuestion,
|
|
730
845
|
requestedMode,
|
|
846
|
+
conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
|
|
731
847
|
route: 'blocked',
|
|
732
848
|
status: 'blocked',
|
|
733
849
|
trustState: 'blocked',
|
|
@@ -754,7 +870,12 @@ export class AgentRunEngine {
|
|
|
754
870
|
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
755
871
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
756
872
|
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
757
|
-
run.
|
|
873
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
874
|
+
run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
|
|
875
|
+
// Observability is deliberately finalized only after the authoritative
|
|
876
|
+
// receipt exists, and before the ordinary run store persists its compact
|
|
877
|
+
// reference. A local trace write failure never changes this outcome.
|
|
878
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
758
879
|
await checkpointQueue;
|
|
759
880
|
await this.store?.save(run);
|
|
760
881
|
return run;
|
|
@@ -765,6 +886,11 @@ export class AgentRunEngine {
|
|
|
765
886
|
// pre-try await escaped the engine and left active UI runs looking endless.
|
|
766
887
|
let routeDecision = buildIntentDecision(request);
|
|
767
888
|
try {
|
|
889
|
+
const classifySpan = traceObserver.startSpan({
|
|
890
|
+
name: 'request.classify',
|
|
891
|
+
stage: 'request',
|
|
892
|
+
payload: { kind: 'stage', requestedMode },
|
|
893
|
+
});
|
|
768
894
|
routeDecision = clarificationContinuation && !request.selectedEvidenceId
|
|
769
895
|
? {
|
|
770
896
|
action: "answer",
|
|
@@ -775,6 +901,11 @@ export class AgentRunEngine {
|
|
|
775
901
|
}
|
|
776
902
|
: await awaitWithAbort(this.decideRoute(request), request.signal);
|
|
777
903
|
routeDecision = enforceOrdinaryAnalyticalPlanBoundary(request, routeDecision);
|
|
904
|
+
traceObserver.finishSpan(classifySpan, { outcome: 'ok', reasonCode: 'route_selected' });
|
|
905
|
+
// Router/cascade evidence is captured after its authoritative decision
|
|
906
|
+
// is sealed. The trace adapter only projects IDs, counters, and typed
|
|
907
|
+
// receipts; it never participates in route selection.
|
|
908
|
+
recordAuthoritativeRouterDecisionV1(traceObserver, routeDecision);
|
|
778
909
|
const defaultRoute = answerAnywayRoute(constrainRouteForAudience(selectRoute(request, routeDecision), audience), request, audience, routeDecision);
|
|
779
910
|
const authoritativeAsk = routeDecision.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
780
911
|
&& requestedMode !== 'research';
|
|
@@ -859,6 +990,10 @@ export class AgentRunEngine {
|
|
|
859
990
|
: `Re-running ${route.replaceAll("_", " ")} executor (repair attempt ${attempt}).`,
|
|
860
991
|
route,
|
|
861
992
|
});
|
|
993
|
+
// Provider readiness belongs at the provider boundary. The engine
|
|
994
|
+
// cannot infer it from an executor return value: a deterministic
|
|
995
|
+
// route may be provider-free and a provider route can fail during
|
|
996
|
+
// preflight before any executor result exists.
|
|
862
997
|
result = await this.executeRoute({
|
|
863
998
|
runId,
|
|
864
999
|
request,
|
|
@@ -873,12 +1008,21 @@ export class AgentRunEngine {
|
|
|
873
1008
|
emit,
|
|
874
1009
|
emitAnswerDelta: onAnswerDelta,
|
|
875
1010
|
});
|
|
1011
|
+
recordExecutionAttemptSummaryV1(traceObserver, result);
|
|
876
1012
|
// The router owns a frozen analytical tier. An executor may report a
|
|
877
1013
|
// same-tier execution failure, but it cannot turn a certified or
|
|
878
1014
|
// semantic plan into generated work (or vice versa) after execution
|
|
879
1015
|
// has started. Keep this guard in the engine as well as host adapters
|
|
880
1016
|
// so an injected/legacy executor cannot redefine durable provenance.
|
|
1017
|
+
const planWasFrozen = routeDecision.analyticalCascadeDecision?.planFrozen === true;
|
|
881
1018
|
routeDecision = applyExploratoryExecutionFreeze(routeDecision, result.analyticalExecutionFreeze);
|
|
1019
|
+
routeDecision = applyExploratoryExecutionFreeze(routeDecision, result.analyticalExecutionRepairFreeze);
|
|
1020
|
+
// The router froze the exploratory plan before SQL generation. The
|
|
1021
|
+
// host receipt below only authorizes this exact SQL/target against
|
|
1022
|
+
// that immutable plan; it never creates a second freeze transition.
|
|
1023
|
+
if (!planWasFrozen && routeDecision.analyticalCascadeDecision?.planFrozen) {
|
|
1024
|
+
recordAuthoritativePlanFreezeV1(traceObserver, routeDecision.analyticalCascadeDecision);
|
|
1025
|
+
}
|
|
882
1026
|
result = preserveFrozenAnalyticalRoute(route, routeDecision, result);
|
|
883
1027
|
result = consumeRepeatedClarificationSelection(request, routeDecision, result);
|
|
884
1028
|
if (result.analyticalTurnPlan)
|
|
@@ -1114,7 +1258,9 @@ export class AgentRunEngine {
|
|
|
1114
1258
|
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
1115
1259
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1116
1260
|
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1117
|
-
run.
|
|
1261
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
1262
|
+
run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
|
|
1263
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
1118
1264
|
await checkpointQueue;
|
|
1119
1265
|
await this.store?.save(run);
|
|
1120
1266
|
return run;
|
|
@@ -1166,6 +1312,7 @@ export class AgentRunEngine {
|
|
|
1166
1312
|
id: runId,
|
|
1167
1313
|
question: submittedQuestion,
|
|
1168
1314
|
requestedMode,
|
|
1315
|
+
conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
|
|
1169
1316
|
route: cancelledRoute,
|
|
1170
1317
|
status: "cancelled",
|
|
1171
1318
|
trustState: "not_applicable",
|
|
@@ -1201,6 +1348,8 @@ export class AgentRunEngine {
|
|
|
1201
1348
|
};
|
|
1202
1349
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1203
1350
|
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1351
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
1352
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
1204
1353
|
await checkpointQueue;
|
|
1205
1354
|
await this.store?.save(run);
|
|
1206
1355
|
return run;
|
|
@@ -1208,7 +1357,9 @@ export class AgentRunEngine {
|
|
|
1208
1357
|
const message = isOrchestrationBudgetExhausted(err)
|
|
1209
1358
|
? 'Ask could not complete within its bounded orchestration. Nothing was executed; narrow the metric or dimension and retry.'
|
|
1210
1359
|
: err instanceof Error && err.name === "TimeoutError"
|
|
1211
|
-
?
|
|
1360
|
+
? requestedMode === 'research'
|
|
1361
|
+
? '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.'
|
|
1362
|
+
: "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
1363
|
: err instanceof Error ? err.message : String(err);
|
|
1213
1364
|
const failedRoute = progress.route;
|
|
1214
1365
|
const failedPhase = progress.lifecycle.phase;
|
|
@@ -1220,7 +1371,7 @@ export class AgentRunEngine {
|
|
|
1220
1371
|
trustState: "blocked",
|
|
1221
1372
|
});
|
|
1222
1373
|
const completedAt = this.timestamp();
|
|
1223
|
-
const failure = diagnosticFailureFromError(err, failedPhase);
|
|
1374
|
+
const failure = diagnosticFailureFromError(err, failedPhase, requestedMode);
|
|
1224
1375
|
const evaluations = [
|
|
1225
1376
|
...progress.evaluations,
|
|
1226
1377
|
{
|
|
@@ -1248,6 +1399,7 @@ export class AgentRunEngine {
|
|
|
1248
1399
|
id: runId,
|
|
1249
1400
|
question: submittedQuestion,
|
|
1250
1401
|
requestedMode,
|
|
1402
|
+
conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
|
|
1251
1403
|
route: "blocked",
|
|
1252
1404
|
status: "blocked",
|
|
1253
1405
|
trustState: "blocked",
|
|
@@ -1285,7 +1437,9 @@ export class AgentRunEngine {
|
|
|
1285
1437
|
};
|
|
1286
1438
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1287
1439
|
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1288
|
-
run.
|
|
1440
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
1441
|
+
run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
|
|
1442
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
1289
1443
|
await checkpointQueue;
|
|
1290
1444
|
await this.store?.save(run);
|
|
1291
1445
|
return run;
|
|
@@ -1302,6 +1456,7 @@ export class AgentRunEngine {
|
|
|
1302
1456
|
id: input.runId,
|
|
1303
1457
|
question: input.request.question,
|
|
1304
1458
|
requestedMode: input.requestedMode,
|
|
1459
|
+
conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
|
|
1305
1460
|
route: "blocked",
|
|
1306
1461
|
status: "blocked",
|
|
1307
1462
|
trustState: "blocked",
|
|
@@ -1348,6 +1503,7 @@ export class AgentRunEngine {
|
|
|
1348
1503
|
id: input.runId,
|
|
1349
1504
|
question: input.request.question,
|
|
1350
1505
|
requestedMode: input.requestedMode,
|
|
1506
|
+
conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
|
|
1351
1507
|
route,
|
|
1352
1508
|
status: finalOutcome.status,
|
|
1353
1509
|
trustState: finalOutcome.trustState,
|
|
@@ -1411,9 +1567,13 @@ export class AgentRunEngine {
|
|
|
1411
1567
|
const signal = context.request.runBudget?.hardSignal ?? context.request.signal;
|
|
1412
1568
|
if (signal?.aborted)
|
|
1413
1569
|
throw signal.reason ?? routeTimeoutError();
|
|
1570
|
+
// Route executors own physical provider/tool/SQL boundaries. Preserve the
|
|
1571
|
+
// non-enumerable observer when adding the run signal; a normal object
|
|
1572
|
+
// spread would otherwise leave canonical routing evidence intact while
|
|
1573
|
+
// silently dropping every physical execution span.
|
|
1414
1574
|
const execution = Promise.resolve(executor({
|
|
1415
1575
|
...context,
|
|
1416
|
-
request: { ...context.request, ...(signal ? { signal } : {}) },
|
|
1576
|
+
request: attachAskTraceObserverV1({ ...context.request, ...(signal ? { signal } : {}) }, askTraceObserverForV1(context.request)),
|
|
1417
1577
|
}));
|
|
1418
1578
|
return awaitWithAbort(execution, signal);
|
|
1419
1579
|
}
|
|
@@ -1431,6 +1591,44 @@ export class AgentRunEngine {
|
|
|
1431
1591
|
return this.now().toISOString();
|
|
1432
1592
|
}
|
|
1433
1593
|
}
|
|
1594
|
+
function traceLinkFingerprint(value) {
|
|
1595
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
1596
|
+
}
|
|
1597
|
+
/** Only a host-produced repair derivation is linkable; ordinary authoring context is not. */
|
|
1598
|
+
function traceDerivedSourceRunId(workspaceContext) {
|
|
1599
|
+
if (!workspaceContext || typeof workspaceContext !== 'object' || Array.isArray(workspaceContext))
|
|
1600
|
+
return undefined;
|
|
1601
|
+
const record = workspaceContext;
|
|
1602
|
+
const derivation = record.traceDerivation;
|
|
1603
|
+
if (derivation !== 'analytical_repair' && derivation !== 'derived_repair')
|
|
1604
|
+
return undefined;
|
|
1605
|
+
return typeof record.sourceRunId === 'string' && record.sourceRunId.trim()
|
|
1606
|
+
? record.sourceRunId
|
|
1607
|
+
: undefined;
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Record only why the conversation boundary was available, never the member,
|
|
1611
|
+
* question, row, or free-text selection that it carried. The local runtime
|
|
1612
|
+
* resolves its typed follow-up before retrieval; this trace label lets an
|
|
1613
|
+
* office reproduction distinguish a missing binding from a later retrieval or
|
|
1614
|
+
* execution failure without turning traces into chat persistence.
|
|
1615
|
+
*/
|
|
1616
|
+
function traceConversationBinding(request, clarification) {
|
|
1617
|
+
if (clarification || request.selectedEvidenceId)
|
|
1618
|
+
return 'structured_clarification';
|
|
1619
|
+
if (request.conversationBinding)
|
|
1620
|
+
return request.conversationBinding;
|
|
1621
|
+
if (request.selectedResultBinding)
|
|
1622
|
+
return 'prior_result';
|
|
1623
|
+
const context = request.conversationContext;
|
|
1624
|
+
if (!context || Object.keys(context).length === 0)
|
|
1625
|
+
return 'none';
|
|
1626
|
+
if ('analyticalTaskDependencyBinding' in context)
|
|
1627
|
+
return 'task_dependency';
|
|
1628
|
+
// A snapshot merely makes a prior binding *available*. It is not evidence
|
|
1629
|
+
// that this self-contained question selected it.
|
|
1630
|
+
return 'none';
|
|
1631
|
+
}
|
|
1434
1632
|
/**
|
|
1435
1633
|
* Ordinary analytical Ask cannot delegate meaning to the legacy answer
|
|
1436
1634
|
* generator. Retrieval/meaning must first produce the immutable RAP consumed by
|
|
@@ -1595,14 +1793,19 @@ function applyExploratoryExecutionFreeze(decision, freeze) {
|
|
|
1595
1793
|
return decision;
|
|
1596
1794
|
const cascade = decision.analyticalCascadeDecision;
|
|
1597
1795
|
const attempt = cascade?.attempts.find((candidate) => candidate.tier === 'exploratory_sql');
|
|
1796
|
+
const selectedPlan = decision.resolvedAnalyticalPlan;
|
|
1797
|
+
const existing = cascade?.exploratoryExecutionFreeze;
|
|
1798
|
+
const existingRepair = cascade?.exploratoryRepairExecutionFreeze;
|
|
1799
|
+
const authorizationAttempt = normalizedExploratoryAuthorizationAttempt(freeze);
|
|
1598
1800
|
const sameCandidates = Boolean(attempt
|
|
1599
1801
|
&& attempt.candidateIds.length === freeze.candidateIds.length
|
|
1600
1802
|
&& attempt.candidateIds.every((candidate, index) => candidate === freeze.candidateIds[index]));
|
|
1601
1803
|
const retrievalSnapshotId = decision.retrievalEvidence?.snapshotId;
|
|
1602
|
-
const
|
|
1804
|
+
const validBaseReceipt = Boolean(cascade
|
|
1603
1805
|
&& cascade.selectedTier === 'exploratory_sql'
|
|
1604
|
-
&& cascade.planFrozen ===
|
|
1806
|
+
&& cascade.planFrozen === true
|
|
1605
1807
|
&& attempt?.outcome === 'executable'
|
|
1808
|
+
&& attempt.planFrozen === true
|
|
1606
1809
|
&& sameCandidates
|
|
1607
1810
|
&& freeze.version === 1
|
|
1608
1811
|
&& freeze.selectedTier === 'exploratory_sql'
|
|
@@ -1612,28 +1815,132 @@ function applyExploratoryExecutionFreeze(decision, freeze) {
|
|
|
1612
1815
|
&& freeze.snapshotId.trim()
|
|
1613
1816
|
&& freeze.targetFingerprint.trim()
|
|
1614
1817
|
&& freeze.sqlFingerprint.trim()
|
|
1818
|
+
&& selectedPlan?.capability === 'bounded_exploration'
|
|
1819
|
+
&& selectedPlan.planId === freeze.planId
|
|
1820
|
+
&& selectedPlan.fingerprint === freeze.planFingerprint
|
|
1821
|
+
&& selectedPlan.snapshotId === freeze.snapshotId
|
|
1822
|
+
&& freezeCarriesRequiredOutputBindings(selectedPlan, freeze)
|
|
1615
1823
|
&& (!retrievalSnapshotId || retrievalSnapshotId === freeze.snapshotId));
|
|
1616
|
-
if (!
|
|
1617
|
-
throw
|
|
1618
|
-
|
|
1619
|
-
|
|
1824
|
+
if (!validBaseReceipt) {
|
|
1825
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
1826
|
+
}
|
|
1827
|
+
// A replay of one exact host handoff is harmless. A repair is a fresh,
|
|
1828
|
+
// separately-minted capability, but its receipt must name the initial SQL
|
|
1829
|
+
// authorization and keep every immutable plan binding identical.
|
|
1830
|
+
if (authorizationAttempt.index === 0) {
|
|
1831
|
+
if (existing) {
|
|
1832
|
+
if (sameExploratoryAuthorizationReceipt(existing, freeze))
|
|
1833
|
+
return decision;
|
|
1834
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
1835
|
+
}
|
|
1836
|
+
if (existingRepair)
|
|
1837
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
1838
|
+
return withExploratoryAuthorizationReceipt(decision, freeze, 'initial');
|
|
1620
1839
|
}
|
|
1840
|
+
if (authorizationAttempt.index !== 1
|
|
1841
|
+
|| !authorizationAttempt.parentSqlFingerprint
|
|
1842
|
+
|| !existing
|
|
1843
|
+
|| existingRepair
|
|
1844
|
+
|| authorizationAttempt.parentSqlFingerprint !== existing.sqlFingerprint
|
|
1845
|
+
|| !sameExploratoryPlanBindings(existing, freeze)) {
|
|
1846
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
1847
|
+
}
|
|
1848
|
+
return withExploratoryAuthorizationReceipt(decision, freeze, 'repair');
|
|
1849
|
+
}
|
|
1850
|
+
function withExploratoryAuthorizationReceipt(decision, freeze, kind) {
|
|
1851
|
+
const cascade = decision.analyticalCascadeDecision;
|
|
1621
1852
|
return {
|
|
1622
1853
|
...decision,
|
|
1623
1854
|
analyticalCascadeDecision: {
|
|
1624
1855
|
...cascade,
|
|
1625
|
-
|
|
1626
|
-
|
|
1856
|
+
...(kind === 'initial'
|
|
1857
|
+
? { exploratoryExecutionFreeze: freeze }
|
|
1858
|
+
: { exploratoryRepairExecutionFreeze: freeze }),
|
|
1627
1859
|
attempts: cascade.attempts.map((candidate) => candidate.tier === 'exploratory_sql'
|
|
1628
1860
|
? {
|
|
1629
1861
|
...candidate,
|
|
1630
|
-
|
|
1631
|
-
|
|
1862
|
+
// The router froze the plan before SQL generation. The host only
|
|
1863
|
+
// binds exact SQL/target bytes to that immutable plan. A repair
|
|
1864
|
+
// cannot choose another tier or mutate the analytical frame.
|
|
1865
|
+
reason: kind === 'repair'
|
|
1866
|
+
? `${candidate.reason} Host authorized one same-plan SQL repair against frozen plan ${freeze.planId}.`
|
|
1867
|
+
: `${candidate.reason} Host authorized SQL execution against frozen plan ${freeze.planId}.`,
|
|
1632
1868
|
}
|
|
1633
1869
|
: candidate),
|
|
1634
1870
|
},
|
|
1635
1871
|
};
|
|
1636
1872
|
}
|
|
1873
|
+
function normalizedExploratoryAuthorizationAttempt(freeze) {
|
|
1874
|
+
const attempt = freeze.authorizationAttempt;
|
|
1875
|
+
// V1/V3 persisted receipts predate explicit authorization-attempt evidence.
|
|
1876
|
+
// They are compatible only as the original handoff, never as a repair.
|
|
1877
|
+
if (!attempt)
|
|
1878
|
+
return { index: 0 };
|
|
1879
|
+
if (attempt.version !== 1 || (attempt.index !== 0 && attempt.index !== 1)) {
|
|
1880
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
1881
|
+
}
|
|
1882
|
+
if (attempt.index === 0) {
|
|
1883
|
+
if ('parentSqlFingerprint' in attempt && attempt.parentSqlFingerprint) {
|
|
1884
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
1885
|
+
}
|
|
1886
|
+
return { index: 0 };
|
|
1887
|
+
}
|
|
1888
|
+
if (!attempt.parentSqlFingerprint?.trim())
|
|
1889
|
+
throw exploratoryAuthorizationStateMismatch();
|
|
1890
|
+
return { index: 1, parentSqlFingerprint: attempt.parentSqlFingerprint };
|
|
1891
|
+
}
|
|
1892
|
+
function sameExploratoryPlanBindings(left, right) {
|
|
1893
|
+
return left.version === right.version
|
|
1894
|
+
&& left.selectedTier === right.selectedTier
|
|
1895
|
+
&& left.planId === right.planId
|
|
1896
|
+
&& left.planFingerprint === right.planFingerprint
|
|
1897
|
+
&& left.snapshotId === right.snapshotId
|
|
1898
|
+
&& left.targetFingerprint === right.targetFingerprint
|
|
1899
|
+
&& left.authorization === right.authorization
|
|
1900
|
+
&& sameFrozenRequiredOutputBindings(left.requiredOutputBindings, right.requiredOutputBindings)
|
|
1901
|
+
&& left.candidateIds.length === right.candidateIds.length
|
|
1902
|
+
&& left.candidateIds.every((candidate, index) => candidate === right.candidateIds[index]);
|
|
1903
|
+
}
|
|
1904
|
+
function freezeCarriesRequiredOutputBindings(plan, freeze) {
|
|
1905
|
+
if (!plan)
|
|
1906
|
+
return false;
|
|
1907
|
+
// Pre-V4 persisted plans did not carry an output contract. They remain
|
|
1908
|
+
// readable, but newly frozen plans with explicit outputs must carry the
|
|
1909
|
+
// exact physical binding proofs below.
|
|
1910
|
+
const required = plan.outputContract?.requiredOutputs ?? [];
|
|
1911
|
+
if (required.length === 0)
|
|
1912
|
+
return true;
|
|
1913
|
+
const expected = frozenRequiredOutputBindingProofsForPlan(plan);
|
|
1914
|
+
const actual = freeze.requiredOutputBindings;
|
|
1915
|
+
return expected.length === required.length
|
|
1916
|
+
&& Array.isArray(actual)
|
|
1917
|
+
&& sameFrozenRequiredOutputBindings(actual, expected);
|
|
1918
|
+
}
|
|
1919
|
+
function sameFrozenRequiredOutputBindings(left, right) {
|
|
1920
|
+
const normalize = (bindings) => (bindings ?? []).map((binding) => [
|
|
1921
|
+
binding.version,
|
|
1922
|
+
binding.outputName.toLowerCase().replace(/["`\[\]]/g, ''),
|
|
1923
|
+
binding.qualifiedId,
|
|
1924
|
+
binding.relation.toLowerCase().replace(/["`\[\]]/g, '').replace(/\s*\.\s*/g, '.'),
|
|
1925
|
+
binding.column.toLowerCase().replace(/["`\[\]]/g, ''),
|
|
1926
|
+
].join('|')).sort();
|
|
1927
|
+
const leftBindings = normalize(left);
|
|
1928
|
+
const rightBindings = normalize(right);
|
|
1929
|
+
return leftBindings.length === rightBindings.length
|
|
1930
|
+
&& leftBindings.every((binding, index) => binding === rightBindings[index]);
|
|
1931
|
+
}
|
|
1932
|
+
function sameExploratoryAuthorizationReceipt(left, right) {
|
|
1933
|
+
const leftAttempt = normalizedExploratoryAuthorizationAttempt(left);
|
|
1934
|
+
const rightAttempt = normalizedExploratoryAuthorizationAttempt(right);
|
|
1935
|
+
return sameExploratoryPlanBindings(left, right)
|
|
1936
|
+
&& left.sqlFingerprint === right.sqlFingerprint
|
|
1937
|
+
&& left.authorization === right.authorization
|
|
1938
|
+
&& leftAttempt.index === rightAttempt.index
|
|
1939
|
+
&& leftAttempt.parentSqlFingerprint === rightAttempt.parentSqlFingerprint;
|
|
1940
|
+
}
|
|
1941
|
+
function exploratoryAuthorizationStateMismatch() {
|
|
1942
|
+
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' });
|
|
1943
|
+
}
|
|
1637
1944
|
/**
|
|
1638
1945
|
* Has this exact clarification already been asked in this thread?
|
|
1639
1946
|
*
|
|
@@ -1946,10 +2253,21 @@ function terminalLifecycle(prior, phase, completedAt, eventCursor) {
|
|
|
1946
2253
|
completedAt,
|
|
1947
2254
|
};
|
|
1948
2255
|
}
|
|
1949
|
-
function diagnosticFailureFromError(error, phase) {
|
|
2256
|
+
function diagnosticFailureFromError(error, phase, requestedMode) {
|
|
1950
2257
|
const name = error instanceof Error ? error.name : "";
|
|
1951
2258
|
const message = error instanceof Error ? error.message : String(error);
|
|
1952
2259
|
const lower = `${name} ${message}`.toLowerCase();
|
|
2260
|
+
if (error
|
|
2261
|
+
&& typeof error === 'object'
|
|
2262
|
+
&& error.code === 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH') {
|
|
2263
|
+
return {
|
|
2264
|
+
code: 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH',
|
|
2265
|
+
phase: 'sql.authorize',
|
|
2266
|
+
message: 'The frozen exploratory plan did not match the SQL authorization receipt. Execution was not attempted.',
|
|
2267
|
+
recoverable: false,
|
|
2268
|
+
safeActions: ['export_redacted_trace'],
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
1953
2271
|
if (isOrchestrationBudgetExhausted(error)) {
|
|
1954
2272
|
return {
|
|
1955
2273
|
code: 'orchestration_budget_exhausted',
|
|
@@ -1960,6 +2278,15 @@ function diagnosticFailureFromError(error, phase) {
|
|
|
1960
2278
|
};
|
|
1961
2279
|
}
|
|
1962
2280
|
if (name === "TimeoutError" || lower.includes("time limit") || lower.includes("timeout")) {
|
|
2281
|
+
if (requestedMode === 'research') {
|
|
2282
|
+
return {
|
|
2283
|
+
code: 'RESEARCH_RUN_DEADLINE',
|
|
2284
|
+
phase: 'research.run',
|
|
2285
|
+
message: 'Research reached its bounded run deadline before finalization.',
|
|
2286
|
+
recoverable: true,
|
|
2287
|
+
safeActions: ['inspect_failure'],
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
1963
2290
|
return {
|
|
1964
2291
|
code: "TIMEOUT",
|
|
1965
2292
|
phase,
|
|
@@ -2058,6 +2385,11 @@ function diagnosticReceiptV3ForRun(run) {
|
|
|
2058
2385
|
// route names or identifier text here: that erased stale/error lane states
|
|
2059
2386
|
// and falsely reported governed-relational success for pure exploration.
|
|
2060
2387
|
const cascade = run.routeDecision?.analyticalCascadeDecision;
|
|
2388
|
+
// The router may retain a broader terminal witness for presentation, while
|
|
2389
|
+
// the cascade carries the only persistable, enumerated relationship-proof
|
|
2390
|
+
// receipt. Prefer that immutable cascade value and do not infer a gap from a
|
|
2391
|
+
// failure message or route label here.
|
|
2392
|
+
const terminalGap = cascade?.terminalGap;
|
|
2061
2393
|
const sourceCoverage = cascade?.sourceCoverage ?? [];
|
|
2062
2394
|
const planFrozen = cascade?.planFrozen ?? false;
|
|
2063
2395
|
const artifactProviderDiagnostic = run.artifacts
|
|
@@ -2079,11 +2411,513 @@ function diagnosticReceiptV3ForRun(run) {
|
|
|
2079
2411
|
runId: run.id,
|
|
2080
2412
|
sourceCoverage,
|
|
2081
2413
|
...(cascade ? { cascade } : {}),
|
|
2414
|
+
...(terminalGap ? { terminalGap } : {}),
|
|
2082
2415
|
planFrozen,
|
|
2083
2416
|
...(provider ? { provider } : {}),
|
|
2084
2417
|
finalStopReason: run.stopReason,
|
|
2085
2418
|
};
|
|
2086
2419
|
}
|
|
2420
|
+
/**
|
|
2421
|
+
* Build the one canonical, content-safe Ask story. This is produced from the
|
|
2422
|
+
* authoritative run receipt once, then joined by both the inspector and the
|
|
2423
|
+
* full local trace. Neither surface is allowed to reconstruct an incident from
|
|
2424
|
+
* spans or a generic error string.
|
|
2425
|
+
*/
|
|
2426
|
+
function diagnosticReceiptV4ForRun(run) {
|
|
2427
|
+
const cascade = run.routeDecision?.analyticalCascadeDecision;
|
|
2428
|
+
const requirements = cascade?.requirements;
|
|
2429
|
+
const candidates = run.routeDecision?.retrievalEvidence?.candidateTraceMetadata ?? [];
|
|
2430
|
+
const roleCounts = new Map();
|
|
2431
|
+
for (const candidate of candidates) {
|
|
2432
|
+
roleCounts.set(candidate.role, (roleCounts.get(candidate.role) ?? 0) + 1);
|
|
2433
|
+
}
|
|
2434
|
+
const researchBranchObservability = researchBranchObservabilityForRun(run);
|
|
2435
|
+
for (const evidence of researchBranchObservability.evidenceByRole) {
|
|
2436
|
+
roleCounts.set(evidence.role, (roleCounts.get(evidence.role) ?? 0) + evidence.candidateCount);
|
|
2437
|
+
}
|
|
2438
|
+
const terminalIncident = terminalIncidentForRun(run, cascade?.stopReason);
|
|
2439
|
+
const summaryInput = {
|
|
2440
|
+
version: 1,
|
|
2441
|
+
understoodRequest: {
|
|
2442
|
+
measures: requirements?.measures.length ?? 0,
|
|
2443
|
+
dimensions: requirements?.dimensions.length ?? 0,
|
|
2444
|
+
entityRequested: Boolean((requirements?.entityTerms.length ?? 0) || (requirements?.entityDisplayTerms.length ?? 0)),
|
|
2445
|
+
outputCount: requirements?.outputTerms?.length ?? 0,
|
|
2446
|
+
...(requirements?.ranking
|
|
2447
|
+
? { ranking: { ...requirements.ranking } }
|
|
2448
|
+
: {}),
|
|
2449
|
+
// This comes from the server-owned request admission, not a generic
|
|
2450
|
+
// `followsUp` heuristic. A complete question with thread history is
|
|
2451
|
+
// still `none` unless it explicitly selected a valid binding.
|
|
2452
|
+
conversationBinding: run.conversationBinding ?? 'none',
|
|
2453
|
+
},
|
|
2454
|
+
evidenceByRole: [...roleCounts.entries()]
|
|
2455
|
+
.map(([role, candidateCount]) => ({ role, candidateCount }))
|
|
2456
|
+
.sort((left, right) => left.role.localeCompare(right.role)),
|
|
2457
|
+
tierDecisions: (cascade?.attempts ?? []).map((attempt) => ({
|
|
2458
|
+
tier: attempt.tier,
|
|
2459
|
+
outcome: attempt.outcome,
|
|
2460
|
+
planFrozen: attempt.planFrozen,
|
|
2461
|
+
})),
|
|
2462
|
+
...(cascade?.selectedTier
|
|
2463
|
+
? {
|
|
2464
|
+
selectedPlan: {
|
|
2465
|
+
tier: cascade.selectedTier,
|
|
2466
|
+
planFrozen: cascade.planFrozen,
|
|
2467
|
+
reviewRequired: cascade.selectedTier === 'exploratory_sql',
|
|
2468
|
+
},
|
|
2469
|
+
}
|
|
2470
|
+
: {}),
|
|
2471
|
+
...(terminalIncident ? { terminalIncident } : {}),
|
|
2472
|
+
...(researchBranchObservability.summary ? { researchBranchSummary: researchBranchObservability.summary } : {}),
|
|
2473
|
+
safeNextAction: terminalIncident?.safeAction
|
|
2474
|
+
?? (researchBranchObservability.summary?.partialSuccess
|
|
2475
|
+
? researchBranchObservability.summary.safeAction
|
|
2476
|
+
: 'none'),
|
|
2477
|
+
};
|
|
2478
|
+
const summary = {
|
|
2479
|
+
...summaryInput,
|
|
2480
|
+
summaryFingerprint: receiptFingerprint(summaryInput),
|
|
2481
|
+
};
|
|
2482
|
+
return {
|
|
2483
|
+
version: 4,
|
|
2484
|
+
runId: run.id,
|
|
2485
|
+
summary,
|
|
2486
|
+
...(terminalIncident ? { terminalIncident } : {}),
|
|
2487
|
+
finalStopReason: run.stopReason,
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
const ASK_SUMMARY_EVIDENCE_ROLES = [
|
|
2491
|
+
'metric',
|
|
2492
|
+
'entity_key',
|
|
2493
|
+
'entity_label',
|
|
2494
|
+
'categorical_dimension',
|
|
2495
|
+
'time_dimension',
|
|
2496
|
+
'member',
|
|
2497
|
+
'relationship',
|
|
2498
|
+
'context',
|
|
2499
|
+
];
|
|
2500
|
+
const ASK_RESEARCH_BRANCH_FAILURE_CODES = [
|
|
2501
|
+
'execution_failed',
|
|
2502
|
+
'research_branch_timeout',
|
|
2503
|
+
'budget_exhausted',
|
|
2504
|
+
'run_deadline',
|
|
2505
|
+
'cancelled',
|
|
2506
|
+
];
|
|
2507
|
+
const ASK_RESEARCH_CHILD_TIERS = [
|
|
2508
|
+
'certified',
|
|
2509
|
+
'semantic',
|
|
2510
|
+
'governed_relational',
|
|
2511
|
+
'exploratory_sql',
|
|
2512
|
+
];
|
|
2513
|
+
/**
|
|
2514
|
+
* Project only persisted, typed Research child evidence into V4. The root
|
|
2515
|
+
* result remains authoritative: this helper never promotes a failed branch
|
|
2516
|
+
* into a root incident or infers a missing plan from spans.
|
|
2517
|
+
*/
|
|
2518
|
+
function researchBranchObservabilityForRun(run) {
|
|
2519
|
+
// A persisted V4 receipt can be reprojected after request normalization by
|
|
2520
|
+
// a host. The root route is therefore the durable authority as well as the
|
|
2521
|
+
// original requested mode: an explicit Research run must not lose its
|
|
2522
|
+
// child-story merely because an older host omitted `requestedMode` while
|
|
2523
|
+
// preserving the authoritative `research` route and research artifact.
|
|
2524
|
+
if (run.requestedMode !== 'research' && run.route !== 'research') {
|
|
2525
|
+
return { evidenceByRole: [] };
|
|
2526
|
+
}
|
|
2527
|
+
const payload = persistedResearchArtifactPayloadForRun(run);
|
|
2528
|
+
if (!payload)
|
|
2529
|
+
return { evidenceByRole: [] };
|
|
2530
|
+
const rawReceipts = Array.isArray(payload.researchBranchReceipts)
|
|
2531
|
+
? payload.researchBranchReceipts
|
|
2532
|
+
: [];
|
|
2533
|
+
const receipts = new Map();
|
|
2534
|
+
for (const value of rawReceipts) {
|
|
2535
|
+
const receipt = clarificationRecord(value);
|
|
2536
|
+
const childRunId = clarificationString(receipt?.childRunId);
|
|
2537
|
+
const branchId = clarificationString(receipt?.branchId);
|
|
2538
|
+
const state = clarificationString(receipt?.state);
|
|
2539
|
+
const stopReason = clarificationString(receipt?.stopReason);
|
|
2540
|
+
// This field is producer-owned. A malformed imported receipt must not
|
|
2541
|
+
// become an apparently successful Research story.
|
|
2542
|
+
if (!receipt || !childRunId || !branchId || !state || !stopReason)
|
|
2543
|
+
continue;
|
|
2544
|
+
const key = `${childRunId}:${branchId}`;
|
|
2545
|
+
if (!receipts.has(key))
|
|
2546
|
+
receipts.set(key, receipt);
|
|
2547
|
+
}
|
|
2548
|
+
if (receipts.size === 0)
|
|
2549
|
+
return { evidenceByRole: [] };
|
|
2550
|
+
const childRuns = persistedResearchChildRuns(payload);
|
|
2551
|
+
const evidenceByRole = persistedResearchChildEvidenceByRole(childRuns);
|
|
2552
|
+
const receiptBackedChildIds = persistedReceiptBackedResearchChildIds(payload);
|
|
2553
|
+
let completedBranches = 0;
|
|
2554
|
+
let failedBranches = 0;
|
|
2555
|
+
let timedOutBranches = 0;
|
|
2556
|
+
let skippedBranches = 0;
|
|
2557
|
+
const failureReasons = new Map();
|
|
2558
|
+
const linkedChildRunIds = new Set();
|
|
2559
|
+
for (const receipt of receipts.values()) {
|
|
2560
|
+
const childRunId = clarificationString(receipt.childRunId);
|
|
2561
|
+
linkedChildRunIds.add(childRunId);
|
|
2562
|
+
const state = clarificationString(receipt.state);
|
|
2563
|
+
const stopReason = clarificationString(receipt.stopReason);
|
|
2564
|
+
if (state === 'completed' && stopReason === 'completed') {
|
|
2565
|
+
completedBranches += 1;
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
if (state === 'timed_out')
|
|
2569
|
+
timedOutBranches += 1;
|
|
2570
|
+
else if (state === 'skipped')
|
|
2571
|
+
skippedBranches += 1;
|
|
2572
|
+
else
|
|
2573
|
+
failedBranches += 1;
|
|
2574
|
+
if (isAskResearchBranchFailureCode(stopReason)) {
|
|
2575
|
+
failureReasons.set(stopReason, (failureReasons.get(stopReason) ?? 0) + 1);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
const receiptBackedBranches = [...receipts.values()]
|
|
2579
|
+
.filter((receipt) => clarificationString(receipt.state) === 'completed'
|
|
2580
|
+
&& clarificationString(receipt.stopReason) === 'completed'
|
|
2581
|
+
&& receiptBackedChildIds.has(clarificationString(receipt.childRunId)))
|
|
2582
|
+
.length;
|
|
2583
|
+
const incompleteBranches = failedBranches + timedOutBranches + skippedBranches;
|
|
2584
|
+
const summary = {
|
|
2585
|
+
version: 1,
|
|
2586
|
+
totalBranches: receipts.size,
|
|
2587
|
+
completedBranches,
|
|
2588
|
+
receiptBackedBranches,
|
|
2589
|
+
failedBranches,
|
|
2590
|
+
timedOutBranches,
|
|
2591
|
+
skippedBranches,
|
|
2592
|
+
partialSuccess: receiptBackedBranches > 0 && incompleteBranches > 0,
|
|
2593
|
+
failureReasons: [...failureReasons.entries()]
|
|
2594
|
+
.map(([code, branchCount]) => ({ code, branchCount }))
|
|
2595
|
+
.sort((left, right) => left.code.localeCompare(right.code)),
|
|
2596
|
+
availableChildPlans: persistedResearchChildPlans(childRuns),
|
|
2597
|
+
linkedChildRunCount: linkedChildRunIds.size,
|
|
2598
|
+
safeAction: 'inspect_research_failures',
|
|
2599
|
+
};
|
|
2600
|
+
return { summary, evidenceByRole };
|
|
2601
|
+
}
|
|
2602
|
+
/** Use only the durable root research artifact with branch receipts. */
|
|
2603
|
+
function persistedResearchArtifactPayloadForRun(run) {
|
|
2604
|
+
let selected;
|
|
2605
|
+
let selectedCount = -1;
|
|
2606
|
+
for (const artifact of run.artifacts) {
|
|
2607
|
+
if (artifact.kind !== 'research_run')
|
|
2608
|
+
continue;
|
|
2609
|
+
const payload = clarificationRecord(artifact.payload);
|
|
2610
|
+
const count = Array.isArray(payload?.researchBranchReceipts) ? payload.researchBranchReceipts.length : 0;
|
|
2611
|
+
if (payload && count > selectedCount) {
|
|
2612
|
+
selected = payload;
|
|
2613
|
+
selectedCount = count;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
return selected;
|
|
2617
|
+
}
|
|
2618
|
+
function persistedResearchChildRuns(payload) {
|
|
2619
|
+
const candidates = [
|
|
2620
|
+
...(Array.isArray(payload.researchRuns) ? payload.researchRuns : []),
|
|
2621
|
+
payload.researchRun,
|
|
2622
|
+
];
|
|
2623
|
+
const byId = new Map();
|
|
2624
|
+
for (const value of candidates) {
|
|
2625
|
+
const child = clarificationRecord(value);
|
|
2626
|
+
const id = clarificationString(child?.id);
|
|
2627
|
+
if (child && id && !byId.has(id))
|
|
2628
|
+
byId.set(id, child);
|
|
2629
|
+
}
|
|
2630
|
+
return [...byId.values()];
|
|
2631
|
+
}
|
|
2632
|
+
function persistedResearchChildEvidenceByRole(childRuns) {
|
|
2633
|
+
const counts = new Map();
|
|
2634
|
+
for (const child of childRuns) {
|
|
2635
|
+
const routeDecision = clarificationRecord(child.routeDecision);
|
|
2636
|
+
const retrieval = clarificationRecord(routeDecision?.retrievalEvidence);
|
|
2637
|
+
const candidates = Array.isArray(retrieval?.candidateTraceMetadata)
|
|
2638
|
+
? retrieval.candidateTraceMetadata
|
|
2639
|
+
: [];
|
|
2640
|
+
for (const value of candidates) {
|
|
2641
|
+
const candidate = clarificationRecord(value);
|
|
2642
|
+
const role = clarificationString(candidate?.role);
|
|
2643
|
+
if (!isAskSummaryEvidenceRole(role))
|
|
2644
|
+
continue;
|
|
2645
|
+
counts.set(role, (counts.get(role) ?? 0) + 1);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
return [...counts.entries()]
|
|
2649
|
+
.map(([role, candidateCount]) => ({ role, candidateCount }))
|
|
2650
|
+
.sort((left, right) => left.role.localeCompare(right.role));
|
|
2651
|
+
}
|
|
2652
|
+
function persistedReceiptBackedResearchChildIds(payload) {
|
|
2653
|
+
const ledger = clarificationRecord(payload.researchLedgerV2);
|
|
2654
|
+
const entries = Array.isArray(ledger?.entries) ? ledger.entries : [];
|
|
2655
|
+
const ids = new Set();
|
|
2656
|
+
for (const value of entries) {
|
|
2657
|
+
const entry = clarificationRecord(value);
|
|
2658
|
+
const id = clarificationString(entry?.id);
|
|
2659
|
+
const receipts = Array.isArray(entry?.receipts) ? entry.receipts : [];
|
|
2660
|
+
if (entry?.status === 'observed' && id && receipts.some((receipt) => clarificationString(receipt))) {
|
|
2661
|
+
ids.add(id);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return ids;
|
|
2665
|
+
}
|
|
2666
|
+
function persistedResearchChildPlans(childRuns) {
|
|
2667
|
+
const plansByTier = new Map();
|
|
2668
|
+
for (const child of childRuns) {
|
|
2669
|
+
const childRunId = clarificationString(child.id);
|
|
2670
|
+
const context = clarificationRecord(child.context);
|
|
2671
|
+
const authority = clarificationRecord(context?.branchAuthority);
|
|
2672
|
+
const tier = clarificationString(authority?.selectedTier);
|
|
2673
|
+
const planId = clarificationString(authority?.planId);
|
|
2674
|
+
const planFingerprint = clarificationString(authority?.planFingerprint);
|
|
2675
|
+
if (!childRunId || authority?.planFrozen !== true || !isAskResearchChildTier(tier) || !planId || !planFingerprint)
|
|
2676
|
+
continue;
|
|
2677
|
+
const entry = plansByTier.get(tier) ?? { planKeys: new Set(), childRunIds: new Set() };
|
|
2678
|
+
entry.planKeys.add(`${planId}:${planFingerprint}`);
|
|
2679
|
+
entry.childRunIds.add(childRunId);
|
|
2680
|
+
plansByTier.set(tier, entry);
|
|
2681
|
+
}
|
|
2682
|
+
return [...plansByTier.entries()]
|
|
2683
|
+
.map(([tier, value]) => ({
|
|
2684
|
+
tier,
|
|
2685
|
+
frozenPlanCount: value.planKeys.size,
|
|
2686
|
+
branchCount: value.childRunIds.size,
|
|
2687
|
+
reviewRequired: tier === 'exploratory_sql',
|
|
2688
|
+
}))
|
|
2689
|
+
.sort((left, right) => left.tier.localeCompare(right.tier));
|
|
2690
|
+
}
|
|
2691
|
+
function isAskSummaryEvidenceRole(value) {
|
|
2692
|
+
return Boolean(value) && ASK_SUMMARY_EVIDENCE_ROLES.includes(value);
|
|
2693
|
+
}
|
|
2694
|
+
function isAskResearchBranchFailureCode(value) {
|
|
2695
|
+
return Boolean(value) && ASK_RESEARCH_BRANCH_FAILURE_CODES.includes(value);
|
|
2696
|
+
}
|
|
2697
|
+
function isAskResearchChildTier(value) {
|
|
2698
|
+
return Boolean(value) && ASK_RESEARCH_CHILD_TIERS.includes(value);
|
|
2699
|
+
}
|
|
2700
|
+
function terminalIncidentForRun(run, cascadeStopReason) {
|
|
2701
|
+
const executionSetupFailure = terminalConnectionSetupFailureForRun(run);
|
|
2702
|
+
if (executionSetupFailure) {
|
|
2703
|
+
return {
|
|
2704
|
+
version: 1,
|
|
2705
|
+
code: 'CONNECTION_NOT_CONFIGURED',
|
|
2706
|
+
boundary: 'sql.execute',
|
|
2707
|
+
origin: 'governance_gate',
|
|
2708
|
+
impact: 'execution_not_attempted',
|
|
2709
|
+
safeAction: 'configure_connection',
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
const failureCode = run.diagnosticReceipt?.failure?.code;
|
|
2713
|
+
if (failureCode === 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH') {
|
|
2714
|
+
return {
|
|
2715
|
+
version: 1,
|
|
2716
|
+
code: 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH',
|
|
2717
|
+
boundary: 'sql.authorize',
|
|
2718
|
+
origin: 'internal_invariant',
|
|
2719
|
+
impact: 'execution_not_attempted',
|
|
2720
|
+
safeAction: 'export_redacted_trace',
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
if (failureCode === 'RESEARCH_RUN_DEADLINE') {
|
|
2724
|
+
return {
|
|
2725
|
+
version: 1,
|
|
2726
|
+
code: 'RESEARCH_RUN_DEADLINE',
|
|
2727
|
+
boundary: 'run',
|
|
2728
|
+
origin: 'governance_gate',
|
|
2729
|
+
impact: 'answer_not_produced',
|
|
2730
|
+
safeAction: 'inspect_failure',
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
// A completed root can still be materially limited when every admitted
|
|
2734
|
+
// Research child exhausted its bounded window. This is producer-owned
|
|
2735
|
+
// receipt evidence, not an incident reconstructed from trace timing. It
|
|
2736
|
+
// must be visible in the same V4 summary used by the inspector and full
|
|
2737
|
+
// trace so the user is never told there was no incident after a zero-finding
|
|
2738
|
+
// investigation.
|
|
2739
|
+
if (terminalResearchBranchTimeoutForRun(run)) {
|
|
2740
|
+
return {
|
|
2741
|
+
version: 1,
|
|
2742
|
+
code: 'RESEARCH_BRANCH_TIMEOUT',
|
|
2743
|
+
boundary: 'run',
|
|
2744
|
+
origin: 'governance_gate',
|
|
2745
|
+
impact: 'answer_not_produced',
|
|
2746
|
+
safeAction: 'inspect_research_failures',
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
if (failureCode === 'RUN_CANCELLED' || run.status === 'cancelled') {
|
|
2750
|
+
return { version: 1, code: 'CANCELLED', boundary: 'run', origin: 'unknown', impact: 'run_cancelled', safeAction: 'none' };
|
|
2751
|
+
}
|
|
2752
|
+
if (failureCode === 'CONNECTION_NOT_CONFIGURED') {
|
|
2753
|
+
return { version: 1, code: 'CONNECTION_NOT_CONFIGURED', boundary: 'sql.execute', origin: 'governance_gate', impact: 'execution_not_attempted', safeAction: 'configure_connection' };
|
|
2754
|
+
}
|
|
2755
|
+
// A frozen semantic/analytical plan may fail while the compiler is resolving
|
|
2756
|
+
// its already-proven identifiers. That is categorically different from a
|
|
2757
|
+
// warehouse failure: no statement was authorized or executed. Preserve the
|
|
2758
|
+
// producer's typed `COMPILATION_FAILED` cause before consulting connector
|
|
2759
|
+
// evidence so both Ask surfaces tell the same pre-SQL story.
|
|
2760
|
+
const compilationFailure = terminalCompilationFailureForRun(run);
|
|
2761
|
+
if (compilationFailure) {
|
|
2762
|
+
const semantic = isSemanticCompilationForRun(run);
|
|
2763
|
+
return {
|
|
2764
|
+
version: 1,
|
|
2765
|
+
code: 'COMPILATION_FAILED',
|
|
2766
|
+
boundary: semantic ? 'semantic.compile' : 'plan.compile',
|
|
2767
|
+
origin: semantic ? 'semantic_compiler' : 'plan_compiler',
|
|
2768
|
+
impact: 'execution_not_attempted',
|
|
2769
|
+
safeAction: compilationFailure.safeAction,
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
const warehouseFailure = terminalWarehouseFailureForRun(run);
|
|
2773
|
+
if (warehouseFailure) {
|
|
2774
|
+
return {
|
|
2775
|
+
version: 1,
|
|
2776
|
+
code: 'ANALYTICAL_EXECUTION_FAILED',
|
|
2777
|
+
boundary: 'sql.execute',
|
|
2778
|
+
origin: 'warehouse',
|
|
2779
|
+
impact: 'execution_failed',
|
|
2780
|
+
// A typed missing relation after a frozen plan reached the connector is
|
|
2781
|
+
// not a generic retry. The target may be an empty local database or a
|
|
2782
|
+
// different approved warehouse, so direct the operator to that target.
|
|
2783
|
+
safeAction: warehouseFailure.category === 'unknown_relation'
|
|
2784
|
+
? 'change_authorized_connection'
|
|
2785
|
+
: 'inspect_failure',
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
if (run.diagnosticReceiptV3?.provider) {
|
|
2789
|
+
return { version: 1, code: 'PROVIDER_FAILURE', boundary: 'provider', origin: 'provider', impact: 'answer_not_produced', safeAction: 'inspect_failure' };
|
|
2790
|
+
}
|
|
2791
|
+
if (cascadeStopReason === 'coverage_gap' || cascadeStopReason === 'ambiguous' || cascadeStopReason === 'denied') {
|
|
2792
|
+
return { version: 1, code: 'ANALYTICAL_COVERAGE_GAP', boundary: 'cascade', origin: 'governance_gate', impact: 'answer_not_produced', safeAction: 'inspect_failure' };
|
|
2793
|
+
}
|
|
2794
|
+
if (run.status === 'blocked') {
|
|
2795
|
+
return { version: 1, code: 'ANALYTICAL_EXECUTION_FAILED', boundary: 'sql.execute', origin: 'unknown', impact: 'execution_failed', safeAction: 'inspect_failure' };
|
|
2796
|
+
}
|
|
2797
|
+
return undefined;
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* Read only the narrow host setup receipt emitted before a connector receives
|
|
2801
|
+
* SQL. This must win over the broad analytical failure payload because a
|
|
2802
|
+
* semantic graph can catch the host error after its compiler work completed.
|
|
2803
|
+
*/
|
|
2804
|
+
function terminalConnectionSetupFailureForRun(run) {
|
|
2805
|
+
return run.artifacts.some((artifact) => {
|
|
2806
|
+
const payload = artifact.payload;
|
|
2807
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
2808
|
+
return false;
|
|
2809
|
+
const setup = payload.observabilityExecutionFailure;
|
|
2810
|
+
if (!setup || typeof setup !== 'object' || Array.isArray(setup))
|
|
2811
|
+
return false;
|
|
2812
|
+
const record = setup;
|
|
2813
|
+
return record.version === 1
|
|
2814
|
+
&& record.phase === 'execution'
|
|
2815
|
+
&& record.cause === 'connection_not_configured'
|
|
2816
|
+
&& record.safeAction === 'configure_connection';
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Read only a producer-owned analytical failure. A compiler failure may have
|
|
2821
|
+
* prepared SQL text, but it is still pre-execution until the durable telemetry
|
|
2822
|
+
* records a SQL call. This guard keeps a real warehouse failure from being
|
|
2823
|
+
* relabeled as semantic/planning just because a legacy adapter reused a broad
|
|
2824
|
+
* failure code in a later stage.
|
|
2825
|
+
*/
|
|
2826
|
+
function terminalCompilationFailureForRun(run) {
|
|
2827
|
+
if ((run.telemetry?.sqlExecutions ?? 0) > 0)
|
|
2828
|
+
return undefined;
|
|
2829
|
+
for (const artifact of run.artifacts) {
|
|
2830
|
+
const payload = artifact.payload;
|
|
2831
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
2832
|
+
continue;
|
|
2833
|
+
const failure = payload.analyticalFailure;
|
|
2834
|
+
if (!failure || typeof failure !== 'object' || Array.isArray(failure))
|
|
2835
|
+
continue;
|
|
2836
|
+
const record = failure;
|
|
2837
|
+
if (record.code !== 'COMPILATION_FAILED' || record.phase !== 'compilation')
|
|
2838
|
+
continue;
|
|
2839
|
+
return {
|
|
2840
|
+
safeAction: terminalIncidentSafeAction(record.safeActions) ?? 'inspect_failure',
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
return undefined;
|
|
2844
|
+
}
|
|
2845
|
+
function isSemanticCompilationForRun(run) {
|
|
2846
|
+
// Only the router-owned cascade may identify a semantic execution tier.
|
|
2847
|
+
// Direct/legacy semantic callers can still carry an immutable plan, but
|
|
2848
|
+
// without that authority their failure is accurately a generic plan compile
|
|
2849
|
+
// incident rather than a reconstructed semantic route.
|
|
2850
|
+
return run.routeDecision?.analyticalCascadeDecision?.selectedTier === 'semantic';
|
|
2851
|
+
}
|
|
2852
|
+
/** Keep V4's recovery action in the same compact vocabulary as trace spans. */
|
|
2853
|
+
function terminalIncidentSafeAction(value) {
|
|
2854
|
+
if (!Array.isArray(value))
|
|
2855
|
+
return undefined;
|
|
2856
|
+
const actions = [
|
|
2857
|
+
'export_redacted_trace',
|
|
2858
|
+
'configure_connection',
|
|
2859
|
+
'change_authorized_connection',
|
|
2860
|
+
'inspect_failure',
|
|
2861
|
+
'retry_same_plan',
|
|
2862
|
+
'refresh_snapshot',
|
|
2863
|
+
'edit_dql',
|
|
2864
|
+
'open_sql_notebook',
|
|
2865
|
+
'request_access',
|
|
2866
|
+
'reapply_semantic_runtime',
|
|
2867
|
+
'review_analytical_failure',
|
|
2868
|
+
'inspect_research_failures',
|
|
2869
|
+
'none',
|
|
2870
|
+
];
|
|
2871
|
+
return value.find((action) => typeof action === 'string' && actions.includes(action));
|
|
2872
|
+
}
|
|
2873
|
+
/**
|
|
2874
|
+
* A Research root is deliberately allowed to complete its receipt-bound
|
|
2875
|
+
* synthesis after child deadlines. Surface a terminal incident only when no
|
|
2876
|
+
* child completed an observation and all admitted children were bounded out;
|
|
2877
|
+
* a partially successful investigation remains a review-required answer with
|
|
2878
|
+
* a limited-scope note rather than a false failure.
|
|
2879
|
+
*/
|
|
2880
|
+
function terminalResearchBranchTimeoutForRun(run) {
|
|
2881
|
+
for (const artifact of run.artifacts) {
|
|
2882
|
+
const payload = artifact.payload;
|
|
2883
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
2884
|
+
continue;
|
|
2885
|
+
const receipts = payload.researchBranchReceipts;
|
|
2886
|
+
if (!Array.isArray(receipts) || receipts.length === 0)
|
|
2887
|
+
continue;
|
|
2888
|
+
const stopReasons = receipts
|
|
2889
|
+
.map((receipt) => receipt && typeof receipt === 'object'
|
|
2890
|
+
? receipt.stopReason
|
|
2891
|
+
: undefined)
|
|
2892
|
+
.filter((reason) => typeof reason === 'string');
|
|
2893
|
+
if (stopReasons.length !== receipts.length)
|
|
2894
|
+
continue;
|
|
2895
|
+
const allBounded = stopReasons.every((reason) => reason === 'research_branch_timeout' || reason === 'budget_exhausted');
|
|
2896
|
+
if (allBounded && stopReasons.some((reason) => reason === 'research_branch_timeout'))
|
|
2897
|
+
return true;
|
|
2898
|
+
}
|
|
2899
|
+
return false;
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Read only enum evidence emitted at the real connector boundary. SQL text and
|
|
2903
|
+
* redacted driver diagnostics remain in the artifact inspector; they cannot
|
|
2904
|
+
* become routing or trace-summary authority.
|
|
2905
|
+
*/
|
|
2906
|
+
function terminalWarehouseFailureForRun(run) {
|
|
2907
|
+
for (const artifact of run.artifacts) {
|
|
2908
|
+
const payload = artifact.payload;
|
|
2909
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
2910
|
+
continue;
|
|
2911
|
+
const failure = payload.warehouseFailure;
|
|
2912
|
+
if (!failure || typeof failure !== 'object' || Array.isArray(failure))
|
|
2913
|
+
continue;
|
|
2914
|
+
const record = failure;
|
|
2915
|
+
if (record.version === 1 && record.origin === 'warehouse' && typeof record.category === 'string') {
|
|
2916
|
+
return { category: record.category };
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
return undefined;
|
|
2920
|
+
}
|
|
2087
2921
|
function emptyRunTelemetry(total, fallbackReason) {
|
|
2088
2922
|
return {
|
|
2089
2923
|
version: 1,
|
|
@@ -2109,7 +2943,7 @@ function durationBetweenMs(startedAt, completedAt) {
|
|
|
2109
2943
|
function receiptFingerprint(value) {
|
|
2110
2944
|
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
2111
2945
|
}
|
|
2112
|
-
function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
|
|
2946
|
+
function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3, receiptV4) {
|
|
2113
2947
|
if (artifacts.length === 0) {
|
|
2114
2948
|
if (!receipt.failure)
|
|
2115
2949
|
return artifacts;
|
|
@@ -2118,7 +2952,7 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
|
|
|
2118
2952
|
kind: "answer",
|
|
2119
2953
|
title: "Agent run diagnostics",
|
|
2120
2954
|
trustState: "blocked",
|
|
2121
|
-
payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}), ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}) },
|
|
2955
|
+
payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}), ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}), ...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}) },
|
|
2122
2956
|
}];
|
|
2123
2957
|
}
|
|
2124
2958
|
const preferredIndex = Math.max(0, artifacts.findIndex((artifact) => artifact.kind === "answer"));
|
|
@@ -2135,6 +2969,7 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
|
|
|
2135
2969
|
diagnosticReceipt: receipt,
|
|
2136
2970
|
...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}),
|
|
2137
2971
|
...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}),
|
|
2972
|
+
...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}),
|
|
2138
2973
|
},
|
|
2139
2974
|
};
|
|
2140
2975
|
});
|