@duckcodeailabs/dql-cli 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/args.d.ts +4 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +16 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{AgentLogPage-BPz-UWFh.js → AgentLogPage-Jbyb4so-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-BEl53WA_.js → AiBuildDialog-Du07X1qO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-B4yfGTTZ.js → AiBuildResult-xKtGthWv.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiSidePanel-CSZAAvuD.js → AiSidePanel-CE_L04dK.js} +1 -1
- package/dist/assets/dql-notebook/assets/AnalyticsHome-CiUE10uF.js +6 -0
- package/dist/assets/dql-notebook/assets/{AppsView-DQOwU9Cg.js → AppsView-DlNaTuwD.js} +30 -35
- package/dist/assets/dql-notebook/assets/AskObservabilityPage-DracZPI9.js +1 -0
- package/dist/assets/dql-notebook/assets/AskTracePage-CSMoMw9a.js +8 -0
- package/dist/assets/dql-notebook/assets/{BlockStudio-B4ap0GdY.js → BlockStudio-DQS5Hcq9.js} +9 -14
- package/dist/assets/dql-notebook/assets/BusinessArtifactView-CqV4i9l_.js +1 -0
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-D72byb2g.js → DbtFirstModelingPage-JKhB_Y2H.js} +3 -3
- package/dist/assets/dql-notebook/assets/{GitPage-lQb1uXH2.js → GitPage-DjaNp0o3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-CVECf6Xj.js → GlobalAiRail-CsV3zlc8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-trOyMCY6.js → GovernedContextPage-uXUHUOIJ.js} +3 -3
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-D8hLS5lE.js → HelpDocsPage-Hbh9IeYB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-eIkfBIep.js → HomePage-CRRl-R_v.js} +1 -1
- package/dist/assets/dql-notebook/assets/LineageDAG-OMUP2sbl.js +1 -0
- package/dist/assets/dql-notebook/assets/LineageDetailView-C964BLuV.js +1 -0
- package/dist/assets/dql-notebook/assets/LineageDrawer-CzY0nfpD.js +1 -0
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-CviIf8PN.js → LineagePathBreadcrumb-0Gi-Fm1B.js} +1 -1
- package/dist/assets/dql-notebook/assets/MiniLineageGraph-wn0wLc9m.js +1 -0
- package/dist/assets/dql-notebook/assets/{NewBlockModal-DbCQg-pj.js → NewBlockModal-kMAzk91d.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-5Vp6XiuK.js → NewNotebookModal-D_cWd_6M.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-DjqJS44s.js → NotebookEditor-Bax0lxaz.js} +25 -30
- package/dist/assets/dql-notebook/assets/{ReadinessPage-BHGrC5ho.js → ReadinessPage-DEUIXQQY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-BfS9Tdx-.js → SetupOnboarding-CyE-Ex41.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SkillsPage-BFH21wSj.js → SkillsPage-QgvKGPxE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-zm6g_SxZ.js → TrustBadge-D5CGp0cu.js} +1 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-BjsTE6yf.js +89 -0
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-DPhxIEzF.js → answer-to-notebook-tACdCsTV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-DNEb86Xc.js → arrow-left-CF6wxXU5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-DpPWbwaD.js → arrow-right-C7G3M_S8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-D7s5jo4X.js → book-open-text-DDBO8G7L.js} +1 -1
- package/dist/assets/dql-notebook/assets/chevron-left-DxajrX2v.js +6 -0
- package/dist/assets/dql-notebook/assets/{circle-x-Db3dXKg7.js → circle-x-HBvXjnT6.js} +1 -1
- package/dist/assets/dql-notebook/assets/clock-3-CJIdsPLt.js +6 -0
- package/dist/assets/dql-notebook/assets/dagre.esm-B6nvU4OB.js +1 -0
- package/dist/assets/dql-notebook/assets/{external-link-BxwXitO_.js → external-link-WciAU8Bu.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-Dt4lkWRi.js → grip-vertical-DVu7ok6x.js} +1 -1
- package/dist/assets/dql-notebook/assets/index-B_kaoARS.css +1 -0
- package/dist/assets/dql-notebook/assets/{index-DKo-bwNw.js → index-D3wnucQC.js} +133 -128
- package/dist/assets/dql-notebook/assets/{link-2-Dfo2P6wi.js → link-2-C6uQx50X.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-DiTmIWAL.js → list-tree-DapsIuQB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-CMTAkPzL.js → minimize-2-BlVoipnT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-DwYr7FW4.js → panel-right-open-DcnKWxfx.js} +1 -1
- package/dist/assets/dql-notebook/assets/{play-BXhHYQ4x.js → play-CNuRuaAs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-BNi6F8pl.js → rotate-ccw-B2p953Zh.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-CNOGysAy.js → semantic-fields-2fWgkTJc.js} +1 -1
- package/dist/assets/dql-notebook/assets/{sliders-horizontal-Ec5MUMUW.js → sliders-horizontal-DwRTu1w4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-B9leDkp_.js → star-Cbaj4F3i.js} +1 -1
- package/dist/assets/dql-notebook/assets/style-NlN9F6JE.js +23 -0
- package/dist/assets/dql-notebook/assets/{triangle-alert-BefTYCzx.js → triangle-alert-BPHvH1wA.js} +1 -1
- package/dist/assets/dql-notebook/assets/{upload-SPiOM2tQ.js → upload-DIqK0KE1.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-C4foXeiQ.js → usePersistedAgentThreadId-DpQXOEDw.js} +1 -1
- package/dist/assets/dql-notebook/assets/{user-round-comGmyw-.js → user-round-DukNupB_.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-BffR4dF8.js → wand-sparkles-CAwLX0b9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-ChmPTEzH.js → workflow-bOT-NqdO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-DovaG_ze.js → wrench-1m0_kcza.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-nRx91AgW.js → x-fp82BETM.js} +1 -1
- package/dist/assets/dql-notebook/index.html +2 -2
- package/dist/commands/agent-eval-cassette.d.ts +97 -6
- package/dist/commands/agent-eval-cassette.d.ts.map +1 -1
- package/dist/commands/agent-eval-cassette.js +165 -22
- package/dist/commands/agent-eval-cassette.js.map +1 -1
- package/dist/commands/agent-eval-runtime.d.ts +23 -2
- package/dist/commands/agent-eval-runtime.d.ts.map +1 -1
- package/dist/commands/agent-eval-runtime.js +19 -2
- package/dist/commands/agent-eval-runtime.js.map +1 -1
- package/dist/commands/agent-trace.d.ts +3 -0
- package/dist/commands/agent-trace.d.ts.map +1 -0
- package/dist/commands/agent-trace.js +169 -0
- package/dist/commands/agent-trace.js.map +1 -0
- package/dist/commands/agent.d.ts +37 -5
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +534 -84
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/compile.d.ts +13 -1
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +36 -4
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/notebook.d.ts +2 -0
- package/dist/commands/notebook.d.ts.map +1 -1
- package/dist/commands/notebook.js +4 -0
- package/dist/commands/notebook.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +11 -3
- package/dist/commands/sync.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts +29 -2
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +1008 -144
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +48 -1
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +271 -33
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +3658 -546
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/dist/providers/oauth/claude-oauth.d.ts.map +1 -1
- package/dist/providers/oauth/claude-oauth.js +52 -18
- package/dist/providers/oauth/claude-oauth.js.map +1 -1
- package/dist/providers/oauth/codex-oauth.d.ts.map +1 -1
- package/dist/providers/oauth/codex-oauth.js +72 -44
- package/dist/providers/oauth/codex-oauth.js.map +1 -1
- package/dist/providers/subscription-cli.d.ts +20 -0
- package/dist/providers/subscription-cli.d.ts.map +1 -1
- package/dist/providers/subscription-cli.js +69 -11
- package/dist/providers/subscription-cli.js.map +1 -1
- package/package.json +10 -10
- package/dist/assets/dql-notebook/assets/AnalyticsHome-D5P6Ujwi.js +0 -6
- package/dist/assets/dql-notebook/assets/BusinessArtifactView-BIfNI-0S.js +0 -1
- package/dist/assets/dql-notebook/assets/LineageDAG-CSqcbDrE.js +0 -1
- package/dist/assets/dql-notebook/assets/LineageDetailView-DJZZjZu-.js +0 -1
- package/dist/assets/dql-notebook/assets/LineageDrawer-BcIipIc3.js +0 -1
- package/dist/assets/dql-notebook/assets/MiniLineageGraph-vH_MY_Ju.js +0 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel--oxmjlgr.js +0 -88
- package/dist/assets/dql-notebook/assets/dagre.esm-C7pppQ1a.js +0 -23
- package/dist/assets/dql-notebook/assets/index-B3shyZsg.css +0 -1
- /package/dist/assets/dql-notebook/assets/{dagre-BZV40eAE.css → style-BZV40eAE.css} +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { deadlineScale, planAnalystTurn, rerankCandidates } from '@duckcodeailabs/dql-agent';
|
|
1
|
+
import { classifyProviderFailure, deadlineScale, planAnalystTurn, attachAskTraceObserverV1, analyticalErrorDetail, rerankCandidates, askTraceObserverForV1, } from '@duckcodeailabs/dql-agent';
|
|
2
2
|
import { buildAnalystLoopTools } from '../analyst-loop-tools.js';
|
|
3
|
-
import { ClaudeProvider, KGStore, MemoryStore, defaultKgPath, defaultMemoryPath, GeminiProvider, loadAgentSemanticLayer, OllamaProvider, OpenAIProvider, answer, buildAnalysisQuestionPlan, buildLocalContextPack, contextRetrievalBudgetForQuestion, ensureAgentProjectReady, isLikelyClarificationReply, isTrustedConversationTurn, createProviderDispatchEgressReceipt, createProviderEgressReceipt, assertProviderPayloadAllowed, prepareProviderContextForDispatch, prepareProviderWireEnvelopeForDispatch, prepareServerOwnedProviderSchemaContext, projectEmbeddingProvider, answerAgentic, qualifyAuthorizationReferences, createAnalystLaneHandler, parseProposal, renderContextValidationRefusalForUser, validateSqlAgainstLocalContext, resolveOrchestratorPolicy, } from '@duckcodeailabs/dql-agent';
|
|
4
|
-
import { CassetteStore, resolveCassetteModeFromEnv, withCassette } from '../../commands/agent-eval-cassette.js';
|
|
5
|
-
import { buildManifest, normalizeDqlArtifactReference, resolveDbtManifestPath } from '@duckcodeailabs/dql-core';
|
|
6
|
-
import {
|
|
3
|
+
import { ClaudeProvider, KGStore, MemoryStore, defaultKgPath, defaultMemoryPath, GeminiProvider, loadAgentSemanticLayer, OllamaProvider, OpenAIProvider, answer, buildAnalysisQuestionPlan, buildLocalContextPack, contextRetrievalBudgetForQuestion, ensureAgentProjectReady, isLikelyClarificationReply, isTrustedConversationTurn, createProviderDispatchEgressReceipt, createProviderEgressReceipt, assertProviderPayloadAllowed, prepareProviderContextForDispatch, prepareProviderWireEnvelopeForDispatch, prepareServerOwnedProviderSchemaContext, projectEmbeddingProvider, answerAgentic, qualifyAuthorizationReferences, scopeContextPackToExploratoryCandidateClosure, createAnalystLaneHandler, parseProposal, renderContextValidationRefusalForUser, validateSqlAgainstLocalContext, resolveOrchestratorPolicy, } from '@duckcodeailabs/dql-agent';
|
|
4
|
+
import { CassetteStore, evalCassetteCanonicalizationV2, resolveCassetteModeFromEnv, withCassette, } from '../../commands/agent-eval-cassette.js';
|
|
5
|
+
import { buildManifest, normalizeDqlArtifactReference, resolveDbtManifestPath, } from '@duckcodeailabs/dql-core';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
7
8
|
import { join } from 'node:path';
|
|
8
9
|
import { buildAnswerLoopTools, createGroundingContextExpander } from '../answer-loop-tools.js';
|
|
9
10
|
import { getSemanticRuntimeStatus } from '../../semantic-runtime.js';
|
|
@@ -12,6 +13,17 @@ import { getEffectiveProviderConfig } from '../../settings/provider-settings.js'
|
|
|
12
13
|
import { ClaudeCodeCliProvider, CodexCliProvider } from '../../providers/subscription-cli.js';
|
|
13
14
|
import { ClaudeOAuthProvider, claudeOAuthConnected } from '../../providers/oauth/claude-oauth.js';
|
|
14
15
|
import { CodexOAuthProvider, codexOAuthConnected } from '../../providers/oauth/codex-oauth.js';
|
|
16
|
+
/**
|
|
17
|
+
* The local runtime rejects a missing connection before it hands a statement
|
|
18
|
+
* to a connector. Keep this check structural: parsing a user-facing message
|
|
19
|
+
* here would let an unrelated warehouse failure disappear from SQL telemetry.
|
|
20
|
+
*/
|
|
21
|
+
function isPreSqlConnectionConfigurationError(error) {
|
|
22
|
+
const detail = analyticalErrorDetail(error);
|
|
23
|
+
return detail?.origin === 'host'
|
|
24
|
+
&& detail.stage === 'execute'
|
|
25
|
+
&& detail.code === 'connection_not_configured';
|
|
26
|
+
}
|
|
15
27
|
const SPECS = {
|
|
16
28
|
anthropic: {
|
|
17
29
|
label: 'Anthropic Claude',
|
|
@@ -75,6 +87,136 @@ const SPECS = {
|
|
|
75
87
|
},
|
|
76
88
|
},
|
|
77
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* Capture a content-free provider failure at the closest boundary. Local
|
|
92
|
+
* runtime may later choose a user-facing headline, but must not need raw
|
|
93
|
+
* provider errors or URLs to reconstruct the cause.
|
|
94
|
+
*/
|
|
95
|
+
function providerBoundaryDiagnostic(input) {
|
|
96
|
+
const config = getEffectiveProviderConfig(input.projectRoot, input.providerId);
|
|
97
|
+
const message = input.error instanceof Error ? input.error.message : String(input.error ?? 'provider readiness failed');
|
|
98
|
+
const code = input.code
|
|
99
|
+
?? (input.error && typeof input.error === 'object' ? String(input.error.code ?? '') : '');
|
|
100
|
+
const fingerprint = (value) => value?.trim()
|
|
101
|
+
? `sha256:${createHash('sha256').update(value.trim()).digest('hex')}`
|
|
102
|
+
: undefined;
|
|
103
|
+
let origin;
|
|
104
|
+
if (config.baseUrl) {
|
|
105
|
+
try {
|
|
106
|
+
origin = new URL(config.baseUrl).origin;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// The full malformed URL never leaves this function; its fingerprint is
|
|
110
|
+
// still a useful support correlation key without disclosing content.
|
|
111
|
+
origin = config.baseUrl;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return classifyProviderFailure({
|
|
115
|
+
code,
|
|
116
|
+
message,
|
|
117
|
+
phase: input.phase,
|
|
118
|
+
providerFingerprint: fingerprint(input.providerId),
|
|
119
|
+
modelFingerprint: fingerprint(config.model),
|
|
120
|
+
baseOriginFingerprint: fingerprint(origin),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** One-way runtime correlation only; never persist the provider/model string. */
|
|
124
|
+
function runtimeFingerprint(value) {
|
|
125
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Tool names are executable contract identifiers, not user/model supplied
|
|
129
|
+
* values. Keep even that narrow surface allowlisted so an unexpected provider
|
|
130
|
+
* payload cannot turn a trace into a metadata side channel.
|
|
131
|
+
*/
|
|
132
|
+
const TRACEABLE_ASK_TOOL_KINDS = new Set([
|
|
133
|
+
'check_compatibility',
|
|
134
|
+
'compile_resolved_analytical_plan',
|
|
135
|
+
'compile_semantic_query',
|
|
136
|
+
'describe_notebook_dataset',
|
|
137
|
+
'execute_local_analysis',
|
|
138
|
+
'explain_metric',
|
|
139
|
+
'get_table_schema',
|
|
140
|
+
'list_notebook_datasets',
|
|
141
|
+
'preview_query',
|
|
142
|
+
'propose_cross_source_join',
|
|
143
|
+
'query_semantic_model',
|
|
144
|
+
'sample_notebook_dataset',
|
|
145
|
+
'scan_manifest',
|
|
146
|
+
'search_metadata',
|
|
147
|
+
'search_project_files',
|
|
148
|
+
'search_semantic_layer',
|
|
149
|
+
'search_values',
|
|
150
|
+
'validate_sql',
|
|
151
|
+
]);
|
|
152
|
+
const ASK_TRACE_TOOL_CALLBACK = Symbol('dql.askTraceToolCallback');
|
|
153
|
+
/**
|
|
154
|
+
* A same-provider transport retry is minted only by this physical runner after
|
|
155
|
+
* it observed a typed transient failure. The public option field is carried
|
|
156
|
+
* to providers for receipt correlation, but it is not itself authority to
|
|
157
|
+
* borrow an earlier receipt.
|
|
158
|
+
*/
|
|
159
|
+
const RUNNER_OWNED_RETRY_LINEAGE = Symbol('dql.runnerOwnedRetryLineage');
|
|
160
|
+
function frozenExploratoryRepairAuthorityForRequest(req) {
|
|
161
|
+
const plan = req.resolvedAnalyticalPlan;
|
|
162
|
+
return req.orchestrationMode !== 'research'
|
|
163
|
+
&& req.selectedCascadeTier === 'exploratory_sql'
|
|
164
|
+
&& plan?.mode === 'authoritative'
|
|
165
|
+
&& plan.capability === 'bounded_exploration'
|
|
166
|
+
&& Boolean(plan.planId && plan.fingerprint && plan.snapshotId)
|
|
167
|
+
&& (plan.sourceRelationIds?.length ?? 0) > 0
|
|
168
|
+
&& Boolean(req.generatedProposalTargetFingerprint
|
|
169
|
+
&& req.prepareExploratorySqlExecution
|
|
170
|
+
&& req.executeAgenticGeneratedSql);
|
|
171
|
+
}
|
|
172
|
+
function repairAuthorityAdmissionError() {
|
|
173
|
+
return Object.assign(new Error('Repair transport admission denied because this ordinary Ask has no matching frozen exploratory repair authority.'), { code: 'PROVIDER_REPAIR_AUTHORITY_ADMISSION_DENIED' });
|
|
174
|
+
}
|
|
175
|
+
function retryLineageAdmissionError() {
|
|
176
|
+
return Object.assign(new Error('Provider retry admission denied because retry lineage was not minted by this runner after a transient same-provider failure.'), { code: 'PROVIDER_RETRY_LINEAGE_ADMISSION_DENIED' });
|
|
177
|
+
}
|
|
178
|
+
function recordPhysicalToolCallTrace(observer, event, attemptIndex) {
|
|
179
|
+
if (!observer.enabled)
|
|
180
|
+
return;
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
const duration = Math.max(0, Math.min(86_400_000, event.durationMs ?? 0));
|
|
183
|
+
const startedAt = new Date(now - duration).toISOString();
|
|
184
|
+
const completedAt = new Date(now).toISOString();
|
|
185
|
+
const call = {
|
|
186
|
+
version: 1,
|
|
187
|
+
toolCallId: `tool-${attemptIndex}`,
|
|
188
|
+
toolKind: TRACEABLE_ASK_TOOL_KINDS.has(event.name) ? event.name : 'unknown_tool',
|
|
189
|
+
attemptIndex,
|
|
190
|
+
...(event.isError ? { safeErrorCode: 'tool_error' } : {}),
|
|
191
|
+
};
|
|
192
|
+
const span = observer.startSpan({
|
|
193
|
+
name: 'tool.call',
|
|
194
|
+
stage: 'tool',
|
|
195
|
+
startedAt,
|
|
196
|
+
payload: { kind: 'tool', call },
|
|
197
|
+
reasonCode: 'started',
|
|
198
|
+
});
|
|
199
|
+
observer.finishSpan(span, {
|
|
200
|
+
completedAt,
|
|
201
|
+
outcome: event.isError ? 'error' : 'ok',
|
|
202
|
+
reasonCode: event.isError ? 'tool_failure' : 'completed',
|
|
203
|
+
payload: { kind: 'tool', call },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function createAskTraceToolCallback(observer) {
|
|
207
|
+
let attemptIndex = 0;
|
|
208
|
+
const callback = (event) => {
|
|
209
|
+
recordPhysicalToolCallTrace(observer, event, ++attemptIndex);
|
|
210
|
+
};
|
|
211
|
+
Object.defineProperty(callback, ASK_TRACE_TOOL_CALLBACK, {
|
|
212
|
+
value: true,
|
|
213
|
+
enumerable: false,
|
|
214
|
+
});
|
|
215
|
+
return callback;
|
|
216
|
+
}
|
|
217
|
+
function isAskTraceToolCallback(callback) {
|
|
218
|
+
return Boolean(callback?.[ASK_TRACE_TOOL_CALLBACK]);
|
|
219
|
+
}
|
|
78
220
|
function createCertifiedFitConfirmation(provider, signal) {
|
|
79
221
|
return async ({ question, questionPlan, block, fit }) => {
|
|
80
222
|
const response = await provider.generate([
|
|
@@ -228,6 +370,40 @@ function stringArray(value) {
|
|
|
228
370
|
function stringValue(value) {
|
|
229
371
|
return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
|
|
230
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* A precomputed exploratory pack is an optimization/witness only. The runner
|
|
375
|
+
* independently derives the pack from the immutable router-selected IDs and
|
|
376
|
+
* accepts the witness only when its snapshot, source fingerprint, physical
|
|
377
|
+
* relations, and metadata object set are exactly the same. This prevents a
|
|
378
|
+
* caller from replacing a safe same-snapshot closure with a broader one.
|
|
379
|
+
*/
|
|
380
|
+
function exploratoryClosureMatches(derived, witness) {
|
|
381
|
+
if (!derived)
|
|
382
|
+
return false;
|
|
383
|
+
if (derived.knowledgeLens.snapshotId !== witness.knowledgeLens.snapshotId)
|
|
384
|
+
return false;
|
|
385
|
+
if (derived.freshness.fingerprint !== witness.freshness.fingerprint)
|
|
386
|
+
return false;
|
|
387
|
+
const normalize = (value) => value
|
|
388
|
+
.trim()
|
|
389
|
+
.split('.')
|
|
390
|
+
.map((part) => part.trim().replace(/^["`\[]|["`\]]$/g, '').toLowerCase())
|
|
391
|
+
.filter(Boolean)
|
|
392
|
+
.join('.');
|
|
393
|
+
const sameSet = (left, right) => {
|
|
394
|
+
if (left.length !== right.length)
|
|
395
|
+
return false;
|
|
396
|
+
const rightSet = new Set(right);
|
|
397
|
+
return left.every((value) => rightSet.has(value));
|
|
398
|
+
};
|
|
399
|
+
const derivedRelations = [...new Set(derived.allowedSqlContext.relations.map((relation) => normalize(relation.relation)))].sort();
|
|
400
|
+
const witnessRelations = [...new Set(witness.allowedSqlContext.relations.map((relation) => normalize(relation.relation)))].sort();
|
|
401
|
+
if (!sameSet(derivedRelations, witnessRelations))
|
|
402
|
+
return false;
|
|
403
|
+
const derivedObjects = [...new Set(derived.objects.map((object) => object.objectKey))].sort();
|
|
404
|
+
const witnessObjects = [...new Set(witness.objects.map((object) => object.objectKey))].sort();
|
|
405
|
+
return sameSet(derivedObjects, witnessObjects);
|
|
406
|
+
}
|
|
231
407
|
function truncateForFitPrompt(value, max) {
|
|
232
408
|
if (!value)
|
|
233
409
|
return undefined;
|
|
@@ -279,16 +455,27 @@ function emitProposalFromText(text, emit) {
|
|
|
279
455
|
* this one (local-runtime.ts:164), so reaching back would close an import cycle
|
|
280
456
|
* through a 34k-line file. A few lines of JSON reading is the cheaper trade.
|
|
281
457
|
*
|
|
282
|
-
* Cached
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* route real questions onto an
|
|
458
|
+
* Cached by a lightweight file fingerprint, not forever. Settings writes must
|
|
459
|
+
* take effect for the next Ask without requiring a notebook-server restart.
|
|
460
|
+
* A malformed file resolves to `null`, which `resolveOrchestratorPolicy` turns
|
|
461
|
+
* into `legacy` — a broken config must not route real questions onto an
|
|
462
|
+
* unproven path.
|
|
286
463
|
*/
|
|
287
464
|
const agentConfigCache = new Map();
|
|
465
|
+
function agentConfigFingerprint(projectRoot) {
|
|
466
|
+
try {
|
|
467
|
+
const stats = statSync(join(projectRoot, 'dql.config.json'));
|
|
468
|
+
return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}`;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return 'missing';
|
|
472
|
+
}
|
|
473
|
+
}
|
|
288
474
|
function readAgentConfig(projectRoot) {
|
|
475
|
+
const fingerprint = agentConfigFingerprint(projectRoot);
|
|
289
476
|
const cached = agentConfigCache.get(projectRoot);
|
|
290
|
-
if (cached
|
|
291
|
-
return cached;
|
|
477
|
+
if (cached && cached.fingerprint === fingerprint)
|
|
478
|
+
return cached.value;
|
|
292
479
|
let resolved = null;
|
|
293
480
|
try {
|
|
294
481
|
const raw = readFileSync(join(projectRoot, 'dql.config.json'), 'utf-8');
|
|
@@ -300,7 +487,7 @@ function readAgentConfig(projectRoot) {
|
|
|
300
487
|
catch {
|
|
301
488
|
resolved = null;
|
|
302
489
|
}
|
|
303
|
-
agentConfigCache.set(projectRoot, resolved);
|
|
490
|
+
agentConfigCache.set(projectRoot, { fingerprint, value: resolved });
|
|
304
491
|
return resolved;
|
|
305
492
|
}
|
|
306
493
|
function readOrchestratorConfig(projectRoot) {
|
|
@@ -325,10 +512,15 @@ function readOrchestratorConfig(projectRoot) {
|
|
|
325
512
|
* measured on ollama, where all three agentic dispatches planned successfully
|
|
326
513
|
* and then died at "soft target elapsed before this provider dispatch could
|
|
327
514
|
* start". Its payoff is a legible trace, and `onStep` is not wired to SSE yet,
|
|
328
|
-
* so today it buys nothing a user can see.
|
|
329
|
-
*
|
|
515
|
+
* so today it buys nothing a user can see. Ordinary Ask already has a
|
|
516
|
+
* candidate-ID meaning call that produces the host-owned analytical plan, so
|
|
517
|
+
* it must never spend an extra provider transport on turn planning even when
|
|
518
|
+
* a stale local config enables it. Research remains the only explicit lane
|
|
519
|
+
* that may opt in to a separate plan stage.
|
|
330
520
|
*/
|
|
331
|
-
function turnPlanningEnabled(projectRoot) {
|
|
521
|
+
function turnPlanningEnabled(projectRoot, orchestrationMode) {
|
|
522
|
+
if (orchestrationMode !== 'research')
|
|
523
|
+
return false;
|
|
332
524
|
const orchestrator = readAgentConfig(projectRoot)?.orchestrator;
|
|
333
525
|
if (!orchestrator || typeof orchestrator !== 'object')
|
|
334
526
|
return false;
|
|
@@ -355,11 +547,43 @@ function valueLookupEnabled(projectRoot) {
|
|
|
355
547
|
function agenticLaneForRequest(req) {
|
|
356
548
|
return req.orchestrationMode === 'research' ? 'research' : 'generated';
|
|
357
549
|
}
|
|
358
|
-
export function applyEvalCassette(provider) {
|
|
550
|
+
export function applyEvalCassette(provider, projectRoot) {
|
|
359
551
|
const dir = process.env.DQL_EVAL_CASSETTE_DIR;
|
|
360
552
|
if (!dir)
|
|
361
553
|
return provider;
|
|
362
|
-
return withCassette(provider, new CassetteStore(dir), resolveCassetteModeFromEnv(process.env));
|
|
554
|
+
return withCassette(provider, new CassetteStore(dir), resolveCassetteModeFromEnv(process.env), evalCassetteCanonicalizationV2(projectRoot));
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Create a replay-only provider when the runtime is launched for an offline
|
|
558
|
+
* evaluation. This is intentionally unavailable outside explicit cassette
|
|
559
|
+
* replay: recording and live modes still require a configured real provider.
|
|
560
|
+
*
|
|
561
|
+
* The cassette's recorded provider identity is part of its key. Recover it
|
|
562
|
+
* from a single-provider cassette directory instead of borrowing a user's
|
|
563
|
+
* active provider or guessing from an API setting. The base provider cannot
|
|
564
|
+
* make a network call; replay misses remain CassetteMissError failures.
|
|
565
|
+
*/
|
|
566
|
+
export function createEvalCassetteReplayProvider(projectRoot) {
|
|
567
|
+
const dir = process.env.DQL_EVAL_CASSETTE_DIR;
|
|
568
|
+
if (!dir || resolveCassetteModeFromEnv(process.env) !== 'replay')
|
|
569
|
+
return undefined;
|
|
570
|
+
const store = new CassetteStore(dir);
|
|
571
|
+
const providerNames = store.providerNames();
|
|
572
|
+
const providerName = providerNames.length === 1 ? asAgentProviderName(providerNames[0]) : undefined;
|
|
573
|
+
if (!providerName)
|
|
574
|
+
return undefined;
|
|
575
|
+
return withCassette({
|
|
576
|
+
name: providerName,
|
|
577
|
+
available: async () => true,
|
|
578
|
+
generate: async () => {
|
|
579
|
+
throw new Error('Eval cassette replay miss: no live provider is available.');
|
|
580
|
+
},
|
|
581
|
+
}, store, 'replay', evalCassetteCanonicalizationV2(projectRoot));
|
|
582
|
+
}
|
|
583
|
+
function asAgentProviderName(value) {
|
|
584
|
+
return value === 'claude' || value === 'openai' || value === 'gemini' || value === 'ollama'
|
|
585
|
+
? value
|
|
586
|
+
: undefined;
|
|
363
587
|
}
|
|
364
588
|
/**
|
|
365
589
|
* A raw text provider for planning calls that are not the answer itself.
|
|
@@ -373,7 +597,7 @@ export function createGovernedTextProvider(id, projectRoot) {
|
|
|
373
597
|
if (!spec)
|
|
374
598
|
return undefined;
|
|
375
599
|
try {
|
|
376
|
-
return applyEvalCassette(spec.create(projectRoot));
|
|
600
|
+
return applyEvalCassette(spec.create(projectRoot), projectRoot);
|
|
377
601
|
}
|
|
378
602
|
catch {
|
|
379
603
|
return undefined;
|
|
@@ -383,9 +607,25 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
383
607
|
return {
|
|
384
608
|
async run(req, emit, signal) {
|
|
385
609
|
const spec = SPECS[id];
|
|
386
|
-
const rawProvider = applyEvalCassette(providerOverride ?? spec.create(req.projectRoot));
|
|
610
|
+
const rawProvider = applyEvalCassette(providerOverride ?? spec.create(req.projectRoot), req.projectRoot);
|
|
611
|
+
const askTrace = askTraceObserverForV1(req);
|
|
612
|
+
// A router-frozen exact certified block is a deterministic execution
|
|
613
|
+
// lane. It may not probe provider readiness merely because it shares the
|
|
614
|
+
// answer-loop adapter with provider-dependent routes. The flag is
|
|
615
|
+
// server-owned and the deterministic provider below throws if a future
|
|
616
|
+
// branch accidentally tries to generate text, so this cannot create a
|
|
617
|
+
// silent provider fallback.
|
|
618
|
+
const providerPreflightRequired = req.providerPreflightRequired !== false;
|
|
387
619
|
const isResearch = req.orchestrationMode === 'research';
|
|
388
|
-
const
|
|
620
|
+
const frozenExploratoryRepairRoute = frozenExploratoryRepairAuthorityForRequest(req);
|
|
621
|
+
// Ordinary analytical Ask has one candidate-ID interpretation, one
|
|
622
|
+
// generation, and — only for an already frozen exploratory plan — one
|
|
623
|
+
// same-plan model-decline correction. The shared run ledger remains the
|
|
624
|
+
// authority across calls; this per-provider ceiling keeps un-ledgered
|
|
625
|
+
// direct callers in the same bounded shape. A legacy text-tool run is
|
|
626
|
+
// not an analytical repair route and retains its established three-tool
|
|
627
|
+
// plus final-response wrapper cap; it cannot acquire the repair phase.
|
|
628
|
+
const maxProviderDispatches = isResearch ? 8 : frozenExploratoryRepairRoute ? 3 : 4;
|
|
389
629
|
const researchRowsOptIn = isResearch && req.researchResultRowsOptIn === true;
|
|
390
630
|
const sharedDispatchEvidence = req.providerDispatchEvidenceSink;
|
|
391
631
|
let providerRoundTrips = 0;
|
|
@@ -394,6 +634,11 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
394
634
|
let pendingColumnCount = 0;
|
|
395
635
|
let pendingCumulativeResultRowCount = 0;
|
|
396
636
|
let pendingResearchPurpose = 'research_narration';
|
|
637
|
+
let physicalToolAttemptIndex = 0;
|
|
638
|
+
let physicalProviderAttemptIndex = 0;
|
|
639
|
+
let lastFailedProviderSpanId;
|
|
640
|
+
let physicalDispatchSequence = 0;
|
|
641
|
+
let lastAdmittedPhysicalDispatch;
|
|
397
642
|
const providerEgressReceipts = [];
|
|
398
643
|
const dispatchEvidence = (fallbackReason) => {
|
|
399
644
|
const shared = sharedDispatchEvidence?.snapshot(fallbackReason);
|
|
@@ -408,106 +653,598 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
408
653
|
fallbackReason,
|
|
409
654
|
};
|
|
410
655
|
};
|
|
656
|
+
/**
|
|
657
|
+
* Count a SQL call at the physical execution boundary, not merely when
|
|
658
|
+
* the answer loop asks the host to prepare one. A tagged missing
|
|
659
|
+
* connection fails before the connector sees SQL, so it must remain zero
|
|
660
|
+
* in the durable receipt and trace. Other rejected execution callbacks
|
|
661
|
+
* retain the historical count: they may have reached the warehouse and
|
|
662
|
+
* failed there.
|
|
663
|
+
*/
|
|
664
|
+
const executeAtSqlBoundary = async (work) => {
|
|
665
|
+
try {
|
|
666
|
+
const result = await work();
|
|
667
|
+
sqlExecutions += 1;
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
if (!isPreSqlConnectionConfigurationError(error))
|
|
672
|
+
sqlExecutions += 1;
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
};
|
|
411
676
|
signal.addEventListener('abort', () => {
|
|
412
677
|
const reason = signal.reason;
|
|
413
678
|
if (reason && typeof reason === 'object') {
|
|
414
679
|
Object.assign(reason, { providerDispatchEvidence: dispatchEvidence('cancelled') });
|
|
415
680
|
}
|
|
416
681
|
}, { once: true });
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
? { mayStartToolCall: () => sharedDispatchEvidence.mayStartToolCall() }
|
|
425
|
-
: {}),
|
|
426
|
-
onProviderDispatch: (event) => {
|
|
427
|
-
const purpose = isResearch ? pendingResearchPurpose : 'answer_generation';
|
|
428
|
-
if (sharedDispatchEvidence) {
|
|
429
|
-
const envelope = sharedDispatchEvidence.observe(event, {
|
|
430
|
-
purpose,
|
|
431
|
-
dispatchPhase: 'generation',
|
|
432
|
-
optIn: pendingResultRowCount > 0 && researchRowsOptIn,
|
|
433
|
-
serializedResultShape: {
|
|
434
|
-
resultRowCount: pendingResultRowCount,
|
|
435
|
-
columnCount: pendingColumnCount,
|
|
436
|
-
},
|
|
437
|
-
...(pendingCumulativeResultRowCount > 0
|
|
438
|
-
? { cumulativeResultRowCount: pendingCumulativeResultRowCount }
|
|
439
|
-
: {}),
|
|
440
|
-
});
|
|
441
|
-
pendingResultRowCount = 0;
|
|
442
|
-
pendingColumnCount = 0;
|
|
443
|
-
pendingCumulativeResultRowCount = 0;
|
|
444
|
-
pendingResearchPurpose = 'research_narration';
|
|
445
|
-
return envelope;
|
|
682
|
+
const providerAttemptPayload = (event, input) => {
|
|
683
|
+
const diagnostic = input.diagnostic;
|
|
684
|
+
const config = getEffectiveProviderConfig(req.projectRoot, id);
|
|
685
|
+
let baseOrigin;
|
|
686
|
+
if (config.baseUrl) {
|
|
687
|
+
try {
|
|
688
|
+
baseOrigin = new URL(config.baseUrl).origin;
|
|
446
689
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
690
|
+
catch {
|
|
691
|
+
baseOrigin = config.baseUrl;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const model = event.model ?? config.model;
|
|
695
|
+
return {
|
|
696
|
+
version: 1,
|
|
697
|
+
phase: input.phase,
|
|
698
|
+
purpose: input.purpose,
|
|
699
|
+
physicalAttemptIndex: ++physicalProviderAttemptIndex,
|
|
700
|
+
providerFingerprint: diagnostic?.providerFingerprint ?? runtimeFingerprint(event.provider),
|
|
701
|
+
...(diagnostic?.modelFingerprint || model ? { modelFingerprint: diagnostic?.modelFingerprint ?? runtimeFingerprint(model) } : {}),
|
|
702
|
+
...(diagnostic?.baseOriginFingerprint || baseOrigin ? { baseOriginFingerprint: diagnostic?.baseOriginFingerprint ?? runtimeFingerprint(baseOrigin) } : {}),
|
|
703
|
+
...(input.retryOfSpanId ? { retryOfSpanId: input.retryOfSpanId } : {}),
|
|
704
|
+
admission: input.admission,
|
|
705
|
+
...(diagnostic?.httpStatusClass ? { httpStatusClass: diagnostic.httpStatusClass } : {}),
|
|
706
|
+
...(diagnostic?.retryable !== undefined ? { retryable: diagnostic.retryable } : {}),
|
|
707
|
+
...(diagnostic?.safeAction ? { safeAction: diagnostic.safeAction } : {}),
|
|
708
|
+
...(diagnostic?.cause ? { cause: diagnostic.cause } : {}),
|
|
709
|
+
provenance: 'live',
|
|
710
|
+
};
|
|
711
|
+
};
|
|
712
|
+
const providerDiagnosticForTrace = (error, phase) => providerBoundaryDiagnostic({
|
|
713
|
+
providerId: id,
|
|
714
|
+
projectRoot: req.projectRoot,
|
|
715
|
+
phase,
|
|
716
|
+
error,
|
|
717
|
+
code: error && typeof error === 'object' ? String(error.code ?? '') : undefined,
|
|
718
|
+
});
|
|
719
|
+
/**
|
|
720
|
+
* The wrapper is the authoritative physical-dispatch boundary. Preserve
|
|
721
|
+
* a Research phase that the host explicitly set, but ordinary Ask only
|
|
722
|
+
* accepts the frozen-plan repair marker; every other Ask transport is
|
|
723
|
+
* answer generation.
|
|
724
|
+
*/
|
|
725
|
+
const providerDispatchIdentity = (options) => {
|
|
726
|
+
const requestedPhase = options?.dispatchPhase;
|
|
727
|
+
const requestedPurpose = options?.egressPurpose;
|
|
728
|
+
// In an ordinary Ask, `repair` and `repair_sql` are a paired,
|
|
729
|
+
// server-owned capability. Preserve the requested repair identity in
|
|
730
|
+
// a denied trace so support can see what was refused, but do not admit
|
|
731
|
+
// it unless this runner captured the immutable exploratory RAP and
|
|
732
|
+
// the answer loop supplied the single-send repair shape.
|
|
733
|
+
const ordinaryRepairRequested = !isResearch
|
|
734
|
+
&& (requestedPhase === 'repair' || requestedPurpose === 'repair_sql');
|
|
735
|
+
const ordinaryRepairAuthorized = ordinaryRepairRequested
|
|
736
|
+
&& frozenExploratoryRepairRoute
|
|
737
|
+
&& requestedPhase === 'repair'
|
|
738
|
+
&& requestedPurpose === 'repair_sql'
|
|
739
|
+
&& options?.maxProviderDispatches === 1
|
|
740
|
+
&& options?.retryOfAttemptIndex === undefined;
|
|
741
|
+
const researchPhase = requestedPhase === 'classification'
|
|
742
|
+
|| requestedPhase === 'meaning_resolution'
|
|
743
|
+
|| requestedPhase === 'planning'
|
|
744
|
+
|| requestedPhase === 'generation'
|
|
745
|
+
|| requestedPhase === 'narration'
|
|
746
|
+
|| requestedPhase === 'repair';
|
|
747
|
+
const dispatchPhase = isResearch && researchPhase
|
|
748
|
+
? requestedPhase
|
|
749
|
+
: ordinaryRepairRequested
|
|
750
|
+
? 'repair'
|
|
751
|
+
: 'generation';
|
|
752
|
+
const purpose = dispatchPhase === 'repair'
|
|
753
|
+
? 'repair_sql'
|
|
754
|
+
: isResearch && (requestedPurpose === 'research_narration'
|
|
755
|
+
|| requestedPurpose === 'research_tool'
|
|
756
|
+
|| requestedPurpose === 'answer_generation')
|
|
757
|
+
? requestedPurpose
|
|
758
|
+
: isResearch
|
|
759
|
+
? pendingResearchPurpose
|
|
760
|
+
: 'answer_generation';
|
|
761
|
+
const retryRequested = options?.retryOfAttemptIndex !== undefined;
|
|
762
|
+
const runnerRetry = options?.[RUNNER_OWNED_RETRY_LINEAGE];
|
|
763
|
+
const retryAuthorized = retryRequested
|
|
764
|
+
&& Boolean(runnerRetry
|
|
765
|
+
&& runnerRetry.parentAttemptIndex === options?.retryOfAttemptIndex
|
|
766
|
+
&& runnerRetry.phase === dispatchPhase
|
|
767
|
+
&& runnerRetry.purpose === purpose);
|
|
768
|
+
return {
|
|
769
|
+
dispatchPhase,
|
|
770
|
+
purpose,
|
|
771
|
+
ordinaryRepairRequested,
|
|
772
|
+
ordinaryRepairAuthorized,
|
|
773
|
+
retryRequested,
|
|
774
|
+
retryAuthorized,
|
|
775
|
+
};
|
|
776
|
+
};
|
|
777
|
+
const withPhysicalDispatchObserver = (options) => {
|
|
778
|
+
// The answer loop can label exactly one frozen-plan correction as a
|
|
779
|
+
// repair. Do not let arbitrary provider options mint another phase:
|
|
780
|
+
// the local runner owns all other ordinary Ask dispatches as generation.
|
|
781
|
+
const identity = providerDispatchIdentity(options);
|
|
782
|
+
const { dispatchPhase, purpose, ordinaryRepairRequested, ordinaryRepairAuthorized, retryRequested, retryAuthorized, } = identity;
|
|
783
|
+
const requestedPhysicalCap = typeof options?.maxProviderDispatches === 'number'
|
|
784
|
+
&& Number.isInteger(options.maxProviderDispatches)
|
|
785
|
+
&& options.maxProviderDispatches > 0
|
|
786
|
+
? options.maxProviderDispatches
|
|
787
|
+
: maxProviderDispatches;
|
|
788
|
+
// A repair response is one physical send even if a lower-level
|
|
789
|
+
// provider supports protocol retries. The frozen plan's one repair
|
|
790
|
+
// reservation must not turn into an unbounded transport loop.
|
|
791
|
+
const physicalDispatchCap = dispatchPhase === 'repair'
|
|
792
|
+
? 1
|
|
793
|
+
: Math.min(maxProviderDispatches, requestedPhysicalCap);
|
|
794
|
+
const onToolCall = options?.onToolCall;
|
|
795
|
+
const onProviderDispatch = options?.onProviderDispatch;
|
|
796
|
+
const onProviderDispatchComplete = options?.onProviderDispatchComplete;
|
|
797
|
+
const onProviderDispatchRejected = options?.onProviderDispatchRejected;
|
|
798
|
+
const pending = new Map();
|
|
799
|
+
const keyForDispatch = (event) => `${event.provider}:${event.operation}:${event.attemptIndex}`;
|
|
800
|
+
// An admission failure can be surfaced both by a throwing admission
|
|
801
|
+
// callback and a provider's optional rejection callback. It is one
|
|
802
|
+
// unsent attempt, not two trace rows.
|
|
803
|
+
const admittedKeys = new Set();
|
|
804
|
+
const deniedKeys = new Set();
|
|
805
|
+
const finish = (entry, outcome, error, httpStatus) => {
|
|
806
|
+
if (!entry.spanId)
|
|
807
|
+
return;
|
|
808
|
+
if (outcome === 'ok') {
|
|
809
|
+
askTrace.finishSpan(entry.spanId, {
|
|
810
|
+
outcome: 'ok',
|
|
811
|
+
reasonCode: 'completed',
|
|
812
|
+
payload: { kind: 'provider', attempt: entry.attempt },
|
|
453
813
|
});
|
|
814
|
+
return;
|
|
454
815
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
816
|
+
const diagnostic = providerDiagnosticForTrace(error ?? (typeof httpStatus === 'number' ? Object.assign(new Error(`HTTP ${httpStatus}`), { code: `HTTP_${httpStatus}` }) : undefined), dispatchPhase);
|
|
817
|
+
const failureAttempt = {
|
|
818
|
+
...entry.attempt,
|
|
819
|
+
...(diagnostic.httpStatusClass ? { httpStatusClass: diagnostic.httpStatusClass } : {}),
|
|
820
|
+
retryable: diagnostic.retryable,
|
|
821
|
+
safeAction: diagnostic.safeAction,
|
|
822
|
+
cause: outcome === 'cancelled' ? 'cancelled' : diagnostic.cause,
|
|
823
|
+
};
|
|
824
|
+
askTrace.finishSpan(entry.spanId, {
|
|
825
|
+
outcome: outcome === 'cancelled' ? 'cancelled' : 'error',
|
|
826
|
+
reasonCode: outcome === 'cancelled' ? 'cancelled' : 'provider_failure',
|
|
827
|
+
payload: { kind: 'provider', attempt: failureAttempt },
|
|
460
828
|
});
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
829
|
+
lastFailedProviderSpanId = entry.spanId;
|
|
830
|
+
};
|
|
831
|
+
const physicalOptions = {
|
|
832
|
+
...(options ?? {}),
|
|
833
|
+
// The raw provider loop has the same outer ceiling as the local Ask
|
|
834
|
+
// contract. The run-scoped ledger is the authority for phase limits:
|
|
835
|
+
// one planning/generation transport plus, only when the answer loop
|
|
836
|
+
// presents a frozen exploratory repair marker, one repair transport.
|
|
837
|
+
maxProviderDispatches: physicalDispatchCap,
|
|
838
|
+
...(sharedDispatchEvidence?.mayStartToolCall
|
|
839
|
+
? { mayStartToolCall: () => sharedDispatchEvidence.mayStartToolCall() }
|
|
840
|
+
: {}),
|
|
841
|
+
onProviderDispatch: (event) => {
|
|
842
|
+
try {
|
|
843
|
+
// This is the last synchronous boundary before a native provider
|
|
844
|
+
// serializes bytes. A caller cannot mint an ordinary Ask repair
|
|
845
|
+
// merely by placing lifecycle labels in `ProviderToolLoopOptions`.
|
|
846
|
+
if (ordinaryRepairRequested && !ordinaryRepairAuthorized) {
|
|
847
|
+
throw repairAuthorityAdmissionError();
|
|
848
|
+
}
|
|
849
|
+
// Retry correlation is likewise host-owned. The shared ledger
|
|
850
|
+
// validates the parent receipt too, but this marker prevents a
|
|
851
|
+
// caller from claiming an arbitrary earlier attempt before that
|
|
852
|
+
// receipt check can admit a wire body.
|
|
853
|
+
if (retryRequested && !retryAuthorized) {
|
|
854
|
+
throw retryLineageAdmissionError();
|
|
855
|
+
}
|
|
856
|
+
if (sharedDispatchEvidence) {
|
|
857
|
+
const envelope = sharedDispatchEvidence.observe(event, {
|
|
858
|
+
purpose,
|
|
859
|
+
dispatchPhase,
|
|
860
|
+
optIn: pendingResultRowCount > 0 && researchRowsOptIn,
|
|
861
|
+
serializedResultShape: {
|
|
862
|
+
resultRowCount: pendingResultRowCount,
|
|
863
|
+
columnCount: pendingColumnCount,
|
|
864
|
+
},
|
|
865
|
+
...(pendingCumulativeResultRowCount > 0
|
|
866
|
+
? { cumulativeResultRowCount: pendingCumulativeResultRowCount }
|
|
867
|
+
: {}),
|
|
868
|
+
...(options?.retryOfAttemptIndex !== undefined
|
|
869
|
+
? { retryOfAttemptIndex: options.retryOfAttemptIndex }
|
|
870
|
+
: {}),
|
|
871
|
+
});
|
|
872
|
+
pendingResultRowCount = 0;
|
|
873
|
+
pendingColumnCount = 0;
|
|
874
|
+
pendingCumulativeResultRowCount = 0;
|
|
875
|
+
pendingResearchPurpose = 'research_narration';
|
|
876
|
+
const observedEnvelope = onProviderDispatch?.(event) ?? envelope;
|
|
877
|
+
const attempt = providerAttemptPayload(event, {
|
|
878
|
+
admission: 'admitted',
|
|
879
|
+
phase: dispatchPhase,
|
|
880
|
+
purpose,
|
|
881
|
+
retryOfSpanId: lastFailedProviderSpanId,
|
|
882
|
+
});
|
|
883
|
+
const spanId = askTrace.startSpan({ name: 'provider.attempt', stage: 'provider', reasonCode: 'started', payload: { kind: 'provider', attempt } });
|
|
884
|
+
const key = keyForDispatch(event);
|
|
885
|
+
lastAdmittedPhysicalDispatch = {
|
|
886
|
+
sequence: ++physicalDispatchSequence,
|
|
887
|
+
attemptIndex: event.attemptIndex,
|
|
888
|
+
phase: dispatchPhase,
|
|
889
|
+
purpose,
|
|
890
|
+
};
|
|
891
|
+
admittedKeys.add(key);
|
|
892
|
+
pending.set(key, [...(pending.get(key) ?? []), { spanId, attempt }]);
|
|
893
|
+
return observedEnvelope;
|
|
894
|
+
}
|
|
895
|
+
// Fallback path only (CLI/MCP-direct runs carry no shared ledger).
|
|
896
|
+
// Kept in step with `maxProviderDispatches` above so the same question
|
|
897
|
+
// does not get a different budget depending on which surface asked it.
|
|
898
|
+
if (!isResearch && providerRoundTrips >= physicalDispatchCap) {
|
|
899
|
+
throw Object.assign(new Error(`Provider dispatch budget exhausted after ${physicalDispatchCap} ordinary Ask attempts.`), {
|
|
900
|
+
code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED',
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(rawProvider.name, event.envelope);
|
|
904
|
+
assertProviderPayloadAllowed(envelope, {
|
|
905
|
+
allowResultRows: false,
|
|
906
|
+
maxResultRows: 0,
|
|
907
|
+
purpose,
|
|
908
|
+
});
|
|
909
|
+
providerRoundTrips += 1;
|
|
910
|
+
providerEgressReceipts.push(createProviderDispatchEgressReceipt({
|
|
911
|
+
purpose,
|
|
912
|
+
dispatchPhase,
|
|
913
|
+
provider: rawProvider.name,
|
|
914
|
+
...(options?.retryOfAttemptIndex !== undefined
|
|
915
|
+
? { retryOfAttemptIndex: options.retryOfAttemptIndex }
|
|
916
|
+
: {}),
|
|
917
|
+
permittedCategories: pendingResultRowCount > 0
|
|
918
|
+
? ['instructions', 'question', 'schema_metadata', 'governed_context', 'result_rows']
|
|
919
|
+
: ['instructions', 'question', 'schema_metadata', 'governed_context'],
|
|
920
|
+
optIn: pendingResultRowCount > 0 && researchRowsOptIn,
|
|
921
|
+
envelope,
|
|
922
|
+
serializedResultShape: {
|
|
923
|
+
resultRowCount: pendingResultRowCount,
|
|
924
|
+
columnCount: pendingColumnCount,
|
|
925
|
+
},
|
|
926
|
+
...(pendingCumulativeResultRowCount > 0
|
|
927
|
+
? { cumulativeResultRowCount: pendingCumulativeResultRowCount }
|
|
928
|
+
: {}),
|
|
929
|
+
}));
|
|
930
|
+
pendingResultRowCount = 0;
|
|
931
|
+
pendingColumnCount = 0;
|
|
932
|
+
pendingCumulativeResultRowCount = 0;
|
|
933
|
+
pendingResearchPurpose = 'research_narration';
|
|
934
|
+
const observedEnvelope = onProviderDispatch?.(event) ?? envelope;
|
|
935
|
+
const attempt = providerAttemptPayload(event, {
|
|
936
|
+
admission: 'admitted',
|
|
937
|
+
phase: dispatchPhase,
|
|
938
|
+
purpose,
|
|
939
|
+
retryOfSpanId: lastFailedProviderSpanId,
|
|
940
|
+
});
|
|
941
|
+
const spanId = askTrace.startSpan({ name: 'provider.attempt', stage: 'provider', reasonCode: 'started', payload: { kind: 'provider', attempt } });
|
|
942
|
+
const key = keyForDispatch(event);
|
|
943
|
+
lastAdmittedPhysicalDispatch = {
|
|
944
|
+
sequence: ++physicalDispatchSequence,
|
|
945
|
+
attemptIndex: event.attemptIndex,
|
|
946
|
+
phase: dispatchPhase,
|
|
947
|
+
purpose,
|
|
948
|
+
};
|
|
949
|
+
admittedKeys.add(key);
|
|
950
|
+
pending.set(key, [...(pending.get(key) ?? []), { spanId, attempt }]);
|
|
951
|
+
return observedEnvelope;
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
// The send was denied before an HTTP attempt. Preserve that
|
|
955
|
+
// distinction so support does not misread a budget/admission guard
|
|
956
|
+
// as a provider outage.
|
|
957
|
+
const key = keyForDispatch(event);
|
|
958
|
+
if (!admittedKeys.has(key) && !deniedKeys.has(key)) {
|
|
959
|
+
deniedKeys.add(key);
|
|
960
|
+
const diagnostic = providerDiagnosticForTrace(error, dispatchPhase);
|
|
961
|
+
const attempt = providerAttemptPayload(event, {
|
|
962
|
+
admission: 'denied',
|
|
963
|
+
phase: dispatchPhase,
|
|
964
|
+
purpose,
|
|
965
|
+
diagnostic,
|
|
966
|
+
retryOfSpanId: lastFailedProviderSpanId,
|
|
967
|
+
});
|
|
968
|
+
const spanId = askTrace.startSpan({ name: 'provider.attempt', stage: 'provider', reasonCode: 'provider_failure', payload: { kind: 'provider', attempt } });
|
|
969
|
+
askTrace.finishSpan(spanId, {
|
|
970
|
+
outcome: 'denied',
|
|
971
|
+
reasonCode: 'provider_failure',
|
|
972
|
+
payload: { kind: 'provider', attempt },
|
|
973
|
+
});
|
|
974
|
+
lastFailedProviderSpanId = spanId;
|
|
975
|
+
}
|
|
976
|
+
try {
|
|
977
|
+
onProviderDispatchRejected?.({
|
|
978
|
+
provider: event.provider,
|
|
979
|
+
operation: event.operation,
|
|
980
|
+
attemptIndex: event.attemptIndex,
|
|
981
|
+
...(event.model ? { model: event.model } : {}),
|
|
982
|
+
error,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
// A source observer cannot turn a denied local admission into a
|
|
987
|
+
// provider send or a second failure record.
|
|
988
|
+
}
|
|
989
|
+
throw error;
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
onProviderDispatchComplete: (event) => {
|
|
993
|
+
const key = keyForDispatch(event);
|
|
994
|
+
const entries = pending.get(key) ?? [];
|
|
995
|
+
const entry = entries[0];
|
|
996
|
+
// A successful HTTP response or subscription child-process exit is
|
|
997
|
+
// merely a physical milestone. Keep the same span open until the
|
|
998
|
+
// parser/stream/result path settles; this prevents a malformed 200
|
|
999
|
+
// response from being recorded as a successful provider attempt.
|
|
1000
|
+
if (entry && event.outcome === 'ok' && (event.settlement === 'transport' || event.settlement === 'process')) {
|
|
1001
|
+
entry.attempt = {
|
|
1002
|
+
...entry.attempt,
|
|
1003
|
+
...(event.settlement === 'transport' ? { transportOutcome: 'ok' } : { processOutcome: 'ok' }),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
const closed = entries.shift();
|
|
1008
|
+
if (entries.length > 0)
|
|
1009
|
+
pending.set(key, entries);
|
|
1010
|
+
else
|
|
1011
|
+
pending.delete(key);
|
|
1012
|
+
if (closed)
|
|
1013
|
+
finish(closed, event.outcome, event.error, event.httpStatus);
|
|
1014
|
+
}
|
|
1015
|
+
onProviderDispatchComplete?.(event);
|
|
1016
|
+
},
|
|
1017
|
+
onProviderDispatchRejected: (event) => {
|
|
1018
|
+
// `prepareProviderHttpDispatch` rejected this before it could send
|
|
1019
|
+
// bytes. Keep it as a denied admission rather than pretending an
|
|
1020
|
+
// HTTP attempt happened (or losing dispatch-budget evidence).
|
|
1021
|
+
const key = keyForDispatch(event);
|
|
1022
|
+
if (!admittedKeys.has(key) && !deniedKeys.has(key)) {
|
|
1023
|
+
deniedKeys.add(key);
|
|
1024
|
+
const diagnostic = providerDiagnosticForTrace(event.error, dispatchPhase);
|
|
1025
|
+
const attempt = providerAttemptPayload(event, {
|
|
1026
|
+
admission: 'denied',
|
|
1027
|
+
phase: dispatchPhase,
|
|
1028
|
+
purpose,
|
|
1029
|
+
diagnostic,
|
|
1030
|
+
retryOfSpanId: lastFailedProviderSpanId,
|
|
1031
|
+
});
|
|
1032
|
+
const spanId = askTrace.startSpan({
|
|
1033
|
+
name: 'provider.attempt',
|
|
1034
|
+
stage: 'provider',
|
|
1035
|
+
reasonCode: 'provider_failure',
|
|
1036
|
+
payload: { kind: 'provider', attempt },
|
|
1037
|
+
});
|
|
1038
|
+
askTrace.finishSpan(spanId, {
|
|
1039
|
+
outcome: 'denied',
|
|
1040
|
+
reasonCode: 'provider_failure',
|
|
1041
|
+
payload: { kind: 'provider', attempt },
|
|
1042
|
+
});
|
|
1043
|
+
lastFailedProviderSpanId = spanId;
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
onProviderDispatchRejected?.(event);
|
|
1047
|
+
}
|
|
1048
|
+
catch { /* observer is fail-open */ }
|
|
1049
|
+
},
|
|
1050
|
+
onToolCall: (event) => {
|
|
1051
|
+
// Native providers call this callback at the physical tool boundary.
|
|
1052
|
+
// Text-protocol loops use the trace-marked callback they received
|
|
1053
|
+
// directly, so this avoids recording a single tool twice.
|
|
1054
|
+
if (!isAskTraceToolCallback(onToolCall)) {
|
|
1055
|
+
recordPhysicalToolCallTrace(askTrace, event, ++physicalToolAttemptIndex);
|
|
1056
|
+
}
|
|
1057
|
+
onToolCall?.(event);
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
return {
|
|
1061
|
+
options: physicalOptions,
|
|
1062
|
+
settle: (outcome, error) => {
|
|
1063
|
+
for (const entries of pending.values()) {
|
|
1064
|
+
for (const entry of entries)
|
|
1065
|
+
finish(entry, outcome, error);
|
|
1066
|
+
}
|
|
1067
|
+
pending.clear();
|
|
1068
|
+
},
|
|
1069
|
+
};
|
|
1070
|
+
};
|
|
1071
|
+
// One retry only, on the SAME configured provider and only for failures
|
|
1072
|
+
// that are normally transient. The physical-dispatch observer remains in
|
|
1073
|
+
// the path for both attempts, so a retry cannot exceed the run-wide
|
|
1074
|
+
// dispatch budget or silently fail over to another provider.
|
|
1075
|
+
let transientRetryUsed = false;
|
|
1076
|
+
const mayRetrySameProvider = (error, identity, parent) => {
|
|
1077
|
+
// A frozen exploratory run deliberately reserves its third physical
|
|
1078
|
+
// dispatch for the same-plan SQL repair. A transient retry of meaning
|
|
1079
|
+
// or generation would consume that reservation, so it is forbidden.
|
|
1080
|
+
if (signal.aborted
|
|
1081
|
+
|| transientRetryUsed
|
|
1082
|
+
|| frozenExploratoryRepairRoute
|
|
1083
|
+
|| identity.dispatchPhase === 'repair'
|
|
1084
|
+
|| !parent
|
|
1085
|
+
|| parent.phase !== identity.dispatchPhase
|
|
1086
|
+
|| parent.purpose !== identity.purpose)
|
|
1087
|
+
return false;
|
|
1088
|
+
const diagnostic = providerDiagnosticForTrace(error, identity.dispatchPhase);
|
|
1089
|
+
return diagnostic.retryable && (diagnostic.cause === 'rate_limited'
|
|
1090
|
+
|| diagnostic.cause === 'gateway'
|
|
1091
|
+
|| diagnostic.cause === 'network'
|
|
1092
|
+
|| diagnostic.cause === 'provider_timeout');
|
|
1093
|
+
};
|
|
1094
|
+
const retrySameProviderOnce = async (sourceOptions, operation) => {
|
|
1095
|
+
const identity = providerDispatchIdentity(sourceOptions);
|
|
1096
|
+
const dispatchSequenceBefore = physicalDispatchSequence;
|
|
1097
|
+
try {
|
|
1098
|
+
return await operation(sourceOptions);
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
const parent = lastAdmittedPhysicalDispatch?.sequence && lastAdmittedPhysicalDispatch.sequence > dispatchSequenceBefore
|
|
1102
|
+
? lastAdmittedPhysicalDispatch
|
|
1103
|
+
: undefined;
|
|
1104
|
+
if (!mayRetrySameProvider(error, identity, parent))
|
|
1105
|
+
throw error;
|
|
1106
|
+
transientRetryUsed = true;
|
|
1107
|
+
emit({ kind: 'thinking', text: 'The configured AI provider had a transient error; retrying it once within this run budget.' });
|
|
1108
|
+
// The retry carries immutable phase/purpose/provider lineage. The
|
|
1109
|
+
// ledger re-validates the parent receipt before it admits any bytes.
|
|
1110
|
+
const retryOptions = {
|
|
1111
|
+
...(sourceOptions ?? {}),
|
|
1112
|
+
dispatchPhase: identity.dispatchPhase,
|
|
1113
|
+
egressPurpose: identity.purpose,
|
|
1114
|
+
retryOfAttemptIndex: parent.attemptIndex,
|
|
1115
|
+
};
|
|
1116
|
+
Object.defineProperty(retryOptions, RUNNER_OWNED_RETRY_LINEAGE, {
|
|
1117
|
+
value: {
|
|
1118
|
+
parentAttemptIndex: parent.attemptIndex,
|
|
1119
|
+
phase: identity.dispatchPhase,
|
|
1120
|
+
purpose: identity.purpose,
|
|
474
1121
|
},
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1122
|
+
enumerable: false,
|
|
1123
|
+
});
|
|
1124
|
+
return operation(retryOptions);
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
const invokePhysicalProvider = async (sourceOptions, invoke) => {
|
|
1128
|
+
const observed = withPhysicalDispatchObserver(sourceOptions);
|
|
1129
|
+
try {
|
|
1130
|
+
const result = await invoke(observed.options);
|
|
1131
|
+
// Built-in HTTP providers close each span via
|
|
1132
|
+
// `onProviderDispatchComplete`; this only closes a custom provider
|
|
1133
|
+
// that exposes admission but not the optional completion callback.
|
|
1134
|
+
observed.settle('ok');
|
|
1135
|
+
return result;
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
observed.settle(signal.aborted ? 'cancelled' : 'error', error);
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
486
1142
|
const provider = {
|
|
487
1143
|
name: rawProvider.name,
|
|
488
1144
|
available: () => rawProvider.available(),
|
|
489
|
-
generate: (...args) =>
|
|
490
|
-
return rawProvider.generate(args[0], withPhysicalDispatchObserver(args[1]));
|
|
491
|
-
},
|
|
1145
|
+
generate: (...args) => retrySameProviderOnce(args[1], (sourceOptions) => invokePhysicalProvider(sourceOptions, (options) => rawProvider.generate(args[0], options))),
|
|
492
1146
|
...(rawProvider.generateWithTools ? {
|
|
493
|
-
generateWithTools: (...args) =>
|
|
494
|
-
return rawProvider.generateWithTools(args[0], args[1], withPhysicalDispatchObserver(args[2]));
|
|
495
|
-
},
|
|
1147
|
+
generateWithTools: (...args) => retrySameProviderOnce(args[2], (sourceOptions) => invokePhysicalProvider(sourceOptions, (options) => rawProvider.generateWithTools(args[0], args[1], options))),
|
|
496
1148
|
} : {}),
|
|
497
1149
|
...(rawProvider.generateStream ? {
|
|
498
|
-
generateStream: (...args) =>
|
|
499
|
-
return rawProvider.generateStream(args[0], withPhysicalDispatchObserver(args[1]), args[2]);
|
|
500
|
-
},
|
|
1150
|
+
generateStream: (...args) => invokePhysicalProvider(args[1], (options) => rawProvider.generateStream(args[0], options, args[2])),
|
|
501
1151
|
} : {}),
|
|
502
1152
|
};
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1153
|
+
if (providerPreflightRequired) {
|
|
1154
|
+
// Readiness is a real provider operation, not a proxy for a later
|
|
1155
|
+
// answer-loop invocation. Record its own outcome before a route can
|
|
1156
|
+
// depend on this provider, while keeping the observer wholly fail-open.
|
|
1157
|
+
const preflightSpan = askTrace.startSpan({
|
|
1158
|
+
name: 'provider.preflight',
|
|
1159
|
+
stage: 'provider',
|
|
1160
|
+
reasonCode: 'started',
|
|
1161
|
+
payload: {
|
|
1162
|
+
kind: 'provider',
|
|
1163
|
+
attempt: {
|
|
1164
|
+
version: 1,
|
|
1165
|
+
phase: 'preflight',
|
|
1166
|
+
physicalAttemptIndex: 0,
|
|
1167
|
+
providerFingerprint: runtimeFingerprint(rawProvider.name),
|
|
1168
|
+
readiness: 'unknown',
|
|
1169
|
+
admission: 'unknown',
|
|
1170
|
+
provenance: 'live',
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
});
|
|
1174
|
+
let preflightError;
|
|
1175
|
+
let available = false;
|
|
1176
|
+
try {
|
|
1177
|
+
available = await provider.available();
|
|
1178
|
+
}
|
|
1179
|
+
catch (error) {
|
|
1180
|
+
preflightError = error;
|
|
1181
|
+
}
|
|
1182
|
+
if (!available) {
|
|
1183
|
+
const message = `${spec.label} is not configured or reachable. ${spec.setup}`;
|
|
1184
|
+
const diagnostic = providerBoundaryDiagnostic({
|
|
1185
|
+
providerId: id,
|
|
1186
|
+
projectRoot: req.projectRoot,
|
|
1187
|
+
phase: 'preflight',
|
|
1188
|
+
error: preflightError ?? message,
|
|
1189
|
+
// Local Ollama readiness is a reachability concern; configured
|
|
1190
|
+
// remote providers missing credentials are authentication issues.
|
|
1191
|
+
code: preflightError && typeof preflightError === 'object'
|
|
1192
|
+
? String(preflightError.code ?? '')
|
|
1193
|
+
: id === 'ollama' ? 'NETWORK_FAILURE' : 'AUTHENTICATION_FAILED',
|
|
1194
|
+
});
|
|
1195
|
+
askTrace.finishSpan(preflightSpan, {
|
|
1196
|
+
outcome: 'unavailable',
|
|
1197
|
+
reasonCode: 'provider_preflight',
|
|
1198
|
+
payload: {
|
|
1199
|
+
kind: 'provider',
|
|
1200
|
+
attempt: {
|
|
1201
|
+
version: 1,
|
|
1202
|
+
phase: 'preflight',
|
|
1203
|
+
physicalAttemptIndex: 0,
|
|
1204
|
+
providerFingerprint: diagnostic.providerFingerprint ?? runtimeFingerprint(rawProvider.name),
|
|
1205
|
+
...(diagnostic.modelFingerprint ? { modelFingerprint: diagnostic.modelFingerprint } : {}),
|
|
1206
|
+
...(diagnostic.baseOriginFingerprint ? { baseOriginFingerprint: diagnostic.baseOriginFingerprint } : {}),
|
|
1207
|
+
readiness: 'unavailable',
|
|
1208
|
+
admission: 'unknown',
|
|
1209
|
+
retryable: diagnostic.retryable,
|
|
1210
|
+
safeAction: diagnostic.safeAction,
|
|
1211
|
+
cause: diagnostic.cause,
|
|
1212
|
+
provenance: 'live',
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
});
|
|
1216
|
+
emit({
|
|
1217
|
+
kind: 'error',
|
|
1218
|
+
message,
|
|
1219
|
+
providerDiagnostic: diagnostic,
|
|
1220
|
+
});
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
askTrace.finishSpan(preflightSpan, {
|
|
1224
|
+
outcome: 'ok',
|
|
1225
|
+
reasonCode: 'completed',
|
|
1226
|
+
payload: {
|
|
1227
|
+
kind: 'provider',
|
|
1228
|
+
attempt: {
|
|
1229
|
+
version: 1,
|
|
1230
|
+
phase: 'preflight',
|
|
1231
|
+
physicalAttemptIndex: 0,
|
|
1232
|
+
providerFingerprint: runtimeFingerprint(rawProvider.name),
|
|
1233
|
+
readiness: 'ready',
|
|
1234
|
+
admission: 'unknown',
|
|
1235
|
+
provenance: 'live',
|
|
1236
|
+
},
|
|
1237
|
+
},
|
|
1238
|
+
});
|
|
507
1239
|
}
|
|
508
1240
|
try {
|
|
509
1241
|
const requestStartedAt = Date.now();
|
|
510
|
-
emit({
|
|
1242
|
+
emit({
|
|
1243
|
+
kind: 'thinking',
|
|
1244
|
+
text: providerPreflightRequired
|
|
1245
|
+
? `Using ${spec.label} through the governed DQL agent.`
|
|
1246
|
+
: 'Executing the router-selected certified plan without an AI provider.',
|
|
1247
|
+
});
|
|
511
1248
|
const kgPath = defaultKgPath(req.projectRoot);
|
|
512
1249
|
if (!existsSync(kgPath)) {
|
|
513
1250
|
emit({ kind: 'thinking', text: 'Building the local agent knowledge graph from terms, business views, blocks, apps, dashboards, dbt, and semantic metadata.' });
|
|
@@ -527,6 +1264,9 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
527
1264
|
const conversationSnapshot = conversationSnapshotFromContext(req.conversationContext);
|
|
528
1265
|
const rawFollowUp = followUpFromConversationContext(req, rawQuestion) ?? inferFollowUpContext(req, rawQuestion);
|
|
529
1266
|
const followUp = applyTopicShiftGuard(rawFollowUp, conversationSnapshot);
|
|
1267
|
+
// This is a server-side decision recorded by the engine/trace. It
|
|
1268
|
+
// never comes from client JSON or an LLM response.
|
|
1269
|
+
req.conversationBinding = followUp?.binding ?? 'none';
|
|
530
1270
|
// CTX-003: retrieval and planning operate on the user's current words.
|
|
531
1271
|
// Prior SQL, DQL source, owners, and result metadata stay in the typed
|
|
532
1272
|
// follow-up envelope rendered separately for the provider; concatenating
|
|
@@ -633,6 +1373,63 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
633
1373
|
const schemaContext = req.getSchemaContext && shouldLoadSchemaContext(contextPack, Boolean(semanticLayer))
|
|
634
1374
|
? await req.getSchemaContext(question, contextPack).catch(() => [])
|
|
635
1375
|
: [];
|
|
1376
|
+
// The router's exploratory candidate IDs are server-owned execution
|
|
1377
|
+
// authority. Once that tier is selected, provider prompt/schema
|
|
1378
|
+
// context must be the candidate closure rather than the broad
|
|
1379
|
+
// retrieval pack. The latter remains available to the host for
|
|
1380
|
+
// receipts only and cannot be used to introduce another relation.
|
|
1381
|
+
const forcedExploratoryTier = req.selectedCascadeTier === 'exploratory_sql';
|
|
1382
|
+
// Re-derive the closure from the broad immutable pack and the
|
|
1383
|
+
// router-selected IDs. `preparedExploratoryContextPack` is only a
|
|
1384
|
+
// server-side consistency witness; it cannot override or widen the
|
|
1385
|
+
// derivation even if a future caller constructs an AgentRunner
|
|
1386
|
+
// request directly.
|
|
1387
|
+
const derivedExploratoryContextPack = forcedExploratoryTier
|
|
1388
|
+
? scopeContextPackToExploratoryCandidateClosure(contextPack, req.exploratoryCandidateIds)
|
|
1389
|
+
: undefined;
|
|
1390
|
+
const closureWitnessMatches = !req.preparedExploratoryContextPack
|
|
1391
|
+
|| exploratoryClosureMatches(derivedExploratoryContextPack, req.preparedExploratoryContextPack);
|
|
1392
|
+
const exploratoryContextPack = closureWitnessMatches
|
|
1393
|
+
? derivedExploratoryContextPack
|
|
1394
|
+
: undefined;
|
|
1395
|
+
if (forcedExploratoryTier && (!exploratoryContextPack || !req.exploratoryCandidateIds?.length)) {
|
|
1396
|
+
const text = 'The router-selected exploratory path no longer has a complete same-snapshot physical closure, so DQL did not send SQL generation or execute a query.';
|
|
1397
|
+
emit({
|
|
1398
|
+
kind: 'tool_result',
|
|
1399
|
+
id: 'governed_answer',
|
|
1400
|
+
output: {
|
|
1401
|
+
kind: 'no_answer',
|
|
1402
|
+
sourceTier: 'no_answer',
|
|
1403
|
+
certification: 'analyst_review_required',
|
|
1404
|
+
reviewStatus: 'none',
|
|
1405
|
+
confidence: 0,
|
|
1406
|
+
text,
|
|
1407
|
+
answer: text,
|
|
1408
|
+
refusalCode: 'grounding_gap',
|
|
1409
|
+
refusalDetails: {
|
|
1410
|
+
code: closureWitnessMatches ? 'EXPLORATORY_CLOSURE_UNAVAILABLE' : 'EXPLORATORY_CLOSURE_MISMATCH',
|
|
1411
|
+
message: text,
|
|
1412
|
+
},
|
|
1413
|
+
contextPack,
|
|
1414
|
+
providerUsed: provider.name,
|
|
1415
|
+
},
|
|
1416
|
+
});
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const answerContextPack = forcedExploratoryTier
|
|
1420
|
+
? exploratoryContextPack ?? contextPack
|
|
1421
|
+
: contextPack;
|
|
1422
|
+
const normalizeQualifiedRelation = (value) => value
|
|
1423
|
+
.trim()
|
|
1424
|
+
.split('.')
|
|
1425
|
+
.map((part) => part.trim().replace(/^["`\[]|["`\]]$/g, '').toLowerCase())
|
|
1426
|
+
.filter(Boolean)
|
|
1427
|
+
.join('.');
|
|
1428
|
+
const closureRelations = new Set((exploratoryContextPack?.allowedSqlContext.relations ?? [])
|
|
1429
|
+
.map((relation) => normalizeQualifiedRelation(relation.relation)));
|
|
1430
|
+
const answerSchemaContext = forcedExploratoryTier && closureRelations.size > 0
|
|
1431
|
+
? schemaContext.filter((table) => closureRelations.has(normalizeQualifiedRelation(table.relation)))
|
|
1432
|
+
: schemaContext;
|
|
636
1433
|
const schemaDurationMs = Date.now() - schemaStartedAt;
|
|
637
1434
|
const selectedBlockHints = shouldUseSelectedBlockHint(req, question, followUp)
|
|
638
1435
|
? extractSelectedBlockHints(req)
|
|
@@ -655,8 +1452,8 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
655
1452
|
question,
|
|
656
1453
|
...(conversationSnapshot ? { conversationSnapshot } : {}),
|
|
657
1454
|
...(memoryContext ? { memoryContext } : {}),
|
|
658
|
-
schemaContext: prepareServerOwnedProviderSchemaContext(
|
|
659
|
-
...(
|
|
1455
|
+
schemaContext: prepareServerOwnedProviderSchemaContext(answerSchemaContext),
|
|
1456
|
+
...(answerContextPack ? { contextPack: answerContextPack } : {}),
|
|
660
1457
|
skills,
|
|
661
1458
|
...(followUp ? { followUp } : {}),
|
|
662
1459
|
}), {
|
|
@@ -667,11 +1464,22 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
667
1464
|
// The strangler seam. `answerAgentic` and `answer` are interchangeable
|
|
668
1465
|
// here; which runs is a per-lane config decision that defaults to
|
|
669
1466
|
// legacy, so this is a no-op until a lane is explicitly enabled.
|
|
670
|
-
|
|
1467
|
+
// Preserve the non-enumerable observer when the provider runner
|
|
1468
|
+
// projects the AgentRun request into the answer-loop input. The
|
|
1469
|
+
// analyst loop receives this projected object, so dropping the
|
|
1470
|
+
// observer here would make actual text-protocol tool calls invisible
|
|
1471
|
+
// even though provider, router, and SQL evidence was recorded.
|
|
1472
|
+
const answerLoopInput = attachAskTraceObserverV1({
|
|
671
1473
|
question,
|
|
672
1474
|
...(req.resolvedAnalyticalPlan
|
|
673
1475
|
? { resolvedAnalyticalPlan: req.resolvedAnalyticalPlan }
|
|
674
1476
|
: {}),
|
|
1477
|
+
...(req.selectedCascadeTier
|
|
1478
|
+
? { selectedCascadeTier: req.selectedCascadeTier }
|
|
1479
|
+
: {}),
|
|
1480
|
+
...(req.exploratoryCandidateIds?.length
|
|
1481
|
+
? { exploratoryCandidateIds: [...req.exploratoryCandidateIds] }
|
|
1482
|
+
: {}),
|
|
675
1483
|
...(req.generatedProposalTargetFingerprint
|
|
676
1484
|
? { generatedProposalTargetFingerprint: req.generatedProposalTargetFingerprint }
|
|
677
1485
|
: {}),
|
|
@@ -692,7 +1500,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
692
1500
|
followUp,
|
|
693
1501
|
conversationSnapshot,
|
|
694
1502
|
memoryContext,
|
|
695
|
-
schemaContext,
|
|
1503
|
+
schemaContext: answerSchemaContext,
|
|
696
1504
|
semanticLayer,
|
|
697
1505
|
// Runtime-aware executability for metric SELECTION: with a full
|
|
698
1506
|
// semantic runtime active (dbt Cloud / MetricFlow CLI) every
|
|
@@ -704,7 +1512,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
704
1512
|
canExecuteSemanticMetric: (metricName) => semanticRuntimeActive !== 'native' || semanticLayer.canComposeMetric(metricName),
|
|
705
1513
|
}
|
|
706
1514
|
: {}),
|
|
707
|
-
contextPack,
|
|
1515
|
+
contextPack: answerContextPack,
|
|
708
1516
|
// The project's configured embedder (dql.config.json ai.embeddings).
|
|
709
1517
|
// Without it, matchSemanticMetric falls back to the offline hashed
|
|
710
1518
|
// provider, whose vectors can never ground a match on similarity
|
|
@@ -721,16 +1529,43 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
721
1529
|
...(req.preferredEvidenceIds?.length ? { preferredEvidenceIds: req.preferredEvidenceIds } : {}),
|
|
722
1530
|
...(req.preferredExecutionId ? { preferredExecutionId: req.preferredExecutionId } : {}),
|
|
723
1531
|
executeCertifiedBlock: req.executeCertifiedBlock
|
|
724
|
-
? async (...args) => {
|
|
1532
|
+
? async (...args) => {
|
|
1533
|
+
guardSnapshot();
|
|
1534
|
+
return executeAtSqlBoundary(() => req.executeCertifiedBlock(...args));
|
|
1535
|
+
}
|
|
725
1536
|
: undefined,
|
|
726
1537
|
executeGeneratedSql: req.executeGeneratedSql
|
|
727
|
-
? async (...args) => {
|
|
1538
|
+
? async (...args) => {
|
|
1539
|
+
guardSnapshot();
|
|
1540
|
+
return executeAtSqlBoundary(() => req.executeGeneratedSql(...args));
|
|
1541
|
+
}
|
|
1542
|
+
: undefined,
|
|
1543
|
+
prepareExploratorySqlExecution: req.prepareExploratorySqlExecution
|
|
1544
|
+
? async (sql, ...args) => {
|
|
1545
|
+
guardSnapshot();
|
|
1546
|
+
// The execution host repeats this validation immediately
|
|
1547
|
+
// before capability minting. Keep the same exact-qualified
|
|
1548
|
+
// closure check here as well, before the provider runner can
|
|
1549
|
+
// even invoke that host boundary. A provider response cannot
|
|
1550
|
+
// use another same-snapshot relation merely because it was
|
|
1551
|
+
// present in broad retrieval diagnostics.
|
|
1552
|
+
if (forcedExploratoryTier && exploratoryContextPack) {
|
|
1553
|
+
const proposalValidation = validateSqlAgainstLocalContext(sql, exploratoryContextPack, {
|
|
1554
|
+
runtimeSchema: answerSchemaContext,
|
|
1555
|
+
});
|
|
1556
|
+
const outsideClosure = !proposalValidation.ok
|
|
1557
|
+
|| proposalValidation.referencedRelations.some((relation) => !closureRelations.has(normalizeQualifiedRelation(relation)));
|
|
1558
|
+
if (outsideClosure) {
|
|
1559
|
+
throw Object.assign(new Error('The generated SQL references a relation outside the router-selected physical closure, so it was not executed.'), { code: 'UNAUTHORIZED_SQL' });
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return req.prepareExploratorySqlExecution(sql, ...args);
|
|
1563
|
+
}
|
|
728
1564
|
: undefined,
|
|
729
1565
|
executeAgenticGeneratedSql: req.executeAgenticGeneratedSql
|
|
730
1566
|
? async (capability, sql, artifact) => {
|
|
731
1567
|
guardSnapshot();
|
|
732
|
-
|
|
733
|
-
return req.executeAgenticGeneratedSql(capability, sql, artifact);
|
|
1568
|
+
return executeAtSqlBoundary(() => req.executeAgenticGeneratedSql(capability, sql, artifact));
|
|
734
1569
|
}
|
|
735
1570
|
: undefined,
|
|
736
1571
|
agenticExecutionScope: {
|
|
@@ -740,7 +1575,10 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
740
1575
|
targetFingerprint: req.generatedProposalTargetFingerprint,
|
|
741
1576
|
},
|
|
742
1577
|
executeDqlArtifact: req.executeDqlArtifact
|
|
743
|
-
? async (...args) => {
|
|
1578
|
+
? async (...args) => {
|
|
1579
|
+
guardSnapshot();
|
|
1580
|
+
return executeAtSqlBoundary(() => req.executeDqlArtifact(...args));
|
|
1581
|
+
}
|
|
744
1582
|
: undefined,
|
|
745
1583
|
expandGroundingContext: createGroundingContextExpander(req.projectRoot, req.probeNamedRelations),
|
|
746
1584
|
answerLoopTools,
|
|
@@ -779,7 +1617,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
779
1617
|
// NOTE: no captureGeneratedDraft here — a plain answer/research question must NOT
|
|
780
1618
|
// auto-write a draft into the blocks space. A draft is created only when the user
|
|
781
1619
|
// explicitly acts (the "Create DQL draft" action → the dql_block_draft route).
|
|
782
|
-
};
|
|
1620
|
+
}, askTrace);
|
|
783
1621
|
const result = await answerAgentic(answerLoopInput, {
|
|
784
1622
|
policy: resolveOrchestratorPolicy({
|
|
785
1623
|
config: readOrchestratorConfig(req.projectRoot),
|
|
@@ -808,14 +1646,15 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
808
1646
|
maxIterations: resolveOrchestratorPolicy({
|
|
809
1647
|
config: readOrchestratorConfig(req.projectRoot),
|
|
810
1648
|
}).maxIterations,
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
1649
|
+
// Keep the raw loop bounded by the same physical ceiling
|
|
1650
|
+
// as the wrapper. The run-scoped ledger still admits only
|
|
1651
|
+
// one ordinary generation/planning transport; a separate
|
|
1652
|
+
// frozen-plan repair marker is required for the third send.
|
|
814
1653
|
maxProviderDispatches,
|
|
815
1654
|
// Scaled by the same knob as every other agent deadline, so
|
|
816
1655
|
// a local model that needs seconds per call is not planned
|
|
817
1656
|
// out of existence by a budget calibrated for a hosted one.
|
|
818
|
-
...(turnPlanningEnabled(req.projectRoot) ? {
|
|
1657
|
+
...(turnPlanningEnabled(req.projectRoot, req.orchestrationMode) ? {
|
|
819
1658
|
planTurn: async (question, toolNames) => {
|
|
820
1659
|
const plan = await planAnalystTurn(loopInput.provider, question, toolNames, {
|
|
821
1660
|
timeoutMs: Math.round(2_500 * deadlineScale()),
|
|
@@ -829,6 +1668,11 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
829
1668
|
return plan;
|
|
830
1669
|
},
|
|
831
1670
|
} : {}),
|
|
1671
|
+
// Text-protocol tool loops execute tools outside the
|
|
1672
|
+
// provider transport, so carry the same physical-boundary
|
|
1673
|
+
// observer through the loop. The marker prevents the
|
|
1674
|
+
// provider wrapper from duplicating native tool records.
|
|
1675
|
+
onToolCall: createAskTraceToolCallback(askTraceObserverForV1(loopInput)),
|
|
832
1676
|
// The loop's trace, on the channel the provider already uses
|
|
833
1677
|
// for progress. `thinking` turns become `onProgress`, which
|
|
834
1678
|
// the run engine emits as `executor.started` over SSE — so
|
|
@@ -985,6 +1829,19 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
985
1829
|
dispatchEvidence: dispatchEvidence(orchestrationBudgetExhausted ? 'orchestration_budget_exhausted'
|
|
986
1830
|
: snapshotDrift ? 'project_snapshot_mismatch'
|
|
987
1831
|
: 'provider_error'),
|
|
1832
|
+
providerDiagnostic: providerBoundaryDiagnostic({
|
|
1833
|
+
providerId: id,
|
|
1834
|
+
projectRoot: req.projectRoot,
|
|
1835
|
+
phase: orchestrationBudgetExhausted
|
|
1836
|
+
? 'planning'
|
|
1837
|
+
: 'generation',
|
|
1838
|
+
error: err,
|
|
1839
|
+
code: orchestrationBudgetExhausted
|
|
1840
|
+
? 'PROVIDER_DISPATCH_BUDGET'
|
|
1841
|
+
: snapshotDrift
|
|
1842
|
+
? 'ADMISSION_DENIED'
|
|
1843
|
+
: budgetCode,
|
|
1844
|
+
}),
|
|
988
1845
|
});
|
|
989
1846
|
}
|
|
990
1847
|
},
|
|
@@ -1279,9 +2136,13 @@ function extractSelectedBlockHints(req) {
|
|
|
1279
2136
|
}
|
|
1280
2137
|
}
|
|
1281
2138
|
function inferFollowUpContext(req, question) {
|
|
1282
|
-
// Messages-only fallback (no structured conversationContext).
|
|
1283
|
-
//
|
|
1284
|
-
|
|
2139
|
+
// Messages-only fallback (no structured conversationContext). A prior
|
|
2140
|
+
// assistant turn is not permission to carry rows, measures, or a block into
|
|
2141
|
+
// a complete new analytical question. Admit it only for an explicit
|
|
2142
|
+
// prior-result reference/operation; otherwise this returns `undefined`.
|
|
2143
|
+
const kind = isGenericFollowUp(question) ? 'generic' : isDrilldownFollowUp(question) ? 'drilldown' : undefined;
|
|
2144
|
+
if (!kind)
|
|
2145
|
+
return undefined;
|
|
1285
2146
|
for (let i = req.messages.length - 1; i >= 0; i--) {
|
|
1286
2147
|
const msg = req.messages[i];
|
|
1287
2148
|
if (msg.role !== 'assistant')
|
|
@@ -1291,6 +2152,7 @@ function inferFollowUpContext(req, question) {
|
|
|
1291
2152
|
continue;
|
|
1292
2153
|
return {
|
|
1293
2154
|
kind,
|
|
2155
|
+
binding: 'prior_result',
|
|
1294
2156
|
sourceBlockName,
|
|
1295
2157
|
sourceAnswer: msg.content.slice(0, 1200),
|
|
1296
2158
|
filters: kind === 'drilldown' ? extractDrilldownFilters(question) : undefined,
|
|
@@ -1328,17 +2190,11 @@ function conversationSnapshotFromContext(context) {
|
|
|
1328
2190
|
function applyTopicShiftGuard(followUp, snapshot) {
|
|
1329
2191
|
if (!followUp || snapshot?.topicRelation !== 'shift')
|
|
1330
2192
|
return followUp;
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
filters: undefined,
|
|
1337
|
-
dimensions: undefined,
|
|
1338
|
-
priorResultValues: undefined,
|
|
1339
|
-
priorMeasures: undefined,
|
|
1340
|
-
priorLimit: undefined,
|
|
1341
|
-
};
|
|
2193
|
+
// A server-produced compound dependency is a direct child input, not an
|
|
2194
|
+
// incidental thread carry. Every other result binding is dropped on a
|
|
2195
|
+
// topic shift so a fresh analytical request cannot receive prior rows or
|
|
2196
|
+
// measures merely because the thread has history.
|
|
2197
|
+
return followUp.binding === 'task_dependency' ? followUp : undefined;
|
|
1342
2198
|
}
|
|
1343
2199
|
/**
|
|
1344
2200
|
* Resolve the prior-turn context a follow-up may build on.
|
|
@@ -1365,6 +2221,7 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
|
|
|
1365
2221
|
if (taskDependency) {
|
|
1366
2222
|
return {
|
|
1367
2223
|
kind: 'drilldown',
|
|
2224
|
+
binding: 'task_dependency',
|
|
1368
2225
|
sourceTurnId: `task:${taskDependency.sourceTaskId}`,
|
|
1369
2226
|
filters: [taskDependency.value],
|
|
1370
2227
|
dimensions: [taskDependency.canonicalColumn],
|
|
@@ -1418,22 +2275,24 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
|
|
|
1418
2275
|
const hasUsefulContext = Boolean(sourceBlockName || priorResultColumns?.length || focusedPriorResultValues || priorDqlArtifact);
|
|
1419
2276
|
if (!hasUsefulContext)
|
|
1420
2277
|
return undefined;
|
|
2278
|
+
// Prior-result material is execution-relevant context, not a friendly
|
|
2279
|
+
// transcript hint. A complete new question therefore receives no carry at
|
|
2280
|
+
// all. The only admission routes are a resolved typed reference, an
|
|
2281
|
+
// explicit prior-result operation, or a deictic/generic follow-up.
|
|
1421
2282
|
const inferredKind = resolvedReferences.memberBindings?.length
|
|
1422
2283
|
? 'drilldown'
|
|
1423
2284
|
: isGenericFollowUp(question)
|
|
1424
2285
|
? 'generic'
|
|
1425
2286
|
: isDrilldownFollowUp(question, priorShapeTerms(priorResultColumns, focusedPriorResultValues))
|
|
1426
2287
|
? 'drilldown'
|
|
1427
|
-
:
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
const kind = inferredKind
|
|
1433
|
-
?? (context.followupKind === 'generic' || context.followupKind === 'drilldown' ? context.followupKind : null)
|
|
1434
|
-
?? 'contextual';
|
|
2288
|
+
: undefined;
|
|
2289
|
+
if (!inferredKind)
|
|
2290
|
+
return undefined;
|
|
2291
|
+
const binding = 'prior_result';
|
|
2292
|
+
const kind = inferredKind;
|
|
1435
2293
|
return {
|
|
1436
2294
|
kind,
|
|
2295
|
+
binding,
|
|
1437
2296
|
sourceTurnId: cleanOptionalString(activeTurn?.id) ?? cleanOptionalString(context.sourceAnswerId),
|
|
1438
2297
|
// A relative comparison needs the named member from history, not the prior
|
|
1439
2298
|
// block's result contract. Carrying a beverage-ranking block into "less tax
|
|
@@ -2088,6 +2947,8 @@ export const __test__ = {
|
|
|
2088
2947
|
shouldSearchProjectFiles,
|
|
2089
2948
|
renderProjectSourceSearch,
|
|
2090
2949
|
researchDispatchPurposeForTool,
|
|
2950
|
+
readAgentConfig,
|
|
2951
|
+
providerBoundaryDiagnostic,
|
|
2091
2952
|
};
|
|
2092
2953
|
function researchDispatchPurposeForTool(toolName) {
|
|
2093
2954
|
return toolName === 'execute_local_analysis' ? 'research_tool' : 'research_narration';
|
|
@@ -2126,7 +2987,7 @@ function isGenericFollowUp(question) {
|
|
|
2126
2987
|
* Now it needs either a deictic reference to the prior result, or an explicit
|
|
2127
2988
|
* drill verb that only makes sense relative to something already on screen.
|
|
2128
2989
|
*/
|
|
2129
|
-
function isDrilldownFollowUp(question,
|
|
2990
|
+
function isDrilldownFollowUp(question, _priorTerms = []) {
|
|
2130
2991
|
const lower = question.toLowerCase();
|
|
2131
2992
|
const deicticDrilldown = /\b(?:this|that|these|those|same|above|previous|prior)\s+(?:amount|value|orders?|results?|rows?|customers?|products?|cat(?:egor|agor|ogor)(?:y|ies)|segments?|regions?)\b/.test(lower)
|
|
2132
2993
|
|| /\b(?:they|their|them|he|she|him|his|her|hers)\b/.test(lower);
|
|
@@ -2135,15 +2996,18 @@ function isDrilldownFollowUp(question, priorTerms = []) {
|
|
|
2135
2996
|
// a short gap between them rather than adjacently.
|
|
2136
2997
|
const explicitDrillVerb = /\b(drills?|breakdowns?|slices?|segments?|splits?|why|drivers?|root cause|variance)\b/.test(lower)
|
|
2137
2998
|
|| /\bbreak\b.{0,16}\bdown\b/.test(lower);
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
//
|
|
2141
|
-
//
|
|
2142
|
-
// a
|
|
2143
|
-
const
|
|
2144
|
-
|
|
2999
|
+
const hasDeicticReference = /\b(?:this|that|these|those|same|above|previous|prior|they|their|them|he|she|him|his|her|hers)\b/.test(lower);
|
|
3000
|
+
const resultStateReference = /\b(?:decline|drop|change|increase|decrease|variance)\b/.test(lower);
|
|
3001
|
+
// A shared word such as customer, revenue, or region is not a prior-result
|
|
3002
|
+
// reference. It appears in many unrelated analytical questions and used to
|
|
3003
|
+
// make a fresh request inherit the prior answer's filters and measures.
|
|
3004
|
+
const explicitPriorResultOperation = /\b(?:top|bottom)\s+\d+\s+of\s+(?:these|those|them|the\s+(?:prior|previous)?\s*results?)\b/.test(lower)
|
|
3005
|
+
|| /\b(?:average|sum|total|compare)\s+(?:of|across)\s+(?:these|those|them|the\s+(?:prior|previous)?\s*results?)\b/.test(lower);
|
|
3006
|
+
if (!deicticDrilldown && !explicitPriorResultOperation && !(explicitDrillVerb && (hasDeicticReference || resultStateReference)))
|
|
2145
3007
|
return false;
|
|
2146
|
-
|
|
3008
|
+
// Bare definitions should not inherit a prior result merely because they
|
|
3009
|
+
// contain a pronoun. An explicit result operation remains authoritative.
|
|
3010
|
+
return explicitPriorResultOperation || !/\b(what is|what are|define|definition|meaning of)\b/.test(lower);
|
|
2147
3011
|
}
|
|
2148
3012
|
/**
|
|
2149
3013
|
* Lower-cased words from the prior result's columns and dimension keys, used to
|