@duckcodeailabs/dql-cli 1.14.0 → 1.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.d.ts +15 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +25 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{AgentLogPage-Ch7VK20X.js → AgentLogPage-DKbGpRQS.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-DBr5TmyM.js → AiBuildDialog-DPSu0Mly.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-jLPzQO7O.js → AiBuildResult-1uaGnpi1.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiSidePanel-CdlGsiVC.js → AiSidePanel-BwMREwa7.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AnalyticsHome-C0DXbOwY.js → AnalyticsHome-BGfey_ve.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AppsView-CcpwjApv.js → AppsView-CM1tPywy.js} +4 -4
- package/dist/assets/dql-notebook/assets/{BlockStudio-CFYxafw-.js → BlockStudio--S6WFVO4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-C0kLYg2p.js → BusinessArtifactView-BEAJ-yNW.js} +1 -1
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-CMwElXD_.js → DbtFirstModelingPage-CNyU5MBX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GitPage-JjhRDeWY.js → GitPage-IcydAai3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-DakE4NdR.js → GlobalAiRail-CSKW-5eD.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-BokDqG6a.js → GovernedContextPage-CFen0eFT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-CjOv6_gz.js → HelpDocsPage-D0D9UCIz.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-nGaNcwdw.js → HomePage-CmSxapR3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDAG-CO6CFJRg.js → LineageDAG-BGUcIt1B.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDetailView-BnGb7OF7.js → LineageDetailView-Dx48Zfzb.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-C5Y0Ht0b.js → LineageDrawer-zaBfSLZO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-Cgh3GR3F.js → LineagePathBreadcrumb-CTZJp4_r.js} +1 -1
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-Kla9PYuj.js → MiniLineageGraph-CdivNR1S.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-BR2SnmPT.js → NewBlockModal-DMBFB7nE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-BaCHMYeB.js → NewNotebookModal-DsC9CWlW.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-CBLqY8cE.js → NotebookEditor-hs-kw8v9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{ReadinessPage-CiN0IWSS.js → ReadinessPage-DJ83cMik.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-BhiCsYF-.js → SetupOnboarding-B1Pu_Bvv.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SkillsPage-CCSf8VMm.js → SkillsPage-BHKDay8n.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-BgQmFe_x.js → TrustBadge-BkyGgob2.js} +1 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-BYdrTaEW.js +89 -0
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-AeDYUDla.js → answer-to-notebook-DmNiuLQA.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-C55x2hq_.js → arrow-left--1rsrxm8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-C1cJrhOm.js → arrow-right-D5TdqY1G.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-CQf_sdv2.js → book-open-text-Bw7nHbzg.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-X8-Z2yLY.js → circle-x-DLe6NNM4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-BjjNYKyY.js → dagre.esm-CW5QZdBt.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-C2zrz5DH.js → external-link-C9Q97sA3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-qGV_PYGU.js → grip-vertical-CztvkIgo.js} +1 -1
- package/dist/assets/dql-notebook/assets/{index-ByTDPDaH.js → index-zHHzDn6l.js} +127 -127
- package/dist/assets/dql-notebook/assets/{link-2-VpyOxQXG.js → link-2-CiKAvumL.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-CH2Jhwms.js → list-tree-BtnP2nQ5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-B2TZJ8BT.js → minimize-2-TSFGxcCP.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-BunF88lt.js → panel-right-open-BfXIUWy0.js} +1 -1
- package/dist/assets/dql-notebook/assets/{play-DAFVF4_G.js → play-DVbSFJHD.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-DGCrrqtY.js → rotate-ccw-D_cesDcX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-Ci-9QL9F.js → semantic-fields-CoVStdYB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{sliders-horizontal-BOAlXXbn.js → sliders-horizontal-l7xV9K5A.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-CkksZXHt.js → star-CSBS0H3b.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-D3mjyJZE.js → triangle-alert-BTrnyY4q.js} +1 -1
- package/dist/assets/dql-notebook/assets/{upload-CTNOAVEO.js → upload-sySLq9zb.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DiQjc7x-.js → usePersistedAgentThreadId-CzwgGdus.js} +1 -1
- package/dist/assets/dql-notebook/assets/{user-round-BWd5tQRg.js → user-round-ChlgXi9j.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-CqsAv8P-.js → wand-sparkles-CGw0ytyT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-CSqsj-sC.js → workflow-C_RltiK5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-DWqzqlX8.js → wrench-D-wLfeu0.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-65M5rLCE.js → x-B28hIJIC.js} +1 -1
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/agent-eval-cassette.d.ts +164 -0
- package/dist/commands/agent-eval-cassette.d.ts.map +1 -0
- package/dist/commands/agent-eval-cassette.js +313 -0
- package/dist/commands/agent-eval-cassette.js.map +1 -0
- package/dist/commands/agent-eval-runtime.d.ts +109 -0
- package/dist/commands/agent-eval-runtime.d.ts.map +1 -0
- package/dist/commands/agent-eval-runtime.js +165 -0
- package/dist/commands/agent-eval-runtime.js.map +1 -0
- package/dist/commands/agent.d.ts +124 -4
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +410 -106
- 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/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 +4 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/analyst-loop-tools.d.ts +19 -0
- package/dist/llm/analyst-loop-tools.d.ts.map +1 -0
- package/dist/llm/analyst-loop-tools.js +56 -0
- package/dist/llm/analyst-loop-tools.js.map +1 -0
- package/dist/llm/providers/dql-agent-provider.d.ts +51 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +563 -14
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +47 -1
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +123 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +1437 -163
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/package.json +10 -10
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Bw5AXNDB.js +0 -88
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { classifyProviderFailure, deadlineScale, planAnalystTurn, rerankCandidates, } from '@duckcodeailabs/dql-agent';
|
|
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, scopeContextPackToExploratoryCandidateClosure, createAnalystLaneHandler, parseProposal, renderContextValidationRefusalForUser, validateSqlAgainstLocalContext, resolveOrchestratorPolicy, } from '@duckcodeailabs/dql-agent';
|
|
4
|
+
import { CassetteStore, evalCassetteCanonicalizationV2, resolveCassetteModeFromEnv, withCassette, } from '../../commands/agent-eval-cassette.js';
|
|
2
5
|
import { buildManifest, normalizeDqlArtifactReference, resolveDbtManifestPath } from '@duckcodeailabs/dql-core';
|
|
3
|
-
import {
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
4
9
|
import { buildAnswerLoopTools, createGroundingContextExpander } from '../answer-loop-tools.js';
|
|
5
10
|
import { getSemanticRuntimeStatus } from '../../semantic-runtime.js';
|
|
6
11
|
import { blockProposalDqlMetadata } from '../proposal-metadata.js';
|
|
@@ -71,6 +76,39 @@ const SPECS = {
|
|
|
71
76
|
},
|
|
72
77
|
},
|
|
73
78
|
};
|
|
79
|
+
/**
|
|
80
|
+
* Capture a content-free provider failure at the closest boundary. Local
|
|
81
|
+
* runtime may later choose a user-facing headline, but must not need raw
|
|
82
|
+
* provider errors or URLs to reconstruct the cause.
|
|
83
|
+
*/
|
|
84
|
+
function providerBoundaryDiagnostic(input) {
|
|
85
|
+
const config = getEffectiveProviderConfig(input.projectRoot, input.providerId);
|
|
86
|
+
const message = input.error instanceof Error ? input.error.message : String(input.error ?? 'provider readiness failed');
|
|
87
|
+
const code = input.code
|
|
88
|
+
?? (input.error && typeof input.error === 'object' ? String(input.error.code ?? '') : '');
|
|
89
|
+
const fingerprint = (value) => value?.trim()
|
|
90
|
+
? `sha256:${createHash('sha256').update(value.trim()).digest('hex')}`
|
|
91
|
+
: undefined;
|
|
92
|
+
let origin;
|
|
93
|
+
if (config.baseUrl) {
|
|
94
|
+
try {
|
|
95
|
+
origin = new URL(config.baseUrl).origin;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// The full malformed URL never leaves this function; its fingerprint is
|
|
99
|
+
// still a useful support correlation key without disclosing content.
|
|
100
|
+
origin = config.baseUrl;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return classifyProviderFailure({
|
|
104
|
+
code,
|
|
105
|
+
message,
|
|
106
|
+
phase: input.phase,
|
|
107
|
+
providerFingerprint: fingerprint(input.providerId),
|
|
108
|
+
modelFingerprint: fingerprint(config.model),
|
|
109
|
+
baseOriginFingerprint: fingerprint(origin),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
74
112
|
function createCertifiedFitConfirmation(provider, signal) {
|
|
75
113
|
return async ({ question, questionPlan, block, fit }) => {
|
|
76
114
|
const response = await provider.generate([
|
|
@@ -224,6 +262,40 @@ function stringArray(value) {
|
|
|
224
262
|
function stringValue(value) {
|
|
225
263
|
return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
|
|
226
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* A precomputed exploratory pack is an optimization/witness only. The runner
|
|
267
|
+
* independently derives the pack from the immutable router-selected IDs and
|
|
268
|
+
* accepts the witness only when its snapshot, source fingerprint, physical
|
|
269
|
+
* relations, and metadata object set are exactly the same. This prevents a
|
|
270
|
+
* caller from replacing a safe same-snapshot closure with a broader one.
|
|
271
|
+
*/
|
|
272
|
+
function exploratoryClosureMatches(derived, witness) {
|
|
273
|
+
if (!derived)
|
|
274
|
+
return false;
|
|
275
|
+
if (derived.knowledgeLens.snapshotId !== witness.knowledgeLens.snapshotId)
|
|
276
|
+
return false;
|
|
277
|
+
if (derived.freshness.fingerprint !== witness.freshness.fingerprint)
|
|
278
|
+
return false;
|
|
279
|
+
const normalize = (value) => value
|
|
280
|
+
.trim()
|
|
281
|
+
.split('.')
|
|
282
|
+
.map((part) => part.trim().replace(/^["`\[]|["`\]]$/g, '').toLowerCase())
|
|
283
|
+
.filter(Boolean)
|
|
284
|
+
.join('.');
|
|
285
|
+
const sameSet = (left, right) => {
|
|
286
|
+
if (left.length !== right.length)
|
|
287
|
+
return false;
|
|
288
|
+
const rightSet = new Set(right);
|
|
289
|
+
return left.every((value) => rightSet.has(value));
|
|
290
|
+
};
|
|
291
|
+
const derivedRelations = [...new Set(derived.allowedSqlContext.relations.map((relation) => normalize(relation.relation)))].sort();
|
|
292
|
+
const witnessRelations = [...new Set(witness.allowedSqlContext.relations.map((relation) => normalize(relation.relation)))].sort();
|
|
293
|
+
if (!sameSet(derivedRelations, witnessRelations))
|
|
294
|
+
return false;
|
|
295
|
+
const derivedObjects = [...new Set(derived.objects.map((object) => object.objectKey))].sort();
|
|
296
|
+
const witnessObjects = [...new Set(witness.objects.map((object) => object.objectKey))].sort();
|
|
297
|
+
return sameSet(derivedObjects, witnessObjects);
|
|
298
|
+
}
|
|
227
299
|
function truncateForFitPrompt(value, max) {
|
|
228
300
|
if (!value)
|
|
229
301
|
return undefined;
|
|
@@ -257,12 +329,174 @@ function emitProposalFromText(text, emit) {
|
|
|
257
329
|
// Ignore malformed proposal text. The visible assistant response still streams as text.
|
|
258
330
|
}
|
|
259
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Route the runtime's own provider through an eval cassette when the host asks.
|
|
334
|
+
*
|
|
335
|
+
* The client-side cassette in `dql agent eval` only covers `--via loop`: with
|
|
336
|
+
* `--via runtime` the SERVER owns the provider, so without this hook the one
|
|
337
|
+
* driver that actually exercises routing and gates could never be made
|
|
338
|
+
* deterministic — and a suite that cannot be deterministic cannot gate a PR.
|
|
339
|
+
*
|
|
340
|
+
* Opt-in through the environment, never through a request field: a caller must
|
|
341
|
+
* not be able to redirect a production run onto recorded responses.
|
|
342
|
+
*/
|
|
343
|
+
/**
|
|
344
|
+
* Read `agent.orchestrator` straight from dql.config.json.
|
|
345
|
+
*
|
|
346
|
+
* Deliberately NOT `loadProjectConfig` from local-runtime: that module imports
|
|
347
|
+
* this one (local-runtime.ts:164), so reaching back would close an import cycle
|
|
348
|
+
* through a 34k-line file. A few lines of JSON reading is the cheaper trade.
|
|
349
|
+
*
|
|
350
|
+
* Cached by a lightweight file fingerprint, not forever. Settings writes must
|
|
351
|
+
* take effect for the next Ask without requiring a notebook-server restart.
|
|
352
|
+
* A malformed file resolves to `null`, which `resolveOrchestratorPolicy` turns
|
|
353
|
+
* into `legacy` — a broken config must not route real questions onto an
|
|
354
|
+
* unproven path.
|
|
355
|
+
*/
|
|
356
|
+
const agentConfigCache = new Map();
|
|
357
|
+
function agentConfigFingerprint(projectRoot) {
|
|
358
|
+
try {
|
|
359
|
+
const stats = statSync(join(projectRoot, 'dql.config.json'));
|
|
360
|
+
return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}`;
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return 'missing';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function readAgentConfig(projectRoot) {
|
|
367
|
+
const fingerprint = agentConfigFingerprint(projectRoot);
|
|
368
|
+
const cached = agentConfigCache.get(projectRoot);
|
|
369
|
+
if (cached && cached.fingerprint === fingerprint)
|
|
370
|
+
return cached.value;
|
|
371
|
+
let resolved = null;
|
|
372
|
+
try {
|
|
373
|
+
const raw = readFileSync(join(projectRoot, 'dql.config.json'), 'utf-8');
|
|
374
|
+
const parsed = JSON.parse(raw);
|
|
375
|
+
resolved = parsed?.agent && typeof parsed.agent === 'object'
|
|
376
|
+
? parsed.agent
|
|
377
|
+
: null;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
resolved = null;
|
|
381
|
+
}
|
|
382
|
+
agentConfigCache.set(projectRoot, { fingerprint, value: resolved });
|
|
383
|
+
return resolved;
|
|
384
|
+
}
|
|
385
|
+
function readOrchestratorConfig(projectRoot) {
|
|
386
|
+
const orchestrator = readAgentConfig(projectRoot)?.orchestrator;
|
|
387
|
+
return orchestrator && typeof orchestrator === 'object' ? orchestrator : null;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Is on-demand value lookup permitted for this project?
|
|
391
|
+
*
|
|
392
|
+
* Mirrors `resolveAgentRuntimeValueGrounding`, read locally rather than imported
|
|
393
|
+
* — local-runtime imports this module, so reaching back would close a cycle
|
|
394
|
+
* through a 34k-line file. Anything other than an explicit `safe_automatic`
|
|
395
|
+
* resolves to disabled: value lookup touches warehouse cell values, so a
|
|
396
|
+
* malformed setting must fail closed.
|
|
397
|
+
*/
|
|
398
|
+
/**
|
|
399
|
+
* Structured turn planning is OFF by default.
|
|
400
|
+
*
|
|
401
|
+
* It costs one provider dispatch before the loop makes any tool call, and on a
|
|
402
|
+
* slow provider that dispatch can consume enough of the discovery window that
|
|
403
|
+
* the loop is then refused admission and falls back to the legacy path —
|
|
404
|
+
* measured on ollama, where all three agentic dispatches planned successfully
|
|
405
|
+
* and then died at "soft target elapsed before this provider dispatch could
|
|
406
|
+
* 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. Opt in with
|
|
408
|
+
* `agent.orchestrator.turnPlanning: true` once streaming lands.
|
|
409
|
+
*/
|
|
410
|
+
function turnPlanningEnabled(projectRoot) {
|
|
411
|
+
const orchestrator = readAgentConfig(projectRoot)?.orchestrator;
|
|
412
|
+
if (!orchestrator || typeof orchestrator !== 'object')
|
|
413
|
+
return false;
|
|
414
|
+
return orchestrator.turnPlanning === true;
|
|
415
|
+
}
|
|
416
|
+
function valueLookupEnabled(projectRoot) {
|
|
417
|
+
const grounding = readAgentConfig(projectRoot)?.runtimeValueGrounding;
|
|
418
|
+
if (!grounding || typeof grounding !== 'object')
|
|
419
|
+
return false;
|
|
420
|
+
return grounding.mode === 'safe_automatic';
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Which migration lane this turn belongs to.
|
|
424
|
+
*
|
|
425
|
+
* Research is identified by the server-resolved `orchestrationMode`, not by
|
|
426
|
+
* `analysisDepth`. A reader can ask Ask AI to think deeply without opting into
|
|
427
|
+
* the Research workflow, its wider dispatch budget, or any row-bearing tools.
|
|
428
|
+
*
|
|
429
|
+
* Deliberately coarse: the seam only needs to know which bucket a turn falls in
|
|
430
|
+
* so a lane can be enabled independently. The fine-grained triage between
|
|
431
|
+
* certified, semantic, and generated stays inside the answer path, where the
|
|
432
|
+
* retrieval evidence lives.
|
|
433
|
+
*/
|
|
434
|
+
function agenticLaneForRequest(req) {
|
|
435
|
+
return req.orchestrationMode === 'research' ? 'research' : 'generated';
|
|
436
|
+
}
|
|
437
|
+
export function applyEvalCassette(provider, projectRoot) {
|
|
438
|
+
const dir = process.env.DQL_EVAL_CASSETTE_DIR;
|
|
439
|
+
if (!dir)
|
|
440
|
+
return provider;
|
|
441
|
+
return withCassette(provider, new CassetteStore(dir), resolveCassetteModeFromEnv(process.env), evalCassetteCanonicalizationV2(projectRoot));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Create a replay-only provider when the runtime is launched for an offline
|
|
445
|
+
* evaluation. This is intentionally unavailable outside explicit cassette
|
|
446
|
+
* replay: recording and live modes still require a configured real provider.
|
|
447
|
+
*
|
|
448
|
+
* The cassette's recorded provider identity is part of its key. Recover it
|
|
449
|
+
* from a single-provider cassette directory instead of borrowing a user's
|
|
450
|
+
* active provider or guessing from an API setting. The base provider cannot
|
|
451
|
+
* make a network call; replay misses remain CassetteMissError failures.
|
|
452
|
+
*/
|
|
453
|
+
export function createEvalCassetteReplayProvider(projectRoot) {
|
|
454
|
+
const dir = process.env.DQL_EVAL_CASSETTE_DIR;
|
|
455
|
+
if (!dir || resolveCassetteModeFromEnv(process.env) !== 'replay')
|
|
456
|
+
return undefined;
|
|
457
|
+
const store = new CassetteStore(dir);
|
|
458
|
+
const providerNames = store.providerNames();
|
|
459
|
+
const providerName = providerNames.length === 1 ? asAgentProviderName(providerNames[0]) : undefined;
|
|
460
|
+
if (!providerName)
|
|
461
|
+
return undefined;
|
|
462
|
+
return withCassette({
|
|
463
|
+
name: providerName,
|
|
464
|
+
available: async () => true,
|
|
465
|
+
generate: async () => {
|
|
466
|
+
throw new Error('Eval cassette replay miss: no live provider is available.');
|
|
467
|
+
},
|
|
468
|
+
}, store, 'replay', evalCassetteCanonicalizationV2(projectRoot));
|
|
469
|
+
}
|
|
470
|
+
function asAgentProviderName(value) {
|
|
471
|
+
return value === 'claude' || value === 'openai' || value === 'gemini' || value === 'ollama'
|
|
472
|
+
? value
|
|
473
|
+
: undefined;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* A raw text provider for planning calls that are not the answer itself.
|
|
477
|
+
*
|
|
478
|
+
* Research hypothesis planning needs `generate`, not the full agent runner —
|
|
479
|
+
* and the runner cannot be reused for it, because the runner IS the governed
|
|
480
|
+
* answer path. Cassettes apply, so a recorded run stays hermetic.
|
|
481
|
+
*/
|
|
482
|
+
export function createGovernedTextProvider(id, projectRoot) {
|
|
483
|
+
const spec = SPECS[id];
|
|
484
|
+
if (!spec)
|
|
485
|
+
return undefined;
|
|
486
|
+
try {
|
|
487
|
+
return applyEvalCassette(spec.create(projectRoot), projectRoot);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
260
493
|
export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
261
494
|
return {
|
|
262
495
|
async run(req, emit, signal) {
|
|
263
496
|
const spec = SPECS[id];
|
|
264
|
-
const rawProvider = providerOverride ?? spec.create(req.projectRoot);
|
|
265
|
-
const isResearch = req.
|
|
497
|
+
const rawProvider = applyEvalCassette(providerOverride ?? spec.create(req.projectRoot), req.projectRoot);
|
|
498
|
+
const isResearch = req.orchestrationMode === 'research';
|
|
499
|
+
const maxProviderDispatches = isResearch ? 8 : 4;
|
|
266
500
|
const researchRowsOptIn = isResearch && req.researchResultRowsOptIn === true;
|
|
267
501
|
const sharedDispatchEvidence = req.providerDispatchEvidenceSink;
|
|
268
502
|
let providerRoundTrips = 0;
|
|
@@ -296,7 +530,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
296
530
|
// A tool-calling loop capped at two physical sends can make exactly one
|
|
297
531
|
// tool call before it must answer, which is not enough to look something
|
|
298
532
|
// up and then use it. The run-scoped ledger is the real guardrail.
|
|
299
|
-
maxProviderDispatches
|
|
533
|
+
maxProviderDispatches,
|
|
300
534
|
...(sharedDispatchEvidence?.mayStartToolCall
|
|
301
535
|
? { mayStartToolCall: () => sharedDispatchEvidence.mayStartToolCall() }
|
|
302
536
|
: {}),
|
|
@@ -360,15 +594,41 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
360
594
|
return envelope;
|
|
361
595
|
},
|
|
362
596
|
});
|
|
597
|
+
// One retry only, on the SAME configured provider and only for failures
|
|
598
|
+
// that are normally transient. The physical-dispatch observer remains in
|
|
599
|
+
// the path for both attempts, so a retry cannot exceed the run-wide
|
|
600
|
+
// dispatch budget or silently fail over to another provider.
|
|
601
|
+
let transientRetryUsed = false;
|
|
602
|
+
const mayRetrySameProvider = (error) => {
|
|
603
|
+
if (signal.aborted || transientRetryUsed)
|
|
604
|
+
return false;
|
|
605
|
+
const code = error && typeof error === 'object' ? String(error.code ?? '') : '';
|
|
606
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
607
|
+
const diagnostic = `${code} ${message}`;
|
|
608
|
+
return !/PROVIDER_DISPATCH_BUDGET|RUN_SOFT_TARGET|RUN_DEADLINE|PROJECT_SNAPSHOT|abort|cancel/i.test(diagnostic)
|
|
609
|
+
&& /(?:429|rate limit|too many requests|502|503|504|gateway|econn|network|fetch failed|connection refused|timeout|timed out)/i.test(diagnostic);
|
|
610
|
+
};
|
|
611
|
+
const retrySameProviderOnce = async (operation) => {
|
|
612
|
+
try {
|
|
613
|
+
return await operation();
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
if (!mayRetrySameProvider(error))
|
|
617
|
+
throw error;
|
|
618
|
+
transientRetryUsed = true;
|
|
619
|
+
emit({ kind: 'thinking', text: 'The configured AI provider had a transient error; retrying it once within this run budget.' });
|
|
620
|
+
return operation();
|
|
621
|
+
}
|
|
622
|
+
};
|
|
363
623
|
const provider = {
|
|
364
624
|
name: rawProvider.name,
|
|
365
625
|
available: () => rawProvider.available(),
|
|
366
626
|
generate: (...args) => {
|
|
367
|
-
return rawProvider.generate(args[0], withPhysicalDispatchObserver(args[1]));
|
|
627
|
+
return retrySameProviderOnce(() => rawProvider.generate(args[0], withPhysicalDispatchObserver(args[1])));
|
|
368
628
|
},
|
|
369
629
|
...(rawProvider.generateWithTools ? {
|
|
370
630
|
generateWithTools: (...args) => {
|
|
371
|
-
return rawProvider.generateWithTools(args[0], args[1], withPhysicalDispatchObserver(args[2]));
|
|
631
|
+
return retrySameProviderOnce(() => rawProvider.generateWithTools(args[0], args[1], withPhysicalDispatchObserver(args[2])));
|
|
372
632
|
},
|
|
373
633
|
} : {}),
|
|
374
634
|
...(rawProvider.generateStream ? {
|
|
@@ -379,7 +639,20 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
379
639
|
};
|
|
380
640
|
const available = await provider.available().catch(() => false);
|
|
381
641
|
if (!available) {
|
|
382
|
-
|
|
642
|
+
const message = `${spec.label} is not configured or reachable. ${spec.setup}`;
|
|
643
|
+
emit({
|
|
644
|
+
kind: 'error',
|
|
645
|
+
message,
|
|
646
|
+
providerDiagnostic: providerBoundaryDiagnostic({
|
|
647
|
+
providerId: id,
|
|
648
|
+
projectRoot: req.projectRoot,
|
|
649
|
+
phase: 'preflight',
|
|
650
|
+
error: message,
|
|
651
|
+
// Local Ollama readiness is a reachability concern; configured
|
|
652
|
+
// remote providers missing credentials are authentication issues.
|
|
653
|
+
code: id === 'ollama' ? 'NETWORK_FAILURE' : 'AUTHENTICATION_FAILED',
|
|
654
|
+
}),
|
|
655
|
+
});
|
|
383
656
|
return;
|
|
384
657
|
}
|
|
385
658
|
try {
|
|
@@ -439,6 +712,14 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
439
712
|
strictness: contextBudget.strictness,
|
|
440
713
|
limit: contextBudget.limit,
|
|
441
714
|
confirmCertifiedFit: createCertifiedFitConfirmation(provider, signal),
|
|
715
|
+
// A cross-encoder pass over the fused candidates. Advisory: it may
|
|
716
|
+
// only reorder what retrieval returned, and a failure or timeout
|
|
717
|
+
// leaves retrieval's ordering untouched — so it can improve the
|
|
718
|
+
// pack and cannot break it.
|
|
719
|
+
rerankCandidates: (rerankQuestion, candidates) => rerankCandidates(provider, rerankQuestion, candidates, {
|
|
720
|
+
...(signal ? { signal } : {}),
|
|
721
|
+
timeoutMs: Math.round(2_500 * deadlineScale()),
|
|
722
|
+
}),
|
|
442
723
|
// Conversation-aware reuse: same-topic follow-ups seed (or, for
|
|
443
724
|
// filter-only refinements, re-stamp) the prior turn's context pack.
|
|
444
725
|
priorContextPackId: priorContextPackIdFromSnapshot(conversationSnapshot),
|
|
@@ -502,6 +783,63 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
502
783
|
const schemaContext = req.getSchemaContext && shouldLoadSchemaContext(contextPack, Boolean(semanticLayer))
|
|
503
784
|
? await req.getSchemaContext(question, contextPack).catch(() => [])
|
|
504
785
|
: [];
|
|
786
|
+
// The router's exploratory candidate IDs are server-owned execution
|
|
787
|
+
// authority. Once that tier is selected, provider prompt/schema
|
|
788
|
+
// context must be the candidate closure rather than the broad
|
|
789
|
+
// retrieval pack. The latter remains available to the host for
|
|
790
|
+
// receipts only and cannot be used to introduce another relation.
|
|
791
|
+
const forcedExploratoryTier = req.selectedCascadeTier === 'exploratory_sql';
|
|
792
|
+
// Re-derive the closure from the broad immutable pack and the
|
|
793
|
+
// router-selected IDs. `preparedExploratoryContextPack` is only a
|
|
794
|
+
// server-side consistency witness; it cannot override or widen the
|
|
795
|
+
// derivation even if a future caller constructs an AgentRunner
|
|
796
|
+
// request directly.
|
|
797
|
+
const derivedExploratoryContextPack = forcedExploratoryTier
|
|
798
|
+
? scopeContextPackToExploratoryCandidateClosure(contextPack, req.exploratoryCandidateIds)
|
|
799
|
+
: undefined;
|
|
800
|
+
const closureWitnessMatches = !req.preparedExploratoryContextPack
|
|
801
|
+
|| exploratoryClosureMatches(derivedExploratoryContextPack, req.preparedExploratoryContextPack);
|
|
802
|
+
const exploratoryContextPack = closureWitnessMatches
|
|
803
|
+
? derivedExploratoryContextPack
|
|
804
|
+
: undefined;
|
|
805
|
+
if (forcedExploratoryTier && (!exploratoryContextPack || !req.exploratoryCandidateIds?.length)) {
|
|
806
|
+
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.';
|
|
807
|
+
emit({
|
|
808
|
+
kind: 'tool_result',
|
|
809
|
+
id: 'governed_answer',
|
|
810
|
+
output: {
|
|
811
|
+
kind: 'no_answer',
|
|
812
|
+
sourceTier: 'no_answer',
|
|
813
|
+
certification: 'analyst_review_required',
|
|
814
|
+
reviewStatus: 'none',
|
|
815
|
+
confidence: 0,
|
|
816
|
+
text,
|
|
817
|
+
answer: text,
|
|
818
|
+
refusalCode: 'grounding_gap',
|
|
819
|
+
refusalDetails: {
|
|
820
|
+
code: closureWitnessMatches ? 'EXPLORATORY_CLOSURE_UNAVAILABLE' : 'EXPLORATORY_CLOSURE_MISMATCH',
|
|
821
|
+
message: text,
|
|
822
|
+
},
|
|
823
|
+
contextPack,
|
|
824
|
+
providerUsed: provider.name,
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const answerContextPack = forcedExploratoryTier
|
|
830
|
+
? exploratoryContextPack ?? contextPack
|
|
831
|
+
: contextPack;
|
|
832
|
+
const normalizeQualifiedRelation = (value) => value
|
|
833
|
+
.trim()
|
|
834
|
+
.split('.')
|
|
835
|
+
.map((part) => part.trim().replace(/^["`\[]|["`\]]$/g, '').toLowerCase())
|
|
836
|
+
.filter(Boolean)
|
|
837
|
+
.join('.');
|
|
838
|
+
const closureRelations = new Set((exploratoryContextPack?.allowedSqlContext.relations ?? [])
|
|
839
|
+
.map((relation) => normalizeQualifiedRelation(relation.relation)));
|
|
840
|
+
const answerSchemaContext = forcedExploratoryTier && closureRelations.size > 0
|
|
841
|
+
? schemaContext.filter((table) => closureRelations.has(normalizeQualifiedRelation(table.relation)))
|
|
842
|
+
: schemaContext;
|
|
505
843
|
const schemaDurationMs = Date.now() - schemaStartedAt;
|
|
506
844
|
const selectedBlockHints = shouldUseSelectedBlockHint(req, question, followUp)
|
|
507
845
|
? extractSelectedBlockHints(req)
|
|
@@ -524,8 +862,8 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
524
862
|
question,
|
|
525
863
|
...(conversationSnapshot ? { conversationSnapshot } : {}),
|
|
526
864
|
...(memoryContext ? { memoryContext } : {}),
|
|
527
|
-
schemaContext: prepareServerOwnedProviderSchemaContext(
|
|
528
|
-
...(
|
|
865
|
+
schemaContext: prepareServerOwnedProviderSchemaContext(answerSchemaContext),
|
|
866
|
+
...(answerContextPack ? { contextPack: answerContextPack } : {}),
|
|
529
867
|
skills,
|
|
530
868
|
...(followUp ? { followUp } : {}),
|
|
531
869
|
}), {
|
|
@@ -533,11 +871,23 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
533
871
|
maxResultRows: 0,
|
|
534
872
|
purpose: 'answer_generation',
|
|
535
873
|
});
|
|
536
|
-
|
|
874
|
+
// The strangler seam. `answerAgentic` and `answer` are interchangeable
|
|
875
|
+
// here; which runs is a per-lane config decision that defaults to
|
|
876
|
+
// legacy, so this is a no-op until a lane is explicitly enabled.
|
|
877
|
+
const answerLoopInput = {
|
|
537
878
|
question,
|
|
538
879
|
...(req.resolvedAnalyticalPlan
|
|
539
880
|
? { resolvedAnalyticalPlan: req.resolvedAnalyticalPlan }
|
|
540
881
|
: {}),
|
|
882
|
+
...(req.selectedCascadeTier
|
|
883
|
+
? { selectedCascadeTier: req.selectedCascadeTier }
|
|
884
|
+
: {}),
|
|
885
|
+
...(req.exploratoryCandidateIds?.length
|
|
886
|
+
? { exploratoryCandidateIds: [...req.exploratoryCandidateIds] }
|
|
887
|
+
: {}),
|
|
888
|
+
...(req.generatedProposalTargetFingerprint
|
|
889
|
+
? { generatedProposalTargetFingerprint: req.generatedProposalTargetFingerprint }
|
|
890
|
+
: {}),
|
|
541
891
|
...(req.analyticalReferenceInstant
|
|
542
892
|
? { analyticalReferenceInstant: req.analyticalReferenceInstant }
|
|
543
893
|
: {}),
|
|
@@ -555,7 +905,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
555
905
|
followUp,
|
|
556
906
|
conversationSnapshot,
|
|
557
907
|
memoryContext,
|
|
558
|
-
schemaContext,
|
|
908
|
+
schemaContext: answerSchemaContext,
|
|
559
909
|
semanticLayer,
|
|
560
910
|
// Runtime-aware executability for metric SELECTION: with a full
|
|
561
911
|
// semantic runtime active (dbt Cloud / MetricFlow CLI) every
|
|
@@ -567,7 +917,13 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
567
917
|
canExecuteSemanticMetric: (metricName) => semanticRuntimeActive !== 'native' || semanticLayer.canComposeMetric(metricName),
|
|
568
918
|
}
|
|
569
919
|
: {}),
|
|
570
|
-
contextPack,
|
|
920
|
+
contextPack: answerContextPack,
|
|
921
|
+
// The project's configured embedder (dql.config.json ai.embeddings).
|
|
922
|
+
// Without it, matchSemanticMetric falls back to the offline hashed
|
|
923
|
+
// provider, whose vectors can never ground a match on similarity
|
|
924
|
+
// alone — so a metric named only by synonym or acronym is invisible
|
|
925
|
+
// and the router dead-ends on a bare-ranking clarification.
|
|
926
|
+
embeddingProvider: projectEmbeddingProvider(req.projectRoot),
|
|
571
927
|
signal,
|
|
572
928
|
reasoningEffort: req.reasoningEffort,
|
|
573
929
|
analysisDepth: contextBudget.analysisDepth,
|
|
@@ -583,6 +939,41 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
583
939
|
executeGeneratedSql: req.executeGeneratedSql
|
|
584
940
|
? async (...args) => { guardSnapshot(); sqlExecutions += 1; return req.executeGeneratedSql(...args); }
|
|
585
941
|
: undefined,
|
|
942
|
+
prepareExploratorySqlExecution: req.prepareExploratorySqlExecution
|
|
943
|
+
? async (sql, ...args) => {
|
|
944
|
+
guardSnapshot();
|
|
945
|
+
// The execution host repeats this validation immediately
|
|
946
|
+
// before capability minting. Keep the same exact-qualified
|
|
947
|
+
// closure check here as well, before the provider runner can
|
|
948
|
+
// even invoke that host boundary. A provider response cannot
|
|
949
|
+
// use another same-snapshot relation merely because it was
|
|
950
|
+
// present in broad retrieval diagnostics.
|
|
951
|
+
if (forcedExploratoryTier && exploratoryContextPack) {
|
|
952
|
+
const proposalValidation = validateSqlAgainstLocalContext(sql, exploratoryContextPack, {
|
|
953
|
+
runtimeSchema: answerSchemaContext,
|
|
954
|
+
});
|
|
955
|
+
const outsideClosure = !proposalValidation.ok
|
|
956
|
+
|| proposalValidation.referencedRelations.some((relation) => !closureRelations.has(normalizeQualifiedRelation(relation)));
|
|
957
|
+
if (outsideClosure) {
|
|
958
|
+
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' });
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return req.prepareExploratorySqlExecution(sql, ...args);
|
|
962
|
+
}
|
|
963
|
+
: undefined,
|
|
964
|
+
executeAgenticGeneratedSql: req.executeAgenticGeneratedSql
|
|
965
|
+
? async (capability, sql, artifact) => {
|
|
966
|
+
guardSnapshot();
|
|
967
|
+
sqlExecutions += 1;
|
|
968
|
+
return req.executeAgenticGeneratedSql(capability, sql, artifact);
|
|
969
|
+
}
|
|
970
|
+
: undefined,
|
|
971
|
+
agenticExecutionScope: {
|
|
972
|
+
runId: req.agentRunId,
|
|
973
|
+
snapshotId: req.projectSnapshot?.snapshotId,
|
|
974
|
+
planId: req.resolvedAnalyticalPlan?.planId,
|
|
975
|
+
targetFingerprint: req.generatedProposalTargetFingerprint,
|
|
976
|
+
},
|
|
586
977
|
executeDqlArtifact: req.executeDqlArtifact
|
|
587
978
|
? async (...args) => { guardSnapshot(); sqlExecutions += 1; return req.executeDqlArtifact(...args); }
|
|
588
979
|
: undefined,
|
|
@@ -623,6 +1014,111 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
623
1014
|
// NOTE: no captureGeneratedDraft here — a plain answer/research question must NOT
|
|
624
1015
|
// auto-write a draft into the blocks space. A draft is created only when the user
|
|
625
1016
|
// explicitly acts (the "Create DQL draft" action → the dql_block_draft route).
|
|
1017
|
+
};
|
|
1018
|
+
const result = await answerAgentic(answerLoopInput, {
|
|
1019
|
+
policy: resolveOrchestratorPolicy({
|
|
1020
|
+
config: readOrchestratorConfig(req.projectRoot),
|
|
1021
|
+
}),
|
|
1022
|
+
lane: agenticLaneForRequest(req),
|
|
1023
|
+
legacy: answer,
|
|
1024
|
+
// The generated lane runs the analyst loop: it verifies every
|
|
1025
|
+
// identifier against a tool observation before the SQL is executed.
|
|
1026
|
+
// Certified and semantic lanes are deliberately NOT registered —
|
|
1027
|
+
// their answers already come from a governed contract, so a
|
|
1028
|
+
// verification pass would add latency and no safety.
|
|
1029
|
+
handlers: {
|
|
1030
|
+
generated: createAnalystLaneHandler({
|
|
1031
|
+
legacy: answer,
|
|
1032
|
+
buildDeps: (loopInput) => {
|
|
1033
|
+
const execute = loopInput.executeGeneratedSql;
|
|
1034
|
+
const valuesEnabled = valueLookupEnabled(req.projectRoot);
|
|
1035
|
+
const tools = buildAnalystLoopTools(loopInput, { valuesEnabled });
|
|
1036
|
+
if (process.env.DQL_ORCHESTRATOR_TRACE) {
|
|
1037
|
+
console.warn(`[dql] analyst loop deps: tools=${tools.length} preview=${Boolean(execute)} values=${valuesEnabled}`);
|
|
1038
|
+
}
|
|
1039
|
+
if (tools.length === 0)
|
|
1040
|
+
return undefined;
|
|
1041
|
+
return {
|
|
1042
|
+
tools,
|
|
1043
|
+
maxIterations: resolveOrchestratorPolicy({
|
|
1044
|
+
config: readOrchestratorConfig(req.projectRoot),
|
|
1045
|
+
}).maxIterations,
|
|
1046
|
+
// Match the physical wrapper cap and reserve the final
|
|
1047
|
+
// composition dispatch: an ordinary cap of four permits
|
|
1048
|
+
// at most three text-protocol tool observations.
|
|
1049
|
+
maxProviderDispatches,
|
|
1050
|
+
// Scaled by the same knob as every other agent deadline, so
|
|
1051
|
+
// a local model that needs seconds per call is not planned
|
|
1052
|
+
// out of existence by a budget calibrated for a hosted one.
|
|
1053
|
+
...(turnPlanningEnabled(req.projectRoot) ? {
|
|
1054
|
+
planTurn: async (question, toolNames) => {
|
|
1055
|
+
const plan = await planAnalystTurn(loopInput.provider, question, toolNames, {
|
|
1056
|
+
timeoutMs: Math.round(2_500 * deadlineScale()),
|
|
1057
|
+
...(loopInput.signal ? { signal: loopInput.signal } : {}),
|
|
1058
|
+
});
|
|
1059
|
+
if (process.env.DQL_ORCHESTRATOR_TRACE) {
|
|
1060
|
+
console.warn(plan
|
|
1061
|
+
? `[dql] analyst turn plan: "${plan.restatement}" establish=${plan.mustEstablish.length} opening=${plan.openingTool ?? 'unset'}`
|
|
1062
|
+
: '[dql] analyst turn plan: unavailable (loop proceeds unplanned)');
|
|
1063
|
+
}
|
|
1064
|
+
return plan;
|
|
1065
|
+
},
|
|
1066
|
+
} : {}),
|
|
1067
|
+
// The loop's trace, on the channel the provider already uses
|
|
1068
|
+
// for progress. `thinking` turns become `onProgress`, which
|
|
1069
|
+
// the run engine emits as `executor.started` over SSE — so
|
|
1070
|
+
// this needs no new event type, no contract change, and no
|
|
1071
|
+
// UI work. Without it the loop does real work (tool calls,
|
|
1072
|
+
// identifier checks, a bounded repair) behind a silent
|
|
1073
|
+
// spinner, which reads as a hang rather than as an analyst
|
|
1074
|
+
// establishing facts.
|
|
1075
|
+
onStep: (step) => {
|
|
1076
|
+
emit({
|
|
1077
|
+
kind: 'thinking',
|
|
1078
|
+
text: step.detail ? `${step.label} — ${step.detail}` : step.label,
|
|
1079
|
+
});
|
|
1080
|
+
},
|
|
1081
|
+
// Reuse the legacy parser and validator rather than forking
|
|
1082
|
+
// a second SQL front end that would drift from the first.
|
|
1083
|
+
parseSql: (raw) => parseProposal(raw).sql,
|
|
1084
|
+
extractReferences: (sql) => {
|
|
1085
|
+
const validation = validateSqlAgainstLocalContext(sql, loopInput.contextPack);
|
|
1086
|
+
const qualified = qualifyAuthorizationReferences(sql, {
|
|
1087
|
+
relations: validation.referencedRelations ?? [],
|
|
1088
|
+
columns: validation.referencedColumns ?? [],
|
|
1089
|
+
});
|
|
1090
|
+
return {
|
|
1091
|
+
relations: validation.referencedRelations ?? [],
|
|
1092
|
+
columns: qualified.filter((reference) => !validation.referencedRelations?.includes(reference)),
|
|
1093
|
+
};
|
|
1094
|
+
},
|
|
1095
|
+
// The safety verifiers keep their logic; only what happens on
|
|
1096
|
+
// failure changes. Instead of ending the turn, the specific
|
|
1097
|
+
// check that fired comes back as something the model can act
|
|
1098
|
+
// on — "joining those tables multiplies rows" rather than
|
|
1099
|
+
// "nothing was executed".
|
|
1100
|
+
verifySql: (sql) => {
|
|
1101
|
+
const validation = validateSqlAgainstLocalContext(sql, loopInput.contextPack);
|
|
1102
|
+
if (validation.ok)
|
|
1103
|
+
return undefined;
|
|
1104
|
+
return renderContextValidationRefusalForUser(validation.code, validation.error, loopInput.followUp?.memberBindings, validation.aggregationSafetyProof?.issueCodes);
|
|
1105
|
+
},
|
|
1106
|
+
};
|
|
1107
|
+
},
|
|
1108
|
+
}),
|
|
1109
|
+
},
|
|
1110
|
+
onDiagnostic: (event) => {
|
|
1111
|
+
if (event.kind === 'fallback') {
|
|
1112
|
+
console.warn(`[dql] agentic orchestrator fell back on ${event.lane}: ${event.reason}`);
|
|
1113
|
+
}
|
|
1114
|
+
else if (process.env.DQL_ORCHESTRATOR_TRACE) {
|
|
1115
|
+
// Opt-in dispatch trace. A migration where the new path silently
|
|
1116
|
+
// never runs looks identical to one where it runs and agrees,
|
|
1117
|
+
// and that ambiguity costs more to debug than the log costs to
|
|
1118
|
+
// carry.
|
|
1119
|
+
console.warn(`[dql] agentic orchestrator dispatched lane=${event.lane} mode=${event.mode}`);
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
626
1122
|
});
|
|
627
1123
|
const answerDurationMs = Date.now() - answerStartedAt;
|
|
628
1124
|
// CTX-002: an answer built from one snapshot must never be published
|
|
@@ -652,7 +1148,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
652
1148
|
sqlExecutions,
|
|
653
1149
|
repairs: result.analysisPlan?.repairAttempts ?? 0,
|
|
654
1150
|
};
|
|
655
|
-
if (
|
|
1151
|
+
if (isResearch && !providerEgressReceipts.some((receipt) => receipt.purpose === 'research_tool')) {
|
|
656
1152
|
providerEgressReceipts.push(createProviderEgressReceipt({
|
|
657
1153
|
purpose: 'research_tool',
|
|
658
1154
|
provider: provider.name,
|
|
@@ -724,6 +1220,19 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
|
|
|
724
1220
|
dispatchEvidence: dispatchEvidence(orchestrationBudgetExhausted ? 'orchestration_budget_exhausted'
|
|
725
1221
|
: snapshotDrift ? 'project_snapshot_mismatch'
|
|
726
1222
|
: 'provider_error'),
|
|
1223
|
+
providerDiagnostic: providerBoundaryDiagnostic({
|
|
1224
|
+
providerId: id,
|
|
1225
|
+
projectRoot: req.projectRoot,
|
|
1226
|
+
phase: orchestrationBudgetExhausted
|
|
1227
|
+
? 'planning'
|
|
1228
|
+
: 'generation',
|
|
1229
|
+
error: err,
|
|
1230
|
+
code: orchestrationBudgetExhausted
|
|
1231
|
+
? 'PROVIDER_DISPATCH_BUDGET'
|
|
1232
|
+
: snapshotDrift
|
|
1233
|
+
? 'ADMISSION_DENIED'
|
|
1234
|
+
: budgetCode,
|
|
1235
|
+
}),
|
|
727
1236
|
});
|
|
728
1237
|
}
|
|
729
1238
|
},
|
|
@@ -1097,6 +1606,28 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
|
|
|
1097
1606
|
const context = rawContext;
|
|
1098
1607
|
if (!context)
|
|
1099
1608
|
return undefined;
|
|
1609
|
+
const taskDependency = analyticalTaskDependencyBindingFromContext(context);
|
|
1610
|
+
// A compound child receives exactly one server-computed parent value plus
|
|
1611
|
+
// result/row proofs. Do not replay parent prose, SQL, or result rows into the
|
|
1612
|
+
// child; its normal governed retrieval and immutable-plan checks still run.
|
|
1613
|
+
if (taskDependency) {
|
|
1614
|
+
return {
|
|
1615
|
+
kind: 'drilldown',
|
|
1616
|
+
sourceTurnId: `task:${taskDependency.sourceTaskId}`,
|
|
1617
|
+
filters: [taskDependency.value],
|
|
1618
|
+
dimensions: [taskDependency.canonicalColumn],
|
|
1619
|
+
priorResultColumns: [taskDependency.canonicalColumn],
|
|
1620
|
+
priorResultValues: { [taskDependency.canonicalColumn]: [taskDependency.value] },
|
|
1621
|
+
memberBindings: [{
|
|
1622
|
+
dimension: taskDependency.canonicalColumn,
|
|
1623
|
+
values: [taskDependency.value],
|
|
1624
|
+
source: 'prior_result',
|
|
1625
|
+
confidence: 'exact',
|
|
1626
|
+
sourceTurnId: `task:${taskDependency.sourceTaskId}`,
|
|
1627
|
+
}],
|
|
1628
|
+
resolvedReferences: [`${taskDependency.canonicalColumn}: ${taskDependency.value}`],
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1100
1631
|
const turns = conversationTurnsFromContext(context);
|
|
1101
1632
|
const activeTurn = activeConversationTurn(context, turns, question);
|
|
1102
1633
|
const activeResult = activeTurn?.result && typeof activeTurn.result === 'object' && !Array.isArray(activeTurn.result)
|
|
@@ -1193,6 +1724,21 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
|
|
|
1193
1724
|
priorResult: relativeComparison ? undefined : priorResultDataFromTurn(activeResult, priorResultColumns),
|
|
1194
1725
|
};
|
|
1195
1726
|
}
|
|
1727
|
+
function analyticalTaskDependencyBindingFromContext(context) {
|
|
1728
|
+
const raw = cleanRecord(context.analyticalTaskDependencyBinding);
|
|
1729
|
+
if (!raw || raw.version !== 1)
|
|
1730
|
+
return undefined;
|
|
1731
|
+
const sourceTaskId = cleanOptionalString(raw.sourceTaskId);
|
|
1732
|
+
const sourceResultFingerprint = cleanOptionalString(raw.sourceResultFingerprint)?.toLowerCase();
|
|
1733
|
+
const canonicalColumn = cleanOptionalString(raw.canonicalColumn);
|
|
1734
|
+
const value = cleanOptionalString(raw.value);
|
|
1735
|
+
const rowFingerprint = cleanOptionalString(raw.rowFingerprint)?.toLowerCase();
|
|
1736
|
+
if (!sourceTaskId || !canonicalColumn || !value
|
|
1737
|
+
|| !/^[a-f0-9]{64}$/.test(sourceResultFingerprint ?? '')
|
|
1738
|
+
|| !/^[a-f0-9]{64}$/.test(rowFingerprint ?? ''))
|
|
1739
|
+
return undefined;
|
|
1740
|
+
return { sourceTaskId, sourceResultFingerprint: sourceResultFingerprint, canonicalColumn, value, rowFingerprint: rowFingerprint };
|
|
1741
|
+
}
|
|
1196
1742
|
/** Build the bounded prior-result rows (aligned to columns) for cross-result
|
|
1197
1743
|
* follow-up computation, from a turn's persisted result sample. */
|
|
1198
1744
|
function priorResultDataFromTurn(activeResult, columns) {
|
|
@@ -1776,6 +2322,7 @@ function normalizePriorValueDimension(value) {
|
|
|
1776
2322
|
return lower.replace(/[_\s-]+name$/, '').replace(/[^a-z0-9_ -]+/g, '').trim();
|
|
1777
2323
|
}
|
|
1778
2324
|
export const __test__ = {
|
|
2325
|
+
agenticLaneForRequest,
|
|
1779
2326
|
applyTopicShiftGuard,
|
|
1780
2327
|
isDrilldownFollowUp,
|
|
1781
2328
|
buildAnswerLoopTools,
|
|
@@ -1789,6 +2336,8 @@ export const __test__ = {
|
|
|
1789
2336
|
shouldSearchProjectFiles,
|
|
1790
2337
|
renderProjectSourceSearch,
|
|
1791
2338
|
researchDispatchPurposeForTool,
|
|
2339
|
+
readAgentConfig,
|
|
2340
|
+
providerBoundaryDiagnostic,
|
|
1792
2341
|
};
|
|
1793
2342
|
function researchDispatchPurposeForTool(toolName) {
|
|
1794
2343
|
return toolName === 'execute_local_analysis' ? 'research_tool' : 'research_narration';
|