@duckcodeailabs/dql-cli 1.14.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) 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-BPz-UWFh.js} +1 -1
  6. package/dist/assets/dql-notebook/assets/{AiBuildDialog-DBr5TmyM.js → AiBuildDialog-BEl53WA_.js} +1 -1
  7. package/dist/assets/dql-notebook/assets/{AiBuildResult-jLPzQO7O.js → AiBuildResult-B4yfGTTZ.js} +1 -1
  8. package/dist/assets/dql-notebook/assets/{AiSidePanel-CdlGsiVC.js → AiSidePanel-CSZAAvuD.js} +1 -1
  9. package/dist/assets/dql-notebook/assets/{AnalyticsHome-C0DXbOwY.js → AnalyticsHome-D5P6Ujwi.js} +1 -1
  10. package/dist/assets/dql-notebook/assets/{AppsView-CcpwjApv.js → AppsView-DQOwU9Cg.js} +4 -4
  11. package/dist/assets/dql-notebook/assets/{BlockStudio-CFYxafw-.js → BlockStudio-B4ap0GdY.js} +1 -1
  12. package/dist/assets/dql-notebook/assets/{BusinessArtifactView-C0kLYg2p.js → BusinessArtifactView-BIfNI-0S.js} +1 -1
  13. package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-CMwElXD_.js → DbtFirstModelingPage-D72byb2g.js} +1 -1
  14. package/dist/assets/dql-notebook/assets/{GitPage-JjhRDeWY.js → GitPage-lQb1uXH2.js} +1 -1
  15. package/dist/assets/dql-notebook/assets/{GlobalAiRail-DakE4NdR.js → GlobalAiRail-CVECf6Xj.js} +1 -1
  16. package/dist/assets/dql-notebook/assets/{GovernedContextPage-BokDqG6a.js → GovernedContextPage-trOyMCY6.js} +1 -1
  17. package/dist/assets/dql-notebook/assets/{HelpDocsPage-CjOv6_gz.js → HelpDocsPage-D8hLS5lE.js} +1 -1
  18. package/dist/assets/dql-notebook/assets/{HomePage-nGaNcwdw.js → HomePage-eIkfBIep.js} +1 -1
  19. package/dist/assets/dql-notebook/assets/{LineageDAG-CO6CFJRg.js → LineageDAG-CSqcbDrE.js} +1 -1
  20. package/dist/assets/dql-notebook/assets/{LineageDetailView-BnGb7OF7.js → LineageDetailView-DJZZjZu-.js} +1 -1
  21. package/dist/assets/dql-notebook/assets/{LineageDrawer-C5Y0Ht0b.js → LineageDrawer-BcIipIc3.js} +1 -1
  22. package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-Cgh3GR3F.js → LineagePathBreadcrumb-CviIf8PN.js} +1 -1
  23. package/dist/assets/dql-notebook/assets/{MiniLineageGraph-Kla9PYuj.js → MiniLineageGraph-vH_MY_Ju.js} +1 -1
  24. package/dist/assets/dql-notebook/assets/{NewBlockModal-BR2SnmPT.js → NewBlockModal-DbCQg-pj.js} +1 -1
  25. package/dist/assets/dql-notebook/assets/{NewNotebookModal-BaCHMYeB.js → NewNotebookModal-5Vp6XiuK.js} +1 -1
  26. package/dist/assets/dql-notebook/assets/{NotebookEditor-CBLqY8cE.js → NotebookEditor-DjqJS44s.js} +1 -1
  27. package/dist/assets/dql-notebook/assets/{ReadinessPage-CiN0IWSS.js → ReadinessPage-BHGrC5ho.js} +1 -1
  28. package/dist/assets/dql-notebook/assets/{SetupOnboarding-BhiCsYF-.js → SetupOnboarding-BfS9Tdx-.js} +1 -1
  29. package/dist/assets/dql-notebook/assets/{SkillsPage-CCSf8VMm.js → SkillsPage-BFH21wSj.js} +1 -1
  30. package/dist/assets/dql-notebook/assets/{TrustBadge-BgQmFe_x.js → TrustBadge-zm6g_SxZ.js} +1 -1
  31. package/dist/assets/dql-notebook/assets/{UnifiedAgentRunPanel-Bw5AXNDB.js → UnifiedAgentRunPanel--oxmjlgr.js} +21 -21
  32. package/dist/assets/dql-notebook/assets/{answer-to-notebook-AeDYUDla.js → answer-to-notebook-DPhxIEzF.js} +1 -1
  33. package/dist/assets/dql-notebook/assets/{arrow-left-C55x2hq_.js → arrow-left-DNEb86Xc.js} +1 -1
  34. package/dist/assets/dql-notebook/assets/{arrow-right-C1cJrhOm.js → arrow-right-DpPWbwaD.js} +1 -1
  35. package/dist/assets/dql-notebook/assets/{book-open-text-CQf_sdv2.js → book-open-text-D7s5jo4X.js} +1 -1
  36. package/dist/assets/dql-notebook/assets/{circle-x-X8-Z2yLY.js → circle-x-Db3dXKg7.js} +1 -1
  37. package/dist/assets/dql-notebook/assets/{dagre.esm-BjjNYKyY.js → dagre.esm-C7pppQ1a.js} +1 -1
  38. package/dist/assets/dql-notebook/assets/{external-link-C2zrz5DH.js → external-link-BxwXitO_.js} +1 -1
  39. package/dist/assets/dql-notebook/assets/{grip-vertical-qGV_PYGU.js → grip-vertical-Dt4lkWRi.js} +1 -1
  40. package/dist/assets/dql-notebook/assets/{index-ByTDPDaH.js → index-DKo-bwNw.js} +2 -2
  41. package/dist/assets/dql-notebook/assets/{link-2-VpyOxQXG.js → link-2-Dfo2P6wi.js} +1 -1
  42. package/dist/assets/dql-notebook/assets/{list-tree-CH2Jhwms.js → list-tree-DiTmIWAL.js} +1 -1
  43. package/dist/assets/dql-notebook/assets/{minimize-2-B2TZJ8BT.js → minimize-2-CMTAkPzL.js} +1 -1
  44. package/dist/assets/dql-notebook/assets/{panel-right-open-BunF88lt.js → panel-right-open-DwYr7FW4.js} +1 -1
  45. package/dist/assets/dql-notebook/assets/{play-DAFVF4_G.js → play-BXhHYQ4x.js} +1 -1
  46. package/dist/assets/dql-notebook/assets/{rotate-ccw-DGCrrqtY.js → rotate-ccw-BNi6F8pl.js} +1 -1
  47. package/dist/assets/dql-notebook/assets/{semantic-fields-Ci-9QL9F.js → semantic-fields-CNOGysAy.js} +1 -1
  48. package/dist/assets/dql-notebook/assets/{sliders-horizontal-BOAlXXbn.js → sliders-horizontal-Ec5MUMUW.js} +1 -1
  49. package/dist/assets/dql-notebook/assets/{star-CkksZXHt.js → star-B9leDkp_.js} +1 -1
  50. package/dist/assets/dql-notebook/assets/{triangle-alert-D3mjyJZE.js → triangle-alert-BefTYCzx.js} +1 -1
  51. package/dist/assets/dql-notebook/assets/{upload-CTNOAVEO.js → upload-SPiOM2tQ.js} +1 -1
  52. package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DiQjc7x-.js → usePersistedAgentThreadId-C4foXeiQ.js} +1 -1
  53. package/dist/assets/dql-notebook/assets/{user-round-BWd5tQRg.js → user-round-comGmyw-.js} +1 -1
  54. package/dist/assets/dql-notebook/assets/{wand-sparkles-CqsAv8P-.js → wand-sparkles-BffR4dF8.js} +1 -1
  55. package/dist/assets/dql-notebook/assets/{workflow-CSqsj-sC.js → workflow-ChmPTEzH.js} +1 -1
  56. package/dist/assets/dql-notebook/assets/{wrench-DWqzqlX8.js → wrench-DovaG_ze.js} +1 -1
  57. package/dist/assets/dql-notebook/assets/{x-65M5rLCE.js → x-nRx91AgW.js} +1 -1
  58. package/dist/assets/dql-notebook/index.html +1 -1
  59. package/dist/commands/agent-eval-cassette.d.ts +73 -0
  60. package/dist/commands/agent-eval-cassette.d.ts.map +1 -0
  61. package/dist/commands/agent-eval-cassette.js +170 -0
  62. package/dist/commands/agent-eval-cassette.js.map +1 -0
  63. package/dist/commands/agent-eval-runtime.d.ts +97 -0
  64. package/dist/commands/agent-eval-runtime.d.ts.map +1 -0
  65. package/dist/commands/agent-eval-runtime.js +155 -0
  66. package/dist/commands/agent-eval-runtime.js.map +1 -0
  67. package/dist/commands/agent.d.ts +115 -1
  68. package/dist/commands/agent.d.ts.map +1 -1
  69. package/dist/commands/agent.js +289 -62
  70. package/dist/commands/agent.js.map +1 -1
  71. package/dist/index.js +4 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/llm/analyst-loop-tools.d.ts +19 -0
  74. package/dist/llm/analyst-loop-tools.d.ts.map +1 -0
  75. package/dist/llm/analyst-loop-tools.js +56 -0
  76. package/dist/llm/analyst-loop-tools.js.map +1 -0
  77. package/dist/llm/providers/dql-agent-provider.d.ts +24 -1
  78. package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
  79. package/dist/llm/providers/dql-agent-provider.js +306 -7
  80. package/dist/llm/providers/dql-agent-provider.js.map +1 -1
  81. package/dist/llm/types.d.ts +19 -1
  82. package/dist/llm/types.d.ts.map +1 -1
  83. package/dist/local-runtime.d.ts +108 -1
  84. package/dist/local-runtime.d.ts.map +1 -1
  85. package/dist/local-runtime.js +626 -110
  86. package/dist/local-runtime.js.map +1 -1
  87. package/dist/package.json +10 -10
  88. package/package.json +10 -10
