@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.
Files changed (96) hide show
  1. package/dist/args.d.ts +15 -0
  2. package/dist/args.d.ts.map +1 -1
  3. package/dist/args.js +25 -0
  4. package/dist/args.js.map +1 -1
  5. package/dist/assets/dql-notebook/assets/{AgentLogPage-Ch7VK20X.js → AgentLogPage-DKbGpRQS.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AiBuildDialog-DBr5TmyM.js → AiBuildDialog-DPSu0Mly.js} +1 -1
  7. package/dist/assets/dql-notebook/assets/{AiBuildResult-jLPzQO7O.js → AiBuildResult-1uaGnpi1.js} +1 -1
  8. package/dist/assets/dql-notebook/assets/{AiSidePanel-CdlGsiVC.js → AiSidePanel-BwMREwa7.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{AnalyticsHome-C0DXbOwY.js → AnalyticsHome-BGfey_ve.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/{AppsView-CcpwjApv.js → AppsView-CM1tPywy.js} +4 -4
  11. package/dist/assets/dql-notebook/assets/{BlockStudio-CFYxafw-.js → BlockStudio--S6WFVO4.js} +1 -1
  12. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-C0kLYg2p.js → BusinessArtifactView-BEAJ-yNW.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-CMwElXD_.js → DbtFirstModelingPage-CNyU5MBX.js} +1 -1
  14. package/dist/assets/dql-notebook/assets/{GitPage-JjhRDeWY.js → GitPage-IcydAai3.js} +1 -1
  15. package/dist/assets/dql-notebook/assets/{GlobalAiRail-DakE4NdR.js → GlobalAiRail-CSKW-5eD.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{GovernedContextPage-BokDqG6a.js → GovernedContextPage-CFen0eFT.js} +1 -1
  17. package/dist/assets/dql-notebook/assets/{HelpDocsPage-CjOv6_gz.js → HelpDocsPage-D0D9UCIz.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{HomePage-nGaNcwdw.js → HomePage-CmSxapR3.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{LineageDAG-CO6CFJRg.js → LineageDAG-BGUcIt1B.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{LineageDetailView-BnGb7OF7.js → LineageDetailView-Dx48Zfzb.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{LineageDrawer-C5Y0Ht0b.js → LineageDrawer-zaBfSLZO.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-Cgh3GR3F.js → LineagePathBreadcrumb-CTZJp4_r.js} +1 -1
  23. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-Kla9PYuj.js → MiniLineageGraph-CdivNR1S.js} +1 -1
  24. package/dist/assets/dql-notebook/assets/{NewBlockModal-BR2SnmPT.js → NewBlockModal-DMBFB7nE.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{NewNotebookModal-BaCHMYeB.js → NewNotebookModal-DsC9CWlW.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{NotebookEditor-CBLqY8cE.js → NotebookEditor-hs-kw8v9.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/{ReadinessPage-CiN0IWSS.js → ReadinessPage-DJ83cMik.js} +1 -1
  28. package/dist/assets/dql-notebook/assets/{SetupOnboarding-BhiCsYF-.js → SetupOnboarding-B1Pu_Bvv.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{SkillsPage-CCSf8VMm.js → SkillsPage-BHKDay8n.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{TrustBadge-BgQmFe_x.js → TrustBadge-BkyGgob2.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-BYdrTaEW.js +89 -0
  32. package/dist/assets/dql-notebook/assets/{answer-to-notebook-AeDYUDla.js → answer-to-notebook-DmNiuLQA.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{arrow-left-C55x2hq_.js → arrow-left--1rsrxm8.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{arrow-right-C1cJrhOm.js → arrow-right-D5TdqY1G.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{book-open-text-CQf_sdv2.js → book-open-text-Bw7nHbzg.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{circle-x-X8-Z2yLY.js → circle-x-DLe6NNM4.js} +1 -1
  37. package/dist/assets/dql-notebook/assets/{dagre.esm-BjjNYKyY.js → dagre.esm-CW5QZdBt.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{external-link-C2zrz5DH.js → external-link-C9Q97sA3.js} +1 -1
  39. package/dist/assets/dql-notebook/assets/{grip-vertical-qGV_PYGU.js → grip-vertical-CztvkIgo.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/{index-ByTDPDaH.js → index-zHHzDn6l.js} +127 -127
  41. package/dist/assets/dql-notebook/assets/{link-2-VpyOxQXG.js → link-2-CiKAvumL.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{list-tree-CH2Jhwms.js → list-tree-BtnP2nQ5.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{minimize-2-B2TZJ8BT.js → minimize-2-TSFGxcCP.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{panel-right-open-BunF88lt.js → panel-right-open-BfXIUWy0.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{play-DAFVF4_G.js → play-DVbSFJHD.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{rotate-ccw-DGCrrqtY.js → rotate-ccw-D_cesDcX.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{semantic-fields-Ci-9QL9F.js → semantic-fields-CoVStdYB.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{sliders-horizontal-BOAlXXbn.js → sliders-horizontal-l7xV9K5A.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{star-CkksZXHt.js → star-CSBS0H3b.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{triangle-alert-D3mjyJZE.js → triangle-alert-BTrnyY4q.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{upload-CTNOAVEO.js → upload-sySLq9zb.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DiQjc7x-.js → usePersistedAgentThreadId-CzwgGdus.js} +1 -1
  53. package/dist/assets/dql-notebook/assets/{user-round-BWd5tQRg.js → user-round-ChlgXi9j.js} +1 -1
  54. package/dist/assets/dql-notebook/assets/{wand-sparkles-CqsAv8P-.js → wand-sparkles-CGw0ytyT.js} +1 -1
  55. package/dist/assets/dql-notebook/assets/{workflow-CSqsj-sC.js → workflow-C_RltiK5.js} +1 -1
  56. package/dist/assets/dql-notebook/assets/{wrench-DWqzqlX8.js → wrench-D-wLfeu0.js} +1 -1
  57. package/dist/assets/dql-notebook/assets/{x-65M5rLCE.js → x-B28hIJIC.js} +1 -1
  58. package/dist/assets/dql-notebook/index.html +1 -1
  59. package/dist/commands/agent-eval-cassette.d.ts +164 -0
  60. package/dist/commands/agent-eval-cassette.d.ts.map +1 -0
  61. package/dist/commands/agent-eval-cassette.js +313 -0
  62. package/dist/commands/agent-eval-cassette.js.map +1 -0
  63. package/dist/commands/agent-eval-runtime.d.ts +109 -0
  64. package/dist/commands/agent-eval-runtime.d.ts.map +1 -0
  65. package/dist/commands/agent-eval-runtime.js +165 -0
  66. package/dist/commands/agent-eval-runtime.js.map +1 -0
  67. package/dist/commands/agent.d.ts +124 -4
  68. package/dist/commands/agent.d.ts.map +1 -1
  69. package/dist/commands/agent.js +410 -106
  70. package/dist/commands/agent.js.map +1 -1
  71. package/dist/commands/compile.d.ts +13 -1
  72. package/dist/commands/compile.d.ts.map +1 -1
  73. package/dist/commands/compile.js +36 -4
  74. package/dist/commands/compile.js.map +1 -1
  75. package/dist/commands/sync.d.ts.map +1 -1
  76. package/dist/commands/sync.js +11 -3
  77. package/dist/commands/sync.js.map +1 -1
  78. package/dist/index.js +4 -0
  79. package/dist/index.js.map +1 -1
  80. package/dist/llm/analyst-loop-tools.d.ts +19 -0
  81. package/dist/llm/analyst-loop-tools.d.ts.map +1 -0
  82. package/dist/llm/analyst-loop-tools.js +56 -0
  83. package/dist/llm/analyst-loop-tools.js.map +1 -0
  84. package/dist/llm/providers/dql-agent-provider.d.ts +51 -1
  85. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  86. package/dist/llm/providers/dql-agent-provider.js +563 -14
  87. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  88. package/dist/llm/types.d.ts +47 -1
  89. package/dist/llm/types.d.ts.map +1 -1
  90. package/dist/local-runtime.d.ts +123 -1
  91. package/dist/local-runtime.d.ts.map +1 -1
  92. package/dist/local-runtime.js +1437 -163
  93. package/dist/local-runtime.js.map +1 -1
  94. package/dist/package.json +10 -10
  95. package/package.json +10 -10
  96. package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Bw5AXNDB.js +0 -88
@@ -1,6 +1,11 @@
1
- import { ClaudeProvider, KGStore, MemoryStore, defaultKgPath, defaultMemoryPath, GeminiProvider, loadAgentSemanticLayer, OllamaProvider, OpenAIProvider, answer, buildAnalysisQuestionPlan, buildLocalContextPack, contextRetrievalBudgetForQuestion, ensureAgentProjectReady, isLikelyClarificationReply, isTrustedConversationTurn, createProviderDispatchEgressReceipt, createProviderEgressReceipt, assertProviderPayloadAllowed, prepareProviderContextForDispatch, prepareProviderWireEnvelopeForDispatch, prepareServerOwnedProviderSchemaContext, } from '@duckcodeailabs/dql-agent';
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 { existsSync } from 'node:fs';
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.analysisDepth === 'deep';
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: isResearch ? 8 : 4,
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
- emit({ kind: 'error', message: `${spec.label} is not configured or reachable. ${spec.setup}` });
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(schemaContext),
528
- ...(contextPack ? { contextPack } : {}),
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
- const result = await answer({
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 (req.analysisDepth === 'deep' && !providerEgressReceipts.some((receipt) => receipt.purpose === 'research_tool')) {
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';