@duckcodeailabs/dql-agent 1.14.1 → 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 +71 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +1048 -31
- 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 +12 -0
- 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 +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 +211 -28
- 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/synthesis.d.ts +4 -0
- package/dist/agentic/research/synthesis.d.ts.map +1 -1
- package/dist/agentic/research/synthesis.js +23 -8
- package/dist/agentic/research/synthesis.js.map +1 -1
- package/dist/agentic/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +13 -3
- 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 +233 -2
- 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 +332 -17
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +613 -0
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +1007 -0
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +110 -6
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1180 -125
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +32 -1
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +49 -3
- 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 +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/conversation/snapshot.d.ts +20 -0
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +30 -1
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/fixtures/ask-ai-office-shaped.d.ts +153 -0
- package/dist/fixtures/ask-ai-office-shaped.d.ts.map +1 -0
- package/dist/fixtures/ask-ai-office-shaped.js +94 -0
- package/dist/fixtures/ask-ai-office-shaped.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 +11 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +38 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +31 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +141 -2
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +444 -30
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +144 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts +18 -0
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +250 -38
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +58 -12
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +19 -3
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +518 -54
- 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/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +23 -1
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +22 -1
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +773 -49
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +14 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +3668 -425
- 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/dist/semantic-bridge/member-select.d.ts.map +1 -1
- package/dist/semantic-bridge/member-select.js +39 -21
- package/dist/semantic-bridge/member-select.js.map +1 -1
- package/package.json +5 -5
package/dist/agent-run-engine.js
CHANGED
|
@@ -2,13 +2,15 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { normalizeProviderEgressReceiptV1, } from '@duckcodeailabs/dql-core';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { classifyConversationalTurn, looksLikeDefinitionalAboutNamedObject, decideAgentAction, looksLikeComposeApp, } from "./intent-controller.js";
|
|
5
|
+
import { classifyConversationalTurn, looksLikeDefinitionalAboutNamedObject, looksLikeNamedCertifiedArtifactMetadataRequest, decideAgentAction, looksLikeComposeApp, } from "./intent-controller.js";
|
|
6
6
|
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 } from './analytical-orchestration.js';
|
|
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',
|
|
@@ -753,7 +869,13 @@ export class AgentRunEngine {
|
|
|
753
869
|
};
|
|
754
870
|
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
755
871
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
756
|
-
run.
|
|
872
|
+
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(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);
|
|
757
879
|
await checkpointQueue;
|
|
758
880
|
await this.store?.save(run);
|
|
759
881
|
return run;
|
|
@@ -764,6 +886,11 @@ export class AgentRunEngine {
|
|
|
764
886
|
// pre-try await escaped the engine and left active UI runs looking endless.
|
|
765
887
|
let routeDecision = buildIntentDecision(request);
|
|
766
888
|
try {
|
|
889
|
+
const classifySpan = traceObserver.startSpan({
|
|
890
|
+
name: 'request.classify',
|
|
891
|
+
stage: 'request',
|
|
892
|
+
payload: { kind: 'stage', requestedMode },
|
|
893
|
+
});
|
|
767
894
|
routeDecision = clarificationContinuation && !request.selectedEvidenceId
|
|
768
895
|
? {
|
|
769
896
|
action: "answer",
|
|
@@ -774,6 +901,11 @@ export class AgentRunEngine {
|
|
|
774
901
|
}
|
|
775
902
|
: await awaitWithAbort(this.decideRoute(request), request.signal);
|
|
776
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);
|
|
777
909
|
const defaultRoute = answerAnywayRoute(constrainRouteForAudience(selectRoute(request, routeDecision), audience), request, audience, routeDecision);
|
|
778
910
|
const authoritativeAsk = routeDecision.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
779
911
|
&& requestedMode !== 'research';
|
|
@@ -858,6 +990,10 @@ export class AgentRunEngine {
|
|
|
858
990
|
: `Re-running ${route.replaceAll("_", " ")} executor (repair attempt ${attempt}).`,
|
|
859
991
|
route,
|
|
860
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.
|
|
861
997
|
result = await this.executeRoute({
|
|
862
998
|
runId,
|
|
863
999
|
request,
|
|
@@ -872,6 +1008,22 @@ export class AgentRunEngine {
|
|
|
872
1008
|
emit,
|
|
873
1009
|
emitAnswerDelta: onAnswerDelta,
|
|
874
1010
|
});
|
|
1011
|
+
recordExecutionAttemptSummaryV1(traceObserver, result);
|
|
1012
|
+
// The router owns a frozen analytical tier. An executor may report a
|
|
1013
|
+
// same-tier execution failure, but it cannot turn a certified or
|
|
1014
|
+
// semantic plan into generated work (or vice versa) after execution
|
|
1015
|
+
// has started. Keep this guard in the engine as well as host adapters
|
|
1016
|
+
// so an injected/legacy executor cannot redefine durable provenance.
|
|
1017
|
+
const planWasFrozen = routeDecision.analyticalCascadeDecision?.planFrozen === true;
|
|
1018
|
+
routeDecision = applyExploratoryExecutionFreeze(routeDecision, result.analyticalExecutionFreeze);
|
|
1019
|
+
routeDecision = applyExploratoryExecutionFreeze(routeDecision, result.analyticalExecutionRepairFreeze);
|
|
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
|
+
}
|
|
1026
|
+
result = preserveFrozenAnalyticalRoute(route, routeDecision, result);
|
|
875
1027
|
result = consumeRepeatedClarificationSelection(request, routeDecision, result);
|
|
876
1028
|
if (result.analyticalTurnPlan)
|
|
877
1029
|
progress.analyticalTurnPlan = result.analyticalTurnPlan;
|
|
@@ -900,7 +1052,7 @@ export class AgentRunEngine {
|
|
|
900
1052
|
// A frozen analytical plan has one route and no downstream planner,
|
|
901
1053
|
// rematch, route escalation, or whole-answer regeneration authority.
|
|
902
1054
|
// Typed server-issued repair is a separate derived run.
|
|
903
|
-
if (authoritativeAsk) {
|
|
1055
|
+
if (authoritativeAsk || routeDecision.analyticalCascadeDecision?.planFrozen === true) {
|
|
904
1056
|
stepStatus = 'needs_review';
|
|
905
1057
|
break;
|
|
906
1058
|
}
|
|
@@ -1105,7 +1257,10 @@ export class AgentRunEngine {
|
|
|
1105
1257
|
run.lifecycle = terminalLifecycle(progress.lifecycle, run.status === "blocked" ? "run.failed" : "run.completed", run.completedAt, events.length);
|
|
1106
1258
|
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
1107
1259
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1108
|
-
run.
|
|
1260
|
+
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1261
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
1262
|
+
run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
|
|
1263
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
1109
1264
|
await checkpointQueue;
|
|
1110
1265
|
await this.store?.save(run);
|
|
1111
1266
|
return run;
|
|
@@ -1157,6 +1312,7 @@ export class AgentRunEngine {
|
|
|
1157
1312
|
id: runId,
|
|
1158
1313
|
question: submittedQuestion,
|
|
1159
1314
|
requestedMode,
|
|
1315
|
+
conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
|
|
1160
1316
|
route: cancelledRoute,
|
|
1161
1317
|
status: "cancelled",
|
|
1162
1318
|
trustState: "not_applicable",
|
|
@@ -1191,6 +1347,9 @@ export class AgentRunEngine {
|
|
|
1191
1347
|
lifecycle: terminalLifecycle(progress.lifecycle, "run.cancelled", completedAt, events.length),
|
|
1192
1348
|
};
|
|
1193
1349
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1350
|
+
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1351
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
1352
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
1194
1353
|
await checkpointQueue;
|
|
1195
1354
|
await this.store?.save(run);
|
|
1196
1355
|
return run;
|
|
@@ -1198,7 +1357,9 @@ export class AgentRunEngine {
|
|
|
1198
1357
|
const message = isOrchestrationBudgetExhausted(err)
|
|
1199
1358
|
? 'Ask could not complete within its bounded orchestration. Nothing was executed; narrow the metric or dimension and retry.'
|
|
1200
1359
|
: err instanceof Error && err.name === "TimeoutError"
|
|
1201
|
-
?
|
|
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."
|
|
1202
1363
|
: err instanceof Error ? err.message : String(err);
|
|
1203
1364
|
const failedRoute = progress.route;
|
|
1204
1365
|
const failedPhase = progress.lifecycle.phase;
|
|
@@ -1210,7 +1371,7 @@ export class AgentRunEngine {
|
|
|
1210
1371
|
trustState: "blocked",
|
|
1211
1372
|
});
|
|
1212
1373
|
const completedAt = this.timestamp();
|
|
1213
|
-
const failure = diagnosticFailureFromError(err, failedPhase);
|
|
1374
|
+
const failure = diagnosticFailureFromError(err, failedPhase, requestedMode);
|
|
1214
1375
|
const evaluations = [
|
|
1215
1376
|
...progress.evaluations,
|
|
1216
1377
|
{
|
|
@@ -1238,6 +1399,7 @@ export class AgentRunEngine {
|
|
|
1238
1399
|
id: runId,
|
|
1239
1400
|
question: submittedQuestion,
|
|
1240
1401
|
requestedMode,
|
|
1402
|
+
conversationBinding: request.conversationBinding ?? traceConversationBinding(request, clarificationContinuation),
|
|
1241
1403
|
route: "blocked",
|
|
1242
1404
|
status: "blocked",
|
|
1243
1405
|
trustState: "blocked",
|
|
@@ -1274,7 +1436,10 @@ export class AgentRunEngine {
|
|
|
1274
1436
|
lifecycle: terminalLifecycle(progress.lifecycle, "run.failed", completedAt, events.length),
|
|
1275
1437
|
};
|
|
1276
1438
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1277
|
-
run.
|
|
1439
|
+
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1440
|
+
run.diagnosticReceiptV4 = diagnosticReceiptV4ForRun(run);
|
|
1441
|
+
run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3, run.diagnosticReceiptV4);
|
|
1442
|
+
finalizeAgentRunTraceV1(traceObserver, run);
|
|
1278
1443
|
await checkpointQueue;
|
|
1279
1444
|
await this.store?.save(run);
|
|
1280
1445
|
return run;
|
|
@@ -1291,6 +1456,7 @@ export class AgentRunEngine {
|
|
|
1291
1456
|
id: input.runId,
|
|
1292
1457
|
question: input.request.question,
|
|
1293
1458
|
requestedMode: input.requestedMode,
|
|
1459
|
+
conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
|
|
1294
1460
|
route: "blocked",
|
|
1295
1461
|
status: "blocked",
|
|
1296
1462
|
trustState: "blocked",
|
|
@@ -1337,6 +1503,7 @@ export class AgentRunEngine {
|
|
|
1337
1503
|
id: input.runId,
|
|
1338
1504
|
question: input.request.question,
|
|
1339
1505
|
requestedMode: input.requestedMode,
|
|
1506
|
+
conversationBinding: input.request.conversationBinding ?? traceConversationBinding(input.request, undefined),
|
|
1340
1507
|
route,
|
|
1341
1508
|
status: finalOutcome.status,
|
|
1342
1509
|
trustState: finalOutcome.trustState,
|
|
@@ -1400,9 +1567,13 @@ export class AgentRunEngine {
|
|
|
1400
1567
|
const signal = context.request.runBudget?.hardSignal ?? context.request.signal;
|
|
1401
1568
|
if (signal?.aborted)
|
|
1402
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.
|
|
1403
1574
|
const execution = Promise.resolve(executor({
|
|
1404
1575
|
...context,
|
|
1405
|
-
request: { ...context.request, ...(signal ? { signal } : {}) },
|
|
1576
|
+
request: attachAskTraceObserverV1({ ...context.request, ...(signal ? { signal } : {}) }, askTraceObserverForV1(context.request)),
|
|
1406
1577
|
}));
|
|
1407
1578
|
return awaitWithAbort(execution, signal);
|
|
1408
1579
|
}
|
|
@@ -1420,6 +1591,44 @@ export class AgentRunEngine {
|
|
|
1420
1591
|
return this.now().toISOString();
|
|
1421
1592
|
}
|
|
1422
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
|
+
}
|
|
1423
1632
|
/**
|
|
1424
1633
|
* Ordinary analytical Ask cannot delegate meaning to the legacy answer
|
|
1425
1634
|
* generator. Retrieval/meaning must first produce the immutable RAP consumed by
|
|
@@ -1460,17 +1669,28 @@ function rescueModelingGapForOrdinaryAsk(request, decision, options = {}) {
|
|
|
1460
1669
|
return undefined;
|
|
1461
1670
|
if (decision.terminalOutcome?.kind !== 'modeling_gap')
|
|
1462
1671
|
return undefined;
|
|
1463
|
-
// A
|
|
1464
|
-
//
|
|
1465
|
-
//
|
|
1466
|
-
//
|
|
1467
|
-
|
|
1672
|
+
// A frozen plan is authoritative. Only a PRE-FREEZE coverage observation may
|
|
1673
|
+
// advance through the later governed-relational/exploratory tiers. This keeps
|
|
1674
|
+
// compiler, policy, validation, and warehouse failures terminal after a plan
|
|
1675
|
+
// has been accepted while restoring the required certified → semantic →
|
|
1676
|
+
// relational → review-required exploration cascade for missing dimensions.
|
|
1677
|
+
if (decision.resolvedAnalyticalPlan?.mode === 'authoritative')
|
|
1468
1678
|
return undefined;
|
|
1469
1679
|
// Genuine user-facing ambiguity and an explicit evidence pick stay terminal.
|
|
1470
1680
|
if (decision.requiresClarification === true)
|
|
1471
1681
|
return undefined;
|
|
1472
1682
|
if (request.selectedEvidenceId)
|
|
1473
1683
|
return undefined;
|
|
1684
|
+
// The router, not this host boundary, owns cascade eligibility. A typed
|
|
1685
|
+
// pre-freeze coverage gap advances only when the same snapshot recorded an
|
|
1686
|
+
// executable exploratory tier. This prevents a semantic-only candidate or a
|
|
1687
|
+
// forged/old terminal decision from quietly becoming generated SQL.
|
|
1688
|
+
const exploratoryAttempt = decision.analyticalCascadeDecision?.attempts.find((attempt) => attempt.tier === 'exploratory_sql');
|
|
1689
|
+
if (decision.analyticalCascadeDecision?.selectedTier !== 'exploratory_sql'
|
|
1690
|
+
|| decision.analyticalCascadeDecision.planFrozen
|
|
1691
|
+
|| exploratoryAttempt?.outcome !== 'executable'
|
|
1692
|
+
|| exploratoryAttempt.candidateIds.length === 0)
|
|
1693
|
+
return undefined;
|
|
1474
1694
|
if (options.requireGovernedEvidence) {
|
|
1475
1695
|
const governedEvidence = (decision.retrievalEvidence?.candidateCount ?? 0) > 0
|
|
1476
1696
|
|| (decision.meaningResolution?.selectedConceptIds.length ?? 0) > 0;
|
|
@@ -1500,6 +1720,227 @@ function rescueModelingGapForOrdinaryAsk(request, decision, options = {}) {
|
|
|
1500
1720
|
: {}),
|
|
1501
1721
|
};
|
|
1502
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Consume the router's immutable cascade decision without reparsing the
|
|
1725
|
+
* question or reconstructing a tier from route/identifier text. The selected
|
|
1726
|
+
* tier is intentionally sufficient for dispatch; compilation and execution
|
|
1727
|
+
* still validate the frozen plan or review-required exploratory SQL.
|
|
1728
|
+
*/
|
|
1729
|
+
function routeFromAnalyticalCascade(decision) {
|
|
1730
|
+
const cascade = decision.analyticalCascadeDecision;
|
|
1731
|
+
if (!cascade)
|
|
1732
|
+
return undefined;
|
|
1733
|
+
if (cascade.stopReason === 'denied' || cascade.stopReason === 'coverage_gap' || cascade.stopReason === 'post_freeze_failure') {
|
|
1734
|
+
return 'blocked';
|
|
1735
|
+
}
|
|
1736
|
+
if (cascade.stopReason === 'ambiguous')
|
|
1737
|
+
return 'clarify';
|
|
1738
|
+
switch (cascade.selectedTier) {
|
|
1739
|
+
case 'certified':
|
|
1740
|
+
return cascade.planFrozen ? 'certified_answer' : undefined;
|
|
1741
|
+
case 'semantic':
|
|
1742
|
+
return cascade.planFrozen ? 'semantic_answer' : undefined;
|
|
1743
|
+
case 'governed_relational':
|
|
1744
|
+
case 'exploratory_sql':
|
|
1745
|
+
return 'generated_answer';
|
|
1746
|
+
default:
|
|
1747
|
+
return undefined;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* A frozen router decision is an immutable execution contract, not a hint that
|
|
1752
|
+
* a downstream answer loop may replace with another meaning/tier. The executor
|
|
1753
|
+
* is still free to return a terminal compilation, provider, adapter, or result
|
|
1754
|
+
* failure, but it must retain the selected route while doing so.
|
|
1755
|
+
*/
|
|
1756
|
+
function preserveFrozenAnalyticalRoute(route, decision, result) {
|
|
1757
|
+
const frozen = decision.analyticalCascadeDecision?.planFrozen === true
|
|
1758
|
+
|| decision.resolvedAnalyticalPlan?.mode === 'authoritative';
|
|
1759
|
+
if (!frozen || !result.resolvedRoute || result.resolvedRoute === route)
|
|
1760
|
+
return result;
|
|
1761
|
+
return {
|
|
1762
|
+
resolvedRoute: route,
|
|
1763
|
+
status: 'blocked',
|
|
1764
|
+
trustState: 'blocked',
|
|
1765
|
+
stopReason: 'blocked',
|
|
1766
|
+
summary: `The frozen ${route.replaceAll('_', ' ')} plan could not execute as selected. DQL did not substitute another analytical tier.`,
|
|
1767
|
+
answer: 'The selected analytical plan could not be executed as selected. No fallback answer was returned.',
|
|
1768
|
+
artifacts: [],
|
|
1769
|
+
evaluations: [{
|
|
1770
|
+
id: 'frozen-plan-route-mismatch',
|
|
1771
|
+
label: 'Frozen analytical route',
|
|
1772
|
+
passed: false,
|
|
1773
|
+
severity: 'blocking',
|
|
1774
|
+
message: `The executor reported ${result.resolvedRoute.replaceAll('_', ' ')} after the router froze ${route.replaceAll('_', ' ')}.`,
|
|
1775
|
+
evidence: {
|
|
1776
|
+
selectedRoute: route,
|
|
1777
|
+
reportedRoute: result.resolvedRoute,
|
|
1778
|
+
selectedTier: decision.analyticalCascadeDecision?.selectedTier,
|
|
1779
|
+
planId: decision.resolvedAnalyticalPlan?.planId,
|
|
1780
|
+
},
|
|
1781
|
+
}],
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Promote only an explicit host-issued exploratory freeze into the router
|
|
1786
|
+
* decision that will be persisted. This deliberately does not inspect route
|
|
1787
|
+
* names, SQL strings, or identifier patterns: a selected tier is immutable
|
|
1788
|
+
* only when its own candidate set, snapshot, target, and capability receipt
|
|
1789
|
+
* all agree.
|
|
1790
|
+
*/
|
|
1791
|
+
function applyExploratoryExecutionFreeze(decision, freeze) {
|
|
1792
|
+
if (!freeze)
|
|
1793
|
+
return decision;
|
|
1794
|
+
const cascade = decision.analyticalCascadeDecision;
|
|
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);
|
|
1800
|
+
const sameCandidates = Boolean(attempt
|
|
1801
|
+
&& attempt.candidateIds.length === freeze.candidateIds.length
|
|
1802
|
+
&& attempt.candidateIds.every((candidate, index) => candidate === freeze.candidateIds[index]));
|
|
1803
|
+
const retrievalSnapshotId = decision.retrievalEvidence?.snapshotId;
|
|
1804
|
+
const validBaseReceipt = Boolean(cascade
|
|
1805
|
+
&& cascade.selectedTier === 'exploratory_sql'
|
|
1806
|
+
&& cascade.planFrozen === true
|
|
1807
|
+
&& attempt?.outcome === 'executable'
|
|
1808
|
+
&& attempt.planFrozen === true
|
|
1809
|
+
&& sameCandidates
|
|
1810
|
+
&& freeze.version === 1
|
|
1811
|
+
&& freeze.selectedTier === 'exploratory_sql'
|
|
1812
|
+
&& freeze.authorization === 'capability_minted'
|
|
1813
|
+
&& freeze.planId.trim()
|
|
1814
|
+
&& freeze.planFingerprint.trim()
|
|
1815
|
+
&& freeze.snapshotId.trim()
|
|
1816
|
+
&& freeze.targetFingerprint.trim()
|
|
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)
|
|
1823
|
+
&& (!retrievalSnapshotId || retrievalSnapshotId === freeze.snapshotId));
|
|
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');
|
|
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;
|
|
1852
|
+
return {
|
|
1853
|
+
...decision,
|
|
1854
|
+
analyticalCascadeDecision: {
|
|
1855
|
+
...cascade,
|
|
1856
|
+
...(kind === 'initial'
|
|
1857
|
+
? { exploratoryExecutionFreeze: freeze }
|
|
1858
|
+
: { exploratoryRepairExecutionFreeze: freeze }),
|
|
1859
|
+
attempts: cascade.attempts.map((candidate) => candidate.tier === 'exploratory_sql'
|
|
1860
|
+
? {
|
|
1861
|
+
...candidate,
|
|
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}.`,
|
|
1868
|
+
}
|
|
1869
|
+
: candidate),
|
|
1870
|
+
},
|
|
1871
|
+
};
|
|
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
|
+
}
|
|
1503
1944
|
/**
|
|
1504
1945
|
* Has this exact clarification already been asked in this thread?
|
|
1505
1946
|
*
|
|
@@ -1571,15 +2012,23 @@ function enforceOrdinaryAnalyticalPlanBoundary(request, decision) {
|
|
|
1571
2012
|
// user to disambiguate the one artifact they just named. The plan cannot see
|
|
1572
2013
|
// it because it reads the artifact's OWN NAME as analytical intent: that name
|
|
1573
2014
|
// contains "vs", so the mode comes back `comparison`.
|
|
2015
|
+
const namedCertifiedArtifactMetadata = looksLikeNamedCertifiedArtifactMetadataRequest(request.question, decision.retrievalEvidence?.candidateIds ?? []);
|
|
1574
2016
|
if (ordinaryAsk
|
|
1575
2017
|
&& !request.selectedEvidenceId
|
|
1576
|
-
&&
|
|
2018
|
+
&& (namedCertifiedArtifactMetadata
|
|
2019
|
+
|| looksLikeDefinitionalAboutNamedObject(request.question, decision.retrievalEvidence?.candidateIds ?? []))) {
|
|
1577
2020
|
return {
|
|
1578
2021
|
...decision,
|
|
1579
|
-
|
|
1580
|
-
|
|
2022
|
+
// Only this explicit selected-block grammar has an artifact-local,
|
|
2023
|
+
// deterministic metadata result. Broader definition wording remains
|
|
2024
|
+
// conversational so a metric phrase cannot acquire certified trust just
|
|
2025
|
+
// because a similarly named block was retrieved.
|
|
2026
|
+
action: namedCertifiedArtifactMetadata ? 'answer' : 'converse',
|
|
2027
|
+
category: namedCertifiedArtifactMetadata ? 'data_lookup' : 'conversational',
|
|
1581
2028
|
confidence: 1,
|
|
1582
|
-
reason:
|
|
2029
|
+
reason: namedCertifiedArtifactMetadata
|
|
2030
|
+
? 'This asks what one selected certified artifact means, so its artifact metadata is returned without running a query.'
|
|
2031
|
+
: 'This asks what a governed artifact means, so it is answered from its definition rather than by running a query.',
|
|
1583
2032
|
requiresClarification: false,
|
|
1584
2033
|
clarifyingQuestion: undefined,
|
|
1585
2034
|
clarificationOptions: undefined,
|
|
@@ -1804,10 +2253,21 @@ function terminalLifecycle(prior, phase, completedAt, eventCursor) {
|
|
|
1804
2253
|
completedAt,
|
|
1805
2254
|
};
|
|
1806
2255
|
}
|
|
1807
|
-
function diagnosticFailureFromError(error, phase) {
|
|
2256
|
+
function diagnosticFailureFromError(error, phase, requestedMode) {
|
|
1808
2257
|
const name = error instanceof Error ? error.name : "";
|
|
1809
2258
|
const message = error instanceof Error ? error.message : String(error);
|
|
1810
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
|
+
}
|
|
1811
2271
|
if (isOrchestrationBudgetExhausted(error)) {
|
|
1812
2272
|
return {
|
|
1813
2273
|
code: 'orchestration_budget_exhausted',
|
|
@@ -1818,6 +2278,15 @@ function diagnosticFailureFromError(error, phase) {
|
|
|
1818
2278
|
};
|
|
1819
2279
|
}
|
|
1820
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
|
+
}
|
|
1821
2290
|
return {
|
|
1822
2291
|
code: "TIMEOUT",
|
|
1823
2292
|
phase,
|
|
@@ -1906,6 +2375,549 @@ function diagnosticReceiptV2ForRun(run) {
|
|
|
1906
2375
|
...(run.repairCapability ? { repairCapabilityFingerprint: receiptFingerprint(run.repairCapability) } : {}),
|
|
1907
2376
|
};
|
|
1908
2377
|
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Build a compact V3 receipt from existing durable run state. It intentionally
|
|
2380
|
+
* records identifiers and outcomes, never raw metadata, result rows, provider
|
|
2381
|
+
* payloads, or secret-bearing URLs.
|
|
2382
|
+
*/
|
|
2383
|
+
function diagnosticReceiptV3ForRun(run) {
|
|
2384
|
+
// The router is the sole cascade authority. Do not reconstruct a tier from
|
|
2385
|
+
// route names or identifier text here: that erased stale/error lane states
|
|
2386
|
+
// and falsely reported governed-relational success for pure exploration.
|
|
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;
|
|
2393
|
+
const sourceCoverage = cascade?.sourceCoverage ?? [];
|
|
2394
|
+
const planFrozen = cascade?.planFrozen ?? false;
|
|
2395
|
+
const artifactProviderDiagnostic = run.artifacts
|
|
2396
|
+
.map((artifact) => artifact.payload)
|
|
2397
|
+
.filter((payload) => Boolean(payload) && typeof payload === 'object' && !Array.isArray(payload))
|
|
2398
|
+
.map((payload) => payload.providerFailure)
|
|
2399
|
+
.find((failure) => Boolean(failure) && typeof failure === 'object' && !Array.isArray(failure));
|
|
2400
|
+
const persistedProviderDiagnostic = artifactProviderDiagnostic?.diagnostic;
|
|
2401
|
+
const provider = persistedProviderDiagnostic && typeof persistedProviderDiagnostic === 'object'
|
|
2402
|
+
? persistedProviderDiagnostic
|
|
2403
|
+
: (() => {
|
|
2404
|
+
const failure = run.diagnosticReceipt?.failure;
|
|
2405
|
+
return failure && (failure.code === 'AI_PROVIDER_FAILURE' || /provider/i.test(failure.code))
|
|
2406
|
+
? classifyProviderFailure({ message: failure.message, code: failure.code, phase: 'generation' })
|
|
2407
|
+
: undefined;
|
|
2408
|
+
})();
|
|
2409
|
+
return {
|
|
2410
|
+
version: 3,
|
|
2411
|
+
runId: run.id,
|
|
2412
|
+
sourceCoverage,
|
|
2413
|
+
...(cascade ? { cascade } : {}),
|
|
2414
|
+
...(terminalGap ? { terminalGap } : {}),
|
|
2415
|
+
planFrozen,
|
|
2416
|
+
...(provider ? { provider } : {}),
|
|
2417
|
+
finalStopReason: run.stopReason,
|
|
2418
|
+
};
|
|
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
|
+
}
|
|
1909
2921
|
function emptyRunTelemetry(total, fallbackReason) {
|
|
1910
2922
|
return {
|
|
1911
2923
|
version: 1,
|
|
@@ -1931,7 +2943,7 @@ function durationBetweenMs(startedAt, completedAt) {
|
|
|
1931
2943
|
function receiptFingerprint(value) {
|
|
1932
2944
|
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
1933
2945
|
}
|
|
1934
|
-
function attachDiagnosticReceipt(artifacts, receipt, receiptV2) {
|
|
2946
|
+
function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3, receiptV4) {
|
|
1935
2947
|
if (artifacts.length === 0) {
|
|
1936
2948
|
if (!receipt.failure)
|
|
1937
2949
|
return artifacts;
|
|
@@ -1940,7 +2952,7 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2) {
|
|
|
1940
2952
|
kind: "answer",
|
|
1941
2953
|
title: "Agent run diagnostics",
|
|
1942
2954
|
trustState: "blocked",
|
|
1943
|
-
payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}) },
|
|
2955
|
+
payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}), ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}), ...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}) },
|
|
1944
2956
|
}];
|
|
1945
2957
|
}
|
|
1946
2958
|
const preferredIndex = Math.max(0, artifacts.findIndex((artifact) => artifact.kind === "answer"));
|
|
@@ -1956,6 +2968,8 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2) {
|
|
|
1956
2968
|
...payload,
|
|
1957
2969
|
diagnosticReceipt: receipt,
|
|
1958
2970
|
...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}),
|
|
2971
|
+
...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}),
|
|
2972
|
+
...(receiptV4 ? { diagnosticReceiptV4: receiptV4 } : {}),
|
|
1959
2973
|
},
|
|
1960
2974
|
};
|
|
1961
2975
|
});
|
|
@@ -2192,6 +3206,9 @@ export function selectRoute(request, decision) {
|
|
|
2192
3206
|
return 'clarify';
|
|
2193
3207
|
return 'blocked';
|
|
2194
3208
|
}
|
|
3209
|
+
const cascadeRoute = routeFromAnalyticalCascade(decision);
|
|
3210
|
+
if (cascadeRoute)
|
|
3211
|
+
return cascadeRoute;
|
|
2195
3212
|
const explicitMode = request.requestedMode;
|
|
2196
3213
|
if (explicitMode === 'modeling')
|
|
2197
3214
|
return 'modeling_draft';
|