@@ -1,6 +1,10 @@
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 { 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, createAnalystLaneHandler, parseProposal, renderContextValidationRefusalForUser, validateSqlAgainstLocalContext, resolveOrchestratorPolicy, } from '@duckcodeailabs/dql-agent';
4
+ import { CassetteStore, 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 { existsSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
4
8
  import { buildAnswerLoopTools, createGroundingContextExpander } from '../answer-loop-tools.js';
5
9
  import { getSemanticRuntimeStatus } from '../../semantic-runtime.js';
6
10
  import { blockProposalDqlMetadata } from '../proposal-metadata.js';
@@ -257,12 +261,131 @@ function emitProposalFromText(text, emit) {
257
261
  // Ignore malformed proposal text. The visible assistant response still streams as text.
258
262
  }
259
263
  }
264
+ /**
265
+ * Route the runtime's own provider through an eval cassette when the host asks.
266
+ *
267
+ * The client-side cassette in `dql agent eval` only covers `--via loop`: with
268
+ * `--via runtime` the SERVER owns the provider, so without this hook the one
269
+ * driver that actually exercises routing and gates could never be made
270
+ * deterministic — and a suite that cannot be deterministic cannot gate a PR.
271
+ *
272
+ * Opt-in through the environment, never through a request field: a caller must
273
+ * not be able to redirect a production run onto recorded responses.
274
+ */
275
+ /**
276
+ * Read `agent.orchestrator` straight from dql.config.json.
277
+ *
278
+ * Deliberately NOT `loadProjectConfig` from local-runtime: that module imports
279
+ * this one (local-runtime.ts:164), so reaching back would close an import cycle
280
+ * through a 34k-line file. A few lines of JSON reading is the cheaper trade.
281
+ *
282
+ * Cached per project root because this runs on every turn and the answer is a
283
+ * migration setting, not live state. A malformed file resolves to `null`, which
284
+ * `resolveOrchestratorPolicy` turns into `legacy` — a broken config must not
285
+ * route real questions onto an unproven path.
286
+ */
287
+ const agentConfigCache = new Map();
288
+ function readAgentConfig(projectRoot) {
289
+ const cached = agentConfigCache.get(projectRoot);
290
+ if (cached !== undefined)
291
+ return cached;
292
+ let resolved = null;
293
+ try {
294
+ const raw = readFileSync(join(projectRoot, 'dql.config.json'), 'utf-8');
295
+ const parsed = JSON.parse(raw);
296
+ resolved = parsed?.agent && typeof parsed.agent === 'object'
297
+ ? parsed.agent
298
+ : null;
299
+ }
300
+ catch {
301
+ resolved = null;
302
+ }
303
+ agentConfigCache.set(projectRoot, resolved);
304
+ return resolved;
305
+ }
306
+ function readOrchestratorConfig(projectRoot) {
307
+ const orchestrator = readAgentConfig(projectRoot)?.orchestrator;
308
+ return orchestrator && typeof orchestrator === 'object' ? orchestrator : null;
309
+ }
310
+ /**
311
+ * Is on-demand value lookup permitted for this project?
312
+ *
313
+ * Mirrors `resolveAgentRuntimeValueGrounding`, read locally rather than imported
314
+ * — local-runtime imports this module, so reaching back would close a cycle
315
+ * through a 34k-line file. Anything other than an explicit `safe_automatic`
316
+ * resolves to disabled: value lookup touches warehouse cell values, so a
317
+ * malformed setting must fail closed.
318
+ */
319
+ /**
320
+ * Structured turn planning is OFF by default.
321
+ *
322
+ * It costs one provider dispatch before the loop makes any tool call, and on a
323
+ * slow provider that dispatch can consume enough of the discovery window that
324
+ * the loop is then refused admission and falls back to the legacy path —
325
+ * measured on ollama, where all three agentic dispatches planned successfully
326
+ * and then died at "soft target elapsed before this provider dispatch could
327
+ * start". Its payoff is a legible trace, and `onStep` is not wired to SSE yet,
328
+ * so today it buys nothing a user can see. Opt in with
329
+ * `agent.orchestrator.turnPlanning: true` once streaming lands.
330
+ */
331
+ function turnPlanningEnabled(projectRoot) {
332
+ const orchestrator = readAgentConfig(projectRoot)?.orchestrator;
333
+ if (!orchestrator || typeof orchestrator !== 'object')
334
+ return false;
335
+ return orchestrator.turnPlanning === true;
336
+ }
337
+ function valueLookupEnabled(projectRoot) {
338
+ const grounding = readAgentConfig(projectRoot)?.runtimeValueGrounding;
339
+ if (!grounding || typeof grounding !== 'object')
340
+ return false;
341
+ return grounding.mode === 'safe_automatic';
342
+ }
343
+ /**
344
+ * Which migration lane this turn belongs to.
345
+ *
346
+ * Research is identified by the server-resolved `orchestrationMode`, not by
347
+ * `analysisDepth`. A reader can ask Ask AI to think deeply without opting into
348
+ * the Research workflow, its wider dispatch budget, or any row-bearing tools.
349
+ *
350
+ * Deliberately coarse: the seam only needs to know which bucket a turn falls in
351
+ * so a lane can be enabled independently. The fine-grained triage between
352
+ * certified, semantic, and generated stays inside the answer path, where the
353
+ * retrieval evidence lives.
354
+ */
355
+ function agenticLaneForRequest(req) {
356
+ return req.orchestrationMode === 'research' ? 'research' : 'generated';
357
+ }
358
+ export function applyEvalCassette(provider) {
359
+ const dir = process.env.DQL_EVAL_CASSETTE_DIR;
360
+ if (!dir)
361
+ return provider;
362
+ return withCassette(provider, new CassetteStore(dir), resolveCassetteModeFromEnv(process.env));
363
+ }
364
+ /**
365
+ * A raw text provider for planning calls that are not the answer itself.
366
+ *
367
+ * Research hypothesis planning needs `generate`, not the full agent runner —
368
+ * and the runner cannot be reused for it, because the runner IS the governed
369
+ * answer path. Cassettes apply, so a recorded run stays hermetic.
370
+ */
371
+ export function createGovernedTextProvider(id, projectRoot) {
372
+ const spec = SPECS[id];
373
+ if (!spec)
374
+ return undefined;
375
+ try {
376
+ return applyEvalCassette(spec.create(projectRoot));
377
+ }
378
+ catch {
379
+ return undefined;
380
+ }
381
+ }
260
382
  export function createDqlAgentProviderRunner(id, providerOverride) {
261
383
  return {
262
384
  async run(req, emit, signal) {
263
385
  const spec = SPECS[id];
264
- const rawProvider = providerOverride ?? spec.create(req.projectRoot);
265
- const isResearch = req.analysisDepth === 'deep';
386
+ const rawProvider = applyEvalCassette(providerOverride ?? spec.create(req.projectRoot));
387
+ const isResearch = req.orchestrationMode === 'research';
388
+ const maxProviderDispatches = isResearch ? 8 : 4;
266
389
  const researchRowsOptIn = isResearch && req.researchResultRowsOptIn === true;
267
390
  const sharedDispatchEvidence = req.providerDispatchEvidenceSink;
268
391
  let providerRoundTrips = 0;
@@ -296,7 +419,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
296
419
  // A tool-calling loop capped at two physical sends can make exactly one
297
420
  // tool call before it must answer, which is not enough to look something
298
421
  // up and then use it. The run-scoped ledger is the real guardrail.
299
- maxProviderDispatches: isResearch ? 8 : 4,
422
+ maxProviderDispatches,
300
423
  ...(sharedDispatchEvidence?.mayStartToolCall
301
424
  ? { mayStartToolCall: () => sharedDispatchEvidence.mayStartToolCall() }
302
425
  : {}),
@@ -439,6 +562,14 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
439
562
  strictness: contextBudget.strictness,
440
563
  limit: contextBudget.limit,
441
564
  confirmCertifiedFit: createCertifiedFitConfirmation(provider, signal),
565
+ // A cross-encoder pass over the fused candidates. Advisory: it may
566
+ // only reorder what retrieval returned, and a failure or timeout
567
+ // leaves retrieval's ordering untouched — so it can improve the
568
+ // pack and cannot break it.
569
+ rerankCandidates: (rerankQuestion, candidates) => rerankCandidates(provider, rerankQuestion, candidates, {
570
+ ...(signal ? { signal } : {}),
571
+ timeoutMs: Math.round(2_500 * deadlineScale()),
572
+ }),
442
573
  // Conversation-aware reuse: same-topic follow-ups seed (or, for
443
574
  // filter-only refinements, re-stamp) the prior turn's context pack.
444
575
  priorContextPackId: priorContextPackIdFromSnapshot(conversationSnapshot),
@@ -533,11 +664,17 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
533
664
  maxResultRows: 0,
534
665
  purpose: 'answer_generation',
535
666
  });
536
- const result = await answer({
667
+ // The strangler seam. `answerAgentic` and `answer` are interchangeable
668
+ // here; which runs is a per-lane config decision that defaults to
669
+ // legacy, so this is a no-op until a lane is explicitly enabled.
670
+ const answerLoopInput = {
537
671
  question,
538
672
  ...(req.resolvedAnalyticalPlan
539
673
  ? { resolvedAnalyticalPlan: req.resolvedAnalyticalPlan }
540
674
  : {}),
675
+ ...(req.generatedProposalTargetFingerprint
676
+ ? { generatedProposalTargetFingerprint: req.generatedProposalTargetFingerprint }
677
+ : {}),
541
678
  ...(req.analyticalReferenceInstant
542
679
  ? { analyticalReferenceInstant: req.analyticalReferenceInstant }
543
680
  : {}),
@@ -568,6 +705,12 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
568
705
  }
569
706
  : {}),
570
707
  contextPack,
708
+ // The project's configured embedder (dql.config.json ai.embeddings).
709
+ // Without it, matchSemanticMetric falls back to the offline hashed
710
+ // provider, whose vectors can never ground a match on similarity
711
+ // alone — so a metric named only by synonym or acronym is invisible
712
+ // and the router dead-ends on a bare-ranking clarification.
713
+ embeddingProvider: projectEmbeddingProvider(req.projectRoot),
571
714
  signal,
572
715
  reasoningEffort: req.reasoningEffort,
573
716
  analysisDepth: contextBudget.analysisDepth,
@@ -583,6 +726,19 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
583
726
  executeGeneratedSql: req.executeGeneratedSql
584
727
  ? async (...args) => { guardSnapshot(); sqlExecutions += 1; return req.executeGeneratedSql(...args); }
585
728
  : undefined,
729
+ executeAgenticGeneratedSql: req.executeAgenticGeneratedSql
730
+ ? async (capability, sql, artifact) => {
731
+ guardSnapshot();
732
+ sqlExecutions += 1;
733
+ return req.executeAgenticGeneratedSql(capability, sql, artifact);
734
+ }
735
+ : undefined,
736
+ agenticExecutionScope: {
737
+ runId: req.agentRunId,
738
+ snapshotId: req.projectSnapshot?.snapshotId,
739
+ planId: req.resolvedAnalyticalPlan?.planId,
740
+ targetFingerprint: req.generatedProposalTargetFingerprint,
741
+ },
586
742
  executeDqlArtifact: req.executeDqlArtifact
587
743
  ? async (...args) => { guardSnapshot(); sqlExecutions += 1; return req.executeDqlArtifact(...args); }
588
744
  : undefined,
@@ -623,6 +779,111 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
623
779
  // NOTE: no captureGeneratedDraft here — a plain answer/research question must NOT
624
780
  // auto-write a draft into the blocks space. A draft is created only when the user
625
781
  // explicitly acts (the "Create DQL draft" action → the dql_block_draft route).
782
+ };
783
+ const result = await answerAgentic(answerLoopInput, {
784
+ policy: resolveOrchestratorPolicy({
785
+ config: readOrchestratorConfig(req.projectRoot),
786
+ }),
787
+ lane: agenticLaneForRequest(req),
788
+ legacy: answer,
789
+ // The generated lane runs the analyst loop: it verifies every
790
+ // identifier against a tool observation before the SQL is executed.
791
+ // Certified and semantic lanes are deliberately NOT registered —
792
+ // their answers already come from a governed contract, so a
793
+ // verification pass would add latency and no safety.
794
+ handlers: {
795
+ generated: createAnalystLaneHandler({
796
+ legacy: answer,
797
+ buildDeps: (loopInput) => {
798
+ const execute = loopInput.executeGeneratedSql;
799
+ const valuesEnabled = valueLookupEnabled(req.projectRoot);
800
+ const tools = buildAnalystLoopTools(loopInput, { valuesEnabled });
801
+ if (process.env.DQL_ORCHESTRATOR_TRACE) {
802
+ console.warn(`[dql] analyst loop deps: tools=${tools.length} preview=${Boolean(execute)} values=${valuesEnabled}`);
803
+ }
804
+ if (tools.length === 0)
805
+ return undefined;
806
+ return {
807
+ tools,
808
+ maxIterations: resolveOrchestratorPolicy({
809
+ config: readOrchestratorConfig(req.projectRoot),
810
+ }).maxIterations,
811
+ // Match the physical wrapper cap and reserve the final
812
+ // composition dispatch: an ordinary cap of four permits
813
+ // at most three text-protocol tool observations.
814
+ maxProviderDispatches,
815
+ // Scaled by the same knob as every other agent deadline, so
816
+ // a local model that needs seconds per call is not planned
817
+ // out of existence by a budget calibrated for a hosted one.
818
+ ...(turnPlanningEnabled(req.projectRoot) ? {
819
+ planTurn: async (question, toolNames) => {
820
+ const plan = await planAnalystTurn(loopInput.provider, question, toolNames, {
821
+ timeoutMs: Math.round(2_500 * deadlineScale()),
822
+ ...(loopInput.signal ? { signal: loopInput.signal } : {}),
823
+ });
824
+ if (process.env.DQL_ORCHESTRATOR_TRACE) {
825
+ console.warn(plan
826
+ ? `[dql] analyst turn plan: "${plan.restatement}" establish=${plan.mustEstablish.length} opening=${plan.openingTool ?? 'unset'}`
827
+ : '[dql] analyst turn plan: unavailable (loop proceeds unplanned)');
828
+ }
829
+ return plan;
830
+ },
831
+ } : {}),
832
+ // The loop's trace, on the channel the provider already uses
833
+ // for progress. `thinking` turns become `onProgress`, which
834
+ // the run engine emits as `executor.started` over SSE — so
835
+ // this needs no new event type, no contract change, and no
836
+ // UI work. Without it the loop does real work (tool calls,
837
+ // identifier checks, a bounded repair) behind a silent
838
+ // spinner, which reads as a hang rather than as an analyst
839
+ // establishing facts.
840
+ onStep: (step) => {
841
+ emit({
842
+ kind: 'thinking',
843
+ text: step.detail ? `${step.label} — ${step.detail}` : step.label,
844
+ });
845
+ },
846
+ // Reuse the legacy parser and validator rather than forking
847
+ // a second SQL front end that would drift from the first.
848
+ parseSql: (raw) => parseProposal(raw).sql,
849
+ extractReferences: (sql) => {
850
+ const validation = validateSqlAgainstLocalContext(sql, loopInput.contextPack);
851
+ const qualified = qualifyAuthorizationReferences(sql, {
852
+ relations: validation.referencedRelations ?? [],
853
+ columns: validation.referencedColumns ?? [],
854
+ });
855
+ return {
856
+ relations: validation.referencedRelations ?? [],
857
+ columns: qualified.filter((reference) => !validation.referencedRelations?.includes(reference)),
858
+ };
859
+ },
860
+ // The safety verifiers keep their logic; only what happens on
861
+ // failure changes. Instead of ending the turn, the specific
862
+ // check that fired comes back as something the model can act
863
+ // on — "joining those tables multiplies rows" rather than
864
+ // "nothing was executed".
865
+ verifySql: (sql) => {
866
+ const validation = validateSqlAgainstLocalContext(sql, loopInput.contextPack);
867
+ if (validation.ok)
868
+ return undefined;
869
+ return renderContextValidationRefusalForUser(validation.code, validation.error, loopInput.followUp?.memberBindings, validation.aggregationSafetyProof?.issueCodes);
870
+ },
871
+ };
872
+ },
873
+ }),
874
+ },
875
+ onDiagnostic: (event) => {
876
+ if (event.kind === 'fallback') {
877
+ console.warn(`[dql] agentic orchestrator fell back on ${event.lane}: ${event.reason}`);
878
+ }
879
+ else if (process.env.DQL_ORCHESTRATOR_TRACE) {
880
+ // Opt-in dispatch trace. A migration where the new path silently
881
+ // never runs looks identical to one where it runs and agrees,
882
+ // and that ambiguity costs more to debug than the log costs to
883
+ // carry.
884
+ console.warn(`[dql] agentic orchestrator dispatched lane=${event.lane} mode=${event.mode}`);
885
+ }
886
+ },
626
887
  });
627
888
  const answerDurationMs = Date.now() - answerStartedAt;
628
889
  // CTX-002: an answer built from one snapshot must never be published
@@ -652,7 +913,7 @@ export function createDqlAgentProviderRunner(id, providerOverride) {
652
913
  sqlExecutions,
653
914
  repairs: result.analysisPlan?.repairAttempts ?? 0,
654
915
  };
655
- if (req.analysisDepth === 'deep' && !providerEgressReceipts.some((receipt) => receipt.purpose === 'research_tool')) {
916
+ if (isResearch && !providerEgressReceipts.some((receipt) => receipt.purpose === 'research_tool')) {
656
917
  providerEgressReceipts.push(createProviderEgressReceipt({
657
918
  purpose: 'research_tool',
658
919
  provider: provider.name,
@@ -1097,6 +1358,28 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
1097
1358
  const context = rawContext;
1098
1359
  if (!context)
1099
1360
  return undefined;
1361
+ const taskDependency = analyticalTaskDependencyBindingFromContext(context);
1362
+ // A compound child receives exactly one server-computed parent value plus
1363
+ // result/row proofs. Do not replay parent prose, SQL, or result rows into the
1364
+ // child; its normal governed retrieval and immutable-plan checks still run.
1365
+ if (taskDependency) {
1366
+ return {
1367
+ kind: 'drilldown',
1368
+ sourceTurnId: `task:${taskDependency.sourceTaskId}`,
1369
+ filters: [taskDependency.value],
1370
+ dimensions: [taskDependency.canonicalColumn],
1371
+ priorResultColumns: [taskDependency.canonicalColumn],
1372
+ priorResultValues: { [taskDependency.canonicalColumn]: [taskDependency.value] },
1373
+ memberBindings: [{
1374
+ dimension: taskDependency.canonicalColumn,
1375
+ values: [taskDependency.value],
1376
+ source: 'prior_result',
1377
+ confidence: 'exact',
1378
+ sourceTurnId: `task:${taskDependency.sourceTaskId}`,
1379
+ }],
1380
+ resolvedReferences: [`${taskDependency.canonicalColumn}: ${taskDependency.value}`],
1381
+ };
1382
+ }
1100
1383
  const turns = conversationTurnsFromContext(context);
1101
1384
  const activeTurn = activeConversationTurn(context, turns, question);
1102
1385
  const activeResult = activeTurn?.result && typeof activeTurn.result === 'object' && !Array.isArray(activeTurn.result)
@@ -1193,6 +1476,21 @@ function resolveAgentFollowUpContextRaw(rawContext, question) {
1193
1476
  priorResult: relativeComparison ? undefined : priorResultDataFromTurn(activeResult, priorResultColumns),
1194
1477
  };
1195
1478
  }
1479
+ function analyticalTaskDependencyBindingFromContext(context) {
1480
+ const raw = cleanRecord(context.analyticalTaskDependencyBinding);
1481
+ if (!raw || raw.version !== 1)
1482
+ return undefined;
1483
+ const sourceTaskId = cleanOptionalString(raw.sourceTaskId);
1484
+ const sourceResultFingerprint = cleanOptionalString(raw.sourceResultFingerprint)?.toLowerCase();
1485
+ const canonicalColumn = cleanOptionalString(raw.canonicalColumn);
1486
+ const value = cleanOptionalString(raw.value);
1487
+ const rowFingerprint = cleanOptionalString(raw.rowFingerprint)?.toLowerCase();
1488
+ if (!sourceTaskId || !canonicalColumn || !value
1489
+ || !/^[a-f0-9]{64}$/.test(sourceResultFingerprint ?? '')
1490
+ || !/^[a-f0-9]{64}$/.test(rowFingerprint ?? ''))
1491
+ return undefined;
1492
+ return { sourceTaskId, sourceResultFingerprint: sourceResultFingerprint, canonicalColumn, value, rowFingerprint: rowFingerprint };
1493
+ }
1196
1494
  /** Build the bounded prior-result rows (aligned to columns) for cross-result
1197
1495
  * follow-up computation, from a turn's persisted result sample. */
1198
1496
  function priorResultDataFromTurn(activeResult, columns) {
@@ -1776,6 +2074,7 @@ function normalizePriorValueDimension(value) {
1776
2074
  return lower.replace(/[_\s-]+name$/, '').replace(/[^a-z0-9_ -]+/g, '').trim();
1777
2075
  }
1778
2076
  export const __test__ = {
2077
+ agenticLaneForRequest,
1779
2078
  applyTopicShiftGuard,
1780
2079
  isDrilldownFollowUp,
1781
2080
  buildAnswerLoopTools,