@duckcodeailabs/dql-cli 1.14.1 → 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/assets/dql-notebook/assets/{AgentLogPage-BPz-UWFh.js → AgentLogPage-DKbGpRQS.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-BEl53WA_.js → AiBuildDialog-DPSu0Mly.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-B4yfGTTZ.js → AiBuildResult-1uaGnpi1.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiSidePanel-CSZAAvuD.js → AiSidePanel-BwMREwa7.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AnalyticsHome-D5P6Ujwi.js → AnalyticsHome-BGfey_ve.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AppsView-DQOwU9Cg.js → AppsView-CM1tPywy.js} +4 -4
- package/dist/assets/dql-notebook/assets/{BlockStudio-B4ap0GdY.js → BlockStudio--S6WFVO4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-BIfNI-0S.js → BusinessArtifactView-BEAJ-yNW.js} +1 -1
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-D72byb2g.js → DbtFirstModelingPage-CNyU5MBX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GitPage-lQb1uXH2.js → GitPage-IcydAai3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-CVECf6Xj.js → GlobalAiRail-CSKW-5eD.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-trOyMCY6.js → GovernedContextPage-CFen0eFT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-D8hLS5lE.js → HelpDocsPage-D0D9UCIz.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-eIkfBIep.js → HomePage-CmSxapR3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDAG-CSqcbDrE.js → LineageDAG-BGUcIt1B.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDetailView-DJZZjZu-.js → LineageDetailView-Dx48Zfzb.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-BcIipIc3.js → LineageDrawer-zaBfSLZO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-CviIf8PN.js → LineagePathBreadcrumb-CTZJp4_r.js} +1 -1
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-vH_MY_Ju.js → MiniLineageGraph-CdivNR1S.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-DbCQg-pj.js → NewBlockModal-DMBFB7nE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-5Vp6XiuK.js → NewNotebookModal-DsC9CWlW.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-DjqJS44s.js → NotebookEditor-hs-kw8v9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{ReadinessPage-BHGrC5ho.js → ReadinessPage-DJ83cMik.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-BfS9Tdx-.js → SetupOnboarding-B1Pu_Bvv.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SkillsPage-BFH21wSj.js → SkillsPage-BHKDay8n.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-zm6g_SxZ.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-DPhxIEzF.js → answer-to-notebook-DmNiuLQA.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-DNEb86Xc.js → arrow-left--1rsrxm8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-DpPWbwaD.js → arrow-right-D5TdqY1G.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-D7s5jo4X.js → book-open-text-Bw7nHbzg.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-Db3dXKg7.js → circle-x-DLe6NNM4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-C7pppQ1a.js → dagre.esm-CW5QZdBt.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-BxwXitO_.js → external-link-C9Q97sA3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-Dt4lkWRi.js → grip-vertical-CztvkIgo.js} +1 -1
- package/dist/assets/dql-notebook/assets/{index-DKo-bwNw.js → index-zHHzDn6l.js} +127 -127
- package/dist/assets/dql-notebook/assets/{link-2-Dfo2P6wi.js → link-2-CiKAvumL.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-DiTmIWAL.js → list-tree-BtnP2nQ5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-CMTAkPzL.js → minimize-2-TSFGxcCP.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-DwYr7FW4.js → panel-right-open-BfXIUWy0.js} +1 -1
- package/dist/assets/dql-notebook/assets/{play-BXhHYQ4x.js → play-DVbSFJHD.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-BNi6F8pl.js → rotate-ccw-D_cesDcX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-CNOGysAy.js → semantic-fields-CoVStdYB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{sliders-horizontal-Ec5MUMUW.js → sliders-horizontal-l7xV9K5A.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-B9leDkp_.js → star-CSBS0H3b.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-BefTYCzx.js → triangle-alert-BTrnyY4q.js} +1 -1
- package/dist/assets/dql-notebook/assets/{upload-SPiOM2tQ.js → upload-sySLq9zb.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-C4foXeiQ.js → usePersistedAgentThreadId-CzwgGdus.js} +1 -1
- package/dist/assets/dql-notebook/assets/{user-round-comGmyw-.js → user-round-ChlgXi9j.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-BffR4dF8.js → wand-sparkles-CGw0ytyT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-ChmPTEzH.js → workflow-C_RltiK5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-DovaG_ze.js → wrench-D-wLfeu0.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-nRx91AgW.js → x-B28hIJIC.js} +1 -1
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/agent-eval-cassette.d.ts +97 -6
- package/dist/commands/agent-eval-cassette.d.ts.map +1 -1
- package/dist/commands/agent-eval-cassette.js +165 -22
- package/dist/commands/agent-eval-cassette.js.map +1 -1
- package/dist/commands/agent-eval-runtime.d.ts +14 -2
- package/dist/commands/agent-eval-runtime.d.ts.map +1 -1
- package/dist/commands/agent-eval-runtime.js +12 -2
- package/dist/commands/agent-eval-runtime.js.map +1 -1
- package/dist/commands/agent.d.ts +9 -3
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +126 -49
- 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/llm/providers/dql-agent-provider.d.ts +28 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +272 -22
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +29 -1
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +16 -1
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +824 -66
- 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--oxmjlgr.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,10 +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 { applyFinding, createResearchState, narrationMaxTokensForFacts, nextHypothesis, rerankCandidates, synthesizeResearchNarrative, AgenticExecutionCapabilityGate, mintFinalSqlAuthorization, verifyAgenticSqlExecutionCapability, qualifyAuthorizationReferences, validateSqlAgainstLocalContext as validateAuthorizedSqlReferences, verifyFinalSql, } from '@duckcodeailabs/dql-agent';
|
|
27
|
-
import { applyEvalCassette, createDqlAgentProviderRunner, createGovernedTextProvider, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
|
|
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';
|
|
28
28
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
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, buildCoverageGap, capResearchBranches, buildResearchEvidenceLedger, buildAnalyticalTurnPlan, resolveTopRankedRegionDependency, 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';
|
|
30
30
|
import { addSqlResultFilter, dashboardFilterableResultColumns, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
|
|
31
31
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
32
32
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization, } from './apps-api.js';
|
|
@@ -314,10 +314,14 @@ export function parseAgentRunRequestBody(body) {
|
|
|
314
314
|
selectedObject,
|
|
315
315
|
executionTarget,
|
|
316
316
|
workspaceContext,
|
|
317
|
-
conversationContext: sanitizeClientConversationContext(agentRunRecord(record.conversationContext)
|
|
317
|
+
conversationContext: sanitizeClientConversationContext(agentRunRecord(record.conversationContext), {
|
|
318
|
+
stripStructuredSelectionEnvelope: Boolean(agentRunString(record.selectedEvidenceId)),
|
|
319
|
+
}),
|
|
318
320
|
history: parseAgentRunHistory(record.history),
|
|
319
321
|
threadId: agentRunString(record.threadId),
|
|
320
|
-
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.
|
|
321
325
|
reasoningEffort: parseAgentRunReasoningEffort(record.reasoningEffort),
|
|
322
326
|
analysisDepth: parseAgentRunAnalysisDepth(record.analysisDepth) ?? parseAgentRunAnalysisDepth(record.depth),
|
|
323
327
|
thinkingMode: coerceThinkingMode(record.thinkingMode),
|
|
@@ -334,12 +338,21 @@ const CLIENT_PLAN_AUTHORITY_KEYS = new Set([
|
|
|
334
338
|
// a child filter or skip ordinary member validation.
|
|
335
339
|
'analyticalTaskDependencyBinding',
|
|
336
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',
|
|
349
|
+
]);
|
|
337
350
|
/**
|
|
338
351
|
* Browser/embedding context is useful retrieval and history input, but it is
|
|
339
352
|
* not a plan-authority channel. Remove plan-shaped fields recursively at HTTP
|
|
340
353
|
* ingress; server-retained thread context is added afterwards from local state.
|
|
341
354
|
*/
|
|
342
|
-
function sanitizeClientConversationContext(context) {
|
|
355
|
+
function sanitizeClientConversationContext(context, options = {}) {
|
|
343
356
|
if (!context)
|
|
344
357
|
return undefined;
|
|
345
358
|
const sanitize = (value) => {
|
|
@@ -348,7 +361,14 @@ function sanitizeClientConversationContext(context) {
|
|
|
348
361
|
const record = agentRunRecord(value);
|
|
349
362
|
if (!record)
|
|
350
363
|
return value;
|
|
351
|
-
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
|
+
}));
|
|
352
372
|
};
|
|
353
373
|
return sanitize(context);
|
|
354
374
|
}
|
|
@@ -537,6 +557,18 @@ export function agentAnswerHasExecutionFailure(governedAnswer) {
|
|
|
537
557
|
return typeof governedAnswer.executionError === 'string'
|
|
538
558
|
&& governedAnswer.executionError.trim().length > 0;
|
|
539
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
|
+
}
|
|
540
572
|
/** Rebuild the immutable failed-run input from the artifact retained by API-007. */
|
|
541
573
|
export function analyticalFailedRunFromAgentRun(run) {
|
|
542
574
|
for (const artifact of run.artifacts) {
|
|
@@ -888,7 +920,7 @@ function businessNarrativeGaps(warnings) {
|
|
|
888
920
|
// When a run carries a threadId, the persisted thread is the authoritative
|
|
889
921
|
// source of prior turns (survives refresh); the client-built context remains
|
|
890
922
|
// the fallback for embedders that never send a threadId.
|
|
891
|
-
async function conversationContextFromThread(store, threadId, clientContext, question) {
|
|
923
|
+
async function conversationContextFromThread(store, threadId, clientContext, question, preservePendingClarification = false) {
|
|
892
924
|
// Token-budget backstop: six verbatim turns with per-field caps. Older turns
|
|
893
925
|
// remain reachable through the rolling summary + semantic recall below —
|
|
894
926
|
// carrying more raw prose mostly slows every provider call on follow-ups.
|
|
@@ -936,7 +968,10 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
936
968
|
const thread = store.getThread(threadId);
|
|
937
969
|
// Bounded structured snapshot (working state + rolling summary + topic relation)
|
|
938
970
|
// for the answer loop's conversation-state prompt section.
|
|
939
|
-
const serverSnapshot = buildConversationSnapshot(store, threadId, {
|
|
971
|
+
const serverSnapshot = buildConversationSnapshot(store, threadId, {
|
|
972
|
+
question,
|
|
973
|
+
preservePendingClarification,
|
|
974
|
+
});
|
|
940
975
|
if (serverSnapshot && question) {
|
|
941
976
|
// Semantic recall over OLDER turns (the recent window is already verbatim).
|
|
942
977
|
serverSnapshot.recalledTurns = await recallRelevantTurns(store, threadId, question, {
|
|
@@ -944,12 +979,26 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
944
979
|
excludeTurnIds: serverSnapshot.recentTurns.map((turn) => turn.id),
|
|
945
980
|
});
|
|
946
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;
|
|
947
995
|
return {
|
|
948
996
|
...(sanitizeClientConversationContext(clientContext) ?? {}),
|
|
949
997
|
conversationStateVersion: 1,
|
|
950
998
|
threadId,
|
|
951
999
|
...(serverSnapshot ? { conversationEnvelope: serverSnapshot } : {}),
|
|
952
1000
|
...(serverSnapshot ? { serverSnapshot } : {}),
|
|
1001
|
+
...(serverIssuedClarificationSelection ? { serverIssuedClarificationSelection } : {}),
|
|
953
1002
|
...(thread?.rollingSummary ? { conversationSummary: thread.rollingSummary } : {}),
|
|
954
1003
|
// `activeTurnId` is the FOLLOW-UP ANCHOR: the turn whose filters, prior DQL
|
|
955
1004
|
// artifact, and source SQL the next question builds on. Anchoring it to the
|
|
@@ -1119,6 +1168,40 @@ export function conversationTurnInputFromRun(run) {
|
|
|
1119
1168
|
const contextPack = agentRunRecord(payload?.contextPack);
|
|
1120
1169
|
const questionPlan = agentRunRecord(contextPack?.questionPlan);
|
|
1121
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;
|
|
1122
1205
|
const rowCountRaw = result?.rowCount;
|
|
1123
1206
|
const measureColumns = conversationMeasureColumns(columns, requestedShape, rows);
|
|
1124
1207
|
return {
|
|
@@ -1127,7 +1210,13 @@ export function conversationTurnInputFromRun(run) {
|
|
|
1127
1210
|
answerSummary: run.answer ?? run.summary,
|
|
1128
1211
|
answerText: run.answer,
|
|
1129
1212
|
route: run.route,
|
|
1130
|
-
|
|
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),
|
|
1131
1220
|
runStatus: run.status,
|
|
1132
1221
|
stopReason: run.stopReason,
|
|
1133
1222
|
// Persisted so a later turn can tell a refusal from an answer. `runStatus`
|
|
@@ -1153,9 +1242,41 @@ export function conversationTurnInputFromRun(run) {
|
|
|
1153
1242
|
rowCount: typeof rowCountRaw === 'number' ? rowCountRaw : rows.length || undefined,
|
|
1154
1243
|
}
|
|
1155
1244
|
: undefined,
|
|
1156
|
-
contract:
|
|
1245
|
+
contract: {
|
|
1246
|
+
...(requestedShape ?? {}),
|
|
1247
|
+
...(clarificationSelection ? { clarificationSelection } : {}),
|
|
1248
|
+
},
|
|
1157
1249
|
};
|
|
1158
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
|
+
}
|
|
1159
1280
|
function conversationResultColumns(value) {
|
|
1160
1281
|
if (!Array.isArray(value))
|
|
1161
1282
|
return [];
|
|
@@ -1167,10 +1288,24 @@ function conversationResultColumns(value) {
|
|
|
1167
1288
|
.slice(0, 24);
|
|
1168
1289
|
}
|
|
1169
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.
|
|
1170
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
|
+
});
|
|
1171
1306
|
const numericColumns = columns.filter((column) => rows.some((row) => typeof row[column] === 'number' && Number.isFinite(row[column])));
|
|
1172
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, ' ')));
|
|
1173
|
-
const unique = Array.from(new Set([...
|
|
1308
|
+
const unique = Array.from(new Set([...actualRequestedColumns, ...numericColumns, ...metricNamedColumns]
|
|
1174
1309
|
.map((value) => value.trim())
|
|
1175
1310
|
.filter(Boolean)));
|
|
1176
1311
|
return unique.length > 0 ? unique.slice(0, 24) : undefined;
|
|
@@ -2228,11 +2363,12 @@ export async function startLocalServer(opts) {
|
|
|
2228
2363
|
runner = createDqlAgentProviderRunner('ollama', deterministicProvider);
|
|
2229
2364
|
}
|
|
2230
2365
|
if (!resolvedProvider || !runner) {
|
|
2231
|
-
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' });
|
|
2232
2367
|
}
|
|
2233
2368
|
let governedAnswer;
|
|
2234
2369
|
let providerError;
|
|
2235
2370
|
let providerDispatchEvidence;
|
|
2371
|
+
let providerBoundaryDiagnostic;
|
|
2236
2372
|
const isRepair = (repair?.attempt ?? 0) > 0 && Boolean(repair?.repairHint);
|
|
2237
2373
|
// The chat composer sends a `thinkingMode` (auto/low/medium/high); resolve it
|
|
2238
2374
|
// into the effort+depth bundle it stands for. An explicit `reasoningEffort` /
|
|
@@ -2299,6 +2435,37 @@ export async function startLocalServer(opts) {
|
|
|
2299
2435
|
const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
|
|
2300
2436
|
const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
|
|
2301
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
|
+
: [];
|
|
2302
2469
|
const domainContext = requestedDomain
|
|
2303
2470
|
? resolveUiDomainContext({
|
|
2304
2471
|
manifest: runProjectSnapshot.manifest,
|
|
@@ -2313,6 +2480,143 @@ export async function startLocalServer(opts) {
|
|
|
2313
2480
|
// Local to this exact answer invocation. Compound children each enter this
|
|
2314
2481
|
// function separately, so no child can consume another child's capability.
|
|
2315
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
|
+
};
|
|
2316
2620
|
await runner.run({
|
|
2317
2621
|
provider: resolvedProvider,
|
|
2318
2622
|
...(agentRunProviderEvidenceContext.getStore()
|
|
@@ -2344,7 +2648,14 @@ export async function startLocalServer(opts) {
|
|
|
2344
2648
|
// Keys the execution authorization, so the proofs the analyst loop
|
|
2345
2649
|
// gathers can be checked against the statement this run executes.
|
|
2346
2650
|
...(request.runId ? { agentRunId: request.runId } : {}),
|
|
2347
|
-
preparedContextPack
|
|
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
|
+
: {}),
|
|
2348
2659
|
domainContext,
|
|
2349
2660
|
projectSnapshot: { snapshotId: runProjectSnapshot.snapshotId, manifest: runProjectSnapshot.manifest },
|
|
2350
2661
|
assertProjectSnapshot: (snapshotId) => {
|
|
@@ -2505,12 +2816,59 @@ export async function startLocalServer(opts) {
|
|
|
2505
2816
|
...(routeDecision?.resolvedAnalyticalPlan
|
|
2506
2817
|
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
2507
2818
|
: {}),
|
|
2819
|
+
...(routeDecision?.analyticalCascadeDecision?.selectedTier
|
|
2820
|
+
? { selectedCascadeTier: routeDecision.analyticalCascadeDecision.selectedTier }
|
|
2821
|
+
: {}),
|
|
2822
|
+
...(exploratoryCandidateIds.length > 0
|
|
2823
|
+
? { exploratoryCandidateIds }
|
|
2824
|
+
: {}),
|
|
2508
2825
|
...(generatedProposalTargetIdentity?.identityFingerprint
|
|
2509
2826
|
? { generatedProposalTargetFingerprint: generatedProposalTargetIdentity.identityFingerprint }
|
|
2510
2827
|
: {}),
|
|
2511
2828
|
analyticalReferenceInstant: new Date().toISOString(),
|
|
2512
|
-
|
|
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
|
+
},
|
|
2513
2870
|
executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName),
|
|
2871
|
+
prepareExploratorySqlExecution,
|
|
2514
2872
|
executeAgenticGeneratedSql: async (capability, sql, artifact) => {
|
|
2515
2873
|
if (!agenticExecutionCapabilityGate.consume(capability)) {
|
|
2516
2874
|
throw analyticalError('This analyst execution capability was already consumed; DQL did not retry it with stale proof.', {
|
|
@@ -2521,8 +2879,8 @@ export async function startLocalServer(opts) {
|
|
|
2521
2879
|
runId: request.runId,
|
|
2522
2880
|
executionId: capability.executionId,
|
|
2523
2881
|
snapshotId: runProjectSnapshot.snapshotId,
|
|
2524
|
-
planId:
|
|
2525
|
-
targetFingerprint:
|
|
2882
|
+
planId: capability.planId,
|
|
2883
|
+
targetFingerprint: capability.targetFingerprint,
|
|
2526
2884
|
});
|
|
2527
2885
|
},
|
|
2528
2886
|
executeDqlArtifact: (artifact) => executeArtifactReferenceForAgent(artifact, request.question, semanticConnection, semanticConnectionName),
|
|
@@ -2539,10 +2897,14 @@ export async function startLocalServer(opts) {
|
|
|
2539
2897
|
if (turn.kind === 'error') {
|
|
2540
2898
|
providerError = turn.message;
|
|
2541
2899
|
providerDispatchEvidence = turn.dispatchEvidence;
|
|
2900
|
+
providerBoundaryDiagnostic = turn.providerDiagnostic;
|
|
2542
2901
|
}
|
|
2543
2902
|
}, runSignal);
|
|
2544
2903
|
if (!governedAnswer) {
|
|
2545
|
-
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
|
+
});
|
|
2546
2908
|
}
|
|
2547
2909
|
return governedAnswer;
|
|
2548
2910
|
}
|
|
@@ -2580,6 +2942,68 @@ export async function startLocalServer(opts) {
|
|
|
2580
2942
|
function resolvedRunRouteFromAnswer(governedAnswer) {
|
|
2581
2943
|
return routeForCascadeAnswerTier(governedAnswer.route?.tier);
|
|
2582
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
|
+
}
|
|
2583
3007
|
function groundingGapRepairHint(governedAnswer) {
|
|
2584
3008
|
const details = governedAnswer.refusalDetails;
|
|
2585
3009
|
if (!details)
|
|
@@ -2793,7 +3217,16 @@ export async function startLocalServer(opts) {
|
|
|
2793
3217
|
if (governedAnswer.result)
|
|
2794
3218
|
delete governedAnswer.result.dqlArtifact;
|
|
2795
3219
|
}
|
|
2796
|
-
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;
|
|
2797
3230
|
const runStartedAtMs = Date.now();
|
|
2798
3231
|
const turnPlan = buildAnalyticalTurnPlan({
|
|
2799
3232
|
question: request.question,
|
|
@@ -2952,6 +3385,15 @@ export async function startLocalServer(opts) {
|
|
|
2952
3385
|
let governedAnswer;
|
|
2953
3386
|
try {
|
|
2954
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
|
+
}
|
|
2955
3397
|
// Keep the canonical result contract on the answer itself, not only on
|
|
2956
3398
|
// the narration preview. Conversation persistence, follow-up member
|
|
2957
3399
|
// resolution, Apply, and the notebook table all read this payload; if a
|
|
@@ -3126,6 +3568,33 @@ export async function startLocalServer(opts) {
|
|
|
3126
3568
|
const message = dispatchBudgetExhausted
|
|
3127
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.'
|
|
3128
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
|
+
});
|
|
3129
3598
|
return {
|
|
3130
3599
|
summary: message,
|
|
3131
3600
|
status: 'blocked',
|
|
@@ -3143,11 +3612,12 @@ export async function startLocalServer(opts) {
|
|
|
3143
3612
|
code: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'AI_PROVIDER_FAILURE',
|
|
3144
3613
|
message,
|
|
3145
3614
|
recoverable: !dispatchBudgetExhausted,
|
|
3615
|
+
diagnostic: providerDiagnostic,
|
|
3146
3616
|
},
|
|
3147
3617
|
}, undefined, 'blocked')],
|
|
3148
3618
|
evaluations: [
|
|
3149
3619
|
agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.'),
|
|
3150
|
-
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 }),
|
|
3151
3621
|
],
|
|
3152
3622
|
nextActions: [
|
|
3153
3623
|
...(dispatchBudgetExhausted
|
|
@@ -3174,7 +3644,10 @@ export async function startLocalServer(opts) {
|
|
|
3174
3644
|
},
|
|
3175
3645
|
};
|
|
3176
3646
|
}
|
|
3177
|
-
|
|
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;
|
|
3178
3651
|
const isCertified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
3179
3652
|
const semanticRouteClaimed = governedAnswer.route?.tier === 'semantic_metric';
|
|
3180
3653
|
const semanticAggregationProofPassed = semanticAnswerHasPassedAggregationProof(governedAnswer);
|
|
@@ -3213,9 +3686,14 @@ export async function startLocalServer(opts) {
|
|
|
3213
3686
|
&& route === 'generated_answer'
|
|
3214
3687
|
&& (request.requestedMode === undefined || request.requestedMode === 'auto' || request.requestedMode === 'ask')
|
|
3215
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';
|
|
3216
3694
|
const typedCoverageGap = (isGroundingGap || isModelDeclined)
|
|
3217
3695
|
? buildCoverageGap({
|
|
3218
|
-
code:
|
|
3696
|
+
code: persistedGapWitness?.code ?? 'MISSING_RUNTIME_CAPABILITY',
|
|
3219
3697
|
phase: 'planning',
|
|
3220
3698
|
message: governedAnswer.refusalDetails?.message
|
|
3221
3699
|
?? (isModelDeclined
|
|
@@ -3223,17 +3701,21 @@ export async function startLocalServer(opts) {
|
|
|
3223
3701
|
: 'The retrieved context did not prove the metadata required for this analytical question.'),
|
|
3224
3702
|
searchedSources: ['certified_blocks', 'semantic_metrics', 'dbt_manifest', 'relationship_graph', 'warehouse_metadata'],
|
|
3225
3703
|
attemptedRoutes: ['certified', 'semantic', 'governed_relational', 'generated'],
|
|
3226
|
-
missing:
|
|
3227
|
-
?
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
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'],
|
|
3232
3712
|
recoverable: canRecoverPreFreezeGap,
|
|
3233
3713
|
planFrozen: routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative',
|
|
3234
3714
|
nextActions: canRecoverPreFreezeGap
|
|
3235
3715
|
? ['continue through DBT-grounded relational context', 'run review-required generated SQL', 'start bounded Research if coverage remains incomplete']
|
|
3236
|
-
:
|
|
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'],
|
|
3237
3719
|
})
|
|
3238
3720
|
: undefined;
|
|
3239
3721
|
// Only a genuinely AMBIGUOUS question is surfaced as "needs clarification".
|
|
@@ -3501,12 +3983,15 @@ export async function startLocalServer(opts) {
|
|
|
3501
3983
|
: isTerminalFailure
|
|
3502
3984
|
? terminalFailureActions
|
|
3503
3985
|
: isGroundingGap
|
|
3504
|
-
?
|
|
3986
|
+
? relationshipSpecificGap
|
|
3505
3987
|
? [
|
|
3506
|
-
{ 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' },
|
|
3507
3993
|
{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' },
|
|
3508
3994
|
]
|
|
3509
|
-
: [{ id: 'research-gap', label: 'Research missing metadata coverage', route: 'research', artifactKind: 'research_run' }]
|
|
3510
3995
|
: [
|
|
3511
3996
|
{ id: 'create-block', label: governedAnswer.dqlArtifact ? 'Review DQL draft' : 'Create DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
|
|
3512
3997
|
{ id: 'research-gap', label: 'Research deeper', route: 'research' },
|
|
@@ -3661,6 +4146,9 @@ export async function startLocalServer(opts) {
|
|
|
3661
4146
|
providerEgressReceipts: finalProviderEgressReceipts,
|
|
3662
4147
|
telemetry: finalTelemetry,
|
|
3663
4148
|
narrationIntegrityReceipt,
|
|
4149
|
+
...(governedAnswer.exploratoryExecutionFreeze
|
|
4150
|
+
? { analyticalExecutionFreeze: governedAnswer.exploratoryExecutionFreeze }
|
|
4151
|
+
: {}),
|
|
3664
4152
|
};
|
|
3665
4153
|
};
|
|
3666
4154
|
/**
|
|
@@ -4198,6 +4686,38 @@ export async function startLocalServer(opts) {
|
|
|
4198
4686
|
if (plan.done && !plan.followUp && request.requestedMode !== 'research') {
|
|
4199
4687
|
return answerRunExecutor(researchContext);
|
|
4200
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]));
|
|
4201
4721
|
const needsClarification = Boolean(plan.followUp);
|
|
4202
4722
|
const notebookPath = agentRunNotebookPath(request, runId);
|
|
4203
4723
|
const researchIntent = agentRunResearchIntent(request);
|
|
@@ -4258,6 +4778,8 @@ export async function startLocalServer(opts) {
|
|
|
4258
4778
|
// attempt, not a fabricated successful finding; its durable status
|
|
4259
4779
|
// and receipt determine the ledger entry.
|
|
4260
4780
|
const fallbackBranch = {
|
|
4781
|
+
hypothesisId: 'fallback:context',
|
|
4782
|
+
validatorKind: 'counter_evidence',
|
|
4261
4783
|
thought: 'Inspect the requested analytical question against the frozen root context.',
|
|
4262
4784
|
action: {
|
|
4263
4785
|
kind: 'lookup_metric',
|
|
@@ -4265,7 +4787,7 @@ export async function startLocalServer(opts) {
|
|
|
4265
4787
|
},
|
|
4266
4788
|
expectation: 'Whether the frozen context contains enough evidence for a bounded answer.',
|
|
4267
4789
|
};
|
|
4268
|
-
const branches = capResearchBranches(
|
|
4790
|
+
const branches = capResearchBranches(executableResearchBranches.length > 0 ? executableResearchBranches : [fallbackBranch], 6);
|
|
4269
4791
|
// The replan edge. Each branch tests one hypothesis; folding its
|
|
4270
4792
|
// outcome back into the state is what lets the investigation stop
|
|
4271
4793
|
// when the question is settled instead of grinding through a plan
|
|
@@ -4292,7 +4814,7 @@ export async function startLocalServer(opts) {
|
|
|
4292
4814
|
});
|
|
4293
4815
|
break;
|
|
4294
4816
|
}
|
|
4295
|
-
const branchId =
|
|
4817
|
+
const branchId = step.hypothesisId;
|
|
4296
4818
|
const branchQuestion = `${request.question}\nResearch branch ${index + 1} (${branchId}): ${step.expectation}`;
|
|
4297
4819
|
const childId = `${created.id}:research:${index + 1}`;
|
|
4298
4820
|
const child = storage.createRun({
|
|
@@ -4313,6 +4835,9 @@ export async function startLocalServer(opts) {
|
|
|
4313
4835
|
rootPlanId: plan.rootPlanId,
|
|
4314
4836
|
branch: {
|
|
4315
4837
|
id: branchId,
|
|
4838
|
+
hypothesisId: step.hypothesisId,
|
|
4839
|
+
hypothesis: step.thought,
|
|
4840
|
+
validatorKind: step.validatorKind,
|
|
4316
4841
|
index: index + 1,
|
|
4317
4842
|
expectation: step.expectation,
|
|
4318
4843
|
action: step.action,
|
|
@@ -4337,7 +4862,15 @@ export async function startLocalServer(opts) {
|
|
|
4337
4862
|
...researchContextEnvelope,
|
|
4338
4863
|
rootRunId: created.id,
|
|
4339
4864
|
rootPlanId: plan.rootPlanId,
|
|
4340
|
-
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
|
+
},
|
|
4341
4874
|
},
|
|
4342
4875
|
executionConnection: researchExecutionConnection,
|
|
4343
4876
|
executionConnectionName: researchExecutionConnectionName,
|
|
@@ -4465,10 +4998,50 @@ export async function startLocalServer(opts) {
|
|
|
4465
4998
|
? 'not_started'
|
|
4466
4999
|
: researchRuns.some((run) => run.status === 'error')
|
|
4467
5000
|
? 'insufficient_evidence'
|
|
4468
|
-
:
|
|
5001
|
+
: executableResearchBranches.length > 6
|
|
4469
5002
|
? 'budget'
|
|
4470
5003
|
: 'completed',
|
|
4471
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
|
+
});
|
|
4472
5045
|
// A query that ran and matched 0 rows STILL executed — treat it as a clean,
|
|
4473
5046
|
// grounded execution (not "no result"), so an empty answer is surfaced as
|
|
4474
5047
|
// "0 rows matched" rather than silently downgraded to review-required.
|
|
@@ -4491,15 +5064,16 @@ export async function startLocalServer(opts) {
|
|
|
4491
5064
|
// finding; narrating only the one result the executor happened to carry
|
|
4492
5065
|
// reported a single fact and discarded the rest, which is the visible
|
|
4493
5066
|
// half of "research answers one question instead of telling a story".
|
|
4494
|
-
const researchStory = !needsClarification &&
|
|
5067
|
+
const researchStory = !needsClarification && researchLedgerV2.entries.length > 0
|
|
4495
5068
|
? synthesizeResearchNarrative({
|
|
4496
5069
|
question: request.question,
|
|
4497
|
-
branches:
|
|
4498
|
-
statement:
|
|
4499
|
-
produced:
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
...(
|
|
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,
|
|
4503
5077
|
})),
|
|
4504
5078
|
})
|
|
4505
5079
|
: undefined;
|
|
@@ -4521,8 +5095,11 @@ export async function startLocalServer(opts) {
|
|
|
4521
5095
|
: plan.done
|
|
4522
5096
|
? 'Prepared a direct grounded-answer plan.'
|
|
4523
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;
|
|
4524
5101
|
return {
|
|
4525
|
-
summary,
|
|
5102
|
+
summary: scopedSummary,
|
|
4526
5103
|
answer: plan.followUp?.question ?? narration?.summary
|
|
4527
5104
|
?? (researchZeroRows ? 'The query executed cleanly and matched 0 rows.' : undefined)
|
|
4528
5105
|
?? researchRun?.summary,
|
|
@@ -4533,7 +5110,9 @@ export async function startLocalServer(opts) {
|
|
|
4533
5110
|
? []
|
|
4534
5111
|
: [agentRunArtifact('research_run', 'Research plan', {
|
|
4535
5112
|
plan,
|
|
5113
|
+
typedResearchPlan,
|
|
4536
5114
|
researchLedger,
|
|
5115
|
+
researchLedgerV2,
|
|
4537
5116
|
researchRun,
|
|
4538
5117
|
researchRuns,
|
|
4539
5118
|
researchRunId: researchRun?.id,
|
|
@@ -4562,6 +5141,9 @@ export async function startLocalServer(opts) {
|
|
|
4562
5141
|
? 'The query executed cleanly against real data and matched 0 rows.'
|
|
4563
5142
|
: 'The query executed cleanly against real data and returned rows.')
|
|
4564
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 }),
|
|
4565
5147
|
],
|
|
4566
5148
|
nextActions: needsClarification
|
|
4567
5149
|
? [{ id: 'answer-follow-up', label: 'Answer follow-up', route: 'research' }]
|
|
@@ -4999,7 +5581,74 @@ export async function startLocalServer(opts) {
|
|
|
4999
5581
|
durationMs: Date.now() - startedAt,
|
|
5000
5582
|
truncated: pack.retrievalDiagnostics.topRejected.length > 0,
|
|
5001
5583
|
});
|
|
5002
|
-
|
|
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
|
+
};
|
|
5003
5652
|
};
|
|
5004
5653
|
const buildRankedAgentRunCatalogContext = async (request) => {
|
|
5005
5654
|
const evidence = await buildAgentRunEvidence(request);
|
|
@@ -5368,6 +6017,16 @@ export async function startLocalServer(opts) {
|
|
|
5368
6017
|
const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
|
|
5369
6018
|
throw analyticalError(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`, { origin: 'dql_compilation', stage: 'compile' });
|
|
5370
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;
|
|
5371
6030
|
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
5372
6031
|
const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
|
|
5373
6032
|
assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
|
|
@@ -5376,7 +6035,7 @@ export async function startLocalServer(opts) {
|
|
|
5376
6035
|
: undefined;
|
|
5377
6036
|
const semanticExecutionHolder = { value: null };
|
|
5378
6037
|
const execution = await analyticalExecutionService.execute({
|
|
5379
|
-
sql:
|
|
6038
|
+
sql: executableSql,
|
|
5380
6039
|
subject: 'DQL artifact query',
|
|
5381
6040
|
connection: activeConnection,
|
|
5382
6041
|
// Preserve the existing contract: previewed Ask artifacts are explicitly
|
|
@@ -5421,7 +6080,7 @@ export async function startLocalServer(opts) {
|
|
|
5421
6080
|
path: metadata.path ?? null,
|
|
5422
6081
|
domain: sourceDomain ?? null,
|
|
5423
6082
|
})),
|
|
5424
|
-
compiledSqlFingerprint: executionFingerprint(
|
|
6083
|
+
compiledSqlFingerprint: executionFingerprint(executableSql ?? preparation.sourceSql),
|
|
5425
6084
|
normalizedSqlFingerprint: executionFingerprint(preparation.decodedSql),
|
|
5426
6085
|
parameterFingerprint: executionReceipt.parameterFingerprint,
|
|
5427
6086
|
provenanceFingerprint: executionFingerprint(stableExecutionValue(invocation.resolvedParameters.map((parameter) => ({
|
|
@@ -5431,7 +6090,7 @@ export async function startLocalServer(opts) {
|
|
|
5431
6090
|
targetFingerprint: targetGenerationFingerprint(activeConnection, executionConnectionName),
|
|
5432
6091
|
snapshotFingerprint: executionFingerprint(projectSnapshot().snapshotId),
|
|
5433
6092
|
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
5434
|
-
sqlFingerprint: executionFingerprint(
|
|
6093
|
+
sqlFingerprint: executionFingerprint(executableSql ?? preparation.sourceSql),
|
|
5435
6094
|
parameterCount: plan?.sqlParams?.length ?? 0,
|
|
5436
6095
|
variableNames: Object.keys(plan?.variables ?? {}).sort(),
|
|
5437
6096
|
chartConfig: plan?.chartConfig ?? null,
|
|
@@ -5463,7 +6122,7 @@ export async function startLocalServer(opts) {
|
|
|
5463
6122
|
} : {}),
|
|
5464
6123
|
};
|
|
5465
6124
|
};
|
|
5466
|
-
const executeCertifiedBlockByNameForAgent = async (blockName, invocationInput, requireCertified = false, executionConnection, executionConnectionName) => {
|
|
6125
|
+
const executeCertifiedBlockByNameForAgent = async (blockName, invocationInput, requireCertified = false, executionConnection, executionConnectionName, qualifiedSchemaContext) => {
|
|
5467
6126
|
const manifest = buildManifest({ projectRoot });
|
|
5468
6127
|
const block = manifest.blocks[blockName];
|
|
5469
6128
|
if (!block) {
|
|
@@ -5478,6 +6137,7 @@ export async function startLocalServer(opts) {
|
|
|
5478
6137
|
path: block.filePath,
|
|
5479
6138
|
domain: block.domain,
|
|
5480
6139
|
chartType: block.chartType,
|
|
6140
|
+
...(qualifiedSchemaContext?.length ? { qualifiedSchemaContext } : {}),
|
|
5481
6141
|
}, invocationInput, executionConnection, executionConnectionName);
|
|
5482
6142
|
return {
|
|
5483
6143
|
...result,
|
|
@@ -5500,11 +6160,11 @@ export async function startLocalServer(opts) {
|
|
|
5500
6160
|
},
|
|
5501
6161
|
};
|
|
5502
6162
|
};
|
|
5503
|
-
const executeCertifiedBlockForAgent = async (node, invocationInput, executionConnection, executionConnectionName) => {
|
|
6163
|
+
const executeCertifiedBlockForAgent = async (node, invocationInput, executionConnection, executionConnectionName, qualifiedSchemaContext, requireCertified = false) => {
|
|
5504
6164
|
if (node.kind !== 'block') {
|
|
5505
6165
|
throw new Error(`Certified ${node.kind} "${node.name}" is a navigation artifact and cannot be executed as a block.`);
|
|
5506
6166
|
}
|
|
5507
|
-
return executeCertifiedBlockByNameForAgent(node.name || node.nodeId.replace(/^block:/, ''), invocationInput,
|
|
6167
|
+
return executeCertifiedBlockByNameForAgent(node.name || node.nodeId.replace(/^block:/, ''), invocationInput, requireCertified, executionConnection, executionConnectionName, qualifiedSchemaContext);
|
|
5508
6168
|
};
|
|
5509
6169
|
// Secondary agent surfaces use this compatibility callback. Delegate to the
|
|
5510
6170
|
// same artifact-first path as Ask so research/app/notebook execution cannot
|
|
@@ -9638,7 +10298,7 @@ export async function startLocalServer(opts) {
|
|
|
9638
10298
|
// state wins; the client-built context stays the no-threadId fallback).
|
|
9639
10299
|
const conversationStore = parsed.request.threadId ? getConversationStore() : null;
|
|
9640
10300
|
if (conversationStore && parsed.request.threadId && conversationStore.getThread(parsed.request.threadId)) {
|
|
9641
|
-
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));
|
|
9642
10302
|
// The persisted thread is authoritative for prior turns. Raw client
|
|
9643
10303
|
// history would duplicate the same conversation into the prompt a
|
|
9644
10304
|
// second time — dropping it keeps follow-up prompts bounded.
|
|
@@ -9646,10 +10306,13 @@ export async function startLocalServer(opts) {
|
|
|
9646
10306
|
parsed.request.history = [];
|
|
9647
10307
|
}
|
|
9648
10308
|
const wantsStream = url.searchParams.get('stream') === '1' || url.searchParams.get('stream') === 'true';
|
|
9649
|
-
|
|
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;
|
|
9650
10314
|
const runController = new AbortController();
|
|
9651
|
-
|
|
9652
|
-
activeAgentRunControllers.set(runId, runController);
|
|
10315
|
+
activeAgentRunControllers.set(runId, runController);
|
|
9653
10316
|
let streamConnected = true;
|
|
9654
10317
|
res.on('close', () => { streamConnected = false; });
|
|
9655
10318
|
const writeStream = (event, data) => {
|
|
@@ -9670,13 +10333,13 @@ export async function startLocalServer(opts) {
|
|
|
9670
10333
|
'X-Accel-Buffering': 'no',
|
|
9671
10334
|
});
|
|
9672
10335
|
}
|
|
9673
|
-
const operation =
|
|
10336
|
+
const operation = operationCoordinator.create({
|
|
9674
10337
|
type: 'agent_run',
|
|
9675
10338
|
scope: `agent-run:${runId}`,
|
|
9676
10339
|
resourceRevision: parsed.request.threadId,
|
|
9677
10340
|
message: 'AI request accepted. You can change pages while it runs.',
|
|
9678
10341
|
cancellable: true,
|
|
9679
|
-
})
|
|
10342
|
+
});
|
|
9680
10343
|
if (wantsStream)
|
|
9681
10344
|
writeStream('agent-run-accepted', { runId, operationId: operation?.id });
|
|
9682
10345
|
let completedRun;
|
|
@@ -9768,8 +10431,7 @@ export async function startLocalServer(opts) {
|
|
|
9768
10431
|
res.end(serializeJSON({ run: slimAgentRunForTransport(completedRun) }));
|
|
9769
10432
|
}
|
|
9770
10433
|
finally {
|
|
9771
|
-
|
|
9772
|
-
activeAgentRunControllers.delete(runId);
|
|
10434
|
+
activeAgentRunControllers.delete(runId);
|
|
9773
10435
|
}
|
|
9774
10436
|
}
|
|
9775
10437
|
catch (error) {
|
|
@@ -18513,6 +19175,16 @@ export function resolveDefaultLLMProvider(projectRoot) {
|
|
|
18513
19175
|
* answer envelope. Everything else uses the Settings-resolved default runner.
|
|
18514
19176
|
*/
|
|
18515
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
|
+
}
|
|
18516
19188
|
const active = getActiveProvider(projectRoot);
|
|
18517
19189
|
if (isGovernedAnswerProviderId(active)) {
|
|
18518
19190
|
return { provider: active, runner: createDqlAgentProviderRunner(active) };
|
|
@@ -18525,6 +19197,14 @@ export function resolveGovernedAnswerRunner(projectRoot) {
|
|
|
18525
19197
|
}
|
|
18526
19198
|
return null;
|
|
18527
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
|
+
}
|
|
18528
19208
|
function isGovernedAnswerProviderId(value) {
|
|
18529
19209
|
return value === 'anthropic'
|
|
18530
19210
|
|| value === 'openai'
|
|
@@ -22092,7 +22772,7 @@ export function buildAgentPreviewSql(sql, rowLimit = 200) {
|
|
|
22092
22772
|
*/
|
|
22093
22773
|
export function repairExploratorySqlBeforeExecution(sql, schemaContext, question = '', dialect = 'duckdb') {
|
|
22094
22774
|
const repairs = [];
|
|
22095
|
-
let repairedSql =
|
|
22775
|
+
let repairedSql = qualifyUnambiguousSqlRelationsFromSchema(sql, schemaContext, repairs, dialect);
|
|
22096
22776
|
repairedSql = repairExploratoryRelationQualifiers(repairedSql, repairs, dialect);
|
|
22097
22777
|
repairedSql = repairExploratoryLifetimeMeasureSelection(repairedSql, schemaContext, question, repairs, dialect);
|
|
22098
22778
|
repairedSql = repairExploratoryMisleadingPercentAliases(repairedSql, question, repairs);
|
|
@@ -22151,7 +22831,13 @@ export function applyRequestedTopNToExploratorySql(sql, requestedTopN) {
|
|
|
22151
22831
|
}
|
|
22152
22832
|
return `${withoutTerminator}\nLIMIT ${requestedTopN}`;
|
|
22153
22833
|
}
|
|
22154
|
-
|
|
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') {
|
|
22155
22841
|
const analysis = analyzeSqlReferences(sql, dialect);
|
|
22156
22842
|
if (!analysis.parsed)
|
|
22157
22843
|
return sql;
|
|
@@ -26290,6 +26976,14 @@ async function buildBlockStudioAiAssistSummary(projectRoot, action, candidate, v
|
|
|
26290
26976
|
}
|
|
26291
26977
|
}
|
|
26292
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;
|
|
26293
26987
|
const settings = listProviderSettings(projectRoot);
|
|
26294
26988
|
const activeProvider = getActiveProvider(projectRoot);
|
|
26295
26989
|
// Subscription CLI providers (Claude Code / Codex) carry no API key — they're
|
|
@@ -26339,7 +27033,7 @@ async function createBlockStudioAssistProvider(projectRoot, requestedProvider) {
|
|
|
26339
27033
|
// exactly the calls whose non-determinism it was recorded to remove. A local
|
|
26340
27034
|
// baseline reproduced that: the same question blocked on one run and answered
|
|
26341
27035
|
// on the next, and zero cassettes were written.
|
|
26342
|
-
return await provider.available() ? applyEvalCassette(provider) : null;
|
|
27036
|
+
return await provider.available() ? applyEvalCassette(provider, projectRoot) : null;
|
|
26343
27037
|
}
|
|
26344
27038
|
/** Convert a governed answer's result payload into a bounded synthesis preview. */
|
|
26345
27039
|
function agentResultToSynthesisPreview(result) {
|
|
@@ -29966,7 +30660,7 @@ function compactSqlForRunHistory(sql) {
|
|
|
29966
30660
|
const clean = sql.replace(/\s+/g, ' ').trim();
|
|
29967
30661
|
return clean.length > 1200 ? `${clean.slice(0, 1197)}...` : clean;
|
|
29968
30662
|
}
|
|
29969
|
-
function buildAgentSchemaContextFromContextPack(question, contextPack) {
|
|
30663
|
+
function buildAgentSchemaContextFromContextPack(question, contextPack, options = {}) {
|
|
29970
30664
|
const byRelation = new Map();
|
|
29971
30665
|
const objectsByKey = new Map(contextPack.objects.map((object) => [object.objectKey, object]));
|
|
29972
30666
|
const upsert = (table) => {
|
|
@@ -30020,11 +30714,75 @@ function buildAgentSchemaContextFromContextPack(question, contextPack) {
|
|
|
30020
30714
|
table,
|
|
30021
30715
|
score: scoreAgentSchemaTable(table, tokens) + (shouldProbeValues ? scoreAgentValueProbeTable(table) : 0),
|
|
30022
30716
|
}))
|
|
30023
|
-
.filter((entry) => entry.table.columns.length > 0 && entry.score > 0)
|
|
30717
|
+
.filter((entry) => entry.table.columns.length > 0 && (options.includeUnscored || entry.score > 0))
|
|
30024
30718
|
.sort((a, b) => b.score - a.score || a.table.relation.localeCompare(b.table.relation))
|
|
30025
|
-
.slice(0, 12)
|
|
30719
|
+
.slice(0, Math.max(1, options.limit ?? 12))
|
|
30026
30720
|
.map((entry) => entry.table);
|
|
30027
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
|
+
}
|
|
30028
30786
|
function metadataObjectToAgentSchemaTable(object) {
|
|
30029
30787
|
if (object.objectType === 'dbt_column' || object.objectType === 'runtime_column') {
|
|
30030
30788
|
const relation = metadataPayloadString(object, 'relation');
|