@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
package/dist/local-runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { execFileSync, execSync } from "node:child_process";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { gzip } from "node:zlib";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync, } from "node:fs";
|
|
@@ -23,9 +23,10 @@ import { getRunner as getLLMRunner } from './llm/index.js';
|
|
|
23
23
|
import { rethrowIfCancelled } from './llm/cancellation.js';
|
|
24
24
|
import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from './version-status.js';
|
|
25
25
|
import { resolveRetrievalHealthStatus } from './retrieval-health.js';
|
|
26
|
-
import {
|
|
26
|
+
import { applyFinding, createResearchState, narrationMaxTokensForFacts, nextHypothesis, rerankCandidates, synthesizeResearchNarrative, AgenticExecutionCapabilityGate, createAgenticSqlExecutionCapability, mintFinalSqlAuthorization, verifyAgenticSqlExecutionCapability, qualifyAuthorizationReferences, scopeContextPackToExploratoryCandidateClosure, validateSqlAgainstLocalContext as validateAuthorizedSqlReferences, verifyFinalSql, } from '@duckcodeailabs/dql-agent';
|
|
27
|
+
import { applyEvalCassette, createDqlAgentProviderRunner, createEvalCassetteReplayProvider, createGovernedTextProvider, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
|
|
27
28
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
28
|
-
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildAggregationSafetyProof, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, previewSkillChange, buildContextAuthoringProposal, contextAuthoringDependencyClosure, FileContextAuthoringProposalStore, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, createAgentRunBudget, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, assertProviderPayloadAllowed, createProviderDispatchEgressReceipt, prepareProviderWireEnvelopeForDispatch, markProviderMetadataArray, createProviderEgressReceipt, redactProviderResultRows, composeVerifiedAnalyticalNarrative, buildCoverageGap, capResearchBranches, buildResearchEvidenceLedger, buildAnalyticalTurnPlan, DEFAULT_ASK_ROW_EGRESS_POLICY, ZERO_ROW_EGRESS_POLICY, resolveProviderResultRowEgressPolicy, normalizeCanonicalQueryResult, normalizeAnalyticalExecutionFingerprint, normalizeAnalyticalExecutionReceipt, createAgentRunCancellationError, } from '@duckcodeailabs/dql-agent';
|
|
29
|
+
import { composeBusinessExplanation, ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildAggregationSafetyProof, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, previewSkillChange, buildContextAuthoringProposal, contextAuthoringDependencyClosure, FileContextAuthoringProposalStore, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, createAgentRunBudget, isProbeSafeColumn, deadlineScale, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, assertProviderPayloadAllowed, createProviderDispatchEgressReceipt, prepareProviderWireEnvelopeForDispatch, markProviderMetadataArray, createProviderEgressReceipt, redactProviderResultRows, composeVerifiedAnalyticalNarrative, classifyProviderFailure, buildCoverageGap, capResearchBranches, buildResearchEvidenceLedger, buildResearchEvidenceLedgerV2, buildResearchHypothesisPlanV2, inferResearchValidatorKind, buildAnalyticalTurnPlan, buildAnalyticalRequirementSet, resolveTopRankedRegionDependency, DEFAULT_ASK_ROW_EGRESS_POLICY, ZERO_ROW_EGRESS_POLICY, resolveProviderResultRowEgressPolicy, normalizeCanonicalQueryResult, normalizeAnalyticalExecutionFingerprint, normalizeAnalyticalExecutionReceipt, createAgentRunCancellationError, } from '@duckcodeailabs/dql-agent';
|
|
29
30
|
import { addSqlResultFilter, dashboardFilterableResultColumns, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
|
|
30
31
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
31
32
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization, } from './apps-api.js';
|
|
@@ -313,10 +314,14 @@ export function parseAgentRunRequestBody(body) {
|
|
|
313
314
|
selectedObject,
|
|
314
315
|
executionTarget,
|
|
315
316
|
workspaceContext,
|
|
316
|
-
conversationContext: sanitizeClientConversationContext(agentRunRecord(record.conversationContext)
|
|
317
|
+
conversationContext: sanitizeClientConversationContext(agentRunRecord(record.conversationContext), {
|
|
318
|
+
stripStructuredSelectionEnvelope: Boolean(agentRunString(record.selectedEvidenceId)),
|
|
319
|
+
}),
|
|
317
320
|
history: parseAgentRunHistory(record.history),
|
|
318
321
|
threadId: agentRunString(record.threadId),
|
|
319
|
-
runId
|
|
322
|
+
// `runId` is deliberately absent at public ingress. It scopes the
|
|
323
|
+
// controller, persisted run, SSE operation, and one-shot SQL capability,
|
|
324
|
+
// so a browser-supplied value must never become execution authority.
|
|
320
325
|
reasoningEffort: parseAgentRunReasoningEffort(record.reasoningEffort),
|
|
321
326
|
analysisDepth: parseAgentRunAnalysisDepth(record.analysisDepth) ?? parseAgentRunAnalysisDepth(record.depth),
|
|
322
327
|
thinkingMode: coerceThinkingMode(record.thinkingMode),
|
|
@@ -328,13 +333,26 @@ const CLIENT_PLAN_AUTHORITY_KEYS = new Set([
|
|
|
328
333
|
'priorResolvedAnalyticalPlan',
|
|
329
334
|
'resolvedAnalyticalPlan',
|
|
330
335
|
'analyticalFrame',
|
|
336
|
+
// Only the local compound executor may inject this after it has derived a
|
|
337
|
+
// canonical parent result binding. A browser-provided lookalike cannot become
|
|
338
|
+
// a child filter or skip ordinary member validation.
|
|
339
|
+
'analyticalTaskDependencyBinding',
|
|
340
|
+
]);
|
|
341
|
+
// A no-thread embedding may retain ordinary conversation context, but a
|
|
342
|
+
// selectedEvidenceId is a structured server continuation—not a client plan
|
|
343
|
+
// hint. Strip this state only for that selection path, while always stripping
|
|
344
|
+
// the host-only authority marker below.
|
|
345
|
+
const CLIENT_STRUCTURED_SELECTION_AUTHORITY_KEYS = new Set([
|
|
346
|
+
'conversationEnvelope',
|
|
347
|
+
'serverSnapshot',
|
|
348
|
+
'serverIssuedClarificationSelection',
|
|
331
349
|
]);
|
|
332
350
|
/**
|
|
333
351
|
* Browser/embedding context is useful retrieval and history input, but it is
|
|
334
352
|
* not a plan-authority channel. Remove plan-shaped fields recursively at HTTP
|
|
335
353
|
* ingress; server-retained thread context is added afterwards from local state.
|
|
336
354
|
*/
|
|
337
|
-
function sanitizeClientConversationContext(context) {
|
|
355
|
+
function sanitizeClientConversationContext(context, options = {}) {
|
|
338
356
|
if (!context)
|
|
339
357
|
return undefined;
|
|
340
358
|
const sanitize = (value) => {
|
|
@@ -343,7 +361,14 @@ function sanitizeClientConversationContext(context) {
|
|
|
343
361
|
const record = agentRunRecord(value);
|
|
344
362
|
if (!record)
|
|
345
363
|
return value;
|
|
346
|
-
return Object.fromEntries(Object.entries(record).flatMap(([key, nested]) =>
|
|
364
|
+
return Object.fromEntries(Object.entries(record).flatMap(([key, nested]) => {
|
|
365
|
+
const isHostOnlySelectionAuthority = key === 'serverIssuedClarificationSelection';
|
|
366
|
+
const isUntrustedSelectionEnvelope = options.stripStructuredSelectionEnvelope
|
|
367
|
+
&& CLIENT_STRUCTURED_SELECTION_AUTHORITY_KEYS.has(key);
|
|
368
|
+
return CLIENT_PLAN_AUTHORITY_KEYS.has(key) || isHostOnlySelectionAuthority || isUntrustedSelectionEnvelope
|
|
369
|
+
? []
|
|
370
|
+
: [[key, sanitize(nested)]];
|
|
371
|
+
}));
|
|
347
372
|
};
|
|
348
373
|
return sanitize(context);
|
|
349
374
|
}
|
|
@@ -378,6 +403,52 @@ export function agentRunDeadlineMs(request, env = process.env, activeProviderId)
|
|
|
378
403
|
? AGENT_RESEARCH_DEADLINE_MS
|
|
379
404
|
: AGENT_LOOKUP_DEADLINE_MS;
|
|
380
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Run ready independent compound clauses concurrently, but wait for a typed
|
|
408
|
+
* parent result before executing a declared dependent clause. The scheduler
|
|
409
|
+
* itself has no authority to query or filter; callers supply both execution and
|
|
410
|
+
* a dependency resolver so immutable-plan and SQL guards remain unchanged.
|
|
411
|
+
*/
|
|
412
|
+
export async function scheduleCompoundAnalyticalTasks(input) {
|
|
413
|
+
const pending = [...input.tasks];
|
|
414
|
+
const settled = new Map();
|
|
415
|
+
while (pending.length > 0) {
|
|
416
|
+
const ready = pending.filter((task) => task.dependencies.every((dependencyId) => settled.has(dependencyId)));
|
|
417
|
+
if (ready.length === 0) {
|
|
418
|
+
for (const task of pending.splice(0)) {
|
|
419
|
+
settled.set(task.id, {
|
|
420
|
+
task,
|
|
421
|
+
error: 'The compound task dependency graph could not be resolved.',
|
|
422
|
+
dependencyError: {
|
|
423
|
+
ok: false,
|
|
424
|
+
code: 'RESULT_CONTRACT_MISMATCH',
|
|
425
|
+
message: 'The dependent task could not run because its parent dependency was unresolved.',
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
for (const task of ready)
|
|
432
|
+
pending.splice(pending.indexOf(task), 1);
|
|
433
|
+
const batch = await Promise.all(ready.map(async (task) => {
|
|
434
|
+
if (!task.dependency || task.dependency.kind !== 'top_ranked_region')
|
|
435
|
+
return input.runTask(task);
|
|
436
|
+
const parent = settled.get(task.dependency.sourceTaskId);
|
|
437
|
+
const resolution = input.resolveDependency(task, parent);
|
|
438
|
+
if (!resolution.ok) {
|
|
439
|
+
const dependencyError = resolution;
|
|
440
|
+
return { task, error: dependencyError.message, dependencyError };
|
|
441
|
+
}
|
|
442
|
+
return input.runTask(task, resolution.binding);
|
|
443
|
+
}));
|
|
444
|
+
for (const result of batch)
|
|
445
|
+
settled.set(result.task.id, result);
|
|
446
|
+
}
|
|
447
|
+
return input.tasks.map((task) => settled.get(task.id) ?? {
|
|
448
|
+
task,
|
|
449
|
+
error: 'The compound task did not produce an outcome.',
|
|
450
|
+
});
|
|
451
|
+
}
|
|
381
452
|
/**
|
|
382
453
|
* Decide how a settled answer gets its business-facing prose.
|
|
383
454
|
*
|
|
@@ -420,11 +491,64 @@ export function shouldSynthesizeAgentRunAnswer(governedAnswer, requestedMode = '
|
|
|
420
491
|
rowEgress: DEFAULT_ASK_ROW_EGRESS_POLICY,
|
|
421
492
|
}).mode !== 'skip';
|
|
422
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* A receipt may be rendered in the local inspector and exported into evaluation
|
|
496
|
+
* output. Keep only stable validation codes there; provider error messages can
|
|
497
|
+
* contain a prompt excerpt, result value, or connector detail and must not
|
|
498
|
+
* become user-visible durable data.
|
|
499
|
+
*/
|
|
500
|
+
function narrationIntegrityFailureCodes(failures) {
|
|
501
|
+
return [...new Set(failures.map((failure) => {
|
|
502
|
+
const match = failure.trim().match(/^([A-Z][A-Z0-9_]{1,80})/);
|
|
503
|
+
return match?.[1] ?? 'NARRATION_VALIDATION_FAILED';
|
|
504
|
+
}).filter(Boolean))].slice(0, 8);
|
|
505
|
+
}
|
|
423
506
|
/**
|
|
424
507
|
* AGT-010 — the semantic route label is descriptive, while the exact
|
|
425
508
|
* route-specific aggregation proof is authoritative for governed trust.
|
|
426
509
|
* Missing proof remains blocked for legacy or malformed results.
|
|
427
510
|
*/
|
|
511
|
+
/**
|
|
512
|
+
* Trust for ONE answer, by the same rule the single-answer path uses: a route
|
|
513
|
+
* label is not authority, and a semantic route earns `governed` only when its
|
|
514
|
+
* aggregation proof actually passed.
|
|
515
|
+
*/
|
|
516
|
+
export function trustStateForAgentAnswer(answer) {
|
|
517
|
+
if (answer.certification === 'certified' || answer.kind === 'certified')
|
|
518
|
+
return 'certified';
|
|
519
|
+
return semanticAnswerHasPassedAggregationProof(answer) ? 'governed' : 'review_required';
|
|
520
|
+
}
|
|
521
|
+
const TRUST_RANK = {
|
|
522
|
+
certified: 3,
|
|
523
|
+
governed: 2,
|
|
524
|
+
grounded: 1,
|
|
525
|
+
review_required: 0,
|
|
526
|
+
};
|
|
527
|
+
/**
|
|
528
|
+
* A compound answer is exactly as trustworthy as its WEAKEST successful child.
|
|
529
|
+
*
|
|
530
|
+
* The previous rule was `every child completed ? 'governed' : 'review_required'`,
|
|
531
|
+
* which stamped `governed` on a parent whose children were review-required
|
|
532
|
+
* generated SQL — completion is not proof. That is a governance violation and
|
|
533
|
+
* the worst possible failure for this product: the reader is told a number
|
|
534
|
+
* carries governed authority when nothing proved it.
|
|
535
|
+
*
|
|
536
|
+
* `certified` is deliberately NOT reachable here. Certified trust is granted
|
|
537
|
+
* only by executing the exact certified artifact; a parent that merely
|
|
538
|
+
* assembled certified children did not execute one, so it caps at `governed`.
|
|
539
|
+
*/
|
|
540
|
+
export function compoundTrustState(childTrust) {
|
|
541
|
+
if (childTrust.length === 0)
|
|
542
|
+
return 'review_required';
|
|
543
|
+
const weakest = childTrust.reduce((low, current) => (TRUST_RANK[current] ?? 0) < (TRUST_RANK[low] ?? 0) ? current : low);
|
|
544
|
+
return weakest === 'certified' ? 'governed' : weakest;
|
|
545
|
+
}
|
|
546
|
+
/** Neutral parent outcome: governed only when every child completed governed. */
|
|
547
|
+
export function compoundStopReason(completedCount, childCount, trustState) {
|
|
548
|
+
return completedCount === childCount && childCount > 0 && trustState === 'governed'
|
|
549
|
+
? 'governed_compound_answer'
|
|
550
|
+
: 'human_review_required';
|
|
551
|
+
}
|
|
428
552
|
export function semanticAnswerHasPassedAggregationProof(governedAnswer) {
|
|
429
553
|
return governedAnswer.route?.tier === 'semantic_metric'
|
|
430
554
|
&& governedAnswer.aggregationSafetyProof?.status === 'safe';
|
|
@@ -433,6 +557,18 @@ export function agentAnswerHasExecutionFailure(governedAnswer) {
|
|
|
433
557
|
return typeof governedAnswer.executionError === 'string'
|
|
434
558
|
&& governedAnswer.executionError.trim().length > 0;
|
|
435
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* Return only a router/producer-issued analytical gap witness.
|
|
562
|
+
*
|
|
563
|
+
* `terminalOutcome.kind === 'modeling_gap'` is deliberately insufficient to
|
|
564
|
+
* claim a relationship problem. Older receipts and generic tuple failures
|
|
565
|
+
* return `undefined`; callers render generic coverage guidance for those.
|
|
566
|
+
*/
|
|
567
|
+
export function persistedAnalyticalGapWitness(routeDecision) {
|
|
568
|
+
return routeDecision?.terminalOutcome?.kind === 'modeling_gap'
|
|
569
|
+
? routeDecision.terminalOutcome.gap
|
|
570
|
+
: undefined;
|
|
571
|
+
}
|
|
436
572
|
/** Rebuild the immutable failed-run input from the artifact retained by API-007. */
|
|
437
573
|
export function analyticalFailedRunFromAgentRun(run) {
|
|
438
574
|
for (const artifact of run.artifacts) {
|
|
@@ -784,7 +920,7 @@ function businessNarrativeGaps(warnings) {
|
|
|
784
920
|
// When a run carries a threadId, the persisted thread is the authoritative
|
|
785
921
|
// source of prior turns (survives refresh); the client-built context remains
|
|
786
922
|
// the fallback for embedders that never send a threadId.
|
|
787
|
-
async function conversationContextFromThread(store, threadId, clientContext, question) {
|
|
923
|
+
async function conversationContextFromThread(store, threadId, clientContext, question, preservePendingClarification = false) {
|
|
788
924
|
// Token-budget backstop: six verbatim turns with per-field caps. Older turns
|
|
789
925
|
// remain reachable through the rolling summary + semantic recall below —
|
|
790
926
|
// carrying more raw prose mostly slows every provider call on follow-ups.
|
|
@@ -832,7 +968,10 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
832
968
|
const thread = store.getThread(threadId);
|
|
833
969
|
// Bounded structured snapshot (working state + rolling summary + topic relation)
|
|
834
970
|
// for the answer loop's conversation-state prompt section.
|
|
835
|
-
const serverSnapshot = buildConversationSnapshot(store, threadId, {
|
|
971
|
+
const serverSnapshot = buildConversationSnapshot(store, threadId, {
|
|
972
|
+
question,
|
|
973
|
+
preservePendingClarification,
|
|
974
|
+
});
|
|
836
975
|
if (serverSnapshot && question) {
|
|
837
976
|
// Semantic recall over OLDER turns (the recent window is already verbatim).
|
|
838
977
|
serverSnapshot.recalledTurns = await recallRelevantTurns(store, threadId, question, {
|
|
@@ -840,12 +979,26 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
840
979
|
excludeTurnIds: serverSnapshot.recentTurns.map((turn) => turn.id),
|
|
841
980
|
});
|
|
842
981
|
}
|
|
982
|
+
const pendingSelection = serverSnapshot?.pendingClarification?.selection;
|
|
983
|
+
const pendingSourceTurnId = serverSnapshot?.pendingClarification?.sourceTurnId;
|
|
984
|
+
// This value never crosses the HTTP boundary from a client. It is rebuilt
|
|
985
|
+
// only after the local conversation store has resolved the requested thread,
|
|
986
|
+
// and the router requires it for every selectedEvidenceId continuation.
|
|
987
|
+
const serverIssuedClarificationSelection = pendingSelection?.snapshotId && pendingSourceTurnId
|
|
988
|
+
? {
|
|
989
|
+
version: 1,
|
|
990
|
+
threadId,
|
|
991
|
+
sourceTurnId: pendingSourceTurnId,
|
|
992
|
+
snapshotId: pendingSelection.snapshotId,
|
|
993
|
+
}
|
|
994
|
+
: undefined;
|
|
843
995
|
return {
|
|
844
996
|
...(sanitizeClientConversationContext(clientContext) ?? {}),
|
|
845
997
|
conversationStateVersion: 1,
|
|
846
998
|
threadId,
|
|
847
999
|
...(serverSnapshot ? { conversationEnvelope: serverSnapshot } : {}),
|
|
848
1000
|
...(serverSnapshot ? { serverSnapshot } : {}),
|
|
1001
|
+
...(serverIssuedClarificationSelection ? { serverIssuedClarificationSelection } : {}),
|
|
849
1002
|
...(thread?.rollingSummary ? { conversationSummary: thread.rollingSummary } : {}),
|
|
850
1003
|
// `activeTurnId` is the FOLLOW-UP ANCHOR: the turn whose filters, prior DQL
|
|
851
1004
|
// artifact, and source SQL the next question builds on. Anchoring it to the
|
|
@@ -1015,6 +1168,40 @@ export function conversationTurnInputFromRun(run) {
|
|
|
1015
1168
|
const contextPack = agentRunRecord(payload?.contextPack);
|
|
1016
1169
|
const questionPlan = agentRunRecord(contextPack?.questionPlan);
|
|
1017
1170
|
const requestedShape = agentRunRecord(questionPlan?.requestedShape);
|
|
1171
|
+
// A structured clarification choice must survive reload/restart with the
|
|
1172
|
+
// exact option IDs and typed requirements that rendered it. This is stored
|
|
1173
|
+
// inside the existing JSON contract envelope so older conversation rows stay
|
|
1174
|
+
// readable; the router treats it as reject-only continuity evidence and
|
|
1175
|
+
// always rechecks the current snapshot before it can freeze a plan.
|
|
1176
|
+
// Most analytical clarifications retain the router-owned cascade requirement
|
|
1177
|
+
// set. Evidence-only ambiguity deliberately has no frozen cascade, however,
|
|
1178
|
+
// and its first rendered options still need a typed, reload-safe contract.
|
|
1179
|
+
// Derive that narrow fallback from the immutable original question and the
|
|
1180
|
+
// already-resolved intent—not from a later click or client context—so the
|
|
1181
|
+
// first valid structured selection can be revalidated without weakening the
|
|
1182
|
+
// server-issued-envelope requirement.
|
|
1183
|
+
const clarificationRequirements = run.diagnosticReceiptV3?.cascade?.requirements
|
|
1184
|
+
?? run.routeDecision?.analyticalCascadeDecision?.requirements
|
|
1185
|
+
?? (run.status === 'needs_clarification' && (run.clarificationOptions?.length ?? 0) > 0
|
|
1186
|
+
? buildAnalyticalRequirementSet({
|
|
1187
|
+
question: run.question,
|
|
1188
|
+
parsedIntent: run.routeDecision?.meaningResolution?.queryIntent,
|
|
1189
|
+
})
|
|
1190
|
+
: undefined);
|
|
1191
|
+
const clarificationSelection = run.status === 'needs_clarification'
|
|
1192
|
+
&& (run.clarificationOptions?.length ?? 0) > 0
|
|
1193
|
+
? {
|
|
1194
|
+
version: 1,
|
|
1195
|
+
optionIds: [...new Set(run.clarificationOptions.map((option) => option.id).filter(Boolean))].slice(0, 16),
|
|
1196
|
+
ambiguityCandidateIds: [...new Set(run.clarificationOptions.map((option) => option.id).filter(Boolean))].slice(0, 16),
|
|
1197
|
+
...(clarificationRequirements
|
|
1198
|
+
? { requirements: clarificationRequirements }
|
|
1199
|
+
: {}),
|
|
1200
|
+
...(run.routeDecision?.retrievalEvidence?.snapshotId
|
|
1201
|
+
? { snapshotId: run.routeDecision.retrievalEvidence.snapshotId }
|
|
1202
|
+
: {}),
|
|
1203
|
+
}
|
|
1204
|
+
: undefined;
|
|
1018
1205
|
const rowCountRaw = result?.rowCount;
|
|
1019
1206
|
const measureColumns = conversationMeasureColumns(columns, requestedShape, rows);
|
|
1020
1207
|
return {
|
|
@@ -1023,7 +1210,13 @@ export function conversationTurnInputFromRun(run) {
|
|
|
1023
1210
|
answerSummary: run.answer ?? run.summary,
|
|
1024
1211
|
answerText: run.answer,
|
|
1025
1212
|
route: run.route,
|
|
1026
|
-
|
|
1213
|
+
// A route/context label is presentation metadata, not the terminal trust
|
|
1214
|
+
// authority. In particular, a certified run can retain a `mixed` context
|
|
1215
|
+
// label from candidates considered before the certified tuple froze. Using
|
|
1216
|
+
// that label here made the persisted conversation contradict the immutable
|
|
1217
|
+
// run. Persist the canonical run state unless the answer itself has
|
|
1218
|
+
// multiple materially different answer sections.
|
|
1219
|
+
trustLabel: conversationTrustLabelFromRun(run, payload),
|
|
1027
1220
|
runStatus: run.status,
|
|
1028
1221
|
stopReason: run.stopReason,
|
|
1029
1222
|
// Persisted so a later turn can tell a refusal from an answer. `runStatus`
|
|
@@ -1039,6 +1232,7 @@ export function conversationTurnInputFromRun(run) {
|
|
|
1039
1232
|
sql: agentRunString(payload?.proposedSql) ?? agentRunString(payload?.sql),
|
|
1040
1233
|
dqlArtifact: agentRunRecord(payload?.dqlArtifact),
|
|
1041
1234
|
cascade: agentRunRecord(payload?.cascade),
|
|
1235
|
+
narrationIntegrityReceipt: run.narrationIntegrityReceipt,
|
|
1042
1236
|
result: columns.length > 0 || rows.length > 0
|
|
1043
1237
|
? {
|
|
1044
1238
|
columns,
|
|
@@ -1048,9 +1242,41 @@ export function conversationTurnInputFromRun(run) {
|
|
|
1048
1242
|
rowCount: typeof rowCountRaw === 'number' ? rowCountRaw : rows.length || undefined,
|
|
1049
1243
|
}
|
|
1050
1244
|
: undefined,
|
|
1051
|
-
contract:
|
|
1245
|
+
contract: {
|
|
1246
|
+
...(requestedShape ?? {}),
|
|
1247
|
+
...(clarificationSelection ? { clarificationSelection } : {}),
|
|
1248
|
+
},
|
|
1052
1249
|
};
|
|
1053
1250
|
}
|
|
1251
|
+
function conversationTrustLabelFromRun(run, payload) {
|
|
1252
|
+
if (!isCanonicalConversationTrustState(run.trustState)) {
|
|
1253
|
+
return agentRunString(payload?.trustLabel);
|
|
1254
|
+
}
|
|
1255
|
+
return hasMixedAnswerSectionTrust(run.artifacts) ? 'mixed' : run.trustState;
|
|
1256
|
+
}
|
|
1257
|
+
function isCanonicalConversationTrustState(value) {
|
|
1258
|
+
return value === 'certified'
|
|
1259
|
+
|| value === 'governed'
|
|
1260
|
+
|| value === 'grounded'
|
|
1261
|
+
|| value === 'review_required'
|
|
1262
|
+
|| value === 'blocked'
|
|
1263
|
+
|| value === 'not_applicable';
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* `mixed` is meaningful only when the durable answer contains sections with
|
|
1267
|
+
* materially different trust states. Supporting artifacts such as a DQL draft
|
|
1268
|
+
* or a diagnostics receipt do not turn one certified answer into mixed trust.
|
|
1269
|
+
*/
|
|
1270
|
+
function hasMixedAnswerSectionTrust(artifacts) {
|
|
1271
|
+
const states = new Set(artifacts
|
|
1272
|
+
.filter((artifact) => artifact.kind === 'answer' || artifact.kind === 'research_run')
|
|
1273
|
+
.map((artifact) => artifact.trustState)
|
|
1274
|
+
.filter((state) => state === 'certified'
|
|
1275
|
+
|| state === 'governed'
|
|
1276
|
+
|| state === 'grounded'
|
|
1277
|
+
|| state === 'review_required'));
|
|
1278
|
+
return states.size > 1;
|
|
1279
|
+
}
|
|
1054
1280
|
function conversationResultColumns(value) {
|
|
1055
1281
|
if (!Array.isArray(value))
|
|
1056
1282
|
return [];
|
|
@@ -1062,10 +1288,24 @@ function conversationResultColumns(value) {
|
|
|
1062
1288
|
.slice(0, 24);
|
|
1063
1289
|
}
|
|
1064
1290
|
function conversationMeasureColumns(columns, requestedShape, rows) {
|
|
1291
|
+
// A requested phrase is not evidence that the execution returned that
|
|
1292
|
+
// measure. In particular, an incomplete certified block used to persist
|
|
1293
|
+
// `revenue` beside its actual `lifetime_spend` output merely because the
|
|
1294
|
+
// request said revenue. Retain an exact requested column only when it is
|
|
1295
|
+
// actually present in the result contract.
|
|
1065
1296
|
const requested = conversationStringArray(requestedShape?.measures) ?? [];
|
|
1297
|
+
const canonical = (value) => value.toLowerCase()
|
|
1298
|
+
.replace(/[_./:-]+/g, ' ')
|
|
1299
|
+
.replace(/[^a-z0-9 ]+/g, ' ')
|
|
1300
|
+
.replace(/\s+/g, ' ')
|
|
1301
|
+
.trim();
|
|
1302
|
+
const actualRequestedColumns = columns.filter((column) => {
|
|
1303
|
+
const identity = canonical(column);
|
|
1304
|
+
return identity.length > 0 && requested.some((measure) => canonical(measure) === identity);
|
|
1305
|
+
});
|
|
1066
1306
|
const numericColumns = columns.filter((column) => rows.some((row) => typeof row[column] === 'number' && Number.isFinite(row[column])));
|
|
1067
1307
|
const metricNamedColumns = columns.filter((column) => /\b(revenue|sales|amount|total|count|average|avg|sum|spend|cost|margin|profit|value|points?|score|quantity|units?|rate|volume)\b/i.test(column.replace(/_/g, ' ')));
|
|
1068
|
-
const unique = Array.from(new Set([...
|
|
1308
|
+
const unique = Array.from(new Set([...actualRequestedColumns, ...numericColumns, ...metricNamedColumns]
|
|
1069
1309
|
.map((value) => value.trim())
|
|
1070
1310
|
.filter(Boolean)));
|
|
1071
1311
|
return unique.length > 0 ? unique.slice(0, 24) : undefined;
|
|
@@ -1166,12 +1406,7 @@ export class RunScopedProviderDispatchEvidence {
|
|
|
1166
1406
|
* from what it already has.
|
|
1167
1407
|
*/
|
|
1168
1408
|
expectedDispatchMs() {
|
|
1169
|
-
|
|
1170
|
-
return ASSUMED_PROVIDER_DISPATCH_MS;
|
|
1171
|
-
const sorted = [...this.observedDispatchDurations].sort((left, right) => left - right);
|
|
1172
|
-
// The slowest observed call is the honest predictor: an optimistic median
|
|
1173
|
-
// still admits a dispatch that the deadline then kills.
|
|
1174
|
-
return sorted[sorted.length - 1];
|
|
1409
|
+
return predictDispatchMs(this.observedDispatchDurations);
|
|
1175
1410
|
}
|
|
1176
1411
|
/** True when the remaining wall clock cannot fit another provider call. */
|
|
1177
1412
|
cannotFitAnotherDispatch() {
|
|
@@ -1364,6 +1599,50 @@ function mergeRunScopedProviderDispatchEvidence(run, evidence) {
|
|
|
1364
1599
|
diagnosticReceiptV2,
|
|
1365
1600
|
};
|
|
1366
1601
|
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Final physical generated-SQL boundary.
|
|
1604
|
+
*
|
|
1605
|
+
* This receives the exact prepared statement immediately before the connector
|
|
1606
|
+
* callback. It intentionally validates before invoking `execute`: a bad
|
|
1607
|
+
* capability or unproven prepared reference must result in zero warehouse
|
|
1608
|
+
* calls, not a post-execution warning. It is module-exported only for the
|
|
1609
|
+
* local-runtime boundary harness; it is never an HTTP API or durable artifact.
|
|
1610
|
+
*
|
|
1611
|
+
* @internal
|
|
1612
|
+
*/
|
|
1613
|
+
export async function executePreparedAgenticSqlBoundary(input) {
|
|
1614
|
+
const capability = input.capability;
|
|
1615
|
+
if (capability) {
|
|
1616
|
+
const authorization = mintFinalSqlAuthorization({
|
|
1617
|
+
sql: input.preparedSql,
|
|
1618
|
+
proven: capability.provenIdentifiers.map((identifier) => ({
|
|
1619
|
+
identifier,
|
|
1620
|
+
evidence: capability.evidence[identifier] ?? 'catalog',
|
|
1621
|
+
})),
|
|
1622
|
+
runId: capability.runId,
|
|
1623
|
+
executionId: capability.executionId,
|
|
1624
|
+
snapshotId: capability.snapshotId,
|
|
1625
|
+
planId: capability.planId,
|
|
1626
|
+
targetFingerprint: capability.targetFingerprint,
|
|
1627
|
+
bindings: input.bindings,
|
|
1628
|
+
});
|
|
1629
|
+
const validation = validateAuthorizedSqlReferences(input.preparedSql, undefined);
|
|
1630
|
+
const verdict = verifyFinalSql(authorization, input.preparedSql, qualifyAuthorizationReferences(input.preparedSql, {
|
|
1631
|
+
relations: validation.referencedRelations ?? [],
|
|
1632
|
+
columns: validation.referencedColumns ?? [],
|
|
1633
|
+
}), {
|
|
1634
|
+
...input.scope,
|
|
1635
|
+
bindings: input.bindings,
|
|
1636
|
+
});
|
|
1637
|
+
if (process.env.DQL_ORCHESTRATOR_TRACE) {
|
|
1638
|
+
console.warn(`[dql] execution authorization: ${verdict.ok ? 'admitted' : 'REFUSED'} proven=${authorization.provenIdentifiers.length}${verdict.ok ? '' : ` reason=${verdict.reason}`}`);
|
|
1639
|
+
}
|
|
1640
|
+
if (!verdict.ok) {
|
|
1641
|
+
throw analyticalError(verdict.reason ?? 'The statement was not authorized for execution.', { origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql' });
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return input.execute();
|
|
1645
|
+
}
|
|
1367
1646
|
export async function startLocalServer(opts) {
|
|
1368
1647
|
const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
|
|
1369
1648
|
const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
|
|
@@ -2061,6 +2340,9 @@ export async function startLocalServer(opts) {
|
|
|
2061
2340
|
});
|
|
2062
2341
|
};
|
|
2063
2342
|
async function runGovernedAgentAnswerForRun(request, repair, route = 'generated_answer', onProgress, routeDecision) {
|
|
2343
|
+
return runGovernedAgentAnswerForRunInner(request, repair, route, onProgress, routeDecision);
|
|
2344
|
+
}
|
|
2345
|
+
async function runGovernedAgentAnswerForRunInner(request, repair, route = 'generated_answer', onProgress, routeDecision) {
|
|
2064
2346
|
const governed = resolveGovernedAnswerRunner(projectRoot);
|
|
2065
2347
|
let resolvedProvider = governed?.provider ?? null;
|
|
2066
2348
|
let runner = governed?.runner ?? null;
|
|
@@ -2081,11 +2363,12 @@ export async function startLocalServer(opts) {
|
|
|
2081
2363
|
runner = createDqlAgentProviderRunner('ollama', deterministicProvider);
|
|
2082
2364
|
}
|
|
2083
2365
|
if (!resolvedProvider || !runner) {
|
|
2084
|
-
throw new Error('No AI provider is configured. Configure a subscription (Claude Code / Codex), OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.');
|
|
2366
|
+
throw Object.assign(new Error('No AI provider is configured. Configure a subscription (Claude Code / Codex), OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.'), { code: 'AUTHENTICATION_FAILED', providerPhase: 'preflight' });
|
|
2085
2367
|
}
|
|
2086
2368
|
let governedAnswer;
|
|
2087
2369
|
let providerError;
|
|
2088
2370
|
let providerDispatchEvidence;
|
|
2371
|
+
let providerBoundaryDiagnostic;
|
|
2089
2372
|
const isRepair = (repair?.attempt ?? 0) > 0 && Boolean(repair?.repairHint);
|
|
2090
2373
|
// The chat composer sends a `thinkingMode` (auto/low/medium/high); resolve it
|
|
2091
2374
|
// into the effort+depth bundle it stands for. An explicit `reasoningEffort` /
|
|
@@ -2152,6 +2435,37 @@ export async function startLocalServer(opts) {
|
|
|
2152
2435
|
const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
|
|
2153
2436
|
const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
|
|
2154
2437
|
const runProjectSnapshot = projectSnapshot();
|
|
2438
|
+
// Keep the execution callback on the exact ranked context pack that the
|
|
2439
|
+
// router used. The engine can invoke the executor after the router's
|
|
2440
|
+
// asynchronous evidence phase, so looking the pack up lazily inside the
|
|
2441
|
+
// callback occasionally observed an empty WeakMap entry and sent an
|
|
2442
|
+
// authored leaf relation straight to the connector. That bypassed the
|
|
2443
|
+
// same-snapshot qualified relation binding despite retrieval having proved
|
|
2444
|
+
// (for example) `jaffle_shop.dev.dim_customers`.
|
|
2445
|
+
//
|
|
2446
|
+
// Building only when this request has no prepared entry retains the normal
|
|
2447
|
+
// single-retrieval path. A frozen certified plan below rejects a pack whose
|
|
2448
|
+
// snapshot/fingerprint does not match the router decision rather than
|
|
2449
|
+
// borrowing a newer catalog to make an old plan executable.
|
|
2450
|
+
const preparedContextPack = preparedAgentContextPacks.get(request)
|
|
2451
|
+
?? await buildAgentRunContextPack(request).catch(() => undefined);
|
|
2452
|
+
const preparedQualifiedSchemaContext = preparedContextPack
|
|
2453
|
+
? buildAgentSchemaContextFromContextPack(request.question, preparedContextPack)
|
|
2454
|
+
: [];
|
|
2455
|
+
const selectedExploratoryAttempt = routeDecision?.analyticalCascadeDecision?.attempts.find((attempt) => attempt.tier === 'exploratory_sql');
|
|
2456
|
+
// The exploratory candidate set belongs to the router's immutable
|
|
2457
|
+
// same-snapshot cascade decision. Build its physical prompt/execution
|
|
2458
|
+
// closure once here; the broad retrieved pack remains receipt-only and is
|
|
2459
|
+
// never handed to SQL validation for this selected tier.
|
|
2460
|
+
const exploratoryCandidateIds = routeDecision?.analyticalCascadeDecision?.selectedTier === 'exploratory_sql'
|
|
2461
|
+
? selectedExploratoryAttempt?.candidateIds ?? []
|
|
2462
|
+
: [];
|
|
2463
|
+
const preparedExploratoryContextPack = exploratoryCandidateIds.length > 0
|
|
2464
|
+
? scopeContextPackToExploratoryCandidateClosure(preparedContextPack, exploratoryCandidateIds)
|
|
2465
|
+
: undefined;
|
|
2466
|
+
const preparedExploratoryQualifiedSchemaContext = preparedExploratoryContextPack
|
|
2467
|
+
? buildAgentSchemaContextFromContextPack(request.question, preparedExploratoryContextPack, { includeUnscored: true, limit: 80 })
|
|
2468
|
+
: [];
|
|
2155
2469
|
const domainContext = requestedDomain
|
|
2156
2470
|
? resolveUiDomainContext({
|
|
2157
2471
|
manifest: runProjectSnapshot.manifest,
|
|
@@ -2163,6 +2477,146 @@ export async function startLocalServer(opts) {
|
|
|
2163
2477
|
snapshotId: runProjectSnapshot.snapshotId,
|
|
2164
2478
|
})
|
|
2165
2479
|
: undefined;
|
|
2480
|
+
// Local to this exact answer invocation. Compound children each enter this
|
|
2481
|
+
// function separately, so no child can consume another child's capability.
|
|
2482
|
+
const agenticExecutionCapabilityGate = new AgenticExecutionCapabilityGate();
|
|
2483
|
+
const prepareExploratorySqlExecution = async (sql) => {
|
|
2484
|
+
const cascade = routeDecision?.analyticalCascadeDecision;
|
|
2485
|
+
const selectedAttempt = selectedExploratoryAttempt;
|
|
2486
|
+
if (!cascade
|
|
2487
|
+
|| cascade.selectedTier !== 'exploratory_sql'
|
|
2488
|
+
|| cascade.planFrozen
|
|
2489
|
+
|| !selectedAttempt
|
|
2490
|
+
|| selectedAttempt.outcome !== 'executable'
|
|
2491
|
+
|| selectedAttempt.candidateIds.length === 0) {
|
|
2492
|
+
throw analyticalError('The generated SQL no longer matches the router-selected exploratory path, so DQL did not authorize execution.', {
|
|
2493
|
+
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
if (!request.runId || !semanticConnection || !preparedContextPack || !preparedExploratoryContextPack) {
|
|
2497
|
+
throw analyticalError('DQL could not bind the selected exploratory query to this run, target, and metadata snapshot, so it was not executed.', {
|
|
2498
|
+
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
const retrievalSnapshotId = routeDecision?.retrievalEvidence?.snapshotId;
|
|
2502
|
+
const retrievalSourceFingerprint = routeDecision?.retrievalEvidence?.sourceFingerprint;
|
|
2503
|
+
const retrievalFreezeSnapshotId = retrievalSnapshotId ?? preparedContextPack.knowledgeLens.snapshotId;
|
|
2504
|
+
if ((retrievalSnapshotId && retrievalSnapshotId !== preparedContextPack.knowledgeLens.snapshotId)
|
|
2505
|
+
|| (retrievalSourceFingerprint
|
|
2506
|
+
&& preparedContextPack.freshness.fingerprint
|
|
2507
|
+
&& retrievalSourceFingerprint !== preparedContextPack.freshness.fingerprint)) {
|
|
2508
|
+
throw analyticalError('The selected exploratory query no longer matches its retrieval snapshot or source fingerprint, so it was not executed.', {
|
|
2509
|
+
origin: 'governance_gate', stage: 'validation', code: 'snapshot_drift',
|
|
2510
|
+
});
|
|
2511
|
+
}
|
|
2512
|
+
projectSnapshot();
|
|
2513
|
+
projectSnapshots.assertCurrent(runProjectSnapshot.snapshotId);
|
|
2514
|
+
const target = await observeWarehouseTargetIdentity(executor, semanticConnection);
|
|
2515
|
+
if (generatedProposalTargetIdentity?.identityFingerprint
|
|
2516
|
+
&& target.identityFingerprint !== generatedProposalTargetIdentity.identityFingerprint) {
|
|
2517
|
+
throw analyticalError('The selected exploratory query no longer matches the observed execution target, so it was not executed.', {
|
|
2518
|
+
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
const validation = validateAuthorizedSqlReferences(sql, preparedExploratoryContextPack, {
|
|
2522
|
+
...(semanticDriver ? { dialect: semanticDriver } : {}),
|
|
2523
|
+
runtimeSchema: preparedExploratoryQualifiedSchemaContext,
|
|
2524
|
+
});
|
|
2525
|
+
if (!validation.ok) {
|
|
2526
|
+
throw analyticalError('The selected exploratory query did not pass the host SQL/context validation, so it was not executed.', {
|
|
2527
|
+
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
const normalizeIdentifier = (value) => value
|
|
2531
|
+
.trim()
|
|
2532
|
+
.split('.')
|
|
2533
|
+
.map((part) => part.trim().replace(/^["`\[]|["`\]]$/g, '').toLowerCase())
|
|
2534
|
+
.filter(Boolean)
|
|
2535
|
+
.join('.');
|
|
2536
|
+
const allowedExploratoryRelations = new Set(preparedExploratoryQualifiedSchemaContext.map((table) => normalizeIdentifier(table.relation)));
|
|
2537
|
+
const outsideClosure = validation.referencedRelations.filter((relation) => !allowedExploratoryRelations.has(normalizeIdentifier(relation)));
|
|
2538
|
+
if (outsideClosure.length > 0) {
|
|
2539
|
+
throw analyticalError('The generated SQL references a relation outside the router-selected physical closure, so it was not executed.', {
|
|
2540
|
+
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
const runtimeByRelation = new Map(preparedExploratoryQualifiedSchemaContext.map((table) => [normalizeIdentifier(table.relation), table]));
|
|
2544
|
+
const qualifiedReferences = qualifyAuthorizationReferences(sql, {
|
|
2545
|
+
relations: validation.referencedRelations,
|
|
2546
|
+
columns: validation.referencedColumns,
|
|
2547
|
+
});
|
|
2548
|
+
const proofs = new Map();
|
|
2549
|
+
for (const relation of validation.referencedRelations) {
|
|
2550
|
+
const table = runtimeByRelation.get(normalizeIdentifier(relation));
|
|
2551
|
+
if (!table) {
|
|
2552
|
+
throw analyticalError('The selected exploratory query references a relation that was not proven by the live runtime schema, so it was not executed.', {
|
|
2553
|
+
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
proofs.set(table.relation, 'schema_tool');
|
|
2557
|
+
}
|
|
2558
|
+
for (const reference of qualifiedReferences) {
|
|
2559
|
+
const normalizedReference = normalizeIdentifier(reference);
|
|
2560
|
+
const table = [...runtimeByRelation.entries()].find(([relation]) => normalizedReference.startsWith(`${relation}.`));
|
|
2561
|
+
if (!table)
|
|
2562
|
+
continue;
|
|
2563
|
+
const [, runtimeTable] = table;
|
|
2564
|
+
const column = normalizedReference.slice(`${table[0]}.`.length);
|
|
2565
|
+
if (!column || !runtimeTable.columns.some((candidate) => normalizeIdentifier(candidate.name) === column)) {
|
|
2566
|
+
throw analyticalError('The selected exploratory query references a column that was not proven by the live runtime schema, so it was not executed.', {
|
|
2567
|
+
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
proofs.set(`${runtimeTable.relation}.${column}`, 'schema_tool');
|
|
2571
|
+
}
|
|
2572
|
+
const candidateIds = [...selectedAttempt.candidateIds];
|
|
2573
|
+
const sqlFingerprint = executionFingerprint(sql);
|
|
2574
|
+
const planFingerprint = executionFingerprint(stableExecutionValue({
|
|
2575
|
+
version: 1,
|
|
2576
|
+
tier: 'exploratory_sql',
|
|
2577
|
+
snapshotId: retrievalFreezeSnapshotId,
|
|
2578
|
+
executionSnapshotId: runProjectSnapshot.snapshotId,
|
|
2579
|
+
sourceFingerprint: preparedContextPack.freshness.fingerprint,
|
|
2580
|
+
targetFingerprint: target.identityFingerprint,
|
|
2581
|
+
candidateIds,
|
|
2582
|
+
sqlFingerprint,
|
|
2583
|
+
}));
|
|
2584
|
+
const planId = `exploratory-${planFingerprint.slice(0, 24)}`;
|
|
2585
|
+
const capability = createAgenticSqlExecutionCapability({
|
|
2586
|
+
sql,
|
|
2587
|
+
proven: [...proofs.entries()].map(([identifier, evidence]) => ({ identifier, evidence })),
|
|
2588
|
+
runId: request.runId,
|
|
2589
|
+
executionId: `${request.runId}:exploratory:${sqlFingerprint.slice(0, 16)}`,
|
|
2590
|
+
snapshotId: runProjectSnapshot.snapshotId,
|
|
2591
|
+
planId,
|
|
2592
|
+
targetFingerprint: target.identityFingerprint,
|
|
2593
|
+
// Keep the capability binding shape identical to the direct execution
|
|
2594
|
+
// boundary below. An omitted generated-artifact binding is represented
|
|
2595
|
+
// there as `{ sqlParams: [], variables: {} }`, not `{}`; minting the
|
|
2596
|
+
// latter made a fully validated immutable proposal fail only after its
|
|
2597
|
+
// exploratory plan had frozen.
|
|
2598
|
+
bindings: { sqlParams: [], variables: {} },
|
|
2599
|
+
});
|
|
2600
|
+
if (!capability) {
|
|
2601
|
+
throw analyticalError('DQL could not mint a request-scoped exploratory execution capability, so it was not executed.', {
|
|
2602
|
+
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
return {
|
|
2606
|
+
capability,
|
|
2607
|
+
freeze: {
|
|
2608
|
+
version: 1,
|
|
2609
|
+
selectedTier: 'exploratory_sql',
|
|
2610
|
+
planId,
|
|
2611
|
+
planFingerprint,
|
|
2612
|
+
snapshotId: retrievalFreezeSnapshotId,
|
|
2613
|
+
targetFingerprint: target.identityFingerprint,
|
|
2614
|
+
sqlFingerprint: capability.candidateSqlFingerprint,
|
|
2615
|
+
candidateIds,
|
|
2616
|
+
authorization: 'capability_minted',
|
|
2617
|
+
},
|
|
2618
|
+
};
|
|
2619
|
+
};
|
|
2166
2620
|
await runner.run({
|
|
2167
2621
|
provider: resolvedProvider,
|
|
2168
2622
|
...(agentRunProviderEvidenceContext.getStore()
|
|
@@ -2187,10 +2641,21 @@ export async function startLocalServer(opts) {
|
|
|
2187
2641
|
},
|
|
2188
2642
|
reasoningEffort,
|
|
2189
2643
|
...(analysisDepth ? { analysisDepth } : {}),
|
|
2644
|
+
orchestrationMode: route === 'research' ? 'research' : 'ask',
|
|
2190
2645
|
allowProviderSemanticMemberSelection: route === 'research',
|
|
2191
2646
|
researchResultRowsOptIn: route === 'research' && request.researchResultRowsOptIn === true,
|
|
2192
2647
|
projectRoot,
|
|
2193
|
-
|
|
2648
|
+
// Keys the execution authorization, so the proofs the analyst loop
|
|
2649
|
+
// gathers can be checked against the statement this run executes.
|
|
2650
|
+
...(request.runId ? { agentRunId: request.runId } : {}),
|
|
2651
|
+
preparedContextPack,
|
|
2652
|
+
// This closure is derived only from the router-owned cascade attempt
|
|
2653
|
+
// above. It is intentionally not sourced from the HTTP request: a
|
|
2654
|
+
// client or provider cannot widen the relations the exploratory prompt
|
|
2655
|
+
// or capability may use.
|
|
2656
|
+
...(preparedExploratoryContextPack
|
|
2657
|
+
? { preparedExploratoryContextPack }
|
|
2658
|
+
: {}),
|
|
2194
2659
|
domainContext,
|
|
2195
2660
|
projectSnapshot: { snapshotId: runProjectSnapshot.snapshotId, manifest: runProjectSnapshot.manifest },
|
|
2196
2661
|
assertProjectSnapshot: (snapshotId) => {
|
|
@@ -2351,12 +2816,73 @@ export async function startLocalServer(opts) {
|
|
|
2351
2816
|
...(routeDecision?.resolvedAnalyticalPlan
|
|
2352
2817
|
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
2353
2818
|
: {}),
|
|
2819
|
+
...(routeDecision?.analyticalCascadeDecision?.selectedTier
|
|
2820
|
+
? { selectedCascadeTier: routeDecision.analyticalCascadeDecision.selectedTier }
|
|
2821
|
+
: {}),
|
|
2822
|
+
...(exploratoryCandidateIds.length > 0
|
|
2823
|
+
? { exploratoryCandidateIds }
|
|
2824
|
+
: {}),
|
|
2354
2825
|
...(generatedProposalTargetIdentity?.identityFingerprint
|
|
2355
2826
|
? { generatedProposalTargetFingerprint: generatedProposalTargetIdentity.identityFingerprint }
|
|
2356
2827
|
: {}),
|
|
2357
2828
|
analyticalReferenceInstant: new Date().toISOString(),
|
|
2358
|
-
|
|
2829
|
+
// A frozen certified artifact may use the human-friendly leaf relation
|
|
2830
|
+
// authored in its DQL. Its execution must still bind to the exact
|
|
2831
|
+
// same-snapshot physical relation that retrieval inspected. This is
|
|
2832
|
+
// deliberately narrower than exploratory preflight: it only qualifies
|
|
2833
|
+
// an unambiguous FROM/JOIN leaf already present in the artifact and it
|
|
2834
|
+
// never supplies a missing table, join, or column.
|
|
2835
|
+
executeCertifiedBlock: (node, invocation) => {
|
|
2836
|
+
const frozenCertifiedPlan = routeDecision?.analyticalCascadeDecision?.planFrozen === true
|
|
2837
|
+
&& routeDecision.analyticalCascadeDecision.selectedTier === 'certified'
|
|
2838
|
+
&& routeDecision.resolvedAnalyticalPlan?.capability === 'certified_execution';
|
|
2839
|
+
if (frozenCertifiedPlan) {
|
|
2840
|
+
const plan = routeDecision.resolvedAnalyticalPlan;
|
|
2841
|
+
const retrievedSourceFingerprint = routeDecision.retrievalEvidence?.sourceFingerprint;
|
|
2842
|
+
const retrievedSnapshotId = routeDecision.retrievalEvidence?.snapshotId;
|
|
2843
|
+
// A certified stamp is valid only for the exact router snapshot
|
|
2844
|
+
// and retrieval source that selected this block. The runner's
|
|
2845
|
+
// standard guard separately confirms the current host snapshot
|
|
2846
|
+
// immediately before this callback. Do not re-fingerprint the
|
|
2847
|
+
// mutable local cache here: cache refresh is not a source edit.
|
|
2848
|
+
if (retrievedSnapshotId && plan.snapshotId !== retrievedSnapshotId) {
|
|
2849
|
+
throw analyticalError('The selected certified plan no longer matches the retrieval snapshot that froze it. Refresh the answer before retrying.', { origin: 'governance_gate', stage: 'validation', code: 'snapshot_drift' });
|
|
2850
|
+
}
|
|
2851
|
+
if (plan.sourceFingerprint && retrievedSourceFingerprint
|
|
2852
|
+
&& plan.sourceFingerprint !== retrievedSourceFingerprint) {
|
|
2853
|
+
throw analyticalError('The selected certified plan no longer matches the retrieval source that froze it. Refresh the answer before retrying.', { origin: 'governance_gate', stage: 'validation', code: 'snapshot_drift' });
|
|
2854
|
+
}
|
|
2855
|
+
if (!preparedContextPack
|
|
2856
|
+
|| plan.snapshotId !== preparedContextPack.knowledgeLens.snapshotId) {
|
|
2857
|
+
throw analyticalError('The selected certified plan no longer matches the retrieved metadata snapshot. Refresh the answer before retrying.', { origin: 'governance_gate', stage: 'validation', code: 'snapshot_drift' });
|
|
2858
|
+
}
|
|
2859
|
+
const preparedSourceFingerprint = preparedContextPack.freshness.fingerprint;
|
|
2860
|
+
if (plan.sourceFingerprint && preparedSourceFingerprint
|
|
2861
|
+
&& plan.sourceFingerprint !== preparedSourceFingerprint) {
|
|
2862
|
+
throw analyticalError('The selected certified plan no longer matches the retrieved metadata source. Refresh the answer before retrying.', { origin: 'governance_gate', stage: 'validation', code: 'snapshot_drift' });
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
const certifiedSchemaContext = frozenCertifiedPlan
|
|
2866
|
+
? buildFrozenCertifiedSchemaContext(preparedContextPack, runProjectSnapshot.manifest)
|
|
2867
|
+
: preparedQualifiedSchemaContext;
|
|
2868
|
+
return executeCertifiedBlockForAgent(node, invocation, semanticConnection, semanticConnectionName, certifiedSchemaContext, frozenCertifiedPlan);
|
|
2869
|
+
},
|
|
2359
2870
|
executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName),
|
|
2871
|
+
prepareExploratorySqlExecution,
|
|
2872
|
+
executeAgenticGeneratedSql: async (capability, sql, artifact) => {
|
|
2873
|
+
if (!agenticExecutionCapabilityGate.consume(capability)) {
|
|
2874
|
+
throw analyticalError('This analyst execution capability was already consumed; DQL did not retry it with stale proof.', {
|
|
2875
|
+
origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql',
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
return executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName, capability, {
|
|
2879
|
+
runId: request.runId,
|
|
2880
|
+
executionId: capability.executionId,
|
|
2881
|
+
snapshotId: runProjectSnapshot.snapshotId,
|
|
2882
|
+
planId: capability.planId,
|
|
2883
|
+
targetFingerprint: capability.targetFingerprint,
|
|
2884
|
+
});
|
|
2885
|
+
},
|
|
2360
2886
|
executeDqlArtifact: (artifact) => executeArtifactReferenceForAgent(artifact, request.question, semanticConnection, semanticConnectionName),
|
|
2361
2887
|
getSchemaContext: (question, preparedContextPack) => getSchemaContextForAgent(question, preparedContextPack, semanticConnection, request.executionTarget?.target === 'connection'
|
|
2362
2888
|
? request.executionTarget.connectionName
|
|
@@ -2371,10 +2897,14 @@ export async function startLocalServer(opts) {
|
|
|
2371
2897
|
if (turn.kind === 'error') {
|
|
2372
2898
|
providerError = turn.message;
|
|
2373
2899
|
providerDispatchEvidence = turn.dispatchEvidence;
|
|
2900
|
+
providerBoundaryDiagnostic = turn.providerDiagnostic;
|
|
2374
2901
|
}
|
|
2375
2902
|
}, runSignal);
|
|
2376
2903
|
if (!governedAnswer) {
|
|
2377
|
-
throw Object.assign(new Error(providerError ?? 'The AI provider did not return a governed answer.'),
|
|
2904
|
+
throw Object.assign(new Error(providerError ?? 'The AI provider did not return a governed answer.'), {
|
|
2905
|
+
...(providerDispatchEvidence ? { providerDispatchEvidence } : {}),
|
|
2906
|
+
...(providerBoundaryDiagnostic ? { providerDiagnostic: providerBoundaryDiagnostic } : {}),
|
|
2907
|
+
});
|
|
2378
2908
|
}
|
|
2379
2909
|
return governedAnswer;
|
|
2380
2910
|
}
|
|
@@ -2412,6 +2942,68 @@ export async function startLocalServer(opts) {
|
|
|
2412
2942
|
function resolvedRunRouteFromAnswer(governedAnswer) {
|
|
2413
2943
|
return routeForCascadeAnswerTier(governedAnswer.route?.tier);
|
|
2414
2944
|
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Return the route the router already froze. This is intentionally derived
|
|
2947
|
+
* from the typed cascade/plan, never from an answer-loop route label.
|
|
2948
|
+
*/
|
|
2949
|
+
function frozenAnalyticalRoute(routeDecision) {
|
|
2950
|
+
if (!routeDecision)
|
|
2951
|
+
return undefined;
|
|
2952
|
+
const cascade = routeDecision.analyticalCascadeDecision;
|
|
2953
|
+
if (cascade?.planFrozen) {
|
|
2954
|
+
switch (cascade.selectedTier) {
|
|
2955
|
+
case 'certified': return 'certified_answer';
|
|
2956
|
+
case 'semantic': return 'semantic_answer';
|
|
2957
|
+
case 'governed_relational':
|
|
2958
|
+
case 'exploratory_sql': return 'generated_answer';
|
|
2959
|
+
default: return undefined;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
const plan = routeDecision.resolvedAnalyticalPlan;
|
|
2963
|
+
if (plan?.mode !== 'authoritative')
|
|
2964
|
+
return undefined;
|
|
2965
|
+
switch (plan.capability) {
|
|
2966
|
+
case 'certified_execution': return 'certified_answer';
|
|
2967
|
+
case 'semantic_execution': return 'semantic_answer';
|
|
2968
|
+
case 'governed_relational':
|
|
2969
|
+
case 'bounded_exploration': return 'generated_answer';
|
|
2970
|
+
default: return undefined;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* A frozen tier may fail, but cannot silently substitute a result selected by
|
|
2975
|
+
* a different answer-loop lane. The engine repeats this guard so host-injected
|
|
2976
|
+
* executors receive the same protection.
|
|
2977
|
+
*/
|
|
2978
|
+
function frozenPlanRouteFailure(route, routeDecision, reportedRoute) {
|
|
2979
|
+
const message = `The frozen ${route.replaceAll('_', ' ')} plan could not execute as selected. DQL did not substitute another analytical tier.`;
|
|
2980
|
+
return {
|
|
2981
|
+
resolvedRoute: route,
|
|
2982
|
+
status: 'blocked',
|
|
2983
|
+
trustState: 'blocked',
|
|
2984
|
+
stopReason: 'blocked',
|
|
2985
|
+
// Keep the public refusal vocabulary stable; the artifact/evaluation
|
|
2986
|
+
// below retains the precise route-integrity diagnostic.
|
|
2987
|
+
answerRefusalCode: 'grounding_gap',
|
|
2988
|
+
summary: message,
|
|
2989
|
+
answer: message,
|
|
2990
|
+
artifacts: [agentRunArtifact('answer', 'Frozen analytical plan could not execute', {
|
|
2991
|
+
kind: 'no_answer',
|
|
2992
|
+
refusalCode: 'grounding_gap',
|
|
2993
|
+
frozenPlanFailureCode: 'frozen_plan_route_mismatch',
|
|
2994
|
+
text: message,
|
|
2995
|
+
selectedTier: routeDecision?.analyticalCascadeDecision?.selectedTier,
|
|
2996
|
+
selectedConceptIds: routeDecision?.meaningResolution?.selectedConceptIds ?? [],
|
|
2997
|
+
...(reportedRoute ? { reportedRoute } : {}),
|
|
2998
|
+
}, undefined, 'blocked')],
|
|
2999
|
+
evaluations: [agentRunEvaluation('frozen-plan-route-mismatch', 'Frozen analytical route', false, 'blocking', `The router froze ${route.replaceAll('_', ' ')}, but the answer executor reported ${reportedRoute?.replaceAll('_', ' ') ?? 'no matching route'}.`, {
|
|
3000
|
+
selectedRoute: route,
|
|
3001
|
+
reportedRoute,
|
|
3002
|
+
selectedTier: routeDecision?.analyticalCascadeDecision?.selectedTier,
|
|
3003
|
+
planId: routeDecision?.resolvedAnalyticalPlan?.planId,
|
|
3004
|
+
})],
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
2415
3007
|
function groundingGapRepairHint(governedAnswer) {
|
|
2416
3008
|
const details = governedAnswer.refusalDetails;
|
|
2417
3009
|
if (!details)
|
|
@@ -2625,10 +3217,20 @@ export async function startLocalServer(opts) {
|
|
|
2625
3217
|
if (governedAnswer.result)
|
|
2626
3218
|
delete governedAnswer.result.dqlArtifact;
|
|
2627
3219
|
}
|
|
2628
|
-
const answerRunExecutor = async ({ request, route, routeDecision, attempt, repairHint, emit }) => {
|
|
3220
|
+
const answerRunExecutor = async ({ runId, request, route, routeDecision, attempt, repairHint, emit }) => {
|
|
3221
|
+
// `AgentRunEngine` owns the canonical run ID when HTTP callers omit one.
|
|
3222
|
+
// Bind that server-issued value to the same request object before the
|
|
3223
|
+
// provider/SQL handoff: the prepared context pack is keyed by this object,
|
|
3224
|
+
// while the exploratory capability must be scoped to the persisted run.
|
|
3225
|
+
// Cloning here would lose the exact retrieval pack; accepting a missing ID
|
|
3226
|
+
// would mint an unbound capability. This is server-side bookkeeping only,
|
|
3227
|
+
// never client-provided execution authority.
|
|
3228
|
+
if (!request.runId)
|
|
3229
|
+
request.runId = runId;
|
|
2629
3230
|
const runStartedAtMs = Date.now();
|
|
2630
3231
|
const turnPlan = buildAnalyticalTurnPlan({
|
|
2631
3232
|
question: request.question,
|
|
3233
|
+
mode: route === 'research' ? 'research' : 'ask',
|
|
2632
3234
|
turnId: request.runId,
|
|
2633
3235
|
candidateIds: routeDecision?.retrievalEvidence?.candidateIds ?? [],
|
|
2634
3236
|
frozen: routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative',
|
|
@@ -2641,18 +3243,27 @@ export async function startLocalServer(opts) {
|
|
|
2641
3243
|
// is copied into several labels. Independent children share the parent's
|
|
2642
3244
|
// signal/deadline and return truthful partial success.
|
|
2643
3245
|
if (turnPlan.tasks.length > 1 && !childTurn && (attempt ?? 0) === 0) {
|
|
2644
|
-
const
|
|
3246
|
+
const runChildTask = async (task, dependencyBinding) => {
|
|
2645
3247
|
if (request.signal?.aborted)
|
|
2646
3248
|
rethrowIfCancelled(request.signal.reason, request.signal);
|
|
2647
3249
|
try {
|
|
2648
3250
|
const childRequest = {
|
|
2649
3251
|
...request,
|
|
2650
3252
|
question: task.question,
|
|
3253
|
+
...(dependencyBinding ? {
|
|
3254
|
+
conversationContext: {
|
|
3255
|
+
...(request.conversationContext ?? {}),
|
|
3256
|
+
// This is the complete parent-to-child data boundary: no
|
|
3257
|
+
// parent prose, SQL, or rows cross into a dependent clause.
|
|
3258
|
+
analyticalTaskDependencyBinding: dependencyBinding,
|
|
3259
|
+
},
|
|
3260
|
+
} : {}),
|
|
2651
3261
|
workspaceContext: {
|
|
2652
3262
|
...(request.workspaceContext && typeof request.workspaceContext === 'object' ? request.workspaceContext : {}),
|
|
2653
3263
|
analyticalTaskChild: true,
|
|
2654
3264
|
analyticalParentRunId: request.runId,
|
|
2655
3265
|
analyticalTaskId: task.id,
|
|
3266
|
+
...(dependencyBinding ? { analyticalTaskDependencyBinding: dependencyBinding } : {}),
|
|
2656
3267
|
},
|
|
2657
3268
|
};
|
|
2658
3269
|
const answer = await runGovernedAgentAnswerForRun(childRequest, { attempt: 0, repairHint }, route, (message) => emit({ type: 'executor.started', message: `Task ${task.id}: ${message}`, route }), undefined);
|
|
@@ -2679,6 +3290,42 @@ export async function startLocalServer(opts) {
|
|
|
2679
3290
|
rethrowIfCancelled(error, request.signal);
|
|
2680
3291
|
return { task, error: error instanceof Error ? error.message : String(error) };
|
|
2681
3292
|
}
|
|
3293
|
+
};
|
|
3294
|
+
const scheduledChildren = await scheduleCompoundAnalyticalTasks({
|
|
3295
|
+
tasks: turnPlan.tasks.slice(0, 6),
|
|
3296
|
+
runTask: async (task, binding) => {
|
|
3297
|
+
const child = await runChildTask(task, binding);
|
|
3298
|
+
return { task, value: child.answer, error: child.error };
|
|
3299
|
+
},
|
|
3300
|
+
resolveDependency: (task, parent) => {
|
|
3301
|
+
const sourceTaskId = task.dependency?.sourceTaskId ?? '';
|
|
3302
|
+
return resolveTopRankedRegionDependency(sourceTaskId, parent?.value?.result
|
|
3303
|
+
? normalizeCanonicalQueryResult({
|
|
3304
|
+
...parent.value.result,
|
|
3305
|
+
resultFingerprint: parent.value.result.resultFingerprint ?? parent.value.result.executionReceipt?.resultFingerprint,
|
|
3306
|
+
executionReceipt: parent.value.result.executionReceipt,
|
|
3307
|
+
answerTier: parent.value.route?.tier ?? parent.value.sourceTier,
|
|
3308
|
+
})
|
|
3309
|
+
: undefined, parent?.task);
|
|
3310
|
+
},
|
|
3311
|
+
});
|
|
3312
|
+
const childResults = scheduledChildren.map(({ task, value, error, dependencyError }) => ({
|
|
3313
|
+
task,
|
|
3314
|
+
answer: value,
|
|
3315
|
+
error,
|
|
3316
|
+
...(dependencyError ? {
|
|
3317
|
+
dependencyGap: buildCoverageGap({
|
|
3318
|
+
code: dependencyError.code,
|
|
3319
|
+
phase: 'planning',
|
|
3320
|
+
message: dependencyError.message,
|
|
3321
|
+
searchedSources: routeDecision?.retrievalEvidence?.candidateIds ?? [],
|
|
3322
|
+
attemptedRoutes: ['certified', 'semantic', 'governed_relational', 'generated'],
|
|
3323
|
+
missing: ['unambiguous_top_region'],
|
|
3324
|
+
recoverable: true,
|
|
3325
|
+
planFrozen: turnPlan.frozen,
|
|
3326
|
+
nextActions: ['Ask for a single top region or review the parent result before retrying the customer task.'],
|
|
3327
|
+
}),
|
|
3328
|
+
} : {}),
|
|
2682
3329
|
}));
|
|
2683
3330
|
const outcomes = childResults.map(({ task, answer, error }) => ({
|
|
2684
3331
|
version: 1,
|
|
@@ -2687,7 +3334,7 @@ export async function startLocalServer(opts) {
|
|
|
2687
3334
|
...(answer?.answer || answer?.text ? { summary: answer.answer ?? answer.text } : {}),
|
|
2688
3335
|
...(answer?.result?.resultFingerprint ? { resultFingerprint: answer.result.resultFingerprint } : {}),
|
|
2689
3336
|
...(error || answer?.kind === 'no_answer' ? {
|
|
2690
|
-
gap: buildCoverageGap({
|
|
3337
|
+
gap: childResults.find((candidate) => candidate.task.id === task.id)?.dependencyGap ?? buildCoverageGap({
|
|
2691
3338
|
code: answer?.refusalCode === 'ambiguous' ? 'AMBIGUOUS_MEANING' : 'EXECUTION_FAILED',
|
|
2692
3339
|
phase: answer?.executionError ? 'execution' : 'meaning',
|
|
2693
3340
|
message: error ?? answer?.answer ?? answer?.text ?? 'The task did not produce an accepted analytical result.',
|
|
@@ -2706,14 +3353,23 @@ export async function startLocalServer(opts) {
|
|
|
2706
3353
|
status: outcomes.find((outcome) => outcome.taskId === task.id)?.status === 'completed' ? 'completed' : 'gap',
|
|
2707
3354
|
}));
|
|
2708
3355
|
const answerText = childResults.map(({ task, answer, error }) => `${task.question}: ${error ?? answer?.answer ?? answer?.text ?? 'No accepted result was produced.'}`).join('\n\n');
|
|
3356
|
+
const compoundTrust = compoundTrustState(childResults
|
|
3357
|
+
.filter(({ answer, error }) => !error && answer && answer.kind !== 'no_answer')
|
|
3358
|
+
.map(({ answer }) => trustStateForAgentAnswer(answer)));
|
|
2709
3359
|
return {
|
|
2710
3360
|
summary: completedCount === outcomes.length
|
|
2711
|
-
? `Answered ${completedCount}
|
|
3361
|
+
? `Answered ${completedCount} analytical clauses.`
|
|
2712
3362
|
: `Answered ${completedCount} of ${outcomes.length} analytical clauses; the remaining clauses need review.`,
|
|
2713
3363
|
answer: answerText,
|
|
2714
3364
|
status: completedCount === outcomes.length ? 'completed' : completedCount > 0 ? 'needs_review' : 'needs_clarification',
|
|
2715
|
-
|
|
2716
|
-
|
|
3365
|
+
// The parent is only as trustworthy as its weakest SUCCESSFUL child.
|
|
3366
|
+
// Completion is not proof: the previous rule stamped `governed` on a
|
|
3367
|
+
// parent assembled from review-required generated SQL.
|
|
3368
|
+
trustState: compoundTrust,
|
|
3369
|
+
// The stop reason has to agree with the trust it reports. Claiming a
|
|
3370
|
+
// governed semantic answer over generated/certified children
|
|
3371
|
+
// misrepresents provenance as much as the trust label does.
|
|
3372
|
+
stopReason: compoundStopReason(completedCount, outcomes.length, compoundTrust),
|
|
2717
3373
|
artifacts: childResults.map(({ task, answer, error }) => agentRunArtifact('answer', `Task: ${task.question}`, {
|
|
2718
3374
|
taskId: task.id,
|
|
2719
3375
|
question: task.question,
|
|
@@ -2729,6 +3385,15 @@ export async function startLocalServer(opts) {
|
|
|
2729
3385
|
let governedAnswer;
|
|
2730
3386
|
try {
|
|
2731
3387
|
governedAnswer = await runGovernedAgentAnswerForRun(request, { attempt, repairHint }, route, (message) => emit({ type: 'executor.started', message, route }), routeDecision);
|
|
3388
|
+
const frozenRoute = frozenAnalyticalRoute(routeDecision);
|
|
3389
|
+
const reportedRoute = resolvedRunRouteFromAnswer(governedAnswer);
|
|
3390
|
+
// Do not let the legacy answer loop reselect meaning and overwrite the
|
|
3391
|
+
// router's immutable tier through `resolvedRunRouteFromAnswer`. A frozen
|
|
3392
|
+
// plan either executes on its selected route or returns a same-tier,
|
|
3393
|
+
// inspectable terminal failure; it never downgrades to generated SQL.
|
|
3394
|
+
if (frozenRoute && (frozenRoute !== route || (reportedRoute && reportedRoute !== frozenRoute))) {
|
|
3395
|
+
return frozenPlanRouteFailure(frozenRoute, routeDecision, reportedRoute);
|
|
3396
|
+
}
|
|
2732
3397
|
// Keep the canonical result contract on the answer itself, not only on
|
|
2733
3398
|
// the narration preview. Conversation persistence, follow-up member
|
|
2734
3399
|
// resolution, Apply, and the notebook table all read this payload; if a
|
|
@@ -2903,6 +3568,33 @@ export async function startLocalServer(opts) {
|
|
|
2903
3568
|
const message = dispatchBudgetExhausted
|
|
2904
3569
|
? 'Ask reached its internal provider-dispatch limit before it froze one executable analytical plan. Nothing was run. Narrow the metric or dimension and retry; this is an orchestration-budget diagnostic, not a provider outage.'
|
|
2905
3570
|
: formatAgentRunInfrastructureError(error, 'AI answer provider');
|
|
3571
|
+
const providerCode = error && typeof error === 'object'
|
|
3572
|
+
? String(error.code ?? '')
|
|
3573
|
+
: '';
|
|
3574
|
+
const boundaryProviderDiagnostic = error && typeof error === 'object'
|
|
3575
|
+
? error.providerDiagnostic
|
|
3576
|
+
: undefined;
|
|
3577
|
+
const providerDiagnostic = boundaryProviderDiagnostic
|
|
3578
|
+
&& typeof boundaryProviderDiagnostic === 'object'
|
|
3579
|
+
&& boundaryProviderDiagnostic.version === 1
|
|
3580
|
+
? boundaryProviderDiagnostic
|
|
3581
|
+
: classifyProviderFailure({
|
|
3582
|
+
code: dispatchBudgetExhausted ? 'PROVIDER_DISPATCH_BUDGET' : providerCode,
|
|
3583
|
+
// Classification happens at the provider boundary before this error is
|
|
3584
|
+
// coalesced into the friendly headline. Persist the classifier output,
|
|
3585
|
+
// never this possibly sensitive raw message.
|
|
3586
|
+
message: error instanceof Error ? error.message : String(error ?? ''),
|
|
3587
|
+
phase: /no ai provider configured|not configured or reachable/i.test(message) ? 'preflight' : 'generation',
|
|
3588
|
+
providerFingerprint: agentRunWorkspaceValue(request, 'provider')
|
|
3589
|
+
? `sha256:${createHash('sha256').update(agentRunWorkspaceValue(request, 'provider')).digest('hex')}`
|
|
3590
|
+
: undefined,
|
|
3591
|
+
modelFingerprint: agentRunWorkspaceValue(request, 'model')
|
|
3592
|
+
? `sha256:${createHash('sha256').update(agentRunWorkspaceValue(request, 'model')).digest('hex')}`
|
|
3593
|
+
: undefined,
|
|
3594
|
+
baseOriginFingerprint: agentRunWorkspaceValue(request, 'providerBaseOrigin')
|
|
3595
|
+
? `sha256:${createHash('sha256').update(agentRunWorkspaceValue(request, 'providerBaseOrigin')).digest('hex')}`
|
|
3596
|
+
: undefined,
|
|
3597
|
+
});
|
|
2906
3598
|
return {
|
|
2907
3599
|
summary: message,
|
|
2908
3600
|
status: 'blocked',
|
|
@@ -2920,11 +3612,12 @@ export async function startLocalServer(opts) {
|
|
|
2920
3612
|
code: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'AI_PROVIDER_FAILURE',
|
|
2921
3613
|
message,
|
|
2922
3614
|
recoverable: !dispatchBudgetExhausted,
|
|
3615
|
+
diagnostic: providerDiagnostic,
|
|
2923
3616
|
},
|
|
2924
3617
|
}, undefined, 'blocked')],
|
|
2925
3618
|
evaluations: [
|
|
2926
3619
|
agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.'),
|
|
2927
|
-
agentRunEvaluation(dispatchBudgetExhausted ? 'orchestration-budget' : 'ai-provider', dispatchBudgetExhausted ? 'Internal orchestration budget' : 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error }),
|
|
3620
|
+
agentRunEvaluation(dispatchBudgetExhausted ? 'orchestration-budget' : 'ai-provider', dispatchBudgetExhausted ? 'Internal orchestration budget' : 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error, providerDiagnostic }),
|
|
2928
3621
|
],
|
|
2929
3622
|
nextActions: [
|
|
2930
3623
|
...(dispatchBudgetExhausted
|
|
@@ -2951,7 +3644,10 @@ export async function startLocalServer(opts) {
|
|
|
2951
3644
|
},
|
|
2952
3645
|
};
|
|
2953
3646
|
}
|
|
2954
|
-
|
|
3647
|
+
// For an unfrozen legacy route, the answer loop can still describe which
|
|
3648
|
+
// tier actually produced the answer. Once the router froze a cascade tier,
|
|
3649
|
+
// that immutable route is the only durable provenance authority.
|
|
3650
|
+
const answeredRoute = frozenAnalyticalRoute(routeDecision) ?? resolvedRunRouteFromAnswer(governedAnswer) ?? route;
|
|
2955
3651
|
const isCertified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
2956
3652
|
const semanticRouteClaimed = governedAnswer.route?.tier === 'semantic_metric';
|
|
2957
3653
|
const semanticAggregationProofPassed = semanticAnswerHasPassedAggregationProof(governedAnswer);
|
|
@@ -2990,9 +3686,14 @@ export async function startLocalServer(opts) {
|
|
|
2990
3686
|
&& route === 'generated_answer'
|
|
2991
3687
|
&& (request.requestedMode === undefined || request.requestedMode === 'auto' || request.requestedMode === 'ask')
|
|
2992
3688
|
&& routeDecision?.resolvedAnalyticalPlan?.mode !== 'authoritative';
|
|
3689
|
+
// A generic `modeling_gap` says only that the complete analytical tuple did
|
|
3690
|
+
// not prove out. Relationship-specific repair language is allowed solely
|
|
3691
|
+
// when the router retained a structured coverage witness.
|
|
3692
|
+
const persistedGapWitness = persistedAnalyticalGapWitness(routeDecision);
|
|
3693
|
+
const relationshipSpecificGap = persistedGapWitness?.code === 'MISSING_RELATIONSHIP';
|
|
2993
3694
|
const typedCoverageGap = (isGroundingGap || isModelDeclined)
|
|
2994
3695
|
? buildCoverageGap({
|
|
2995
|
-
code:
|
|
3696
|
+
code: persistedGapWitness?.code ?? 'MISSING_RUNTIME_CAPABILITY',
|
|
2996
3697
|
phase: 'planning',
|
|
2997
3698
|
message: governedAnswer.refusalDetails?.message
|
|
2998
3699
|
?? (isModelDeclined
|
|
@@ -3000,17 +3701,21 @@ export async function startLocalServer(opts) {
|
|
|
3000
3701
|
: 'The retrieved context did not prove the metadata required for this analytical question.'),
|
|
3001
3702
|
searchedSources: ['certified_blocks', 'semantic_metrics', 'dbt_manifest', 'relationship_graph', 'warehouse_metadata'],
|
|
3002
3703
|
attemptedRoutes: ['certified', 'semantic', 'governed_relational', 'generated'],
|
|
3003
|
-
missing:
|
|
3004
|
-
?
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3704
|
+
missing: persistedGapWitness?.missing.length
|
|
3705
|
+
? persistedGapWitness.missing
|
|
3706
|
+
: governedAnswer.refusalDetails?.offending
|
|
3707
|
+
? [
|
|
3708
|
+
governedAnswer.refusalDetails.offending.relation,
|
|
3709
|
+
governedAnswer.refusalDetails.offending.column,
|
|
3710
|
+
].filter((value) => Boolean(value))
|
|
3711
|
+
: ['the complete requested analytical tuple'],
|
|
3009
3712
|
recoverable: canRecoverPreFreezeGap,
|
|
3010
3713
|
planFrozen: routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative',
|
|
3011
3714
|
nextActions: canRecoverPreFreezeGap
|
|
3012
3715
|
? ['continue through DBT-grounded relational context', 'run review-required generated SQL', 'start bounded Research if coverage remains incomplete']
|
|
3013
|
-
:
|
|
3716
|
+
: relationshipSpecificGap
|
|
3717
|
+
? ['review the retained relationship proof', 'repair the certified relationship in Modeling', 'retry after relationship validation']
|
|
3718
|
+
: ['review the retained metadata gap', 'select a governed metric or dimension', 'repair the missing metric, dimension, or output contract'],
|
|
3014
3719
|
})
|
|
3015
3720
|
: undefined;
|
|
3016
3721
|
// Only a genuinely AMBIGUOUS question is surfaced as "needs clarification".
|
|
@@ -3062,7 +3767,34 @@ export async function startLocalServer(opts) {
|
|
|
3062
3767
|
? { ...preview, rows: redactProviderResultRows(preview.rows, narrationMaxRows) }
|
|
3063
3768
|
: undefined;
|
|
3064
3769
|
let narrationSource;
|
|
3065
|
-
|
|
3770
|
+
// The receipt is the durable source of truth for evaluation and inspector
|
|
3771
|
+
// display. Do not reconstruct this later from rows or the reader-facing
|
|
3772
|
+
// fallback sentence: both are presentation artifacts, not evidence that a
|
|
3773
|
+
// fact-grounded narrator actually ran.
|
|
3774
|
+
const verifiedFactNarration = narrationPlan.mode === 'verified_facts'
|
|
3775
|
+
&& Boolean(governedAnswer.analyticalFacts && governedAnswer.resolvedAnalyticalPlan?.analyticalFrame);
|
|
3776
|
+
let narrationIntegrityReceipt = narrationPlan.mode === 'skip'
|
|
3777
|
+
? {
|
|
3778
|
+
version: 1,
|
|
3779
|
+
mode: 'skip',
|
|
3780
|
+
outcome: 'skipped',
|
|
3781
|
+
attempted: false,
|
|
3782
|
+
factCount: 0,
|
|
3783
|
+
maxRows: 0,
|
|
3784
|
+
validationFailures: [],
|
|
3785
|
+
skipReason: narrationPlan.reason,
|
|
3786
|
+
}
|
|
3787
|
+
: {
|
|
3788
|
+
version: 1,
|
|
3789
|
+
mode: verifiedFactNarration ? 'verified_facts' : 'preview_grounded',
|
|
3790
|
+
// This is deliberately pessimistic until a narration outcome is
|
|
3791
|
+
// observed, so an exception cannot be persisted as a silent skip.
|
|
3792
|
+
outcome: 'error',
|
|
3793
|
+
attempted: true,
|
|
3794
|
+
factCount: verifiedFactNarration ? governedAnswer.analyticalFacts?.facts.length ?? 0 : 0,
|
|
3795
|
+
maxRows: narrationPlan.maxRows,
|
|
3796
|
+
validationFailures: [],
|
|
3797
|
+
};
|
|
3066
3798
|
if (narrationPlan.mode !== 'skip' && narrationProvider) {
|
|
3067
3799
|
const narrationStartedAtMs = Date.now();
|
|
3068
3800
|
const draft = governedAnswer.answer ?? governedAnswer.text;
|
|
@@ -3075,8 +3807,16 @@ export async function startLocalServer(opts) {
|
|
|
3075
3807
|
columnCount: providerPreview?.columns.length ?? 0,
|
|
3076
3808
|
},
|
|
3077
3809
|
});
|
|
3078
|
-
const narrationDispatchOptions = () => ({
|
|
3079
|
-
|
|
3810
|
+
const narrationDispatchOptions = (factCount = 0) => ({
|
|
3811
|
+
// The ceiling has to grow with the result. Every claim must echo the
|
|
3812
|
+
// fact ids it rests on, and a fact id is a long hex string that
|
|
3813
|
+
// tokenizes badly — ten of them consume most of the budget before a
|
|
3814
|
+
// word of prose is written. At a flat 350 a ten-row answer was
|
|
3815
|
+
// truncated mid-sentence ("...Elizabeth Shea (875), and Dyl"), so the
|
|
3816
|
+
// JSON never closed, BOTH attempts failed as UNPARSEABLE_CLAIMS, and
|
|
3817
|
+
// the reader got "Verified narration was unavailable" above a robot
|
|
3818
|
+
// dump of the very rows the model had just described correctly.
|
|
3819
|
+
maxTokens: narrationMaxTokensForFacts(factCount),
|
|
3080
3820
|
temperature: 0.3,
|
|
3081
3821
|
maxProviderDispatches: 2,
|
|
3082
3822
|
...(agentRunProviderEvidenceContext.getStore()
|
|
@@ -3093,10 +3833,19 @@ export async function startLocalServer(opts) {
|
|
|
3093
3833
|
factSet: governedAnswer.analyticalFacts,
|
|
3094
3834
|
question: request.question,
|
|
3095
3835
|
maxRows: narrationPlan.maxRows,
|
|
3096
|
-
complete: async ({ system, user }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], narrationDispatchOptions(), () => { }),
|
|
3836
|
+
complete: async ({ system, user }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], narrationDispatchOptions(governedAnswer.analyticalFacts?.facts.length ?? 0), () => { }),
|
|
3097
3837
|
});
|
|
3098
3838
|
narrationSource = composed.source;
|
|
3099
|
-
|
|
3839
|
+
narrationIntegrityReceipt = {
|
|
3840
|
+
...narrationIntegrityReceipt,
|
|
3841
|
+
outcome: composed.source === 'llm' ? 'success' : 'deterministic_fallback',
|
|
3842
|
+
validationFailures: narrationIntegrityFailureCodes(composed.validationFailures),
|
|
3843
|
+
};
|
|
3844
|
+
if (process.env.DQL_ORCHESTRATOR_TRACE) {
|
|
3845
|
+
console.warn(`[dql] narration: source=${composed.source}${narrationIntegrityReceipt.validationFailures.length > 0
|
|
3846
|
+
? ` rejected=${narrationIntegrityReceipt.validationFailures.join(',')}`
|
|
3847
|
+
: ''}`);
|
|
3848
|
+
}
|
|
3100
3849
|
synthesizedAnswer = composed.source === 'llm'
|
|
3101
3850
|
? composed.narrative.text
|
|
3102
3851
|
// A verification failure is not silent: the deterministic join is a
|
|
@@ -3126,11 +3875,22 @@ export async function startLocalServer(opts) {
|
|
|
3126
3875
|
narrationSource = result.source;
|
|
3127
3876
|
if (result.text)
|
|
3128
3877
|
synthesizedAnswer = result.text;
|
|
3878
|
+
narrationIntegrityReceipt = {
|
|
3879
|
+
...narrationIntegrityReceipt,
|
|
3880
|
+
outcome: result.source === 'llm' ? 'success' : 'deterministic_fallback',
|
|
3881
|
+
validationFailures: [],
|
|
3882
|
+
};
|
|
3129
3883
|
}
|
|
3130
3884
|
}
|
|
3131
3885
|
catch {
|
|
3132
3886
|
// Keep the governed draft on any narration failure.
|
|
3133
3887
|
synthesizedAnswer = undefined;
|
|
3888
|
+
narrationIntegrityReceipt = {
|
|
3889
|
+
...narrationIntegrityReceipt,
|
|
3890
|
+
outcome: 'error',
|
|
3891
|
+
validationFailures: [],
|
|
3892
|
+
errorCode: 'narration_error',
|
|
3893
|
+
};
|
|
3134
3894
|
}
|
|
3135
3895
|
finally {
|
|
3136
3896
|
narrationDurationMs = Date.now() - narrationStartedAtMs;
|
|
@@ -3223,12 +3983,15 @@ export async function startLocalServer(opts) {
|
|
|
3223
3983
|
: isTerminalFailure
|
|
3224
3984
|
? terminalFailureActions
|
|
3225
3985
|
: isGroundingGap
|
|
3226
|
-
?
|
|
3986
|
+
? relationshipSpecificGap
|
|
3227
3987
|
? [
|
|
3228
|
-
{ id: '
|
|
3988
|
+
{ id: 'repair-relationship-proof', label: 'Repair relationship proof in Modeling', route: 'modeling_draft', artifactKind: 'modeling_change_proposal' },
|
|
3989
|
+
{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' },
|
|
3990
|
+
]
|
|
3991
|
+
: [
|
|
3992
|
+
{ id: 'review-metadata-gap', label: 'Review missing metadata coverage', route: 'blocked' },
|
|
3229
3993
|
{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' },
|
|
3230
3994
|
]
|
|
3231
|
-
: [{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' }]
|
|
3232
3995
|
: [
|
|
3233
3996
|
{ id: 'create-block', label: governedAnswer.dqlArtifact ? 'Review DQL draft' : 'Create DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
|
|
3234
3997
|
{ id: 'research-gap', label: 'Research deeper', route: 'research' },
|
|
@@ -3270,7 +4033,11 @@ export async function startLocalServer(opts) {
|
|
|
3270
4033
|
trustState,
|
|
3271
4034
|
stopReason,
|
|
3272
4035
|
artifacts: isTerminalFailure
|
|
3273
|
-
? [agentRunArtifact('answer',
|
|
4036
|
+
? [agentRunArtifact('answer',
|
|
4037
|
+
// Was 'Failed governed analytical run' — internal orchestration state
|
|
4038
|
+
// used as the card heading a user reads. It names our pipeline, not
|
|
4039
|
+
// what happened to their question.
|
|
4040
|
+
terminalFailureTitle(governedAnswer), governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, 'blocked')]
|
|
3274
4041
|
: governedAnswer.kind === 'no_answer'
|
|
3275
4042
|
// A refusal still keeps the DQL draft the answer loop produced (when any),
|
|
3276
4043
|
// so the "Review DQL draft" next-action isn't a dead link and the user can
|
|
@@ -3367,20 +4134,41 @@ export async function startLocalServer(opts) {
|
|
|
3367
4134
|
...(governedAnswer.executionError ? [
|
|
3368
4135
|
agentRunEvaluation('execution-error', 'Execution error', false, 'warning', governedAnswer.executionError),
|
|
3369
4136
|
] : []),
|
|
3370
|
-
//
|
|
3371
|
-
//
|
|
3372
|
-
//
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
agentRunEvaluation('narration-verification', 'Narration verification', false, 'warning', `The drafted narration was rejected against the result fact set, so the deterministic record was shown instead: ${narrationValidationFailures.join('; ')}`, { narrationSource, validationFailures: narrationValidationFailures }),
|
|
4137
|
+
// Keep only the content-free receipt codes in the durable inspection
|
|
4138
|
+
// record. Raw verifier prose can contain a result value, prompt excerpt,
|
|
4139
|
+
// or provider error and is not safe evidence to surface or persist.
|
|
4140
|
+
...(narrationIntegrityReceipt.outcome === 'deterministic_fallback'
|
|
4141
|
+
&& narrationIntegrityReceipt.validationFailures.length > 0 ? [
|
|
4142
|
+
agentRunEvaluation('narration-verification', 'Narration verification', false, 'warning', `The drafted narration was rejected against the result fact set, so the deterministic record was shown instead: ${narrationIntegrityReceipt.validationFailures.join(', ')}.`, { narrationSource, validationFailures: narrationIntegrityReceipt.validationFailures }),
|
|
3377
4143
|
] : []),
|
|
3378
4144
|
],
|
|
3379
4145
|
nextActions,
|
|
3380
4146
|
providerEgressReceipts: finalProviderEgressReceipts,
|
|
3381
4147
|
telemetry: finalTelemetry,
|
|
4148
|
+
narrationIntegrityReceipt,
|
|
4149
|
+
...(governedAnswer.exploratoryExecutionFreeze
|
|
4150
|
+
? { analyticalExecutionFreeze: governedAnswer.exploratoryExecutionFreeze }
|
|
4151
|
+
: {}),
|
|
3382
4152
|
};
|
|
3383
4153
|
};
|
|
4154
|
+
/**
|
|
4155
|
+
* A heading for a run that ended without an answer, in the user's terms.
|
|
4156
|
+
*
|
|
4157
|
+
* Says WHICH stage stopped, because "it failed" and "it was stopped before
|
|
4158
|
+
* running" call for different next moves: one is worth retrying, the other
|
|
4159
|
+
* needs the question or the model changed.
|
|
4160
|
+
*/
|
|
4161
|
+
const terminalFailureTitle = (answer) => {
|
|
4162
|
+
switch (answer.refusalCode) {
|
|
4163
|
+
case 'policy_blocked': return 'Blocked by a governance policy';
|
|
4164
|
+
case 'modeling_gap': return 'Not modeled yet';
|
|
4165
|
+
case 'grounding_gap': return 'Not enough context to answer safely';
|
|
4166
|
+
case 'model_declined': return 'The assistant declined to answer';
|
|
4167
|
+
case 'provider_error': return 'The AI provider did not respond';
|
|
4168
|
+
case 'ambiguous': return 'Needs one detail before running';
|
|
4169
|
+
default: return 'No answer was produced';
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
3384
4172
|
const conversationRunExecutor = async ({ request, routeDecision, emitAnswerDelta }) => {
|
|
3385
4173
|
const kind = routeDecision?.conversationalKind ?? 'smalltalk';
|
|
3386
4174
|
const isGeneralKnowledge = routeDecision?.category === 'general_knowledge';
|
|
@@ -3397,6 +4185,15 @@ export async function startLocalServer(opts) {
|
|
|
3397
4185
|
let text = kind === 'answer_explanation'
|
|
3398
4186
|
? buildPriorAnswerExplanation(request.question, request.conversationContext)
|
|
3399
4187
|
: undefined;
|
|
4188
|
+
// A definitional question that NAMES a governed artifact is answerable from
|
|
4189
|
+
// the catalog: the description, domain, and dimensions are already recorded.
|
|
4190
|
+
// Reaching for a provider to paraphrase facts we hold can only add drift, and
|
|
4191
|
+
// the generic conversational reply this replaces used none of them.
|
|
4192
|
+
//
|
|
4193
|
+
// Returns undefined unless the question names something real, so a turn that
|
|
4194
|
+
// does not match keeps today's behaviour exactly.
|
|
4195
|
+
if (!text)
|
|
4196
|
+
text = buildGovernedObjectExplanation(request.question);
|
|
3400
4197
|
if (text) {
|
|
3401
4198
|
emitAnswerDelta?.(text);
|
|
3402
4199
|
}
|
|
@@ -3863,10 +4660,18 @@ export async function startLocalServer(opts) {
|
|
|
3863
4660
|
const conversationHistory = request.history?.length
|
|
3864
4661
|
? request.history
|
|
3865
4662
|
: conversationHistoryFromContext(request.conversationContext);
|
|
4663
|
+
// The provider that will plan the investigation as hypotheses. Absent or
|
|
4664
|
+
// unreachable, `planResearch` keeps its deterministic template, so
|
|
4665
|
+
// research never depends on a model being available.
|
|
4666
|
+
const researchPlanner = resolveGovernedAnswerRunner(projectRoot);
|
|
4667
|
+
const researchPlannerProvider = researchPlanner
|
|
4668
|
+
? createGovernedTextProvider(researchPlanner.provider, projectRoot)
|
|
4669
|
+
: undefined;
|
|
3866
4670
|
const plan = await planResearch({
|
|
3867
4671
|
question: request.question,
|
|
3868
4672
|
metrics,
|
|
3869
4673
|
blocks,
|
|
4674
|
+
...(researchPlannerProvider ? { provider: researchPlannerProvider } : {}),
|
|
3870
4675
|
intent: request.intent,
|
|
3871
4676
|
isFollowUp: conversationHistory.length > 0,
|
|
3872
4677
|
history: conversationHistory,
|
|
@@ -3881,6 +4686,38 @@ export async function startLocalServer(opts) {
|
|
|
3881
4686
|
if (plan.done && !plan.followUp && request.requestedMode !== 'research') {
|
|
3882
4687
|
return answerRunExecutor(researchContext);
|
|
3883
4688
|
}
|
|
4689
|
+
// This is the executable, receipt-bound research plan. It carries the
|
|
4690
|
+
// branch hypothesis/expectation/validator kind into every child rather
|
|
4691
|
+
// than treating V2 as a presentation-only wrapper after the work ends.
|
|
4692
|
+
const typedResearchPlan = buildResearchHypothesisPlanV2({
|
|
4693
|
+
hypotheses: plan.steps.map((step, index) => ({
|
|
4694
|
+
id: `h${index + 1}`,
|
|
4695
|
+
statement: step.thought,
|
|
4696
|
+
expectation: step.expectation,
|
|
4697
|
+
targetId: step.action.target,
|
|
4698
|
+
validatorKind: inferResearchValidatorKind(step.thought, step.expectation),
|
|
4699
|
+
})),
|
|
4700
|
+
});
|
|
4701
|
+
// The V2 contract is the executable branch authority: retain its stable
|
|
4702
|
+
// hypothesis ID, wording, expectation, target, and validator kind while
|
|
4703
|
+
// borrowing only the already-grounded action kind from the planner. This
|
|
4704
|
+
// prevents a presentation-only V2 ledger from drifting away from the
|
|
4705
|
+
// child runs that actually produced the receipts.
|
|
4706
|
+
const executableResearchBranches = typedResearchPlan.hypotheses.flatMap((hypothesis) => {
|
|
4707
|
+
const planned = plan.steps.find((step) => step.thought.trim() === hypothesis.statement
|
|
4708
|
+
&& step.action.target === hypothesis.targetId)
|
|
4709
|
+
?? plan.steps.find((step) => step.action.target === hypothesis.targetId);
|
|
4710
|
+
if (!planned)
|
|
4711
|
+
return [];
|
|
4712
|
+
return [{
|
|
4713
|
+
hypothesisId: hypothesis.id,
|
|
4714
|
+
validatorKind: hypothesis.validatorKind,
|
|
4715
|
+
thought: hypothesis.statement,
|
|
4716
|
+
expectation: hypothesis.expectation,
|
|
4717
|
+
action: { ...planned.action, target: hypothesis.targetId },
|
|
4718
|
+
}];
|
|
4719
|
+
});
|
|
4720
|
+
const typedHypothesesById = new Map(typedResearchPlan.hypotheses.map((hypothesis) => [hypothesis.id, hypothesis]));
|
|
3884
4721
|
const needsClarification = Boolean(plan.followUp);
|
|
3885
4722
|
const notebookPath = agentRunNotebookPath(request, runId);
|
|
3886
4723
|
const researchIntent = agentRunResearchIntent(request);
|
|
@@ -3941,6 +4778,8 @@ export async function startLocalServer(opts) {
|
|
|
3941
4778
|
// attempt, not a fabricated successful finding; its durable status
|
|
3942
4779
|
// and receipt determine the ledger entry.
|
|
3943
4780
|
const fallbackBranch = {
|
|
4781
|
+
hypothesisId: 'fallback:context',
|
|
4782
|
+
validatorKind: 'counter_evidence',
|
|
3944
4783
|
thought: 'Inspect the requested analytical question against the frozen root context.',
|
|
3945
4784
|
action: {
|
|
3946
4785
|
kind: 'lookup_metric',
|
|
@@ -3948,12 +4787,34 @@ export async function startLocalServer(opts) {
|
|
|
3948
4787
|
},
|
|
3949
4788
|
expectation: 'Whether the frozen context contains enough evidence for a bounded answer.',
|
|
3950
4789
|
};
|
|
3951
|
-
const branches = capResearchBranches(
|
|
4790
|
+
const branches = capResearchBranches(executableResearchBranches.length > 0 ? executableResearchBranches : [fallbackBranch], 6);
|
|
4791
|
+
// The replan edge. Each branch tests one hypothesis; folding its
|
|
4792
|
+
// outcome back into the state is what lets the investigation stop
|
|
4793
|
+
// when the question is settled instead of grinding through a plan
|
|
4794
|
+
// frozen before any observation. `nextHypothesis` returning
|
|
4795
|
+
// undefined is how the loop learns to stop — it enforces the hop
|
|
4796
|
+
// budget and reports when nothing is open.
|
|
4797
|
+
let researchState = createResearchState(request.question, branches.map((branch, position) => ({
|
|
4798
|
+
id: `h${position + 1}`,
|
|
4799
|
+
statement: branch.thought,
|
|
4800
|
+
priorConfidence: 1 - position / (branches.length + 1),
|
|
4801
|
+
})));
|
|
3952
4802
|
for (let index = 0; index < branches.length; index += 1) {
|
|
3953
4803
|
const step = branches[index];
|
|
3954
4804
|
if (request.signal?.aborted)
|
|
3955
4805
|
rethrowIfCancelled(request.signal.reason, request.signal);
|
|
3956
|
-
|
|
4806
|
+
// A hypothesis an earlier finding already closed is not
|
|
4807
|
+
// re-investigated, and an exhausted hop budget stops the run.
|
|
4808
|
+
const stillOpen = nextHypothesis(researchState);
|
|
4809
|
+
if (!stillOpen) {
|
|
4810
|
+
emit({
|
|
4811
|
+
type: 'executor.started',
|
|
4812
|
+
message: `Stopping early: ${researchState.hopsUsed} of ${branches.length} branches settled what could be settled.`,
|
|
4813
|
+
route: 'research',
|
|
4814
|
+
});
|
|
4815
|
+
break;
|
|
4816
|
+
}
|
|
4817
|
+
const branchId = step.hypothesisId;
|
|
3957
4818
|
const branchQuestion = `${request.question}\nResearch branch ${index + 1} (${branchId}): ${step.expectation}`;
|
|
3958
4819
|
const childId = `${created.id}:research:${index + 1}`;
|
|
3959
4820
|
const child = storage.createRun({
|
|
@@ -3974,6 +4835,9 @@ export async function startLocalServer(opts) {
|
|
|
3974
4835
|
rootPlanId: plan.rootPlanId,
|
|
3975
4836
|
branch: {
|
|
3976
4837
|
id: branchId,
|
|
4838
|
+
hypothesisId: step.hypothesisId,
|
|
4839
|
+
hypothesis: step.thought,
|
|
4840
|
+
validatorKind: step.validatorKind,
|
|
3977
4841
|
index: index + 1,
|
|
3978
4842
|
expectation: step.expectation,
|
|
3979
4843
|
action: step.action,
|
|
@@ -3998,7 +4862,15 @@ export async function startLocalServer(opts) {
|
|
|
3998
4862
|
...researchContextEnvelope,
|
|
3999
4863
|
rootRunId: created.id,
|
|
4000
4864
|
rootPlanId: plan.rootPlanId,
|
|
4001
|
-
branch: {
|
|
4865
|
+
branch: {
|
|
4866
|
+
id: branchId,
|
|
4867
|
+
hypothesisId: step.hypothesisId,
|
|
4868
|
+
hypothesis: step.thought,
|
|
4869
|
+
validatorKind: step.validatorKind,
|
|
4870
|
+
index: index + 1,
|
|
4871
|
+
expectation: step.expectation,
|
|
4872
|
+
action: step.action,
|
|
4873
|
+
},
|
|
4002
4874
|
},
|
|
4003
4875
|
executionConnection: researchExecutionConnection,
|
|
4004
4876
|
executionConnectionName: researchExecutionConnectionName,
|
|
@@ -4007,7 +4879,27 @@ export async function startLocalServer(opts) {
|
|
|
4007
4879
|
baselineDqlArtifact: researchSource?.dqlArtifact,
|
|
4008
4880
|
baselineRunId: agentRunString(researchSource?.runId),
|
|
4009
4881
|
});
|
|
4010
|
-
|
|
4882
|
+
const branchRun = withNotebookResearchChecklist(executed);
|
|
4883
|
+
researchRuns.push(branchRun);
|
|
4884
|
+
// Observe, then decide. A branch that produced rows is evidence
|
|
4885
|
+
// for its hypothesis; one that did not is inconclusive, which is
|
|
4886
|
+
// a real outcome and not a failure.
|
|
4887
|
+
// Rows are not support. A branch that returned data has been
|
|
4888
|
+
// OBSERVED, not confirmed — deciding whether the observation
|
|
4889
|
+
// matches what the hypothesis predicted needs the expectation,
|
|
4890
|
+
// and nothing available at this layer can judge it. Recording
|
|
4891
|
+
// rows as `supports` would let the dossier report a driver the
|
|
4892
|
+
// evidence never established, which is the failure mode the
|
|
4893
|
+
// whole verified-fact chain exists to prevent.
|
|
4894
|
+
researchState = applyFinding(researchState, {
|
|
4895
|
+
id: `f${index + 1}`,
|
|
4896
|
+
hypothesisId: `h${index + 1}`,
|
|
4897
|
+
verdict: 'inconclusive',
|
|
4898
|
+
summary: branchRun.summary ?? '',
|
|
4899
|
+
strength: (branchRun.resultPreview?.rows?.length ?? 0) > 0
|
|
4900
|
+
? 0.5
|
|
4901
|
+
: 0.1,
|
|
4902
|
+
});
|
|
4011
4903
|
}
|
|
4012
4904
|
catch (error) {
|
|
4013
4905
|
// A child is a real durable run even when cancellation stops the
|
|
@@ -4023,6 +4915,13 @@ export async function startLocalServer(opts) {
|
|
|
4023
4915
|
const stopped = storage.getRun(child.id);
|
|
4024
4916
|
if (stopped)
|
|
4025
4917
|
researchRuns.push(withNotebookResearchChecklist(stopped));
|
|
4918
|
+
researchState = applyFinding(researchState, {
|
|
4919
|
+
id: `f${index + 1}`,
|
|
4920
|
+
hypothesisId: `h${index + 1}`,
|
|
4921
|
+
verdict: 'inconclusive',
|
|
4922
|
+
summary: message,
|
|
4923
|
+
strength: 0,
|
|
4924
|
+
});
|
|
4026
4925
|
rethrowIfCancelled(error, request.signal);
|
|
4027
4926
|
}
|
|
4028
4927
|
}
|
|
@@ -4099,10 +4998,50 @@ export async function startLocalServer(opts) {
|
|
|
4099
4998
|
? 'not_started'
|
|
4100
4999
|
: researchRuns.some((run) => run.status === 'error')
|
|
4101
5000
|
? 'insufficient_evidence'
|
|
4102
|
-
:
|
|
5001
|
+
: executableResearchBranches.length > 6
|
|
4103
5002
|
? 'budget'
|
|
4104
5003
|
: 'completed',
|
|
4105
5004
|
});
|
|
5005
|
+
// V2 carries a verdict per bounded hypothesis and makes a deliberately
|
|
5006
|
+
// small investigation visible to the caller. A returned row is still
|
|
5007
|
+
// only an observation: without a deterministic expectation validator it
|
|
5008
|
+
// remains inconclusive rather than being promoted to causal support.
|
|
5009
|
+
const researchLedgerV2 = buildResearchEvidenceLedgerV2({
|
|
5010
|
+
rootQuestion: request.question,
|
|
5011
|
+
planId: plan.rootPlanId,
|
|
5012
|
+
snapshotId: routeDecision?.resolvedAnalyticalPlan?.snapshotId,
|
|
5013
|
+
groundableBranchCount: typedResearchPlan.hypotheses.length,
|
|
5014
|
+
entries: researchLedger.entries.map((entry, index) => ({
|
|
5015
|
+
...entry,
|
|
5016
|
+
hypothesis: typedHypothesesById.get(entry.branchId)?.statement ?? plan.steps[index]?.thought,
|
|
5017
|
+
verdict: entry.status === 'failed'
|
|
5018
|
+
? 'failed'
|
|
5019
|
+
: entry.status === 'skipped'
|
|
5020
|
+
? 'skipped'
|
|
5021
|
+
: entry.rowCount === 0
|
|
5022
|
+
? 'contradicted'
|
|
5023
|
+
: 'inconclusive',
|
|
5024
|
+
...(entry.status === 'observed' && entry.resultFingerprint
|
|
5025
|
+
? {
|
|
5026
|
+
validator: {
|
|
5027
|
+
version: 1,
|
|
5028
|
+
kind: typedHypothesesById.get(entry.branchId)?.validatorKind
|
|
5029
|
+
?? inferResearchValidatorKind(plan.steps[index]?.thought ?? '', plan.steps[index]?.expectation ?? ''),
|
|
5030
|
+
// This proves that the branch completed the deterministic
|
|
5031
|
+
// receipt-bound observation. It deliberately does not claim
|
|
5032
|
+
// the hypothesis was true: rows/correlation alone remain
|
|
5033
|
+
// inconclusive until a stronger domain-specific predicate is
|
|
5034
|
+
// supplied by a future validator.
|
|
5035
|
+
evaluated: true,
|
|
5036
|
+
...(entry.rowCount === 0 ? { outcome: 'contradicts_observation' } : {}),
|
|
5037
|
+
receiptFingerprints: [entry.resultFingerprint],
|
|
5038
|
+
},
|
|
5039
|
+
}
|
|
5040
|
+
: {}),
|
|
5041
|
+
counterEvidenceFactIds: entry.rowCount === 0 ? entry.facts.slice(0, 1) : [],
|
|
5042
|
+
})),
|
|
5043
|
+
stoppingReason: researchLedger.stoppingReason,
|
|
5044
|
+
});
|
|
4106
5045
|
// A query that ran and matched 0 rows STILL executed — treat it as a clean,
|
|
4107
5046
|
// grounded execution (not "no result"), so an empty answer is surfaced as
|
|
4108
5047
|
// "0 rows matched" rather than silently downgraded to review-required.
|
|
@@ -4121,22 +5060,46 @@ export async function startLocalServer(opts) {
|
|
|
4121
5060
|
reviewRequired: true,
|
|
4122
5061
|
}, request.researchResultRowsOptIn === true)
|
|
4123
5062
|
: undefined;
|
|
5063
|
+
// The cross-branch story. Every branch tested a hypothesis and produced a
|
|
5064
|
+
// finding; narrating only the one result the executor happened to carry
|
|
5065
|
+
// reported a single fact and discarded the rest, which is the visible
|
|
5066
|
+
// half of "research answers one question instead of telling a story".
|
|
5067
|
+
const researchStory = !needsClarification && researchLedgerV2.entries.length > 0
|
|
5068
|
+
? synthesizeResearchNarrative({
|
|
5069
|
+
question: request.question,
|
|
5070
|
+
branches: researchLedgerV2.entries.map((entry) => ({
|
|
5071
|
+
statement: entry.hypothesis ?? entry.question,
|
|
5072
|
+
produced: entry.status === 'observed',
|
|
5073
|
+
verdict: entry.verdict,
|
|
5074
|
+
counterEvidenceFactIds: entry.counterEvidenceFactIds,
|
|
5075
|
+
...(entry.error ? { summary: entry.error } : {}),
|
|
5076
|
+
status: entry.status,
|
|
5077
|
+
})),
|
|
5078
|
+
})
|
|
5079
|
+
: undefined;
|
|
4124
5080
|
const summary = needsClarification
|
|
4125
5081
|
? 'Needs clarification before running deeper research.'
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
5082
|
+
// The story leads; the verified-fact narration follows it, so the
|
|
5083
|
+
// numbers still come from the narrator that checks them.
|
|
5084
|
+
: researchStory
|
|
5085
|
+
? `${researchStory}${narration?.summary ? `\n\n${narration.summary}` : ''}`
|
|
5086
|
+
: narration?.summary
|
|
5087
|
+
?? (researchZeroRows
|
|
5088
|
+
? 'The query executed cleanly against real data and matched 0 rows.'
|
|
5089
|
+
: researchRun?.status === 'ready'
|
|
5090
|
+
? 'Saved a grounded research dossier with context evidence and next review actions.'
|
|
5091
|
+
: researchRun?.status === 'error'
|
|
5092
|
+
? 'Saved a research dossier, but the preview needs review before promotion.'
|
|
5093
|
+
: researchWorkspaceError
|
|
5094
|
+
? 'Prepared a grounded research plan; durable research storage is unavailable in this runtime.'
|
|
5095
|
+
: plan.done
|
|
5096
|
+
? 'Prepared a direct grounded-answer plan.'
|
|
5097
|
+
: 'Prepared a grounded research plan over real DQL assets.');
|
|
5098
|
+
const scopedSummary = !needsClarification && researchLedgerV2.limitedScope
|
|
5099
|
+
? `Limited research scope: fewer than three groundable branches were available. ${summary}`
|
|
5100
|
+
: summary;
|
|
4138
5101
|
return {
|
|
4139
|
-
summary,
|
|
5102
|
+
summary: scopedSummary,
|
|
4140
5103
|
answer: plan.followUp?.question ?? narration?.summary
|
|
4141
5104
|
?? (researchZeroRows ? 'The query executed cleanly and matched 0 rows.' : undefined)
|
|
4142
5105
|
?? researchRun?.summary,
|
|
@@ -4147,7 +5110,9 @@ export async function startLocalServer(opts) {
|
|
|
4147
5110
|
? []
|
|
4148
5111
|
: [agentRunArtifact('research_run', 'Research plan', {
|
|
4149
5112
|
plan,
|
|
5113
|
+
typedResearchPlan,
|
|
4150
5114
|
researchLedger,
|
|
5115
|
+
researchLedgerV2,
|
|
4151
5116
|
researchRun,
|
|
4152
5117
|
researchRuns,
|
|
4153
5118
|
researchRunId: researchRun?.id,
|
|
@@ -4176,6 +5141,9 @@ export async function startLocalServer(opts) {
|
|
|
4176
5141
|
? 'The query executed cleanly against real data and matched 0 rows.'
|
|
4177
5142
|
: 'The query executed cleanly against real data and returned rows.')
|
|
4178
5143
|
: 'No executed result was available; the output stays exploratory pending review.', { rowCount: Array.isArray(researchResultRecord?.rows) ? researchResultRecord.rows.length : 0 }),
|
|
5144
|
+
agentRunEvaluation('research-scope', 'Research scope', !researchLedgerV2.limitedScope, researchLedgerV2.limitedScope ? 'warning' : 'info', researchLedgerV2.limitedScope
|
|
5145
|
+
? `Limited research scope: ${researchLedgerV2.groundableBranchCount} of at least 3 branches produced groundable evidence.`
|
|
5146
|
+
: `${researchLedgerV2.groundableBranchCount} groundable branches were retained with verdicts and counter-evidence slots.`, { groundableBranchCount: researchLedgerV2.groundableBranchCount, limitedScope: researchLedgerV2.limitedScope }),
|
|
4179
5147
|
],
|
|
4180
5148
|
nextActions: needsClarification
|
|
4181
5149
|
? [{ id: 'answer-follow-up', label: 'Answer follow-up', route: 'research' }]
|
|
@@ -4482,6 +5450,22 @@ export async function startLocalServer(opts) {
|
|
|
4482
5450
|
// lookup, and governed execution for the lifetime of a request. This removes
|
|
4483
5451
|
// both positional catalog truncation and the previous duplicate retrieval pass.
|
|
4484
5452
|
const preparedAgentContextPacks = new WeakMap();
|
|
5453
|
+
/**
|
|
5454
|
+
* Cross-encoder pass over the fused candidates, when a provider is available.
|
|
5455
|
+
* Advisory throughout: it may only reorder ids retrieval returned, and any
|
|
5456
|
+
* failure leaves retrieval's own ordering in place.
|
|
5457
|
+
*/
|
|
5458
|
+
const agentRerankCandidates = (() => {
|
|
5459
|
+
const governed = resolveGovernedAnswerRunner(projectRoot);
|
|
5460
|
+
const provider = governed
|
|
5461
|
+
? createGovernedTextProvider(governed.provider, projectRoot)
|
|
5462
|
+
: undefined;
|
|
5463
|
+
if (!provider)
|
|
5464
|
+
return undefined;
|
|
5465
|
+
return (question, candidates) => rerankCandidates(provider, question, candidates, {
|
|
5466
|
+
timeoutMs: Math.round(2_500 * deadlineScale()),
|
|
5467
|
+
});
|
|
5468
|
+
})();
|
|
4485
5469
|
const pendingAgentContextPacks = new WeakMap();
|
|
4486
5470
|
const buildAgentRunContextPack = async (request) => {
|
|
4487
5471
|
const prepared = preparedAgentContextPacks.get(request);
|
|
@@ -4549,6 +5533,10 @@ export async function startLocalServer(opts) {
|
|
|
4549
5533
|
},
|
|
4550
5534
|
strictness: request.analysisDepth === 'deep' ? 'exploratory' : 'balanced',
|
|
4551
5535
|
limit: request.analysisDepth === 'deep' ? 120 : 80,
|
|
5536
|
+
// The runtime PRE-BUILDS this pack, so wiring the reranker only at the
|
|
5537
|
+
// provider's own `buildLocalContextPack` left it unreachable on the
|
|
5538
|
+
// common path — the prepared pack is used and that call never happens.
|
|
5539
|
+
...(agentRerankCandidates ? { rerankCandidates: agentRerankCandidates } : {}),
|
|
4552
5540
|
domainContext: requestedDomain
|
|
4553
5541
|
? resolveUiDomainContext({
|
|
4554
5542
|
manifest: snapshot.manifest,
|
|
@@ -4593,7 +5581,74 @@ export async function startLocalServer(opts) {
|
|
|
4593
5581
|
durationMs: Date.now() - startedAt,
|
|
4594
5582
|
truncated: pack.retrievalDiagnostics.topRejected.length > 0,
|
|
4595
5583
|
});
|
|
4596
|
-
|
|
5584
|
+
// Preserve real snapshot/lane outcomes for the router receipt. These are
|
|
5585
|
+
// not reconstructed later from candidate IDs: a source with no selected
|
|
5586
|
+
// card can be empty, stale, errored, or intentionally skipped.
|
|
5587
|
+
const sourceCoverage = [];
|
|
5588
|
+
const fusionLanes = pack.retrievalDiagnostics.fusion?.lanes;
|
|
5589
|
+
const retrievalLaneStates = Object.values(fusionLanes ?? {});
|
|
5590
|
+
const retrievalErrored = retrievalLaneStates.length > 0
|
|
5591
|
+
&& retrievalLaneStates.every((item) => item.status === 'error');
|
|
5592
|
+
const retrievalSkipped = retrievalLaneStates.length > 0
|
|
5593
|
+
&& retrievalLaneStates.every((item) => item.status === 'skipped');
|
|
5594
|
+
const snapshotStale = /\bstale\b|out[- ]of[- ]date/i.test(pack.warnings.join(' '));
|
|
5595
|
+
const coverageStatus = (hasCandidate) => {
|
|
5596
|
+
if (snapshotStale)
|
|
5597
|
+
return 'stale';
|
|
5598
|
+
if (hasCandidate)
|
|
5599
|
+
return 'available';
|
|
5600
|
+
if (retrievalErrored)
|
|
5601
|
+
return 'errored';
|
|
5602
|
+
if (retrievalSkipped)
|
|
5603
|
+
return 'skipped';
|
|
5604
|
+
return 'empty';
|
|
5605
|
+
};
|
|
5606
|
+
const sourceDescriptors = [
|
|
5607
|
+
{ source: 'certified', matches: (candidate) => candidate.kind === 'certified_block' },
|
|
5608
|
+
{ source: 'semantic', matches: (candidate) => candidate.kind === 'semantic_metric' || candidate.kind === 'semantic_member' || candidate.trustTier === 'semantic' },
|
|
5609
|
+
{ source: 'governed_relational', matches: (candidate) => candidate.kind === 'dql_modeling' || (candidate.relationshipEvidence?.length ?? 0) > 0 },
|
|
5610
|
+
{ source: 'exploratory', matches: (candidate) => candidate.kind === 'dbt_model' || candidate.kind === 'dbt_source' || candidate.kind === 'sql_table' || candidate.kind === 'sql_column' },
|
|
5611
|
+
{ source: 'dbt_manifest', matches: (candidate) => candidate.kind === 'dbt_model' || candidate.kind === 'dbt_source' },
|
|
5612
|
+
{ source: 'runtime_schema', matches: (candidate) => candidate.kind === 'sql_table' || candidate.kind === 'sql_column' },
|
|
5613
|
+
];
|
|
5614
|
+
// `compatible` is declared immediately below. Build IDs from the adapter
|
|
5615
|
+
// output first, then retain them unchanged through compatibility filtering.
|
|
5616
|
+
const compatible = applyContextPackCompatibility(evidence, pack, request.selectedEvidenceId);
|
|
5617
|
+
for (const descriptor of sourceDescriptors) {
|
|
5618
|
+
const ids = compatible.candidates
|
|
5619
|
+
.filter(descriptor.matches)
|
|
5620
|
+
.map((candidate) => candidate.qualifiedId ?? candidate.id)
|
|
5621
|
+
.slice(0, 32);
|
|
5622
|
+
sourceCoverage.push({
|
|
5623
|
+
version: 1,
|
|
5624
|
+
source: descriptor.source,
|
|
5625
|
+
status: coverageStatus(ids.length > 0),
|
|
5626
|
+
candidateIds: ids,
|
|
5627
|
+
...(snapshotStale ? { reason: 'The snapshot freshness warning marked this source stale.' } : {}),
|
|
5628
|
+
});
|
|
5629
|
+
}
|
|
5630
|
+
const lane = pack.retrievalDiagnostics.fusion?.lanes?.vector;
|
|
5631
|
+
if (lane) {
|
|
5632
|
+
sourceCoverage.push({
|
|
5633
|
+
version: 1,
|
|
5634
|
+
source: 'vector',
|
|
5635
|
+
status: lane.status === 'ok' ? 'available' : lane.status === 'error' ? 'errored' : lane.status === 'empty' ? 'empty' : 'skipped',
|
|
5636
|
+
candidateIds: [],
|
|
5637
|
+
...(lane.error ? { reason: lane.error } : lane.skippedReason ? { reason: lane.skippedReason } : {}),
|
|
5638
|
+
});
|
|
5639
|
+
}
|
|
5640
|
+
const hasConversation = Boolean(request.conversationContext && Object.keys(request.conversationContext).length > 0);
|
|
5641
|
+
sourceCoverage.push({
|
|
5642
|
+
version: 1,
|
|
5643
|
+
source: 'conversation',
|
|
5644
|
+
status: hasConversation ? 'available' : 'skipped',
|
|
5645
|
+
candidateIds: [],
|
|
5646
|
+
reason: hasConversation ? 'Persisted conversation context was supplied for this turn.' : 'No persisted conversation context was supplied for this turn.',
|
|
5647
|
+
});
|
|
5648
|
+
return {
|
|
5649
|
+
...compatible,
|
|
5650
|
+
diagnostics: { ...compatible.diagnostics, sourceCoverage },
|
|
5651
|
+
};
|
|
4597
5652
|
};
|
|
4598
5653
|
const buildRankedAgentRunCatalogContext = async (request) => {
|
|
4599
5654
|
const evidence = await buildAgentRunEvidence(request);
|
|
@@ -4604,6 +5659,56 @@ export async function startLocalServer(opts) {
|
|
|
4604
5659
|
};
|
|
4605
5660
|
// Compact fallback used only for plain conversational replies. Analytical
|
|
4606
5661
|
// turns use the structured, question-ranked evidence path above.
|
|
5662
|
+
/**
|
|
5663
|
+
* Explain a governed artifact the question names, from catalog metadata alone.
|
|
5664
|
+
*
|
|
5665
|
+
* Certified blocks are offered first: when a concept exists both as a
|
|
5666
|
+
* certified block and a raw model, the certified one is the authored
|
|
5667
|
+
* definition and the other is an implementation detail.
|
|
5668
|
+
*/
|
|
5669
|
+
const buildGovernedObjectExplanation = (question) => {
|
|
5670
|
+
try {
|
|
5671
|
+
const blocks = collectPlanBlocks(projectRoot, { certifiedOnly: true });
|
|
5672
|
+
const certifiedNames = new Set(blocks.map((block) => block.name));
|
|
5673
|
+
const all = [
|
|
5674
|
+
...blocks.map((block) => ({ block, status: 'certified' })),
|
|
5675
|
+
...collectPlanBlocks(projectRoot, { certifiedOnly: false })
|
|
5676
|
+
.filter((block) => !certifiedNames.has(block.name))
|
|
5677
|
+
.map((block) => ({ block, status: 'draft' })),
|
|
5678
|
+
];
|
|
5679
|
+
// Metrics as well as blocks. "How is revenue defined here?" names a
|
|
5680
|
+
// semantic metric, not a block, and answering it from the metric's own
|
|
5681
|
+
// description is the whole point of holding one.
|
|
5682
|
+
const metricObjects = loadSemanticMetrics(projectRoot).map((metric) => ({
|
|
5683
|
+
objectKey: `semantic:metric:${metric.name}`,
|
|
5684
|
+
objectType: 'semantic_metric',
|
|
5685
|
+
name: metric.name,
|
|
5686
|
+
...(metric.description ? { description: metric.description } : {}),
|
|
5687
|
+
...(metric.domain ? { domain: metric.domain } : {}),
|
|
5688
|
+
status: 'governed',
|
|
5689
|
+
payload: {},
|
|
5690
|
+
}));
|
|
5691
|
+
const explanation = composeBusinessExplanation(question, [
|
|
5692
|
+
...all.map(({ block, status }) => ({
|
|
5693
|
+
objectKey: `dql:block:${block.name}`,
|
|
5694
|
+
objectType: 'dql_block',
|
|
5695
|
+
name: block.name,
|
|
5696
|
+
...(block.description ? { description: block.description } : {}),
|
|
5697
|
+
...(block.domain ? { domain: block.domain } : {}),
|
|
5698
|
+
status,
|
|
5699
|
+
payload: {
|
|
5700
|
+
...(block.dimensions?.length ? { dimensions: block.dimensions } : {}),
|
|
5701
|
+
},
|
|
5702
|
+
})),
|
|
5703
|
+
...metricObjects,
|
|
5704
|
+
]);
|
|
5705
|
+
return explanation?.text;
|
|
5706
|
+
}
|
|
5707
|
+
catch {
|
|
5708
|
+
// Never let an explanation attempt break a conversational turn.
|
|
5709
|
+
return undefined;
|
|
5710
|
+
}
|
|
5711
|
+
};
|
|
4607
5712
|
const buildAgentRunCatalogContext = () => {
|
|
4608
5713
|
try {
|
|
4609
5714
|
const blocks = collectPlanBlocks(projectRoot, { certifiedOnly: true });
|
|
@@ -4912,6 +6017,16 @@ export async function startLocalServer(opts) {
|
|
|
4912
6017
|
const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
|
|
4913
6018
|
throw analyticalError(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`, { origin: 'dql_compilation', stage: 'compile' });
|
|
4914
6019
|
}
|
|
6020
|
+
// A certified block is authored as DQL rather than warehouse-specific SQL.
|
|
6021
|
+
// When its source uses an unqualified leaf relation, bind it only if the
|
|
6022
|
+
// immutable context snapshot proves one unique physical relation. Do not
|
|
6023
|
+
// reuse broad exploratory repairs here: certified execution may not alter
|
|
6024
|
+
// joins, aggregation, aliases, or any other frozen artifact semantics.
|
|
6025
|
+
const compiledSql = semanticCompose?.sql ?? plan.sql;
|
|
6026
|
+
const qualificationRepairs = [];
|
|
6027
|
+
const executableSql = !semanticCompose?.sql && metadata.qualifiedSchemaContext?.length
|
|
6028
|
+
? qualifyUnambiguousSqlRelationsFromSchema(compiledSql, metadata.qualifiedSchemaContext, qualificationRepairs, activeConnection.driver)
|
|
6029
|
+
: compiledSql;
|
|
4915
6030
|
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
4916
6031
|
const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
|
|
4917
6032
|
assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
|
|
@@ -4920,7 +6035,7 @@ export async function startLocalServer(opts) {
|
|
|
4920
6035
|
: undefined;
|
|
4921
6036
|
const semanticExecutionHolder = { value: null };
|
|
4922
6037
|
const execution = await analyticalExecutionService.execute({
|
|
4923
|
-
sql:
|
|
6038
|
+
sql: executableSql,
|
|
4924
6039
|
subject: 'DQL artifact query',
|
|
4925
6040
|
connection: activeConnection,
|
|
4926
6041
|
// Preserve the existing contract: previewed Ask artifacts are explicitly
|
|
@@ -4965,7 +6080,7 @@ export async function startLocalServer(opts) {
|
|
|
4965
6080
|
path: metadata.path ?? null,
|
|
4966
6081
|
domain: sourceDomain ?? null,
|
|
4967
6082
|
})),
|
|
4968
|
-
compiledSqlFingerprint: executionFingerprint(
|
|
6083
|
+
compiledSqlFingerprint: executionFingerprint(executableSql ?? preparation.sourceSql),
|
|
4969
6084
|
normalizedSqlFingerprint: executionFingerprint(preparation.decodedSql),
|
|
4970
6085
|
parameterFingerprint: executionReceipt.parameterFingerprint,
|
|
4971
6086
|
provenanceFingerprint: executionFingerprint(stableExecutionValue(invocation.resolvedParameters.map((parameter) => ({
|
|
@@ -4975,7 +6090,7 @@ export async function startLocalServer(opts) {
|
|
|
4975
6090
|
targetFingerprint: targetGenerationFingerprint(activeConnection, executionConnectionName),
|
|
4976
6091
|
snapshotFingerprint: executionFingerprint(projectSnapshot().snapshotId),
|
|
4977
6092
|
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
4978
|
-
sqlFingerprint: executionFingerprint(
|
|
6093
|
+
sqlFingerprint: executionFingerprint(executableSql ?? preparation.sourceSql),
|
|
4979
6094
|
parameterCount: plan?.sqlParams?.length ?? 0,
|
|
4980
6095
|
variableNames: Object.keys(plan?.variables ?? {}).sort(),
|
|
4981
6096
|
chartConfig: plan?.chartConfig ?? null,
|
|
@@ -5007,7 +6122,7 @@ export async function startLocalServer(opts) {
|
|
|
5007
6122
|
} : {}),
|
|
5008
6123
|
};
|
|
5009
6124
|
};
|
|
5010
|
-
const executeCertifiedBlockByNameForAgent = async (blockName, invocationInput, requireCertified = false, executionConnection, executionConnectionName) => {
|
|
6125
|
+
const executeCertifiedBlockByNameForAgent = async (blockName, invocationInput, requireCertified = false, executionConnection, executionConnectionName, qualifiedSchemaContext) => {
|
|
5011
6126
|
const manifest = buildManifest({ projectRoot });
|
|
5012
6127
|
const block = manifest.blocks[blockName];
|
|
5013
6128
|
if (!block) {
|
|
@@ -5022,6 +6137,7 @@ export async function startLocalServer(opts) {
|
|
|
5022
6137
|
path: block.filePath,
|
|
5023
6138
|
domain: block.domain,
|
|
5024
6139
|
chartType: block.chartType,
|
|
6140
|
+
...(qualifiedSchemaContext?.length ? { qualifiedSchemaContext } : {}),
|
|
5025
6141
|
}, invocationInput, executionConnection, executionConnectionName);
|
|
5026
6142
|
return {
|
|
5027
6143
|
...result,
|
|
@@ -5044,11 +6160,11 @@ export async function startLocalServer(opts) {
|
|
|
5044
6160
|
},
|
|
5045
6161
|
};
|
|
5046
6162
|
};
|
|
5047
|
-
const executeCertifiedBlockForAgent = async (node, invocationInput, executionConnection, executionConnectionName) => {
|
|
6163
|
+
const executeCertifiedBlockForAgent = async (node, invocationInput, executionConnection, executionConnectionName, qualifiedSchemaContext, requireCertified = false) => {
|
|
5048
6164
|
if (node.kind !== 'block') {
|
|
5049
6165
|
throw new Error(`Certified ${node.kind} "${node.name}" is a navigation artifact and cannot be executed as a block.`);
|
|
5050
6166
|
}
|
|
5051
|
-
return executeCertifiedBlockByNameForAgent(node.name || node.nodeId.replace(/^block:/, ''), invocationInput,
|
|
6167
|
+
return executeCertifiedBlockByNameForAgent(node.name || node.nodeId.replace(/^block:/, ''), invocationInput, requireCertified, executionConnection, executionConnectionName, qualifiedSchemaContext);
|
|
5052
6168
|
};
|
|
5053
6169
|
// Secondary agent surfaces use this compatibility callback. Delegate to the
|
|
5054
6170
|
// same artifact-first path as Ask so research/app/notebook execution cannot
|
|
@@ -5155,12 +6271,43 @@ export async function startLocalServer(opts) {
|
|
|
5155
6271
|
* string, so the only remaining reasons to differ are the connection and the
|
|
5156
6272
|
* governance gates, both of which report themselves honestly.
|
|
5157
6273
|
*/
|
|
5158
|
-
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings, executionConnectionName) => {
|
|
6274
|
+
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings, executionConnectionName, agenticCapability, agenticScope) => {
|
|
5159
6275
|
const activeConnection = requireActiveConnection(executionConnection);
|
|
5160
6276
|
const rowBound = clampAnalyticalRowBound(seed?.limit ?? 200);
|
|
5161
6277
|
const trimmed = sql.trim().replace(/;\s*$/, '').trim();
|
|
5162
6278
|
if (!trimmed)
|
|
5163
6279
|
throw analyticalError('The generated SQL was empty.', { origin: 'host', stage: 'execute' });
|
|
6280
|
+
const bindingValue = {
|
|
6281
|
+
sqlParams: bindings?.sqlParams ?? [],
|
|
6282
|
+
variables: bindings?.variables ?? {},
|
|
6283
|
+
};
|
|
6284
|
+
if (agenticCapability) {
|
|
6285
|
+
// The proposal itself is immutable. A changed literal, comment, or
|
|
6286
|
+
// whitespace is drift here rather than a benign formatting change.
|
|
6287
|
+
const currentSnapshotId = projectSnapshot().snapshotId;
|
|
6288
|
+
let targetFingerprint;
|
|
6289
|
+
if (agenticCapability.targetFingerprint) {
|
|
6290
|
+
try {
|
|
6291
|
+
targetFingerprint = (await observeWarehouseTargetIdentity(executor, activeConnection)).identityFingerprint;
|
|
6292
|
+
}
|
|
6293
|
+
catch {
|
|
6294
|
+
throw analyticalError('DQL could not re-confirm the selected execution target, so the analyst-approved query was not run.', {
|
|
6295
|
+
origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql',
|
|
6296
|
+
});
|
|
6297
|
+
}
|
|
6298
|
+
}
|
|
6299
|
+
const capabilityVerdict = verifyAgenticSqlExecutionCapability(agenticCapability, sql, {
|
|
6300
|
+
...agenticScope,
|
|
6301
|
+
bindings: bindingValue,
|
|
6302
|
+
snapshotId: currentSnapshotId,
|
|
6303
|
+
...(targetFingerprint ? { targetFingerprint } : {}),
|
|
6304
|
+
});
|
|
6305
|
+
if (!capabilityVerdict.ok) {
|
|
6306
|
+
throw analyticalError(capabilityVerdict.reason ?? 'The analyst-approved SQL no longer matches this execution.', {
|
|
6307
|
+
origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql',
|
|
6308
|
+
});
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
5164
6311
|
// RESOLVE FIRST, VALIDATE SECOND. Executing verbatim means executing the
|
|
5165
6312
|
// statement the notebook would execute — and the notebook resolves
|
|
5166
6313
|
// `@metric()` / `@dim()` refs and dbt macros before it runs anything.
|
|
@@ -5185,8 +6332,6 @@ export async function startLocalServer(opts) {
|
|
|
5185
6332
|
// release denied; a governance boundary must not move as a side effect.
|
|
5186
6333
|
const sourceDomain = seed?.source?.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1] ?? 'uncategorized';
|
|
5187
6334
|
assertAppAccess({ app, domain: sourceDomain, level: 'execute' });
|
|
5188
|
-
// A semantic query the loop already compiled (MetricFlow / dbt Cloud) must
|
|
5189
|
-
// execute through its pinned target binding, not as loose SQL.
|
|
5190
6335
|
const semanticExecutionHolder = { value: null };
|
|
5191
6336
|
const execution = await analyticalExecutionService.execute({
|
|
5192
6337
|
sql: semantic.sql,
|
|
@@ -5198,27 +6343,46 @@ export async function startLocalServer(opts) {
|
|
|
5198
6343
|
variables: bindings?.variables,
|
|
5199
6344
|
semanticRefs: semantic.semanticRefs,
|
|
5200
6345
|
executePrepared: async (preparation) => {
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
6346
|
+
// Both the native executor and the target-bound semantic adapter can
|
|
6347
|
+
// reach the warehouse from this callback. Admit the exact prepared
|
|
6348
|
+
// bytes before either path so a matching cached semantic compile cannot
|
|
6349
|
+
// bypass the generated proposal's capability.
|
|
6350
|
+
return executePreparedAgenticSqlBoundary({
|
|
6351
|
+
capability: agenticCapability,
|
|
6352
|
+
preparedSql: preparation.executedSql,
|
|
6353
|
+
bindings: bindingValue,
|
|
6354
|
+
scope: {
|
|
6355
|
+
...agenticScope,
|
|
6356
|
+
snapshotId: projectSnapshot().snapshotId,
|
|
6357
|
+
...(agenticCapability ? { targetFingerprint: agenticCapability.targetFingerprint } : {}),
|
|
6358
|
+
},
|
|
6359
|
+
execute: async () => {
|
|
6360
|
+
// A semantic query the loop already compiled (MetricFlow / dbt
|
|
6361
|
+
// Cloud) must execute through its pinned target binding, not as
|
|
6362
|
+
// loose SQL.
|
|
6363
|
+
const pinnedSemanticCompile = compiledSemanticQueries.get(executionFingerprint(preparation.preparedSql))
|
|
6364
|
+
?? compiledSemanticQueries.get(executionFingerprint(trimmed));
|
|
6365
|
+
if (pinnedSemanticCompile) {
|
|
6366
|
+
const semanticExecution = await executeTargetBoundSemanticQuery({
|
|
6367
|
+
executor,
|
|
6368
|
+
connection: activeConnection,
|
|
6369
|
+
projectRoot,
|
|
6370
|
+
plannedAdapter: pinnedSemanticCompile.engine,
|
|
6371
|
+
metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
|
|
6372
|
+
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
6373
|
+
: undefined,
|
|
6374
|
+
compile: async () => pinnedSemanticCompile,
|
|
6375
|
+
prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
|
|
6376
|
+
rowBound,
|
|
6377
|
+
});
|
|
6378
|
+
if (semanticExecution) {
|
|
6379
|
+
semanticExecutionHolder.value = semanticExecution;
|
|
6380
|
+
return semanticExecution.result;
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
return executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
|
|
5219
6384
|
}
|
|
5220
|
-
}
|
|
5221
|
-
return executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
|
|
6385
|
+
});
|
|
5222
6386
|
},
|
|
5223
6387
|
});
|
|
5224
6388
|
const semanticExecution = semanticExecutionHolder.value;
|
|
@@ -5276,14 +6440,19 @@ export async function startLocalServer(opts) {
|
|
|
5276
6440
|
executableArtifact,
|
|
5277
6441
|
};
|
|
5278
6442
|
};
|
|
5279
|
-
const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName) => {
|
|
6443
|
+
const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName, agenticCapability, agenticScope) => {
|
|
5280
6444
|
// A seed that is already a certified/saved artifact keeps the DQL-first
|
|
5281
6445
|
// path: there the `.dql` source IS the contract, and its parameters and
|
|
5282
6446
|
// semantic refs must be compiled, not bypassed.
|
|
5283
6447
|
if (seed && seed.kind !== 'sql_block') {
|
|
6448
|
+
if (agenticCapability) {
|
|
6449
|
+
throw analyticalError('The analyst-approved SQL cannot be redirected through a saved artifact.', {
|
|
6450
|
+
origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql',
|
|
6451
|
+
});
|
|
6452
|
+
}
|
|
5284
6453
|
return executeArtifactReferenceForAgent({ ...seed, limit: seed.limit ?? 200 }, question, executionConnection, executionConnectionName);
|
|
5285
6454
|
}
|
|
5286
|
-
return executeGeneratedSqlDirect(question, sql, seed, executionConnection, undefined, executionConnectionName);
|
|
6455
|
+
return executeGeneratedSqlDirect(question, sql, seed, executionConnection, undefined, executionConnectionName, agenticCapability, agenticScope);
|
|
5287
6456
|
};
|
|
5288
6457
|
/**
|
|
5289
6458
|
* EXP-001: execution host for the deliberately narrow non-governed lane.
|
|
@@ -5891,7 +7060,10 @@ export async function startLocalServer(opts) {
|
|
|
5891
7060
|
let invocation;
|
|
5892
7061
|
let plan;
|
|
5893
7062
|
try {
|
|
5894
|
-
|
|
7063
|
+
// The source label is echoed into parse errors, which reach the user.
|
|
7064
|
+
// `<bounded-dql-repair>` is an internal artifact id and tells them nothing;
|
|
7065
|
+
// it appeared verbatim in a reported failure card.
|
|
7066
|
+
const program = new Parser(repairedSource, 'repaired query').parse();
|
|
5895
7067
|
const blocks = program.statements.filter((statement) => statement.kind === NodeKind.BlockDecl);
|
|
5896
7068
|
if (blocks.length !== 1 || program.statements.length !== 1) {
|
|
5897
7069
|
throw new Error('The repaired source must contain exactly one DQL block.');
|
|
@@ -9126,7 +10298,7 @@ export async function startLocalServer(opts) {
|
|
|
9126
10298
|
// state wins; the client-built context stays the no-threadId fallback).
|
|
9127
10299
|
const conversationStore = parsed.request.threadId ? getConversationStore() : null;
|
|
9128
10300
|
if (conversationStore && parsed.request.threadId && conversationStore.getThread(parsed.request.threadId)) {
|
|
9129
|
-
parsed.request.conversationContext = await conversationContextFromThread(conversationStore, parsed.request.threadId, parsed.request.conversationContext, parsed.request.question);
|
|
10301
|
+
parsed.request.conversationContext = await conversationContextFromThread(conversationStore, parsed.request.threadId, parsed.request.conversationContext, parsed.request.question, Boolean(parsed.request.selectedEvidenceId));
|
|
9130
10302
|
// The persisted thread is authoritative for prior turns. Raw client
|
|
9131
10303
|
// history would duplicate the same conversation into the prompt a
|
|
9132
10304
|
// second time — dropping it keeps follow-up prompts bounded.
|
|
@@ -9134,10 +10306,13 @@ export async function startLocalServer(opts) {
|
|
|
9134
10306
|
parsed.request.history = [];
|
|
9135
10307
|
}
|
|
9136
10308
|
const wantsStream = url.searchParams.get('stream') === '1' || url.searchParams.get('stream') === 'true';
|
|
9137
|
-
|
|
10309
|
+
// The public body may carry UI correlation data, but the run identity
|
|
10310
|
+
// is server-owned. Mint it before every controller/SSE/engine handoff
|
|
10311
|
+
// so no client-provided string can bind a SQL capability or operation.
|
|
10312
|
+
const runId = randomUUID();
|
|
10313
|
+
parsed.request.runId = runId;
|
|
9138
10314
|
const runController = new AbortController();
|
|
9139
|
-
|
|
9140
|
-
activeAgentRunControllers.set(runId, runController);
|
|
10315
|
+
activeAgentRunControllers.set(runId, runController);
|
|
9141
10316
|
let streamConnected = true;
|
|
9142
10317
|
res.on('close', () => { streamConnected = false; });
|
|
9143
10318
|
const writeStream = (event, data) => {
|
|
@@ -9158,13 +10333,13 @@ export async function startLocalServer(opts) {
|
|
|
9158
10333
|
'X-Accel-Buffering': 'no',
|
|
9159
10334
|
});
|
|
9160
10335
|
}
|
|
9161
|
-
const operation =
|
|
10336
|
+
const operation = operationCoordinator.create({
|
|
9162
10337
|
type: 'agent_run',
|
|
9163
10338
|
scope: `agent-run:${runId}`,
|
|
9164
10339
|
resourceRevision: parsed.request.threadId,
|
|
9165
10340
|
message: 'AI request accepted. You can change pages while it runs.',
|
|
9166
10341
|
cancellable: true,
|
|
9167
|
-
})
|
|
10342
|
+
});
|
|
9168
10343
|
if (wantsStream)
|
|
9169
10344
|
writeStream('agent-run-accepted', { runId, operationId: operation?.id });
|
|
9170
10345
|
let completedRun;
|
|
@@ -9256,8 +10431,7 @@ export async function startLocalServer(opts) {
|
|
|
9256
10431
|
res.end(serializeJSON({ run: slimAgentRunForTransport(completedRun) }));
|
|
9257
10432
|
}
|
|
9258
10433
|
finally {
|
|
9259
|
-
|
|
9260
|
-
activeAgentRunControllers.delete(runId);
|
|
10434
|
+
activeAgentRunControllers.delete(runId);
|
|
9261
10435
|
}
|
|
9262
10436
|
}
|
|
9263
10437
|
catch (error) {
|
|
@@ -18001,6 +19175,16 @@ export function resolveDefaultLLMProvider(projectRoot) {
|
|
|
18001
19175
|
* answer envelope. Everything else uses the Settings-resolved default runner.
|
|
18002
19176
|
*/
|
|
18003
19177
|
export function resolveGovernedAnswerRunner(projectRoot) {
|
|
19178
|
+
// Runtime eval cassettes are an explicit, offline provider source. Resolve
|
|
19179
|
+
// them before Settings because the CI fixture intentionally has no user
|
|
19180
|
+
// provider configuration; otherwise Ask exits at this earlier gate and never
|
|
19181
|
+
// reaches the cassette-wrapped provider used by the answer loop.
|
|
19182
|
+
const cassetteProvider = createEvalCassetteReplayProvider(projectRoot);
|
|
19183
|
+
if (cassetteProvider) {
|
|
19184
|
+
const provider = governedRunnerProviderForCassette(cassetteProvider.name);
|
|
19185
|
+
if (provider)
|
|
19186
|
+
return { provider, runner: createDqlAgentProviderRunner(provider, cassetteProvider) };
|
|
19187
|
+
}
|
|
18004
19188
|
const active = getActiveProvider(projectRoot);
|
|
18005
19189
|
if (isGovernedAnswerProviderId(active)) {
|
|
18006
19190
|
return { provider: active, runner: createDqlAgentProviderRunner(active) };
|
|
@@ -18013,6 +19197,14 @@ export function resolveGovernedAnswerRunner(projectRoot) {
|
|
|
18013
19197
|
}
|
|
18014
19198
|
return null;
|
|
18015
19199
|
}
|
|
19200
|
+
function governedRunnerProviderForCassette(name) {
|
|
19201
|
+
switch (name) {
|
|
19202
|
+
case 'claude': return 'anthropic';
|
|
19203
|
+
case 'openai': return 'openai';
|
|
19204
|
+
case 'gemini': return 'gemini';
|
|
19205
|
+
case 'ollama': return 'ollama';
|
|
19206
|
+
}
|
|
19207
|
+
}
|
|
18016
19208
|
function isGovernedAnswerProviderId(value) {
|
|
18017
19209
|
return value === 'anthropic'
|
|
18018
19210
|
|| value === 'openai'
|
|
@@ -21580,7 +22772,7 @@ export function buildAgentPreviewSql(sql, rowLimit = 200) {
|
|
|
21580
22772
|
*/
|
|
21581
22773
|
export function repairExploratorySqlBeforeExecution(sql, schemaContext, question = '', dialect = 'duckdb') {
|
|
21582
22774
|
const repairs = [];
|
|
21583
|
-
let repairedSql =
|
|
22775
|
+
let repairedSql = qualifyUnambiguousSqlRelationsFromSchema(sql, schemaContext, repairs, dialect);
|
|
21584
22776
|
repairedSql = repairExploratoryRelationQualifiers(repairedSql, repairs, dialect);
|
|
21585
22777
|
repairedSql = repairExploratoryLifetimeMeasureSelection(repairedSql, schemaContext, question, repairs, dialect);
|
|
21586
22778
|
repairedSql = repairExploratoryMisleadingPercentAliases(repairedSql, question, repairs);
|
|
@@ -21639,7 +22831,13 @@ export function applyRequestedTopNToExploratorySql(sql, requestedTopN) {
|
|
|
21639
22831
|
}
|
|
21640
22832
|
return `${withoutTerminator}\nLIMIT ${requestedTopN}`;
|
|
21641
22833
|
}
|
|
21642
|
-
|
|
22834
|
+
/**
|
|
22835
|
+
* Bind an already-authored unqualified relation to a unique inspected physical
|
|
22836
|
+
* relation. The caller owns whether that binding is permitted (exploratory
|
|
22837
|
+
* preflight or a frozen certified artifact); this helper itself never adds a
|
|
22838
|
+
* relation, key, join, predicate, or column.
|
|
22839
|
+
*/
|
|
22840
|
+
export function qualifyUnambiguousSqlRelationsFromSchema(sql, schemaContext, repairs, dialect = 'duckdb') {
|
|
21643
22841
|
const analysis = analyzeSqlReferences(sql, dialect);
|
|
21644
22842
|
if (!analysis.parsed)
|
|
21645
22843
|
return sql;
|
|
@@ -25778,6 +26976,14 @@ async function buildBlockStudioAiAssistSummary(projectRoot, action, candidate, v
|
|
|
25778
26976
|
}
|
|
25779
26977
|
}
|
|
25780
26978
|
async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
|
|
26979
|
+
// Runtime-driven evals intentionally start from a clean fixture with no user
|
|
26980
|
+
// provider settings. When replay cassettes are explicitly enabled, their
|
|
26981
|
+
// recorded identity is the authoritative provider and never performs network
|
|
26982
|
+
// readiness checks. Normal product startup has no cassette env and follows
|
|
26983
|
+
// the unchanged configured-provider path below.
|
|
26984
|
+
const cassetteProvider = createEvalCassetteReplayProvider(projectRoot);
|
|
26985
|
+
if (cassetteProvider)
|
|
26986
|
+
return cassetteProvider;
|
|
25781
26987
|
const settings = listProviderSettings(projectRoot);
|
|
25782
26988
|
const activeProvider = getActiveProvider(projectRoot);
|
|
25783
26989
|
// Subscription CLI providers (Claude Code / Codex) carry no API key — they're
|
|
@@ -25821,7 +27027,13 @@ async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
|
|
|
25821
27027
|
default:
|
|
25822
27028
|
return null;
|
|
25823
27029
|
}
|
|
25824
|
-
|
|
27030
|
+
// Route through the eval cassette too. This constructor serves the MEANING
|
|
27031
|
+
// call and narration — the two dispatches that decide routing and wording —
|
|
27032
|
+
// so leaving it unwrapped meant a recorded suite still hit a live model for
|
|
27033
|
+
// exactly the calls whose non-determinism it was recorded to remove. A local
|
|
27034
|
+
// baseline reproduced that: the same question blocked on one run and answered
|
|
27035
|
+
// on the next, and zero cassettes were written.
|
|
27036
|
+
return await provider.available() ? applyEvalCassette(provider, projectRoot) : null;
|
|
25825
27037
|
}
|
|
25826
27038
|
/** Convert a governed answer's result payload into a bounded synthesis preview. */
|
|
25827
27039
|
function agentResultToSynthesisPreview(result) {
|
|
@@ -28658,7 +29870,45 @@ async function buildAgentSchemaContextFromCatalog(projectRoot, question, prepare
|
|
|
28658
29870
|
const RUNTIME_SNAPSHOT_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
28659
29871
|
// A resolver compares at most 12 compact cards and never performs tool calls;
|
|
28660
29872
|
// ten seconds is the full allowance, not the start of another planning loop.
|
|
28661
|
-
|
|
29873
|
+
/**
|
|
29874
|
+
* Ceiling on the one bounded meaning-resolution call.
|
|
29875
|
+
*
|
|
29876
|
+
* 10s assumes a hosted model. A local Ollama model needs ~7s for a ONE-WORD
|
|
29877
|
+
* reply, so a 600-token resolution over a dozen candidates never lands: it
|
|
29878
|
+
* aborts, the router falls back to its evidence-only decision, and
|
|
29879
|
+
* `mayAssumeInterpretation` goes false — which sends every ambiguous question to
|
|
29880
|
+
* the clarification gate (AGT-017). The effect is that a local model cannot
|
|
29881
|
+
* answer anything ambiguous, in a product whose whole positioning is local-first.
|
|
29882
|
+
*
|
|
29883
|
+
* Scaled by the same `DQL_AGENT_DEADLINE_SCALE` as the run budget, so one
|
|
29884
|
+
* setting moves the provider's whole time envelope together rather than leaving
|
|
29885
|
+
* an inner bound to silently cap an outer one.
|
|
29886
|
+
*/
|
|
29887
|
+
/**
|
|
29888
|
+
* Predict how long the next provider call will take, for admission control.
|
|
29889
|
+
*
|
|
29890
|
+
* With fewer than three samples the MAX is the only honest predictor: there is
|
|
29891
|
+
* no distribution yet, and admitting a call the deadline then kills wastes the
|
|
29892
|
+
* whole remaining budget.
|
|
29893
|
+
*
|
|
29894
|
+
* With a real sample, p75 rather than the max. One slow response — a cold model
|
|
29895
|
+
* load, a retried connection — otherwise poisons admission control for the rest
|
|
29896
|
+
* of the run: every later call is refused against a worst case that already
|
|
29897
|
+
* passed. A recorded run tripped RUN_DEADLINE_INSUFFICIENT 6.4s into a 45s
|
|
29898
|
+
* budget for exactly that reason. p75 still errs slow, so a genuinely slow
|
|
29899
|
+
* provider is still respected.
|
|
29900
|
+
*/
|
|
29901
|
+
export function predictDispatchMs(observed, assumedMs = ASSUMED_PROVIDER_DISPATCH_MS) {
|
|
29902
|
+
if (observed.length === 0)
|
|
29903
|
+
return assumedMs;
|
|
29904
|
+
const sorted = [...observed].sort((left, right) => left - right);
|
|
29905
|
+
if (sorted.length < 3)
|
|
29906
|
+
return sorted[sorted.length - 1];
|
|
29907
|
+
const index = Math.max(0, Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.75) - 1));
|
|
29908
|
+
return sorted[index];
|
|
29909
|
+
}
|
|
29910
|
+
const AGENT_MEANING_TIMEOUT_BASE_MS = 10_000;
|
|
29911
|
+
const AGENT_MEANING_TIMEOUT_MS = AGENT_MEANING_TIMEOUT_BASE_MS * deadlineScale();
|
|
28662
29912
|
export function boundedAgentMeaningSignal(signal, timeoutMs = AGENT_MEANING_TIMEOUT_MS) {
|
|
28663
29913
|
const timeout = AbortSignal.timeout(Math.max(1, timeoutMs));
|
|
28664
29914
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
@@ -29410,7 +30660,7 @@ function compactSqlForRunHistory(sql) {
|
|
|
29410
30660
|
const clean = sql.replace(/\s+/g, ' ').trim();
|
|
29411
30661
|
return clean.length > 1200 ? `${clean.slice(0, 1197)}...` : clean;
|
|
29412
30662
|
}
|
|
29413
|
-
function buildAgentSchemaContextFromContextPack(question, contextPack) {
|
|
30663
|
+
function buildAgentSchemaContextFromContextPack(question, contextPack, options = {}) {
|
|
29414
30664
|
const byRelation = new Map();
|
|
29415
30665
|
const objectsByKey = new Map(contextPack.objects.map((object) => [object.objectKey, object]));
|
|
29416
30666
|
const upsert = (table) => {
|
|
@@ -29464,11 +30714,75 @@ function buildAgentSchemaContextFromContextPack(question, contextPack) {
|
|
|
29464
30714
|
table,
|
|
29465
30715
|
score: scoreAgentSchemaTable(table, tokens) + (shouldProbeValues ? scoreAgentValueProbeTable(table) : 0),
|
|
29466
30716
|
}))
|
|
29467
|
-
.filter((entry) => entry.table.columns.length > 0 && entry.score > 0)
|
|
30717
|
+
.filter((entry) => entry.table.columns.length > 0 && (options.includeUnscored || entry.score > 0))
|
|
29468
30718
|
.sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation))
|
|
29469
|
-
.slice(0, 12)
|
|
30719
|
+
.slice(0, Math.max(1, options.limit ?? 12))
|
|
29470
30720
|
.map((entry) => entry.table);
|
|
29471
30721
|
}
|
|
30722
|
+
/**
|
|
30723
|
+
* Exact frozen certified execution needs the physical relation closure from
|
|
30724
|
+
* the same source snapshot, not only the twelve tables that happened to rank
|
|
30725
|
+
* for the natural-language prompt. An artifact may project `category` while
|
|
30726
|
+
* its authored SQL says `FROM order_items`, so prompt relevance alone is not
|
|
30727
|
+
* a safe authority to remove that relation from the execution handoff.
|
|
30728
|
+
*
|
|
30729
|
+
* This remains deliberately narrower than an exploratory repair: it exposes
|
|
30730
|
+
* only snapshot-indexed dbt models plus the already retrieved local objects.
|
|
30731
|
+
* `qualifyUnambiguousSqlRelationsFromSchema` still changes a leaf only when
|
|
30732
|
+
* exactly one physical relation in that closure owns it. Duplicate leaves
|
|
30733
|
+
* therefore remain a same-tier certified failure rather than a guessed bind.
|
|
30734
|
+
*/
|
|
30735
|
+
function buildFrozenCertifiedSchemaContext(contextPack, manifest) {
|
|
30736
|
+
const byRelation = new Map();
|
|
30737
|
+
const upsert = (table) => {
|
|
30738
|
+
if (!table.relation || !table.name)
|
|
30739
|
+
return;
|
|
30740
|
+
const key = table.relation.toLowerCase();
|
|
30741
|
+
const existing = byRelation.get(key);
|
|
30742
|
+
if (!existing) {
|
|
30743
|
+
byRelation.set(key, {
|
|
30744
|
+
...table,
|
|
30745
|
+
columns: dedupeAgentSchemaColumns(table.columns).slice(0, 80),
|
|
30746
|
+
});
|
|
30747
|
+
return;
|
|
30748
|
+
}
|
|
30749
|
+
byRelation.set(key, {
|
|
30750
|
+
...existing,
|
|
30751
|
+
description: existing.description ?? table.description,
|
|
30752
|
+
source: existing.source === table.source ? existing.source : 'local metadata catalog',
|
|
30753
|
+
columns: dedupeAgentSchemaColumns([...existing.columns, ...table.columns]).slice(0, 80),
|
|
30754
|
+
});
|
|
30755
|
+
};
|
|
30756
|
+
if (contextPack) {
|
|
30757
|
+
for (const object of contextPack.objects) {
|
|
30758
|
+
const table = metadataObjectToAgentSchemaTable(object);
|
|
30759
|
+
if (table)
|
|
30760
|
+
upsert(table);
|
|
30761
|
+
}
|
|
30762
|
+
}
|
|
30763
|
+
for (const model of manifest.dbtImport?.dbtDag?.models ?? []) {
|
|
30764
|
+
const relation = [model.database, model.schema, model.name].filter(Boolean).join('.');
|
|
30765
|
+
// A bare dbt identity does not prove a physical catalog/schema and must
|
|
30766
|
+
// not be promoted into a qualifier. It remains harmless context only.
|
|
30767
|
+
if (!relation || relation === model.name)
|
|
30768
|
+
continue;
|
|
30769
|
+
upsert({
|
|
30770
|
+
relation,
|
|
30771
|
+
schema: model.schema,
|
|
30772
|
+
name: model.name,
|
|
30773
|
+
description: model.description,
|
|
30774
|
+
columns: (model.columns ?? []).map((column) => ({
|
|
30775
|
+
name: column.name,
|
|
30776
|
+
type: column.type,
|
|
30777
|
+
description: column.description,
|
|
30778
|
+
})),
|
|
30779
|
+
source: 'local metadata catalog',
|
|
30780
|
+
});
|
|
30781
|
+
}
|
|
30782
|
+
return Array.from(byRelation.values())
|
|
30783
|
+
.filter((table) => table.columns.length > 0)
|
|
30784
|
+
.sort((left, right) => left.relation.localeCompare(right.relation));
|
|
30785
|
+
}
|
|
29472
30786
|
function metadataObjectToAgentSchemaTable(object) {
|
|
29473
30787
|
if (object.objectType === 'dbt_column' || object.objectType === 'runtime_column') {
|
|
29474
30788
|
const relation = metadataPayloadString(object, 'relation');
|
|
@@ -30043,49 +31357,9 @@ function scoreAgentValueProbeColumn(table, column) {
|
|
|
30043
31357
|
return score;
|
|
30044
31358
|
}
|
|
30045
31359
|
export function isAgentValueProbeColumn(column) {
|
|
30046
|
-
|
|
30047
|
-
//
|
|
30048
|
-
|
|
30049
|
-
// can never be probed through automatic grounding.
|
|
30050
|
-
const normalizedName = column.name
|
|
30051
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
30052
|
-
.replace(/[_-]+/g, ' ')
|
|
30053
|
-
.toLowerCase();
|
|
30054
|
-
if (/\b(password|secret|token|credential|hash|salt|notes?|comments?|description|message|body|payload|content)\b/.test(normalizedName))
|
|
30055
|
-
return false;
|
|
30056
|
-
if (/\bemail\b/.test(normalizedName))
|
|
30057
|
-
return false;
|
|
30058
|
-
if (!hasAgentSchemaToken(name, [
|
|
30059
|
-
'account',
|
|
30060
|
-
'category',
|
|
30061
|
-
'channel',
|
|
30062
|
-
'city',
|
|
30063
|
-
'code',
|
|
30064
|
-
'country',
|
|
30065
|
-
'customer',
|
|
30066
|
-
'email',
|
|
30067
|
-
'full',
|
|
30068
|
-
'id',
|
|
30069
|
-
'key',
|
|
30070
|
-
'member',
|
|
30071
|
-
'name',
|
|
30072
|
-
'number',
|
|
30073
|
-
'product',
|
|
30074
|
-
'region',
|
|
30075
|
-
'segment',
|
|
30076
|
-
'sku',
|
|
30077
|
-
'state',
|
|
30078
|
-
'status',
|
|
30079
|
-
'subscriber',
|
|
30080
|
-
'type',
|
|
30081
|
-
'user',
|
|
30082
|
-
])) {
|
|
30083
|
-
return false;
|
|
30084
|
-
}
|
|
30085
|
-
const type = column.type?.toLowerCase() ?? '';
|
|
30086
|
-
if (!type)
|
|
30087
|
-
return true;
|
|
30088
|
-
return /\b(char|character|clob|email|string|text|uuid|varchar)\b/.test(type);
|
|
31360
|
+
// Delegates to the canonical predicate in dql-agent. Two copies of a security
|
|
31361
|
+
// rule drift, and the one that drifts is the one nobody is looking at.
|
|
31362
|
+
return isProbeSafeColumn({ name: column.name, ...(column.type ? { type: column.type } : {}) });
|
|
30089
31363
|
}
|
|
30090
31364
|
export function buildAgentValueProbeSql(table, column, searchTerms, connection) {
|
|
30091
31365
|
const relation = quoteAgentRelation(table.relation, connection);
|