@duckcodeailabs/dql-agent 1.14.0 → 1.14.2
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 +68 -2
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +520 -26
- 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 +25 -0
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +91 -0
- package/dist/agentic/analyst-loop.d.ts.map +1 -0
- package/dist/agentic/analyst-loop.js +331 -0
- package/dist/agentic/analyst-loop.js.map +1 -0
- package/dist/agentic/assumptions.d.ts +62 -0
- package/dist/agentic/assumptions.d.ts.map +1 -0
- package/dist/agentic/assumptions.js +73 -0
- package/dist/agentic/assumptions.js.map +1 -0
- package/dist/agentic/business-explanation.d.ts +56 -0
- package/dist/agentic/business-explanation.d.ts.map +1 -0
- package/dist/agentic/business-explanation.js +130 -0
- package/dist/agentic/business-explanation.js.map +1 -0
- package/dist/agentic/identifier-ledger.d.ts +74 -0
- package/dist/agentic/identifier-ledger.d.ts.map +1 -0
- package/dist/agentic/identifier-ledger.js +151 -0
- package/dist/agentic/identifier-ledger.js.map +1 -0
- package/dist/agentic/ledger-tools.d.ts +68 -0
- package/dist/agentic/ledger-tools.d.ts.map +1 -0
- package/dist/agentic/ledger-tools.js +198 -0
- package/dist/agentic/ledger-tools.js.map +1 -0
- package/dist/agentic/orchestrator-policy.d.ts +60 -0
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -0
- package/dist/agentic/orchestrator-policy.js +73 -0
- package/dist/agentic/orchestrator-policy.js.map +1 -0
- package/dist/agentic/orchestrator.d.ts +54 -0
- package/dist/agentic/orchestrator.d.ts.map +1 -0
- package/dist/agentic/orchestrator.js +67 -0
- package/dist/agentic/orchestrator.js.map +1 -0
- package/dist/agentic/preview-tool.d.ts +36 -0
- package/dist/agentic/preview-tool.d.ts.map +1 -0
- package/dist/agentic/preview-tool.js +71 -0
- package/dist/agentic/preview-tool.js.map +1 -0
- package/dist/agentic/rerank.d.ts +50 -0
- package/dist/agentic/rerank.d.ts.map +1 -0
- package/dist/agentic/rerank.js +140 -0
- package/dist/agentic/rerank.js.map +1 -0
- package/dist/agentic/research/hypothesis.d.ts +110 -0
- package/dist/agentic/research/hypothesis.d.ts.map +1 -0
- package/dist/agentic/research/hypothesis.js +125 -0
- package/dist/agentic/research/hypothesis.js.map +1 -0
- package/dist/agentic/research/synthesis.d.ts +36 -0
- package/dist/agentic/research/synthesis.d.ts.map +1 -0
- package/dist/agentic/research/synthesis.js +109 -0
- package/dist/agentic/research/synthesis.js.map +1 -0
- package/dist/agentic/research-agent.d.ts +54 -0
- package/dist/agentic/research-agent.d.ts.map +1 -0
- package/dist/agentic/research-agent.js +177 -0
- package/dist/agentic/research-agent.js.map +1 -0
- package/dist/agentic/sql-authorization.d.ts +131 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -0
- package/dist/agentic/sql-authorization.js +460 -0
- package/dist/agentic/sql-authorization.js.map +1 -0
- package/dist/agentic/tool-loop.d.ts +29 -0
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +129 -52
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/agentic/toolset.d.ts.map +1 -1
- package/dist/agentic/toolset.js +155 -0
- package/dist/agentic/toolset.js.map +1 -1
- package/dist/agentic/turn-plan.d.ts +35 -0
- package/dist/agentic/turn-plan.d.ts.map +1 -0
- package/dist/agentic/turn-plan.js +108 -0
- package/dist/agentic/turn-plan.js.map +1 -0
- package/dist/agentic/value-probe.d.ts +89 -0
- package/dist/agentic/value-probe.d.ts.map +1 -0
- package/dist/agentic/value-probe.js +304 -0
- package/dist/agentic/value-probe.js.map +1 -0
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +10 -1
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +382 -0
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +782 -6
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/analytical-request-policy.d.ts +26 -0
- package/dist/analytical-request-policy.d.ts.map +1 -0
- package/dist/analytical-request-policy.js +82 -0
- package/dist/analytical-request-policy.js.map +1 -0
- package/dist/analytical-result-facts.d.ts +13 -0
- package/dist/analytical-result-facts.d.ts.map +1 -1
- package/dist/analytical-result-facts.js +37 -1
- package/dist/analytical-result-facts.js.map +1 -1
- package/dist/answer-loop.d.ts +105 -16
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +833 -108
- 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/cascade/budgets.d.ts.map +1 -1
- package/dist/cascade/budgets.js +8 -1
- package/dist/cascade/budgets.js.map +1 -1
- package/dist/cascade/packer.d.ts +88 -0
- package/dist/cascade/packer.d.ts.map +1 -0
- package/dist/cascade/packer.js +106 -0
- package/dist/cascade/packer.js.map +1 -0
- package/dist/conversation/result-ops.d.ts +18 -3
- package/dist/conversation/result-ops.d.ts.map +1 -1
- package/dist/conversation/result-ops.js +77 -5
- package/dist/conversation/result-ops.js.map +1 -1
- package/dist/conversation/rolling-summary.d.ts.map +1 -1
- package/dist/conversation/rolling-summary.js +7 -2
- package/dist/conversation/rolling-summary.js.map +1 -1
- package/dist/conversation/session-store.d.ts +3 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +8 -4
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts +20 -0
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +40 -2
- 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/hints/store.d.ts.map +1 -1
- package/dist/hints/store.js +15 -5
- package/dist/hints/store.js.map +1 -1
- package/dist/index.d.ts +29 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -6
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +36 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +95 -0
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +69 -2
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +128 -7
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +124 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts +36 -0
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +290 -38
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts +20 -23
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +193 -22
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +10 -1
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +275 -53
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/metadata/metric-match.d.ts +32 -0
- package/dist/metadata/metric-match.d.ts.map +1 -1
- package/dist/metadata/metric-match.js +120 -9
- package/dist/metadata/metric-match.js.map +1 -1
- package/dist/research-loop.d.ts +33 -2
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +66 -4
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +7 -0
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +11 -4
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +32 -19
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1977 -73
- package/dist/router.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,11 +2,13 @@ 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, 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, classifyProviderFailure, } from './analytical-orchestration.js';
|
|
11
|
+
import { evaluateAnalyticalRequestPolicy } from './analytical-request-policy.js';
|
|
10
12
|
/** Routes a stakeholder may never land on (analyst authoring lives in the Notebook). */
|
|
11
13
|
const ANALYST_ONLY_ROUTES = new Set(["sql_cell", "dql_block_draft", "modeling_draft", "skill_draft"]);
|
|
12
14
|
/**
|
|
@@ -64,30 +66,60 @@ export function agentRouteDeadlineMs(route) {
|
|
|
64
66
|
return 120_000;
|
|
65
67
|
return undefined;
|
|
66
68
|
}
|
|
69
|
+
/** Default request-ingress deadlines. */
|
|
70
|
+
const DEFAULT_ASK_DEADLINE_MS = 45_000;
|
|
71
|
+
const DEFAULT_RESEARCH_DEADLINE_MS = 120_000;
|
|
72
|
+
/** Ceiling on any override, so a typo cannot hang a run indefinitely. */
|
|
73
|
+
const MAX_DEADLINE_MS = 600_000;
|
|
74
|
+
/**
|
|
75
|
+
* A deadline multiplier for slow providers.
|
|
76
|
+
*
|
|
77
|
+
* The 45s Ask budget assumes a hosted model. A local Ollama model needs ~7s for
|
|
78
|
+
* a one-word reply, so the meaning call alone can exhaust the whole window and
|
|
79
|
+
* every question comes back "The discovery window ended before an exact plan was
|
|
80
|
+
* frozen" — which makes a local model unusable at the default, and DQL is
|
|
81
|
+
* local-first by design.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately a MULTIPLIER rather than an absolute: the relationship between
|
|
84
|
+
* the Ask and Research budgets, and between each and its soft targets, is
|
|
85
|
+
* load-bearing, so scaling keeps them proportional instead of letting one
|
|
86
|
+
* setting invert them.
|
|
87
|
+
*/
|
|
88
|
+
export function deadlineScale(env = process.env) {
|
|
89
|
+
const raw = Number(env.DQL_AGENT_DEADLINE_SCALE);
|
|
90
|
+
if (!Number.isFinite(raw) || raw <= 0)
|
|
91
|
+
return 1;
|
|
92
|
+
// Bounded: below 1 would tighten a safety deadline someone is relying on.
|
|
93
|
+
return Math.min(Math.max(raw, 1), 20);
|
|
94
|
+
}
|
|
67
95
|
/** The request envelope starts before retrieval/routing, so a stuck router can
|
|
68
96
|
* never evade the route-specific deadline that is selected later. */
|
|
69
97
|
export function agentRequestDeadlineMs(requestedMode) {
|
|
70
|
-
|
|
98
|
+
const base = requestedMode === 'research' ? DEFAULT_RESEARCH_DEADLINE_MS : DEFAULT_ASK_DEADLINE_MS;
|
|
99
|
+
return Math.min(base * deadlineScale(), MAX_DEADLINE_MS);
|
|
71
100
|
}
|
|
72
101
|
/** Create the one request-ingress deadline authority used by every stage. */
|
|
73
102
|
export function createAgentRunBudget(input) {
|
|
74
103
|
const nowMs = input.nowMs ?? Date.now;
|
|
75
104
|
const startedAtMs = input.startedAtMs ?? nowMs();
|
|
76
105
|
const mode = input.requestedMode === 'research' ? 'research' : 'ask';
|
|
77
|
-
const
|
|
106
|
+
const scale = deadlineScale();
|
|
107
|
+
const hardDeadlineMs = Math.min((mode === 'research' ? DEFAULT_RESEARCH_DEADLINE_MS : DEFAULT_ASK_DEADLINE_MS) * scale, MAX_DEADLINE_MS);
|
|
78
108
|
const timeout = (input.timeoutSignal ?? AbortSignal.timeout)(hardDeadlineMs);
|
|
79
109
|
const hardSignal = input.inheritedSignal
|
|
80
110
|
? AbortSignal.any([input.inheritedSignal, timeout])
|
|
81
111
|
: timeout;
|
|
82
112
|
const elapsedMs = () => Math.max(0, nowMs() - startedAtMs);
|
|
83
113
|
const softTargetMs = (route) => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
114
|
+
const base = mode === 'research' ? 90_000 : (agentRouteDeadlineMs(route) ?? 15_000);
|
|
115
|
+
// Scaled with the hard deadline: a soft target that stayed fixed while the
|
|
116
|
+
// ceiling moved would stop new work long before the run was actually out of
|
|
117
|
+
// time, which is the same dead end by a different route.
|
|
118
|
+
return Math.min(base * scale, hardDeadlineMs);
|
|
87
119
|
};
|
|
88
120
|
// Narration must still be reachable after a full generation window, and must
|
|
89
121
|
// leave the hard deadline (45s ask / 120s research) room to land.
|
|
90
|
-
const narrationSoftTargetMs = () => (mode === 'research' ? 100_000 : 38_000);
|
|
122
|
+
const narrationSoftTargetMs = () => Math.min((mode === 'research' ? 100_000 : 38_000) * scale, hardDeadlineMs);
|
|
91
123
|
return Object.freeze({
|
|
92
124
|
startedAtMs,
|
|
93
125
|
hardDeadlineMs,
|
|
@@ -613,6 +645,120 @@ export class AgentRunEngine {
|
|
|
613
645
|
...(clarificationContinuation ? { clarificationResolved: true } : {}),
|
|
614
646
|
},
|
|
615
647
|
});
|
|
648
|
+
// This check is intentionally before route selection. A restricted direct
|
|
649
|
+
// disclosure must not be embedded, retrieved, sent to a provider, value
|
|
650
|
+
// probed, or compiled merely to explain why it cannot be answered.
|
|
651
|
+
const ingressPolicy = evaluateAnalyticalRequestPolicy(submittedQuestion);
|
|
652
|
+
if (!ingressPolicy.allowed) {
|
|
653
|
+
const routeDecision = {
|
|
654
|
+
action: 'answer',
|
|
655
|
+
confidence: 1,
|
|
656
|
+
reason: 'The request is unavailable under the Ask data-safety policy.',
|
|
657
|
+
followsUp: false,
|
|
658
|
+
source: 'heuristic',
|
|
659
|
+
terminalOutcome: {
|
|
660
|
+
kind: 'policy_blocked',
|
|
661
|
+
code: 'ANALYTICAL_POLICY_BLOCKED',
|
|
662
|
+
message: ingressPolicy.message,
|
|
663
|
+
candidateIds: [],
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
const coverageGap = buildCoverageGap({
|
|
667
|
+
code: 'POLICY_BLOCKED',
|
|
668
|
+
phase: 'retrieval',
|
|
669
|
+
message: ingressPolicy.message,
|
|
670
|
+
searchedSources: [],
|
|
671
|
+
attemptedRoutes: [],
|
|
672
|
+
missing: [],
|
|
673
|
+
recoverable: false,
|
|
674
|
+
planFrozen: false,
|
|
675
|
+
nextActions: ingressPolicy.nextActions,
|
|
676
|
+
});
|
|
677
|
+
const evaluation = {
|
|
678
|
+
id: 'request-policy',
|
|
679
|
+
label: 'Ask request policy',
|
|
680
|
+
passed: false,
|
|
681
|
+
severity: 'blocking',
|
|
682
|
+
message: ingressPolicy.message,
|
|
683
|
+
};
|
|
684
|
+
const artifact = {
|
|
685
|
+
id: `${runId}:policy`,
|
|
686
|
+
kind: 'answer',
|
|
687
|
+
title: 'Ask request unavailable',
|
|
688
|
+
trustState: 'blocked',
|
|
689
|
+
payload: {
|
|
690
|
+
analyticalCoverageGap: coverageGap,
|
|
691
|
+
analyticalFailure: {
|
|
692
|
+
code: 'POLICY_BLOCKED',
|
|
693
|
+
phase: 'request_policy',
|
|
694
|
+
message: ingressPolicy.message,
|
|
695
|
+
recoverable: false,
|
|
696
|
+
safeActions: ingressPolicy.nextActions,
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
emit({
|
|
701
|
+
type: 'route.decided',
|
|
702
|
+
message: routeDecision.reason,
|
|
703
|
+
route: 'blocked',
|
|
704
|
+
payload: routeDecision,
|
|
705
|
+
});
|
|
706
|
+
emit({
|
|
707
|
+
type: 'evaluation.recorded',
|
|
708
|
+
message: evaluation.message,
|
|
709
|
+
route: 'blocked',
|
|
710
|
+
payload: evaluation,
|
|
711
|
+
});
|
|
712
|
+
emit({
|
|
713
|
+
type: 'artifact.created',
|
|
714
|
+
message: 'Created answer artifact.',
|
|
715
|
+
route: 'blocked',
|
|
716
|
+
trustState: 'blocked',
|
|
717
|
+
payload: artifact,
|
|
718
|
+
});
|
|
719
|
+
emit({
|
|
720
|
+
type: 'run.failed',
|
|
721
|
+
message: ingressPolicy.message,
|
|
722
|
+
route: 'blocked',
|
|
723
|
+
status: 'blocked',
|
|
724
|
+
trustState: 'blocked',
|
|
725
|
+
});
|
|
726
|
+
const completedAt = this.timestamp();
|
|
727
|
+
const run = {
|
|
728
|
+
id: runId,
|
|
729
|
+
question: submittedQuestion,
|
|
730
|
+
requestedMode,
|
|
731
|
+
route: 'blocked',
|
|
732
|
+
status: 'blocked',
|
|
733
|
+
trustState: 'blocked',
|
|
734
|
+
stopReason: 'blocked',
|
|
735
|
+
startedAt,
|
|
736
|
+
completedAt,
|
|
737
|
+
selectedObject: request.selectedObject,
|
|
738
|
+
executionTarget: request.executionTarget,
|
|
739
|
+
routeDecision,
|
|
740
|
+
steps: [],
|
|
741
|
+
summary: ingressPolicy.message,
|
|
742
|
+
answer: ingressPolicy.message,
|
|
743
|
+
answerKind: 'governed',
|
|
744
|
+
artifacts: [artifact],
|
|
745
|
+
evaluations: [evaluation],
|
|
746
|
+
events,
|
|
747
|
+
nextActions: ingressPolicy.nextActions.map((id) => ({ id, label: id === 'ask_for_an_approved_aggregate' ? 'Ask for an approved aggregate' : 'Inspect data policy' })),
|
|
748
|
+
repairAttempts: 0,
|
|
749
|
+
escalationAttempts: 0,
|
|
750
|
+
budgetUsage: cascadeBudgetTrace(createCascadeBudgetState(this.budgetModel)),
|
|
751
|
+
telemetry: emptyRunTelemetry(durationBetweenMs(startedAt, completedAt), 'policy_blocked'),
|
|
752
|
+
lifecycle: terminalLifecycle(progress.lifecycle, 'run.failed', completedAt, events.length),
|
|
753
|
+
};
|
|
754
|
+
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
755
|
+
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
756
|
+
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
757
|
+
run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3);
|
|
758
|
+
await checkpointQueue;
|
|
759
|
+
await this.store?.save(run);
|
|
760
|
+
return run;
|
|
761
|
+
}
|
|
616
762
|
const audience = resolveAudience(request);
|
|
617
763
|
// Initialize a deterministic decision so router/provider timeouts can still
|
|
618
764
|
// be persisted as a complete blocked run with an inspectable trace. The old
|
|
@@ -722,10 +868,18 @@ export class AgentRunEngine {
|
|
|
722
868
|
attempt,
|
|
723
869
|
stepGoal: planned.goal,
|
|
724
870
|
priorEvaluations,
|
|
871
|
+
priorArtifacts: progress.artifacts,
|
|
725
872
|
repairHint,
|
|
726
873
|
emit,
|
|
727
874
|
emitAnswerDelta: onAnswerDelta,
|
|
728
875
|
});
|
|
876
|
+
// The router owns a frozen analytical tier. An executor may report a
|
|
877
|
+
// same-tier execution failure, but it cannot turn a certified or
|
|
878
|
+
// semantic plan into generated work (or vice versa) after execution
|
|
879
|
+
// has started. Keep this guard in the engine as well as host adapters
|
|
880
|
+
// so an injected/legacy executor cannot redefine durable provenance.
|
|
881
|
+
routeDecision = applyExploratoryExecutionFreeze(routeDecision, result.analyticalExecutionFreeze);
|
|
882
|
+
result = preserveFrozenAnalyticalRoute(route, routeDecision, result);
|
|
729
883
|
result = consumeRepeatedClarificationSelection(request, routeDecision, result);
|
|
730
884
|
if (result.analyticalTurnPlan)
|
|
731
885
|
progress.analyticalTurnPlan = result.analyticalTurnPlan;
|
|
@@ -754,7 +908,7 @@ export class AgentRunEngine {
|
|
|
754
908
|
// A frozen analytical plan has one route and no downstream planner,
|
|
755
909
|
// rematch, route escalation, or whole-answer regeneration authority.
|
|
756
910
|
// Typed server-issued repair is a separate derived run.
|
|
757
|
-
if (authoritativeAsk) {
|
|
911
|
+
if (authoritativeAsk || routeDecision.analyticalCascadeDecision?.planFrozen === true) {
|
|
758
912
|
stepStatus = 'needs_review';
|
|
759
913
|
break;
|
|
760
914
|
}
|
|
@@ -959,7 +1113,8 @@ export class AgentRunEngine {
|
|
|
959
1113
|
run.lifecycle = terminalLifecycle(progress.lifecycle, run.status === "blocked" ? "run.failed" : "run.completed", run.completedAt, events.length);
|
|
960
1114
|
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
961
1115
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
962
|
-
run.
|
|
1116
|
+
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1117
|
+
run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3);
|
|
963
1118
|
await checkpointQueue;
|
|
964
1119
|
await this.store?.save(run);
|
|
965
1120
|
return run;
|
|
@@ -1045,6 +1200,7 @@ export class AgentRunEngine {
|
|
|
1045
1200
|
lifecycle: terminalLifecycle(progress.lifecycle, "run.cancelled", completedAt, events.length),
|
|
1046
1201
|
};
|
|
1047
1202
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1203
|
+
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1048
1204
|
await checkpointQueue;
|
|
1049
1205
|
await this.store?.save(run);
|
|
1050
1206
|
return run;
|
|
@@ -1128,7 +1284,8 @@ export class AgentRunEngine {
|
|
|
1128
1284
|
lifecycle: terminalLifecycle(progress.lifecycle, "run.failed", completedAt, events.length),
|
|
1129
1285
|
};
|
|
1130
1286
|
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1131
|
-
run.
|
|
1287
|
+
run.diagnosticReceiptV3 = diagnosticReceiptV3ForRun(run);
|
|
1288
|
+
run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2, run.diagnosticReceiptV3);
|
|
1132
1289
|
await checkpointQueue;
|
|
1133
1290
|
await this.store?.save(run);
|
|
1134
1291
|
return run;
|
|
@@ -1220,6 +1377,9 @@ export class AgentRunEngine {
|
|
|
1220
1377
|
...(finalResult.telemetry ? {
|
|
1221
1378
|
telemetry: withTotalDuration(finalResult.telemetry, durationBetweenMs(input.startedAt, completedAt)),
|
|
1222
1379
|
} : {}),
|
|
1380
|
+
...(finalResult.narrationIntegrityReceipt ? {
|
|
1381
|
+
narrationIntegrityReceipt: finalResult.narrationIntegrityReceipt,
|
|
1382
|
+
} : {}),
|
|
1223
1383
|
escalationAttempts,
|
|
1224
1384
|
budgetUsage: input.budgetUsage,
|
|
1225
1385
|
...(finalResult.analyticalTurnPlan ? { analyticalTurnPlan: finalResult.analyticalTurnPlan } : {}),
|
|
@@ -1246,7 +1406,7 @@ export class AgentRunEngine {
|
|
|
1246
1406
|
&& !frozenPlan
|
|
1247
1407
|
&& context.request.runBudget
|
|
1248
1408
|
&& !context.request.runBudget.mayStartDiscovery(context.route)) {
|
|
1249
|
-
return softBoundaryResult(context.route, context.request.runBudget);
|
|
1409
|
+
return softBoundaryResult(context.route, context.request.runBudget, context.priorArtifacts);
|
|
1250
1410
|
}
|
|
1251
1411
|
const signal = context.request.runBudget?.hardSignal ?? context.request.signal;
|
|
1252
1412
|
if (signal?.aborted)
|
|
@@ -1278,6 +1438,235 @@ export class AgentRunEngine {
|
|
|
1278
1438
|
* inject or deserialize an older router decision as well as the canonical
|
|
1279
1439
|
* router producer.
|
|
1280
1440
|
*/
|
|
1441
|
+
/** An ordinary Ask turn (not Research, App Build, or another explicit mode). */
|
|
1442
|
+
function isOrdinaryAskRequest(request) {
|
|
1443
|
+
return request.requestedMode === undefined
|
|
1444
|
+
|| request.requestedMode === 'auto'
|
|
1445
|
+
|| request.requestedMode === 'ask';
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* A modeling/coverage gap is a pre-freeze DISCOVERY result, not permission to
|
|
1449
|
+
* terminate an ordinary Ask. Keep the diagnostic as a typed reason, clear the
|
|
1450
|
+
* blocked RAP, and let the answer executor continue through the governed
|
|
1451
|
+
* relational and review-required generated lanes (AGT-028, EXP-001).
|
|
1452
|
+
*
|
|
1453
|
+
* `requireGovernedEvidence` separates the two callers, and it is what keeps the
|
|
1454
|
+
* analytical-RAP safety boundary intact:
|
|
1455
|
+
*
|
|
1456
|
+
* - A gap the ROUTER reported is already proof that discovery ran and found a
|
|
1457
|
+
* modelling limit, so it is rescued unconditionally (the original AGT-028
|
|
1458
|
+
* behaviour).
|
|
1459
|
+
* - A block this module SYNTHESIZES only means "nothing froze here". That is
|
|
1460
|
+
* also what an absent, LLM-authored, or forged router decision looks like,
|
|
1461
|
+
* so it is rescued only when retrieval actually surfaced governed
|
|
1462
|
+
* candidates. With no evidence at all it must still fail closed — a forged
|
|
1463
|
+
* `converse`/`answer` must never bypass the boundary on an analytical
|
|
1464
|
+
* question.
|
|
1465
|
+
*
|
|
1466
|
+
* Returns `undefined` when the decision is not a rescuable modeling gap, so
|
|
1467
|
+
* callers can fall through to their own handling.
|
|
1468
|
+
*/
|
|
1469
|
+
function rescueModelingGapForOrdinaryAsk(request, decision, options = {}) {
|
|
1470
|
+
if (!isOrdinaryAskRequest(request))
|
|
1471
|
+
return undefined;
|
|
1472
|
+
if (decision.terminalOutcome?.kind !== 'modeling_gap')
|
|
1473
|
+
return undefined;
|
|
1474
|
+
// A frozen plan is authoritative. Only a PRE-FREEZE coverage observation may
|
|
1475
|
+
// advance through the later governed-relational/exploratory tiers. This keeps
|
|
1476
|
+
// compiler, policy, validation, and warehouse failures terminal after a plan
|
|
1477
|
+
// has been accepted while restoring the required certified → semantic →
|
|
1478
|
+
// relational → review-required exploration cascade for missing dimensions.
|
|
1479
|
+
if (decision.resolvedAnalyticalPlan?.mode === 'authoritative')
|
|
1480
|
+
return undefined;
|
|
1481
|
+
// Genuine user-facing ambiguity and an explicit evidence pick stay terminal.
|
|
1482
|
+
if (decision.requiresClarification === true)
|
|
1483
|
+
return undefined;
|
|
1484
|
+
if (request.selectedEvidenceId)
|
|
1485
|
+
return undefined;
|
|
1486
|
+
// The router, not this host boundary, owns cascade eligibility. A typed
|
|
1487
|
+
// pre-freeze coverage gap advances only when the same snapshot recorded an
|
|
1488
|
+
// executable exploratory tier. This prevents a semantic-only candidate or a
|
|
1489
|
+
// forged/old terminal decision from quietly becoming generated SQL.
|
|
1490
|
+
const exploratoryAttempt = decision.analyticalCascadeDecision?.attempts.find((attempt) => attempt.tier === 'exploratory_sql');
|
|
1491
|
+
if (decision.analyticalCascadeDecision?.selectedTier !== 'exploratory_sql'
|
|
1492
|
+
|| decision.analyticalCascadeDecision.planFrozen
|
|
1493
|
+
|| exploratoryAttempt?.outcome !== 'executable'
|
|
1494
|
+
|| exploratoryAttempt.candidateIds.length === 0)
|
|
1495
|
+
return undefined;
|
|
1496
|
+
if (options.requireGovernedEvidence) {
|
|
1497
|
+
const governedEvidence = (decision.retrievalEvidence?.candidateCount ?? 0) > 0
|
|
1498
|
+
|| (decision.meaningResolution?.selectedConceptIds.length ?? 0) > 0;
|
|
1499
|
+
if (!governedEvidence)
|
|
1500
|
+
return undefined;
|
|
1501
|
+
}
|
|
1502
|
+
return {
|
|
1503
|
+
...decision,
|
|
1504
|
+
action: 'answer',
|
|
1505
|
+
confidence: Math.min(decision.confidence, 0.55),
|
|
1506
|
+
reason: `${decision.terminalOutcome.message} Continuing through DBT-grounded relational and review-required generated analysis before asking for a modeling change.`,
|
|
1507
|
+
terminalOutcome: undefined,
|
|
1508
|
+
resolvedAnalyticalPlan: undefined,
|
|
1509
|
+
requiresClarification: false,
|
|
1510
|
+
// The premise of this rescue is that NO governed plan froze, so the turn
|
|
1511
|
+
// must not take `selectRoute`'s certified/semantic shortcut — that path
|
|
1512
|
+
// stamps `certified`/`governed` trust off a bare RECOMMENDATION, which is a
|
|
1513
|
+
// suggestion, not a bound execution contract. Keep the meaning evidence for
|
|
1514
|
+
// citations and downgrade the route to governed SQL, which is review-required.
|
|
1515
|
+
...(decision.meaningResolution
|
|
1516
|
+
? {
|
|
1517
|
+
meaningResolution: {
|
|
1518
|
+
...decision.meaningResolution,
|
|
1519
|
+
recommendedRoute: 'governed_sql',
|
|
1520
|
+
},
|
|
1521
|
+
}
|
|
1522
|
+
: {}),
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Consume the router's immutable cascade decision without reparsing the
|
|
1527
|
+
* question or reconstructing a tier from route/identifier text. The selected
|
|
1528
|
+
* tier is intentionally sufficient for dispatch; compilation and execution
|
|
1529
|
+
* still validate the frozen plan or review-required exploratory SQL.
|
|
1530
|
+
*/
|
|
1531
|
+
function routeFromAnalyticalCascade(decision) {
|
|
1532
|
+
const cascade = decision.analyticalCascadeDecision;
|
|
1533
|
+
if (!cascade)
|
|
1534
|
+
return undefined;
|
|
1535
|
+
if (cascade.stopReason === 'denied' || cascade.stopReason === 'coverage_gap' || cascade.stopReason === 'post_freeze_failure') {
|
|
1536
|
+
return 'blocked';
|
|
1537
|
+
}
|
|
1538
|
+
if (cascade.stopReason === 'ambiguous')
|
|
1539
|
+
return 'clarify';
|
|
1540
|
+
switch (cascade.selectedTier) {
|
|
1541
|
+
case 'certified':
|
|
1542
|
+
return cascade.planFrozen ? 'certified_answer' : undefined;
|
|
1543
|
+
case 'semantic':
|
|
1544
|
+
return cascade.planFrozen ? 'semantic_answer' : undefined;
|
|
1545
|
+
case 'governed_relational':
|
|
1546
|
+
case 'exploratory_sql':
|
|
1547
|
+
return 'generated_answer';
|
|
1548
|
+
default:
|
|
1549
|
+
return undefined;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* A frozen router decision is an immutable execution contract, not a hint that
|
|
1554
|
+
* a downstream answer loop may replace with another meaning/tier. The executor
|
|
1555
|
+
* is still free to return a terminal compilation, provider, adapter, or result
|
|
1556
|
+
* failure, but it must retain the selected route while doing so.
|
|
1557
|
+
*/
|
|
1558
|
+
function preserveFrozenAnalyticalRoute(route, decision, result) {
|
|
1559
|
+
const frozen = decision.analyticalCascadeDecision?.planFrozen === true
|
|
1560
|
+
|| decision.resolvedAnalyticalPlan?.mode === 'authoritative';
|
|
1561
|
+
if (!frozen || !result.resolvedRoute || result.resolvedRoute === route)
|
|
1562
|
+
return result;
|
|
1563
|
+
return {
|
|
1564
|
+
resolvedRoute: route,
|
|
1565
|
+
status: 'blocked',
|
|
1566
|
+
trustState: 'blocked',
|
|
1567
|
+
stopReason: 'blocked',
|
|
1568
|
+
summary: `The frozen ${route.replaceAll('_', ' ')} plan could not execute as selected. DQL did not substitute another analytical tier.`,
|
|
1569
|
+
answer: 'The selected analytical plan could not be executed as selected. No fallback answer was returned.',
|
|
1570
|
+
artifacts: [],
|
|
1571
|
+
evaluations: [{
|
|
1572
|
+
id: 'frozen-plan-route-mismatch',
|
|
1573
|
+
label: 'Frozen analytical route',
|
|
1574
|
+
passed: false,
|
|
1575
|
+
severity: 'blocking',
|
|
1576
|
+
message: `The executor reported ${result.resolvedRoute.replaceAll('_', ' ')} after the router froze ${route.replaceAll('_', ' ')}.`,
|
|
1577
|
+
evidence: {
|
|
1578
|
+
selectedRoute: route,
|
|
1579
|
+
reportedRoute: result.resolvedRoute,
|
|
1580
|
+
selectedTier: decision.analyticalCascadeDecision?.selectedTier,
|
|
1581
|
+
planId: decision.resolvedAnalyticalPlan?.planId,
|
|
1582
|
+
},
|
|
1583
|
+
}],
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Promote only an explicit host-issued exploratory freeze into the router
|
|
1588
|
+
* decision that will be persisted. This deliberately does not inspect route
|
|
1589
|
+
* names, SQL strings, or identifier patterns: a selected tier is immutable
|
|
1590
|
+
* only when its own candidate set, snapshot, target, and capability receipt
|
|
1591
|
+
* all agree.
|
|
1592
|
+
*/
|
|
1593
|
+
function applyExploratoryExecutionFreeze(decision, freeze) {
|
|
1594
|
+
if (!freeze)
|
|
1595
|
+
return decision;
|
|
1596
|
+
const cascade = decision.analyticalCascadeDecision;
|
|
1597
|
+
const attempt = cascade?.attempts.find((candidate) => candidate.tier === 'exploratory_sql');
|
|
1598
|
+
const sameCandidates = Boolean(attempt
|
|
1599
|
+
&& attempt.candidateIds.length === freeze.candidateIds.length
|
|
1600
|
+
&& attempt.candidateIds.every((candidate, index) => candidate === freeze.candidateIds[index]));
|
|
1601
|
+
const retrievalSnapshotId = decision.retrievalEvidence?.snapshotId;
|
|
1602
|
+
const valid = Boolean(cascade
|
|
1603
|
+
&& cascade.selectedTier === 'exploratory_sql'
|
|
1604
|
+
&& cascade.planFrozen === false
|
|
1605
|
+
&& attempt?.outcome === 'executable'
|
|
1606
|
+
&& sameCandidates
|
|
1607
|
+
&& freeze.version === 1
|
|
1608
|
+
&& freeze.selectedTier === 'exploratory_sql'
|
|
1609
|
+
&& freeze.authorization === 'capability_minted'
|
|
1610
|
+
&& freeze.planId.trim()
|
|
1611
|
+
&& freeze.planFingerprint.trim()
|
|
1612
|
+
&& freeze.snapshotId.trim()
|
|
1613
|
+
&& freeze.targetFingerprint.trim()
|
|
1614
|
+
&& freeze.sqlFingerprint.trim()
|
|
1615
|
+
&& (!retrievalSnapshotId || retrievalSnapshotId === freeze.snapshotId));
|
|
1616
|
+
if (!valid) {
|
|
1617
|
+
throw Object.assign(new Error('The exploratory execution receipt did not match the router-selected candidate set and was not accepted.'), {
|
|
1618
|
+
code: 'EXPLORATORY_FREEZE_RECEIPT_MISMATCH',
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
return {
|
|
1622
|
+
...decision,
|
|
1623
|
+
analyticalCascadeDecision: {
|
|
1624
|
+
...cascade,
|
|
1625
|
+
planFrozen: true,
|
|
1626
|
+
exploratoryExecutionFreeze: freeze,
|
|
1627
|
+
attempts: cascade.attempts.map((candidate) => candidate.tier === 'exploratory_sql'
|
|
1628
|
+
? {
|
|
1629
|
+
...candidate,
|
|
1630
|
+
planFrozen: true,
|
|
1631
|
+
reason: `${candidate.reason} Host-authorized immutable exploratory execution plan ${freeze.planId}.`,
|
|
1632
|
+
}
|
|
1633
|
+
: candidate),
|
|
1634
|
+
},
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Has this exact clarification already been asked in this thread?
|
|
1639
|
+
*
|
|
1640
|
+
* The reported loop: "who are the top customers for BCM" returned "Top by which
|
|
1641
|
+
* governed metric?", the user answered in prose, and the IDENTICAL question came
|
|
1642
|
+
* back. Their reply reads as a complete new question — which is a defensible
|
|
1643
|
+
* classification — so it re-entered the cascade fresh, retrieved the same
|
|
1644
|
+
* evidence, and produced the same clarification. Nothing in the loop noticed it
|
|
1645
|
+
* had already been there.
|
|
1646
|
+
*
|
|
1647
|
+
* Asking twice is proof the question does not work: the user has already seen it
|
|
1648
|
+
* and responded, and a third identical prompt cannot produce a different reply.
|
|
1649
|
+
* Whatever the best available interpretation is, committing to it and saying so
|
|
1650
|
+
* beats asking again.
|
|
1651
|
+
*/
|
|
1652
|
+
function clarificationAlreadyAsked(clarifyingQuestion, history) {
|
|
1653
|
+
const asking = clarifyingQuestion?.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1654
|
+
if (!asking || !history?.length)
|
|
1655
|
+
return false;
|
|
1656
|
+
let sawAssistantAsk = false;
|
|
1657
|
+
for (const turn of history) {
|
|
1658
|
+
if (turn.role !== 'assistant')
|
|
1659
|
+
continue;
|
|
1660
|
+
if (turn.text.replace(/\s+/g, ' ').trim().toLowerCase().includes(asking))
|
|
1661
|
+
sawAssistantAsk = true;
|
|
1662
|
+
}
|
|
1663
|
+
if (!sawAssistantAsk)
|
|
1664
|
+
return false;
|
|
1665
|
+
// Only a loop once the user has actually replied to it. A clarification still
|
|
1666
|
+
// waiting for its first answer is not a repeat.
|
|
1667
|
+
const lastAsk = [...history].reverse().findIndex((turn) => turn.role === 'assistant' && turn.text.replace(/\s+/g, ' ').trim().toLowerCase().includes(asking));
|
|
1668
|
+
return lastAsk > 0;
|
|
1669
|
+
}
|
|
1281
1670
|
function enforceOrdinaryAnalyticalPlanBoundary(request, decision) {
|
|
1282
1671
|
const ordinaryAsk = request.requestedMode === undefined
|
|
1283
1672
|
|| request.requestedMode === 'auto'
|
|
@@ -1286,24 +1675,58 @@ function enforceOrdinaryAnalyticalPlanBoundary(request, decision) {
|
|
|
1286
1675
|
|| decision.action === 'block'
|
|
1287
1676
|
|| decision.requiresClarification === true
|
|
1288
1677
|
|| Boolean(decision.terminalOutcome);
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1678
|
+
const inboundRescue = rescueModelingGapForOrdinaryAsk(request, decision);
|
|
1679
|
+
if (inboundRescue)
|
|
1680
|
+
return inboundRescue;
|
|
1681
|
+
// A clarification the user has already seen and answered cannot be asked
|
|
1682
|
+
// again. Checked before the `terminal` guard for the same reason as the
|
|
1683
|
+
// definitional case: the gate has already set `action: 'clarify'` by here.
|
|
1295
1684
|
if (ordinaryAsk
|
|
1296
|
-
&& decision.
|
|
1297
|
-
&& decision.
|
|
1298
|
-
&& !request.selectedEvidenceId) {
|
|
1685
|
+
&& decision.action === 'clarify'
|
|
1686
|
+
&& clarificationAlreadyAsked(decision.clarifyingQuestion, request.history?.length ? request.history : conversationHistoryFromContext(request.conversationContext))) {
|
|
1299
1687
|
return {
|
|
1300
1688
|
...decision,
|
|
1301
1689
|
action: 'answer',
|
|
1302
|
-
confidence: Math.min(decision.confidence, 0.
|
|
1303
|
-
reason:
|
|
1690
|
+
confidence: Math.min(decision.confidence, 0.6),
|
|
1691
|
+
reason: `This clarification was already asked and answered in this thread, so DQL proceeded with the best supported interpretation instead of repeating it: ${decision.clarifyingQuestion ?? ''}`.trim(),
|
|
1692
|
+
requiresClarification: false,
|
|
1693
|
+
clarifyingQuestion: undefined,
|
|
1304
1694
|
terminalOutcome: undefined,
|
|
1305
1695
|
resolvedAnalyticalPlan: undefined,
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
// A DEFINITIONAL question about an artifact it names is not an analytical
|
|
1699
|
+
// request, and must be caught before the `terminal` guard below — the
|
|
1700
|
+
// ambiguity gate has already set `action: 'clarify'` by this point, so any
|
|
1701
|
+
// check placed after it is unreachable.
|
|
1702
|
+
//
|
|
1703
|
+
// Without this, "what is food_vs_drink_revenue?" is answered with "Which
|
|
1704
|
+
// governed meaning should DQL bind: food_vs_drink_revenue or …?" — asking the
|
|
1705
|
+
// user to disambiguate the one artifact they just named. The plan cannot see
|
|
1706
|
+
// it because it reads the artifact's OWN NAME as analytical intent: that name
|
|
1707
|
+
// contains "vs", so the mode comes back `comparison`.
|
|
1708
|
+
const namedCertifiedArtifactMetadata = looksLikeNamedCertifiedArtifactMetadataRequest(request.question, decision.retrievalEvidence?.candidateIds ?? []);
|
|
1709
|
+
if (ordinaryAsk
|
|
1710
|
+
&& !request.selectedEvidenceId
|
|
1711
|
+
&& (namedCertifiedArtifactMetadata
|
|
1712
|
+
|| looksLikeDefinitionalAboutNamedObject(request.question, decision.retrievalEvidence?.candidateIds ?? []))) {
|
|
1713
|
+
return {
|
|
1714
|
+
...decision,
|
|
1715
|
+
// Only this explicit selected-block grammar has an artifact-local,
|
|
1716
|
+
// deterministic metadata result. Broader definition wording remains
|
|
1717
|
+
// conversational so a metric phrase cannot acquire certified trust just
|
|
1718
|
+
// because a similarly named block was retrieved.
|
|
1719
|
+
action: namedCertifiedArtifactMetadata ? 'answer' : 'converse',
|
|
1720
|
+
category: namedCertifiedArtifactMetadata ? 'data_lookup' : 'conversational',
|
|
1721
|
+
confidence: 1,
|
|
1722
|
+
reason: namedCertifiedArtifactMetadata
|
|
1723
|
+
? 'This asks what one selected certified artifact means, so its artifact metadata is returned without running a query.'
|
|
1724
|
+
: 'This asks what a governed artifact means, so it is answered from its definition rather than by running a query.',
|
|
1306
1725
|
requiresClarification: false,
|
|
1726
|
+
clarifyingQuestion: undefined,
|
|
1727
|
+
clarificationOptions: undefined,
|
|
1728
|
+
terminalOutcome: undefined,
|
|
1729
|
+
resolvedAnalyticalPlan: undefined,
|
|
1307
1730
|
};
|
|
1308
1731
|
}
|
|
1309
1732
|
const exactSemanticContinuation = Boolean(request.selectedEvidenceId
|
|
@@ -1368,7 +1791,7 @@ function enforceOrdinaryAnalyticalPlanBoundary(request, decision) {
|
|
|
1368
1791
|
if (!analytical)
|
|
1369
1792
|
return decision;
|
|
1370
1793
|
const message = 'DQL could not freeze an exact analytical plan for the requested metric, grain, filters, ordering, and outputs. Choose a governed identifier or model the missing capability before retrying.';
|
|
1371
|
-
|
|
1794
|
+
const blocked = {
|
|
1372
1795
|
...decision,
|
|
1373
1796
|
action: 'block',
|
|
1374
1797
|
confidence: 1,
|
|
@@ -1381,9 +1804,40 @@ function enforceOrdinaryAnalyticalPlanBoundary(request, decision) {
|
|
|
1381
1804
|
candidateIds: decision.retrievalEvidence?.candidateIds ?? [],
|
|
1382
1805
|
},
|
|
1383
1806
|
};
|
|
1807
|
+
// The block synthesized HERE is a modeling gap too, so it must face the same
|
|
1808
|
+
// rescue as one arriving from the router. Without this, the rescue above
|
|
1809
|
+
// silently did not apply to the most common way an ordinary Ask gets blocked:
|
|
1810
|
+
// retrieval found governed candidates, no exact tuple froze, and the run
|
|
1811
|
+
// dead-ended instead of continuing to the generated lane.
|
|
1812
|
+
return rescueModelingGapForOrdinaryAsk(request, blocked, { requireGovernedEvidence: true }) ?? blocked;
|
|
1384
1813
|
}
|
|
1385
|
-
function softBoundaryResult(route, budget) {
|
|
1814
|
+
function softBoundaryResult(route, budget, priorArtifacts = []) {
|
|
1386
1815
|
const seconds = Math.round(budget.softTargetMs(route) / 1_000);
|
|
1816
|
+
// Admission control must never turn work that already validated into a
|
|
1817
|
+
// refusal. A run that established findings and then ran out of clock has
|
|
1818
|
+
// something true to say; answering `blocked` discards it and asks the user
|
|
1819
|
+
// to start the same investigation over. Partial and labelled beats nothing.
|
|
1820
|
+
const established = priorArtifacts.filter((artifact) => artifact.trustState !== "blocked" && artifact.trustState !== "not_applicable");
|
|
1821
|
+
if (established.length > 0) {
|
|
1822
|
+
const titles = established.map((artifact) => artifact.title).filter((title) => Boolean(title));
|
|
1823
|
+
const named = titles.length === 1
|
|
1824
|
+
? titles[0]
|
|
1825
|
+
: `${titles.slice(0, -1).join(", ")} and ${titles[titles.length - 1]}`;
|
|
1826
|
+
const plural = established.length === 1;
|
|
1827
|
+
return {
|
|
1828
|
+
resolvedRoute: route,
|
|
1829
|
+
status: "needs_review",
|
|
1830
|
+
trustState: "review_required",
|
|
1831
|
+
stopReason: "human_review_required",
|
|
1832
|
+
summary: `The ${seconds}-second discovery target elapsed. Returning what was established rather than discarding it.`,
|
|
1833
|
+
answer: titles.length > 0
|
|
1834
|
+
? `The discovery window ended before the whole question was covered, so this is partial: ${named}. ${plural ? "It was" : "They were"} validated before the clock ran out; nothing beyond ${plural ? "it" : "them"} was investigated. Continue to pick up from here.`
|
|
1835
|
+
: `The discovery window ended before the whole question was covered. ${established.length} validated finding${plural ? "" : "s"} from this run ${plural ? "is" : "are"} attached; nothing beyond ${plural ? "it" : "them"} was investigated.`,
|
|
1836
|
+
artifacts: [...established],
|
|
1837
|
+
evaluations: [],
|
|
1838
|
+
nextActions: [{ id: "continue-after-soft-target", label: "Continue the investigation" }],
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1387
1841
|
return {
|
|
1388
1842
|
resolvedRoute: 'clarify',
|
|
1389
1843
|
status: 'needs_clarification',
|
|
@@ -1594,6 +2048,42 @@ function diagnosticReceiptV2ForRun(run) {
|
|
|
1594
2048
|
...(run.repairCapability ? { repairCapabilityFingerprint: receiptFingerprint(run.repairCapability) } : {}),
|
|
1595
2049
|
};
|
|
1596
2050
|
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Build a compact V3 receipt from existing durable run state. It intentionally
|
|
2053
|
+
* records identifiers and outcomes, never raw metadata, result rows, provider
|
|
2054
|
+
* payloads, or secret-bearing URLs.
|
|
2055
|
+
*/
|
|
2056
|
+
function diagnosticReceiptV3ForRun(run) {
|
|
2057
|
+
// The router is the sole cascade authority. Do not reconstruct a tier from
|
|
2058
|
+
// route names or identifier text here: that erased stale/error lane states
|
|
2059
|
+
// and falsely reported governed-relational success for pure exploration.
|
|
2060
|
+
const cascade = run.routeDecision?.analyticalCascadeDecision;
|
|
2061
|
+
const sourceCoverage = cascade?.sourceCoverage ?? [];
|
|
2062
|
+
const planFrozen = cascade?.planFrozen ?? false;
|
|
2063
|
+
const artifactProviderDiagnostic = run.artifacts
|
|
2064
|
+
.map((artifact) => artifact.payload)
|
|
2065
|
+
.filter((payload) => Boolean(payload) && typeof payload === 'object' && !Array.isArray(payload))
|
|
2066
|
+
.map((payload) => payload.providerFailure)
|
|
2067
|
+
.find((failure) => Boolean(failure) && typeof failure === 'object' && !Array.isArray(failure));
|
|
2068
|
+
const persistedProviderDiagnostic = artifactProviderDiagnostic?.diagnostic;
|
|
2069
|
+
const provider = persistedProviderDiagnostic && typeof persistedProviderDiagnostic === 'object'
|
|
2070
|
+
? persistedProviderDiagnostic
|
|
2071
|
+
: (() => {
|
|
2072
|
+
const failure = run.diagnosticReceipt?.failure;
|
|
2073
|
+
return failure && (failure.code === 'AI_PROVIDER_FAILURE' || /provider/i.test(failure.code))
|
|
2074
|
+
? classifyProviderFailure({ message: failure.message, code: failure.code, phase: 'generation' })
|
|
2075
|
+
: undefined;
|
|
2076
|
+
})();
|
|
2077
|
+
return {
|
|
2078
|
+
version: 3,
|
|
2079
|
+
runId: run.id,
|
|
2080
|
+
sourceCoverage,
|
|
2081
|
+
...(cascade ? { cascade } : {}),
|
|
2082
|
+
planFrozen,
|
|
2083
|
+
...(provider ? { provider } : {}),
|
|
2084
|
+
finalStopReason: run.stopReason,
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
1597
2087
|
function emptyRunTelemetry(total, fallbackReason) {
|
|
1598
2088
|
return {
|
|
1599
2089
|
version: 1,
|
|
@@ -1619,7 +2109,7 @@ function durationBetweenMs(startedAt, completedAt) {
|
|
|
1619
2109
|
function receiptFingerprint(value) {
|
|
1620
2110
|
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
1621
2111
|
}
|
|
1622
|
-
function attachDiagnosticReceipt(artifacts, receipt, receiptV2) {
|
|
2112
|
+
function attachDiagnosticReceipt(artifacts, receipt, receiptV2, receiptV3) {
|
|
1623
2113
|
if (artifacts.length === 0) {
|
|
1624
2114
|
if (!receipt.failure)
|
|
1625
2115
|
return artifacts;
|
|
@@ -1628,7 +2118,7 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2) {
|
|
|
1628
2118
|
kind: "answer",
|
|
1629
2119
|
title: "Agent run diagnostics",
|
|
1630
2120
|
trustState: "blocked",
|
|
1631
|
-
payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}) },
|
|
2121
|
+
payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}), ...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}) },
|
|
1632
2122
|
}];
|
|
1633
2123
|
}
|
|
1634
2124
|
const preferredIndex = Math.max(0, artifacts.findIndex((artifact) => artifact.kind === "answer"));
|
|
@@ -1644,6 +2134,7 @@ function attachDiagnosticReceipt(artifacts, receipt, receiptV2) {
|
|
|
1644
2134
|
...payload,
|
|
1645
2135
|
diagnosticReceipt: receipt,
|
|
1646
2136
|
...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}),
|
|
2137
|
+
...(receiptV3 ? { diagnosticReceiptV3: receiptV3 } : {}),
|
|
1647
2138
|
},
|
|
1648
2139
|
};
|
|
1649
2140
|
});
|
|
@@ -1880,6 +2371,9 @@ export function selectRoute(request, decision) {
|
|
|
1880
2371
|
return 'clarify';
|
|
1881
2372
|
return 'blocked';
|
|
1882
2373
|
}
|
|
2374
|
+
const cascadeRoute = routeFromAnalyticalCascade(decision);
|
|
2375
|
+
if (cascadeRoute)
|
|
2376
|
+
return cascadeRoute;
|
|
1883
2377
|
const explicitMode = request.requestedMode;
|
|
1884
2378
|
if (explicitMode === 'modeling')
|
|
1885
2379
|
return 'modeling_draft';
|