@duckcodeailabs/dql-cli 1.14.2 → 1.14.3-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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-DKbGpRQS.js → AgentLogPage-Jbyb4so-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-DPSu0Mly.js → AiBuildDialog-Du07X1qO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-1uaGnpi1.js → AiBuildResult-xKtGthWv.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiSidePanel-BwMREwa7.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-CM1tPywy.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--S6WFVO4.js → BlockStudio-DQS5Hcq9.js} +9 -14
- package/dist/assets/dql-notebook/assets/BusinessArtifactView-CqV4i9l_.js +1 -0
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-CNyU5MBX.js → DbtFirstModelingPage-JKhB_Y2H.js} +3 -3
- package/dist/assets/dql-notebook/assets/{GitPage-IcydAai3.js → GitPage-DjaNp0o3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-CSKW-5eD.js → GlobalAiRail-CsV3zlc8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-CFen0eFT.js → GovernedContextPage-uXUHUOIJ.js} +3 -3
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-D0D9UCIz.js → HelpDocsPage-Hbh9IeYB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-CmSxapR3.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-CTZJp4_r.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-DMBFB7nE.js → NewBlockModal-kMAzk91d.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-DsC9CWlW.js → NewNotebookModal-D_cWd_6M.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-hs-kw8v9.js → NotebookEditor-Bax0lxaz.js} +25 -30
- package/dist/assets/dql-notebook/assets/{ReadinessPage-DJ83cMik.js → ReadinessPage-DEUIXQQY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-B1Pu_Bvv.js → SetupOnboarding-CyE-Ex41.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SkillsPage-BHKDay8n.js → SkillsPage-QgvKGPxE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-BkyGgob2.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-DmNiuLQA.js → answer-to-notebook-tACdCsTV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left--1rsrxm8.js → arrow-left-CF6wxXU5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-D5TdqY1G.js → arrow-right-C7G3M_S8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-Bw7nHbzg.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-DLe6NNM4.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-C9Q97sA3.js → external-link-WciAU8Bu.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-CztvkIgo.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-zHHzDn6l.js → index-D3wnucQC.js} +133 -128
- package/dist/assets/dql-notebook/assets/{link-2-CiKAvumL.js → link-2-C6uQx50X.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-BtnP2nQ5.js → list-tree-DapsIuQB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-TSFGxcCP.js → minimize-2-BlVoipnT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-BfXIUWy0.js → panel-right-open-DcnKWxfx.js} +1 -1
- package/dist/assets/dql-notebook/assets/{play-DVbSFJHD.js → play-CNuRuaAs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-D_cesDcX.js → rotate-ccw-B2p953Zh.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-CoVStdYB.js → semantic-fields-2fWgkTJc.js} +1 -1
- package/dist/assets/dql-notebook/assets/{sliders-horizontal-l7xV9K5A.js → sliders-horizontal-DwRTu1w4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-CSBS0H3b.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-BTrnyY4q.js → triangle-alert-BPHvH1wA.js} +1 -1
- package/dist/assets/dql-notebook/assets/{upload-sySLq9zb.js → upload-DIqK0KE1.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-CzwgGdus.js → usePersistedAgentThreadId-DpQXOEDw.js} +1 -1
- package/dist/assets/dql-notebook/assets/{user-round-ChlgXi9j.js → user-round-DukNupB_.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-CGw0ytyT.js → wand-sparkles-CAwLX0b9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-C_RltiK5.js → workflow-bOT-NqdO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-D-wLfeu0.js → wrench-1m0_kcza.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-B28hIJIC.js → x-fp82BETM.js} +1 -1
- package/dist/assets/dql-notebook/index.html +2 -2
- package/dist/commands/agent-eval-runtime.d.ts +9 -0
- package/dist/commands/agent-eval-runtime.d.ts.map +1 -1
- package/dist/commands/agent-eval-runtime.js +7 -0
- 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 +28 -2
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +408 -35
- package/dist/commands/agent.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/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +759 -145
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +21 -2
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +256 -33
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +2844 -490
- 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-BGfey_ve.js +0 -6
- package/dist/assets/dql-notebook/assets/BusinessArtifactView-BEAJ-yNW.js +0 -1
- package/dist/assets/dql-notebook/assets/LineageDAG-BGUcIt1B.js +0 -1
- package/dist/assets/dql-notebook/assets/LineageDetailView-Dx48Zfzb.js +0 -1
- package/dist/assets/dql-notebook/assets/LineageDrawer-zaBfSLZO.js +0 -1
- package/dist/assets/dql-notebook/assets/MiniLineageGraph-CdivNR1S.js +0 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-BYdrTaEW.js +0 -89
- package/dist/assets/dql-notebook/assets/dagre.esm-CW5QZdBt.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,8 +1,8 @@
|
|
|
1
|
-
import { classifyProviderFailure, 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
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
4
|
import { CassetteStore, evalCassetteCanonicalizationV2, resolveCassetteModeFromEnv, withCassette, } from '../../commands/agent-eval-cassette.js';
|
|
5
|
-
import { buildManifest, normalizeDqlArtifactReference, resolveDbtManifestPath } from '@duckcodeailabs/dql-core';
|
|
5
|
+
import { buildManifest, normalizeDqlArtifactReference, resolveDbtManifestPath, } from '@duckcodeailabs/dql-core';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
@@ -13,6 +13,17 @@ import { getEffectiveProviderConfig } from '../../settings/provider-settings.js'
|
|
|
13
13
|
import { ClaudeCodeCliProvider, CodexCliProvider } from '../../providers/subscription-cli.js';
|
|
14
14
|
import { ClaudeOAuthProvider, claudeOAuthConnected } from '../../providers/oauth/claude-oauth.js';
|
|
15
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
|
+
}
|
|
16
27
|
const SPECS = {
|
|
17
28
|
anthropic: {
|
|
18
29
|
label: 'Anthropic Claude',
|
|
@@ -109,6 +120,103 @@ function providerBoundaryDiagnostic(input) {
|
|
|
109
120
|
baseOriginFingerprint: fingerprint(origin),
|
|
110
121
|
});
|
|
111
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
|
+
}
|
|
112
220
|
function createCertifiedFitConfirmation(provider, signal) {
|
|
113
221
|
return async ({ question, questionPlan, block, fit }) => {
|
|
114
222
|
const response = await provider.generate([
|
|
@@ -404,10 +512,15 @@ function readOrchestratorConfig(projectRoot) {
|
|
|
404
512
|
* measured on ollama, where all three agentic dispatches planned successfully
|
|
405
513
|
* and then died at "soft target elapsed before this provider dispatch could
|
|
406
514
|
* start". Its payoff is a legible trace, and `onStep` is not wired to SSE yet,
|
|
407
|
-
* so today it buys nothing a user can see.
|
|
408
|
-
*
|
|
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.
|
|
409
520
|
*/
|
|
410
|
-
function turnPlanningEnabled(projectRoot) {
|
|
521
|
+
function turnPlanningEnabled(projectRoot, orchestrationMode) {
|
|
522
|
+
if (orchestrationMode !== 'research')
|
|
523
|
+
return false;
|
|
411
524
|
const orchestrator = readAgentConfig(projectRoot)?.orchestrator;
|
|
412
525
|
if (!orchestrator || typeof orchestrator !== 'object')
|
|
413
526
|
return false;
|
|
@@ -495,8 +608,24 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
495
608
|
async run(req, emit, signal) {
|
|
496
609
|
const spec = SPECS[id];
|
|
497
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;
|
|
498
619
|
const isResearch = req.orchestrationMode === 'research';
|
|
499
|
-
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;
|
|
500
629
|
const researchRowsOptIn = isResearch && req.researchResultRowsOptIn === true;
|
|
501
630
|
const sharedDispatchEvidence = req.providerDispatchEvidenceSink;
|
|
502
631
|
let providerRoundTrips = 0;
|
|
@@ -505,6 +634,11 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
505
634
|
let pendingColumnCount = 0;
|
|
506
635
|
let pendingCumulativeResultRowCount = 0;
|
|
507
636
|
let pendingResearchPurpose = 'research_narration';
|
|
637
|
+
let physicalToolAttemptIndex = 0;
|
|
638
|
+
let physicalProviderAttemptIndex = 0;
|
|
639
|
+
let lastFailedProviderSpanId;
|
|
640
|
+
let physicalDispatchSequence = 0;
|
|
641
|
+
let lastAdmittedPhysicalDispatch;
|
|
508
642
|
const providerEgressReceipts = [];
|
|
509
643
|
const dispatchEvidence = (fallbackReason) => {
|
|
510
644
|
const shared = sharedDispatchEvidence?.snapshot(fallbackReason);
|
|
@@ -519,145 +653,598 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
519
653
|
fallbackReason,
|
|
520
654
|
};
|
|
521
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
|
+
};
|
|
522
676
|
signal.addEventListener('abort', () => {
|
|
523
677
|
const reason = signal.reason;
|
|
524
678
|
if (reason && typeof reason === 'object') {
|
|
525
679
|
Object.assign(reason, { providerDispatchEvidence: dispatchEvidence('cancelled') });
|
|
526
680
|
}
|
|
527
681
|
}, { once: true });
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
? { mayStartToolCall: () => sharedDispatchEvidence.mayStartToolCall() }
|
|
536
|
-
: {}),
|
|
537
|
-
onProviderDispatch: (event) => {
|
|
538
|
-
const purpose = isResearch ? pendingResearchPurpose : 'answer_generation';
|
|
539
|
-
if (sharedDispatchEvidence) {
|
|
540
|
-
const envelope = sharedDispatchEvidence.observe(event, {
|
|
541
|
-
purpose,
|
|
542
|
-
dispatchPhase: 'generation',
|
|
543
|
-
optIn: pendingResultRowCount > 0 && researchRowsOptIn,
|
|
544
|
-
serializedResultShape: {
|
|
545
|
-
resultRowCount: pendingResultRowCount,
|
|
546
|
-
columnCount: pendingColumnCount,
|
|
547
|
-
},
|
|
548
|
-
...(pendingCumulativeResultRowCount > 0
|
|
549
|
-
? { cumulativeResultRowCount: pendingCumulativeResultRowCount }
|
|
550
|
-
: {}),
|
|
551
|
-
});
|
|
552
|
-
pendingResultRowCount = 0;
|
|
553
|
-
pendingColumnCount = 0;
|
|
554
|
-
pendingCumulativeResultRowCount = 0;
|
|
555
|
-
pendingResearchPurpose = 'research_narration';
|
|
556
|
-
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;
|
|
557
689
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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 },
|
|
564
813
|
});
|
|
814
|
+
return;
|
|
565
815
|
}
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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 },
|
|
571
828
|
});
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
+
};
|
|
597
1071
|
// One retry only, on the SAME configured provider and only for failures
|
|
598
1072
|
// that are normally transient. The physical-dispatch observer remains in
|
|
599
1073
|
// the path for both attempts, so a retry cannot exceed the run-wide
|
|
600
1074
|
// dispatch budget or silently fail over to another provider.
|
|
601
1075
|
let transientRetryUsed = false;
|
|
602
|
-
const mayRetrySameProvider = (error) => {
|
|
603
|
-
|
|
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)
|
|
604
1087
|
return false;
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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');
|
|
610
1093
|
};
|
|
611
|
-
const retrySameProviderOnce = async (operation) => {
|
|
1094
|
+
const retrySameProviderOnce = async (sourceOptions, operation) => {
|
|
1095
|
+
const identity = providerDispatchIdentity(sourceOptions);
|
|
1096
|
+
const dispatchSequenceBefore = physicalDispatchSequence;
|
|
612
1097
|
try {
|
|
613
|
-
return await operation();
|
|
1098
|
+
return await operation(sourceOptions);
|
|
614
1099
|
}
|
|
615
1100
|
catch (error) {
|
|
616
|
-
|
|
1101
|
+
const parent = lastAdmittedPhysicalDispatch?.sequence && lastAdmittedPhysicalDispatch.sequence > dispatchSequenceBefore
|
|
1102
|
+
? lastAdmittedPhysicalDispatch
|
|
1103
|
+
: undefined;
|
|
1104
|
+
if (!mayRetrySameProvider(error, identity, parent))
|
|
617
1105
|
throw error;
|
|
618
1106
|
transientRetryUsed = true;
|
|
619
1107
|
emit({ kind: 'thinking', text: 'The configured AI provider had a transient error; retrying it once within this run budget.' });
|
|
620
|
-
|
|
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,
|
|
1121
|
+
},
|
|
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;
|
|
621
1140
|
}
|
|
622
1141
|
};
|
|
623
1142
|
const provider = {
|
|
624
1143
|
name: rawProvider.name,
|
|
625
1144
|
available: () => rawProvider.available(),
|
|
626
|
-
generate: (...args) =>
|
|
627
|
-
return retrySameProviderOnce(() => rawProvider.generate(args[0], withPhysicalDispatchObserver(args[1])));
|
|
628
|
-
},
|
|
1145
|
+
generate: (...args) => retrySameProviderOnce(args[1], (sourceOptions) => invokePhysicalProvider(sourceOptions, (options) => rawProvider.generate(args[0], options))),
|
|
629
1146
|
...(rawProvider.generateWithTools ? {
|
|
630
|
-
generateWithTools: (...args) =>
|
|
631
|
-
return retrySameProviderOnce(() => rawProvider.generateWithTools(args[0], args[1], withPhysicalDispatchObserver(args[2])));
|
|
632
|
-
},
|
|
1147
|
+
generateWithTools: (...args) => retrySameProviderOnce(args[2], (sourceOptions) => invokePhysicalProvider(sourceOptions, (options) => rawProvider.generateWithTools(args[0], args[1], options))),
|
|
633
1148
|
} : {}),
|
|
634
1149
|
...(rawProvider.generateStream ? {
|
|
635
|
-
generateStream: (...args) =>
|
|
636
|
-
return rawProvider.generateStream(args[0], withPhysicalDispatchObserver(args[1]), args[2]);
|
|
637
|
-
},
|
|
1150
|
+
generateStream: (...args) => invokePhysicalProvider(args[1], (options) => rawProvider.generateStream(args[0], options, args[2])),
|
|
638
1151
|
} : {}),
|
|
639
1152
|
};
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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({
|
|
647
1185
|
providerId: id,
|
|
648
1186
|
projectRoot: req.projectRoot,
|
|
649
1187
|
phase: 'preflight',
|
|
650
|
-
error: message,
|
|
1188
|
+
error: preflightError ?? message,
|
|
651
1189
|
// Local Ollama readiness is a reachability concern; configured
|
|
652
1190
|
// remote providers missing credentials are authentication issues.
|
|
653
|
-
code:
|
|
654
|
-
|
|
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
|
+
},
|
|
655
1238
|
});
|
|
656
|
-
return;
|
|
657
1239
|
}
|
|
658
1240
|
try {
|
|
659
1241
|
const requestStartedAt = Date.now();
|
|
660
|
-
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
|
+
});
|
|
661
1248
|
const kgPath = defaultKgPath(req.projectRoot);
|
|
662
1249
|
if (!existsSync(kgPath)) {
|
|
663
1250
|
emit({ kind: 'thinking', text: 'Building the local agent knowledge graph from terms, business views, blocks, apps, dashboards, dbt, and semantic metadata.' });
|
|
@@ -677,6 +1264,9 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
677
1264
|
const conversationSnapshot = conversationSnapshotFromContext(req.conversationContext);
|
|
678
1265
|
const rawFollowUp = followUpFromConversationContext(req, rawQuestion) ?? inferFollowUpContext(req, rawQuestion);
|
|
679
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';
|
|
680
1270
|
// CTX-003: retrieval and planning operate on the user's current words.
|
|
681
1271
|
// Prior SQL, DQL source, owners, and result metadata stay in the typed
|
|
682
1272
|
// follow-up envelope rendered separately for the provider; concatenating
|
|
@@ -874,7 +1464,12 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
874
1464
|
// The strangler seam. `answerAgentic` and `answer` are interchangeable
|
|
875
1465
|
// here; which runs is a per-lane config decision that defaults to
|
|
876
1466
|
// legacy, so this is a no-op until a lane is explicitly enabled.
|
|
877
|
-
|
|
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({
|
|
878
1473
|
question,
|
|
879
1474
|
...(req.resolvedAnalyticalPlan
|
|
880
1475
|
? { resolvedAnalyticalPlan: req.resolvedAnalyticalPlan }
|
|
@@ -934,10 +1529,16 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
934
1529
|
...(req.preferredEvidenceIds?.length ? { preferredEvidenceIds: req.preferredEvidenceIds } : {}),
|
|
935
1530
|
...(req.preferredExecutionId ? { preferredExecutionId: req.preferredExecutionId } : {}),
|
|
936
1531
|
executeCertifiedBlock: req.executeCertifiedBlock
|
|
937
|
-
? async (...args) => {
|
|
1532
|
+
? async (...args) => {
|
|
1533
|
+
guardSnapshot();
|
|
1534
|
+
return executeAtSqlBoundary(() => req.executeCertifiedBlock(...args));
|
|
1535
|
+
}
|
|
938
1536
|
: undefined,
|
|
939
1537
|
executeGeneratedSql: req.executeGeneratedSql
|
|
940
|
-
? async (...args) => {
|
|
1538
|
+
? async (...args) => {
|
|
1539
|
+
guardSnapshot();
|
|
1540
|
+
return executeAtSqlBoundary(() => req.executeGeneratedSql(...args));
|
|
1541
|
+
}
|
|
941
1542
|
: undefined,
|
|
942
1543
|
prepareExploratorySqlExecution: req.prepareExploratorySqlExecution
|
|
943
1544
|
? async (sql, ...args) => {
|
|
@@ -964,8 +1565,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
964
1565
|
executeAgenticGeneratedSql: req.executeAgenticGeneratedSql
|
|
965
1566
|
? async (capability, sql, artifact) => {
|
|
966
1567
|
guardSnapshot();
|
|
967
|
-
|
|
968
|
-
return req.executeAgenticGeneratedSql(capability, sql, artifact);
|
|
1568
|
+
return executeAtSqlBoundary(() => req.executeAgenticGeneratedSql(capability, sql, artifact));
|
|
969
1569
|
}
|
|
970
1570
|
: undefined,
|
|
971
1571
|
agenticExecutionScope: {
|
|
@@ -975,7 +1575,10 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
975
1575
|
targetFingerprint: req.generatedProposalTargetFingerprint,
|
|
976
1576
|
},
|
|
977
1577
|
executeDqlArtifact: req.executeDqlArtifact
|
|
978
|
-
? async (...args) => {
|
|
1578
|
+
? async (...args) => {
|
|
1579
|
+
guardSnapshot();
|
|
1580
|
+
return executeAtSqlBoundary(() => req.executeDqlArtifact(...args));
|
|
1581
|
+
}
|
|
979
1582
|
: undefined,
|
|
980
1583
|
expandGroundingContext: createGroundingContextExpander(req.projectRoot, req.probeNamedRelations),
|
|
981
1584
|
answerLoopTools,
|
|
@@ -1014,7 +1617,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
1014
1617
|
// NOTE: no captureGeneratedDraft here — a plain answer/research question must NOT
|
|
1015
1618
|
// auto-write a draft into the blocks space. A draft is created only when the user
|
|
1016
1619
|
// explicitly acts (the "Create DQL draft" action → the dql_block_draft route).
|
|
1017
|
-
};
|
|
1620
|
+
}, askTrace);
|
|
1018
1621
|
const result = await answerAgentic(answerLoopInput, {
|
|
1019
1622
|
policy: resolveOrchestratorPolicy({
|
|
1020
1623
|
config: readOrchestratorConfig(req.projectRoot),
|
|
@@ -1043,14 +1646,15 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
1043
1646
|
maxIterations: resolveOrchestratorPolicy({
|
|
1044
1647
|
config: readOrchestratorConfig(req.projectRoot),
|
|
1045
1648
|
}).maxIterations,
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
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.
|
|
1049
1653
|
maxProviderDispatches,
|
|
1050
1654
|
// Scaled by the same knob as every other agent deadline, so
|
|
1051
1655
|
// a local model that needs seconds per call is not planned
|
|
1052
1656
|
// out of existence by a budget calibrated for a hosted one.
|
|
1053
|
-
...(turnPlanningEnabled(req.projectRoot) ? {
|
|
1657
|
+
...(turnPlanningEnabled(req.projectRoot, req.orchestrationMode) ? {
|
|
1054
1658
|
planTurn: async (question, toolNames) => {
|
|
1055
1659
|
const plan = await planAnalystTurn(loopInput.provider, question, toolNames, {
|
|
1056
1660
|
timeoutMs: Math.round(2_500 * deadlineScale()),
|
|
@@ -1064,6 +1668,11 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
1064
1668
|
return plan;
|
|
1065
1669
|
},
|
|
1066
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)),
|
|
1067
1676
|
// The loop's trace, on the channel the provider already uses
|
|
1068
1677
|
// for progress. `thinking` turns become `onProgress`, which
|
|
1069
1678
|
// the run engine emits as `executor.started` over SSE — so
|
|
@@ -1527,9 +2136,13 @@ function extractSelectedBlockHints(req) {
|
|
|
1527
2136
|
}
|
|
1528
2137
|
}
|
|
1529
2138
|
function inferFollowUpContext(req, question) {
|
|
1530
|
-
// Messages-only fallback (no structured conversationContext).
|
|
1531
|
-
//
|
|
1532
|
-
|
|
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;
|
|
1533
2146
|
for (let i = req.messages.length - 1; i >= 0; i--) {
|
|
1534
2147
|
const msg = req.messages[i];
|
|
1535
2148
|
if (msg.role !== 'assistant')
|
|
@@ -1539,6 +2152,7 @@ function inferFollowUpContext(req, question) {
|
|
|
1539
2152
|
continue;
|
|
1540
2153
|
return {
|
|
1541
2154
|
kind,
|
|
2155
|
+
binding: 'prior_result',
|
|
1542
2156
|
sourceBlockName,
|
|
1543
2157
|
sourceAnswer: msg.content.slice(0, 1200),
|
|
1544
2158
|
filters: kind === 'drilldown' ? extractDrilldownFilters(question) : undefined,
|
|
@@ -1576,17 +2190,11 @@ function conversationSnapshotFromContext(context) {
|
|
|
1576
2190
|
function applyTopicShiftGuard(followUp, snapshot) {
|
|
1577
2191
|
if (!followUp || snapshot?.topicRelation !== 'shift')
|
|
1578
2192
|
return followUp;
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
filters: undefined,
|
|
1585
|
-
dimensions: undefined,
|
|
1586
|
-
priorResultValues: undefined,
|
|
1587
|
-
priorMeasures: undefined,
|
|
1588
|
-
priorLimit: undefined,
|
|
1589
|
-
};
|
|
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;
|
|
1590
2198
|
}
|
|
1591
2199
|
/**
|
|
1592
2200
|
* Resolve the prior-turn context a follow-up may build on.
|
|
@@ -1613,6 +2221,7 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
|
|
|
1613
2221
|
if (taskDependency) {
|
|
1614
2222
|
return {
|
|
1615
2223
|
kind: 'drilldown',
|
|
2224
|
+
binding: 'task_dependency',
|
|
1616
2225
|
sourceTurnId: `task:${taskDependency.sourceTaskId}`,
|
|
1617
2226
|
filters: [taskDependency.value],
|
|
1618
2227
|
dimensions: [taskDependency.canonicalColumn],
|
|
@@ -1666,22 +2275,24 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
|
|
|
1666
2275
|
const hasUsefulContext = Boolean(sourceBlockName || priorResultColumns?.length || focusedPriorResultValues || priorDqlArtifact);
|
|
1667
2276
|
if (!hasUsefulContext)
|
|
1668
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.
|
|
1669
2282
|
const inferredKind = resolvedReferences.memberBindings?.length
|
|
1670
2283
|
? 'drilldown'
|
|
1671
2284
|
: isGenericFollowUp(question)
|
|
1672
2285
|
? 'generic'
|
|
1673
2286
|
: isDrilldownFollowUp(question, priorShapeTerms(priorResultColumns, focusedPriorResultValues))
|
|
1674
2287
|
? 'drilldown'
|
|
1675
|
-
:
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
const kind = inferredKind
|
|
1681
|
-
?? (context.followupKind === 'generic' || context.followupKind === 'drilldown' ? context.followupKind : null)
|
|
1682
|
-
?? 'contextual';
|
|
2288
|
+
: undefined;
|
|
2289
|
+
if (!inferredKind)
|
|
2290
|
+
return undefined;
|
|
2291
|
+
const binding = 'prior_result';
|
|
2292
|
+
const kind = inferredKind;
|
|
1683
2293
|
return {
|
|
1684
2294
|
kind,
|
|
2295
|
+
binding,
|
|
1685
2296
|
sourceTurnId: cleanOptionalString(activeTurn?.id) ?? cleanOptionalString(context.sourceAnswerId),
|
|
1686
2297
|
// A relative comparison needs the named member from history, not the prior
|
|
1687
2298
|
// block's result contract. Carrying a beverage-ranking block into "less tax
|
|
@@ -2376,7 +2987,7 @@ function isGenericFollowUp(question) {
|
|
|
2376
2987
|
* Now it needs either a deictic reference to the prior result, or an explicit
|
|
2377
2988
|
* drill verb that only makes sense relative to something already on screen.
|
|
2378
2989
|
*/
|
|
2379
|
-
function isDrilldownFollowUp(question,
|
|
2990
|
+
function isDrilldownFollowUp(question, _priorTerms = []) {
|
|
2380
2991
|
const lower = question.toLowerCase();
|
|
2381
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)
|
|
2382
2993
|
|| /\b(?:they|their|them|he|she|him|his|her|hers)\b/.test(lower);
|
|
@@ -2385,15 +2996,18 @@ function isDrilldownFollowUp(question, priorTerms = []) {
|
|
|
2385
2996
|
// a short gap between them rather than adjacently.
|
|
2386
2997
|
const explicitDrillVerb = /\b(drills?|breakdowns?|slices?|segments?|splits?|why|drivers?|root cause|variance)\b/.test(lower)
|
|
2387
2998
|
|| /\bbreak\b.{0,16}\bdown\b/.test(lower);
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
//
|
|
2391
|
-
//
|
|
2392
|
-
// a
|
|
2393
|
-
const
|
|
2394
|
-
|
|
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)))
|
|
2395
3007
|
return false;
|
|
2396
|
-
|
|
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);
|
|
2397
3011
|
}
|
|
2398
3012
|
/**
|
|
2399
3013
|
* Lower-cased words from the prior result's columns and dimension keys, used to
|