@duckcodeailabs/dql-cli 1.13.3 → 1.13.5
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/apps-api.d.ts +1 -0
- package/dist/apps-api.d.ts.map +1 -1
- package/dist/apps-api.js +31 -23
- package/dist/apps-api.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{AgentLogPage-B3ugEb3H.js → AgentLogPage-BzQOKjyV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-BK_TDNcM.js → AiBuildDialog-CTxha499.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-Cob_Lj05.js → AiBuildResult--MuD_I6g.js} +3 -3
- package/dist/assets/dql-notebook/assets/{AiSidePanel-CzIgxmP9.js → AiSidePanel-DcI4PMJ1.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AnalyticsHome-C2vJ2fxY.js → AnalyticsHome-Dq54vjNz.js} +1 -1
- package/dist/assets/dql-notebook/assets/AppsView-0SlwWYex.js +5595 -0
- package/dist/assets/dql-notebook/assets/{BlockStudio-CaDNTKMM.js → BlockStudio-CxxXZD3M.js} +12 -12
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-zwUOtddi.js → BusinessArtifactView-DxdAmeUN.js} +1 -1
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-BlnhTMu3.js → DbtFirstModelingPage-y81gFg_b.js} +4 -4
- package/dist/assets/dql-notebook/assets/{GitPage-Coevr9NJ.js → GitPage-GQtcwncb.js} +3 -3
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-BTZS11x5.js → GlobalAiRail-DM4wkxR_.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-CjyyM_ne.js → GovernedContextPage-B8Ft6JES.js} +3 -3
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-C1UeP85v.js → HelpDocsPage-DnD4nMuF.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-zGPFIvOY.js → HomePage-Ehb0ITj-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDAG-DbXnQWu3.js → LineageDAG-Lvjc2AQX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDetailView-Ch1Db01I.js → LineageDetailView-DX32pfbp.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-DLxqJO6D.js → LineageDrawer-CFq3jPfk.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-B2tNdkr_.js → LineagePathBreadcrumb-CVHXuHls.js} +1 -1
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-0gY-ZwCy.js → MiniLineageGraph-BKaeT5Kt.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-DKVkXD20.js → NewBlockModal-DI-JDzFH.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-BUIByvYT.js → NewNotebookModal-Csw6eF74.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-D2fIUxjE.js → NotebookEditor-FEqs3789.js} +1 -1
- package/dist/assets/dql-notebook/assets/{ReadinessPage-Ci4LyNiQ.js → ReadinessPage-Bk0S_MEA.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-BF7ai_0t.js → SetupOnboarding-DyLaPFGn.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SkillsPage-BgYrgirJ.js → SkillsPage-DLHyvhci.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-Bob1Laiq.js → TrustBadge-CIDLj2A6.js} +1 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-C0oKTU6G.js +88 -0
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-Dk42N0pU.js → answer-to-notebook-CFEJHLvs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-DKuuTw9l.js → arrow-left-B-Zdcyvm.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-TH8_ufKG.js → arrow-right-Cn8TM7cp.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-B-dcHHn0.js → book-open-text-Bko2NNUs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-BOc2KCZQ.js → circle-x-AWJAmUBB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-CPT_vn5p.js → dagre.esm-Cl_ucrRh.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-DtOfpYc6.js → external-link-DN57tb5f.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-B8joDDH9.js → grip-vertical-BOWwFFva.js} +1 -1
- package/dist/assets/dql-notebook/assets/{index-DJRMz64S.js → index-Ck-wqvV2.js} +153 -153
- package/dist/assets/dql-notebook/assets/{link-2-DMZPHawg.js → link-2-xUhbfBs8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-Dw0qlxPK.js → list-tree-BDWcBr67.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-DUiSrPS3.js → minimize-2-CEeNoMTS.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-BrrFrRZQ.js → panel-right-open-DnSeUvOY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{play-Ced0HT9g.js → play-EoDj8-fa.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-D_9U9o7X.js → rotate-ccw-BAMxWT9A.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-BlDOaonV.js → semantic-fields-BwLOs1kl.js} +1 -1
- package/dist/assets/dql-notebook/assets/{sliders-horizontal-BDt6KRGR.js → sliders-horizontal-BYkm8SWW.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-CxUbF_BW.js → star-sVexqNCs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-COWnsSdh.js → triangle-alert-D2njv4o4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{upload-DSFC2kJX.js → upload-CXTMxIA8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DYdx896E.js → usePersistedAgentThreadId-CPDeaAgL.js} +1 -1
- package/dist/assets/dql-notebook/assets/{user-round-Bfu7fosn.js → user-round-zVsI_uud.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-DSpG0c0j.js → wand-sparkles-BMIwIzXT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-B1_pj9gq.js → workflow-Dj9y1sNk.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-CvfM03KZ.js → wrench-DPQi6zrs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-B4coLiOl.js → x-XhhbtinL.js} +1 -1
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/llm/answer-loop-tools.d.ts +3 -1
- package/dist/llm/answer-loop-tools.d.ts.map +1 -1
- package/dist/llm/answer-loop-tools.js +31 -23
- package/dist/llm/answer-loop-tools.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts +3 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +278 -19
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +29 -2
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-operation-coordinator.d.ts.map +1 -1
- package/dist/local-operation-coordinator.js +16 -2
- package/dist/local-operation-coordinator.js.map +1 -1
- package/dist/local-runtime.d.ts +184 -4
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +1858 -387
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/dist/providers/oauth/claude-oauth.d.ts +1 -1
- package/dist/providers/oauth/claude-oauth.d.ts.map +1 -1
- package/dist/providers/oauth/claude-oauth.js +18 -11
- package/dist/providers/oauth/claude-oauth.js.map +1 -1
- package/dist/providers/oauth/codex-oauth.d.ts +1 -1
- package/dist/providers/oauth/codex-oauth.d.ts.map +1 -1
- package/dist/providers/oauth/codex-oauth.js +9 -1
- package/dist/providers/oauth/codex-oauth.js.map +1 -1
- package/dist/providers/subscription-cli.d.ts +1 -1
- package/dist/providers/subscription-cli.d.ts.map +1 -1
- package/dist/providers/subscription-cli.js +33 -4
- package/dist/providers/subscription-cli.js.map +1 -1
- package/package.json +10 -10
- package/dist/assets/dql-notebook/assets/AppsView-DEWQ0sHT.js +0 -5588
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-IxDUKSG8.js +0 -88
package/dist/local-runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
import { execFileSync, execSync } from "node:child_process";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { gzip } from "node:zlib";
|
|
@@ -15,7 +16,7 @@ import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, pla
|
|
|
15
16
|
import { resolveNpmInvocation } from './npm-runtime.js';
|
|
16
17
|
import { ensureDqlGitignore, isGovernedSourceFile, isLegacyBroadDqlIgnore, } from './git-contract.js';
|
|
17
18
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
18
|
-
import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, previewModelingChanges, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, relationshipValidationProofFingerprint, renderSemanticBlockSource, discoverDbtDomains, collectDbtRelationshipTests, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, semanticDimensionReference, modelAreaLocalId, DEFAULT_MODEL_AREA_ID, } from '@duckcodeailabs/dql-core';
|
|
19
|
+
import { loadSemanticLayerFromDir, normalizeDqlArtifactReference, serializeMetricDefinitionToYaml, resolveSemanticLayerAsync, resolveRepoSource, getDialect, Parser, NodeKind, blockParameterDefinitions, buildLineageGraph, buildManifest, collectInputFiles, findAppDocuments, findDashboardsForApp, isBlockIdRef, loadAppDocument, loadDashboardDocument, analyzeImpact, buildTrustChain, detectDomainFlows, getDomainTrustOverview, queryLineage, queryBusiness360, queryCompleteLineagePaths, LineageGraph, canonicalize, canonicalizeNotebook, diffDQL, diffNotebook, domainFolderSlug, previewModelingChange, previewModelingChanges, applyModelingChange, previewDbtSourcePatch, applyDbtSourcePatch, loadDomainPackageRegistry, loadDbtNodeAuthoringDetail, normalizeAnalyticalFailureV1, normalizeAnalyticalRepairCapabilityV1, relationshipValidationProofFingerprint, renderSemanticBlockSource, discoverDbtDomains, collectDbtRelationshipTests, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, buildSqlAnalyticalSignature, buildGeneratedAnalyticalSqlSignature, buildSqlOutputExpressionSignature, semanticDimensionReference, semanticExecutionFingerprint, modelAreaLocalId, DEFAULT_MODEL_AREA_ID, } from '@duckcodeailabs/dql-core';
|
|
19
20
|
import { load as loadYaml } from 'js-yaml';
|
|
20
21
|
import { listBlockTemplates } from './block-templates.js';
|
|
21
22
|
import { getRunner as getLLMRunner } from './llm/index.js';
|
|
@@ -24,7 +25,7 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
|
|
|
24
25
|
import { resolveRetrievalHealthStatus } from './retrieval-health.js';
|
|
25
26
|
import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
|
|
26
27
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
27
|
-
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, 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, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, } from '@duckcodeailabs/dql-agent';
|
|
28
|
+
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildAggregationSafetyProof, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, previewSkillChange, buildContextAuthoringProposal, contextAuthoringDependencyClosure, FileContextAuthoringProposalStore, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, createAgentRunBudget, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, assertProviderPayloadAllowed, createProviderDispatchEgressReceipt, prepareProviderWireEnvelopeForDispatch, markProviderMetadataArray, createProviderEgressReceipt, redactProviderResultRows, composeVerifiedAnalyticalNarrative, DEFAULT_ASK_ROW_EGRESS_POLICY, ZERO_ROW_EGRESS_POLICY, resolveProviderResultRowEgressPolicy, } from '@duckcodeailabs/dql-agent';
|
|
28
29
|
import { addSqlResultFilter, dashboardFilterableResultColumns, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
|
|
29
30
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
30
31
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization, } from './apps-api.js';
|
|
@@ -48,6 +49,7 @@ import { buildSemanticTargetBinding } from './semantic-execution/target-binding.
|
|
|
48
49
|
import { discoverWarehouseMetadataScopes, normalizeConnectionMetadataScope, syncWarehouseMetadata, warehouseMetadataStatus, } from './warehouse-metadata.js';
|
|
49
50
|
import { NotebookDatasetWorkspace, } from "./notebook-datasets.js";
|
|
50
51
|
import { prepareBlockInvocation } from './block-invocation.js';
|
|
52
|
+
export const APP_SOURCE_REUSABLE_TAG = 'app-source';
|
|
51
53
|
const NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT = 500;
|
|
52
54
|
const NOTEBOOK_FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#6d5dfc"/><path d="M9 9h14v14H9z" fill="none" stroke="#fff" stroke-width="2"/><path d="M13 13h6M13 17h6M13 21h4" stroke="#fff" stroke-width="2" stroke-linecap="round"/></svg>';
|
|
53
55
|
export function resolveProjectSemanticConfig(projectConfig, projectRoot) {
|
|
@@ -290,6 +292,12 @@ export function parseAgentRunRequestBody(body) {
|
|
|
290
292
|
const workspaceContext = agentRunRecord(record.workspaceContext) ?? agentRunRecord(record.context);
|
|
291
293
|
const signals = agentRunRecord(record.signals);
|
|
292
294
|
const requestedMode = parseAgentRunRequestedMode(record.requestedMode) ?? parseAgentRunRequestedMode(record.mode);
|
|
295
|
+
if (record.researchResultRowsOptIn !== undefined && typeof record.researchResultRowsOptIn !== 'boolean') {
|
|
296
|
+
return { error: 'researchResultRowsOptIn must be a boolean.' };
|
|
297
|
+
}
|
|
298
|
+
if (record.researchResultRowsOptIn === true && requestedMode !== 'research') {
|
|
299
|
+
return { error: 'researchResultRowsOptIn is accepted only for an explicitly requested Research run.' };
|
|
300
|
+
}
|
|
293
301
|
const audience = record.audience === 'stakeholder' || record.audience === 'analyst'
|
|
294
302
|
? record.audience
|
|
295
303
|
: undefined;
|
|
@@ -305,29 +313,47 @@ export function parseAgentRunRequestBody(body) {
|
|
|
305
313
|
selectedObject,
|
|
306
314
|
executionTarget,
|
|
307
315
|
workspaceContext,
|
|
308
|
-
conversationContext: agentRunRecord(record.conversationContext),
|
|
316
|
+
conversationContext: sanitizeClientConversationContext(agentRunRecord(record.conversationContext)),
|
|
309
317
|
history: parseAgentRunHistory(record.history),
|
|
310
318
|
threadId: agentRunString(record.threadId),
|
|
311
319
|
runId: agentRunString(record.runId),
|
|
312
320
|
reasoningEffort: parseAgentRunReasoningEffort(record.reasoningEffort),
|
|
313
321
|
analysisDepth: parseAgentRunAnalysisDepth(record.analysisDepth) ?? parseAgentRunAnalysisDepth(record.depth),
|
|
314
322
|
thinkingMode: coerceThinkingMode(record.thinkingMode),
|
|
323
|
+
researchResultRowsOptIn: requestedMode === 'research' && record.researchResultRowsOptIn === true,
|
|
315
324
|
},
|
|
316
325
|
};
|
|
317
326
|
}
|
|
327
|
+
const CLIENT_PLAN_AUTHORITY_KEYS = new Set([
|
|
328
|
+
'priorResolvedAnalyticalPlan',
|
|
329
|
+
'resolvedAnalyticalPlan',
|
|
330
|
+
'analyticalFrame',
|
|
331
|
+
]);
|
|
332
|
+
/**
|
|
333
|
+
* Browser/embedding context is useful retrieval and history input, but it is
|
|
334
|
+
* not a plan-authority channel. Remove plan-shaped fields recursively at HTTP
|
|
335
|
+
* ingress; server-retained thread context is added afterwards from local state.
|
|
336
|
+
*/
|
|
337
|
+
function sanitizeClientConversationContext(context) {
|
|
338
|
+
if (!context)
|
|
339
|
+
return undefined;
|
|
340
|
+
const sanitize = (value) => {
|
|
341
|
+
if (Array.isArray(value))
|
|
342
|
+
return value.map(sanitize);
|
|
343
|
+
const record = agentRunRecord(value);
|
|
344
|
+
if (!record)
|
|
345
|
+
return value;
|
|
346
|
+
return Object.fromEntries(Object.entries(record).flatMap(([key, nested]) => CLIENT_PLAN_AUTHORITY_KEYS.has(key) ? [] : [[key, sanitize(nested)]]));
|
|
347
|
+
};
|
|
348
|
+
return sanitize(context);
|
|
349
|
+
}
|
|
318
350
|
const AGENT_LOOKUP_DEADLINE_MS = 45_000;
|
|
319
351
|
const AGENT_RESEARCH_DEADLINE_MS = 120_000;
|
|
320
|
-
/** Env-tunable run deadline (e.g. slow subscription-CLI providers), clamped to sane bounds. */
|
|
321
|
-
function resolveAgentDeadlineMs(envKey, fallback, env = process.env) {
|
|
322
|
-
const configured = Number(env[envKey]);
|
|
323
|
-
if (!Number.isFinite(configured) || configured <= 0)
|
|
324
|
-
return fallback;
|
|
325
|
-
return Math.max(15_000, Math.min(600_000, Math.floor(configured)));
|
|
326
|
-
}
|
|
327
352
|
/**
|
|
328
353
|
* PERF-002: one wall-clock budget follows the request through routing, provider
|
|
329
354
|
* calls, repair, and execution. Ordinary Ask never inherits Research's budget
|
|
330
|
-
*
|
|
355
|
+
* because of question wording, depth, provider, or relational shape; only an
|
|
356
|
+
* explicit `requestedMode: research` activates the Research budget.
|
|
331
357
|
*/
|
|
332
358
|
/** Resolve governed display formats for result columns from the semantic layer (best-effort, cached per call). */
|
|
333
359
|
function agentColumnDisplayFormats(projectRoot, columns) {
|
|
@@ -345,91 +371,63 @@ function agentColumnDisplayFormats(projectRoot, columns) {
|
|
|
345
371
|
catch { /* formatting is presentation-only; never fail an answer over it */ }
|
|
346
372
|
return formats;
|
|
347
373
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
/** High reasoning effort makes every LLM call materially slower, so the same
|
|
355
|
-
* multi-step build that fits a budget at medium effort overruns it at high. The
|
|
356
|
-
* budget must scale with effort or "High · thorough" reliably times out. */
|
|
357
|
-
function reasoningEffortBudgetMultiplier(effort) {
|
|
358
|
-
return effort === 'high' ? 1.8 : effort === 'low' ? 0.9 : 1;
|
|
374
|
+
export function agentRunDeadlineMs(request, env = process.env, activeProviderId) {
|
|
375
|
+
void env;
|
|
376
|
+
void activeProviderId;
|
|
377
|
+
return request.requestedMode === 'research'
|
|
378
|
+
? AGENT_RESEARCH_DEADLINE_MS
|
|
379
|
+
: AGENT_LOOKUP_DEADLINE_MS;
|
|
359
380
|
}
|
|
360
381
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
382
|
+
* Decide how a settled answer gets its business-facing prose.
|
|
383
|
+
*
|
|
384
|
+
* This deliberately does NOT read `requestedMode`. Gating narration on
|
|
385
|
+
* `requestedMode === 'research'` meant every ordinary Ask — which the UI sends
|
|
386
|
+
* as `'auto'` — skipped synthesis entirely and shipped the answer loop's
|
|
387
|
+
* deterministic fact-join as the primary answer: the reported `column: value`
|
|
388
|
+
* dump. Narration is owed to any run that actually produced values.
|
|
389
|
+
*
|
|
390
|
+
* Certification, a DQL artifact, and the exploratory candidate are no longer
|
|
391
|
+
* vetoes. EXP-001's grain concern is real, but the answer to "the model might
|
|
392
|
+
* relabel an entity-level measure" is to VERIFY the claims against the fact set
|
|
393
|
+
* (`verified_facts`) and pass the grain statement in as a caveat — not to refuse
|
|
394
|
+
* to write a sentence.
|
|
370
395
|
*/
|
|
371
|
-
function
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return plan.requestedShape.dimensions.length > 0
|
|
375
|
-
|| plan.dimensionTerms.length > 0
|
|
376
|
-
|| plan.requestedShape.filters.length > 0
|
|
377
|
-
|| plan.timeTerms.length > 0
|
|
378
|
-
|| Boolean(plan.requestedShape.topN);
|
|
379
|
-
}
|
|
380
|
-
export function agentRunDeadlineMs(request, env = process.env, activeProviderId) {
|
|
381
|
-
const cliProvider = activeProviderId === 'claude-code' || activeProviderId === 'codex';
|
|
382
|
-
const lookupDeadline = resolveAgentDeadlineMs('DQL_AGENT_LOOKUP_DEADLINE_MS', cliProvider ? AGENT_CLI_LOOKUP_DEADLINE_MS : AGENT_LOOKUP_DEADLINE_MS, env);
|
|
383
|
-
const researchDeadline = resolveAgentDeadlineMs('DQL_AGENT_RESEARCH_DEADLINE_MS', cliProvider ? AGENT_CLI_RESEARCH_DEADLINE_MS : AGENT_RESEARCH_DEADLINE_MS, env);
|
|
384
|
-
// The "Thinking" control sends a thinkingMode; explicit reasoningEffort/
|
|
385
|
-
// analysisDepth (CLI flags) take precedence but the mode is the common case.
|
|
386
|
-
// The deadline MUST see the resolved values — "High" both slows every call
|
|
387
|
-
// AND means deep — or picking High reliably times out at the lookup budget.
|
|
388
|
-
const resolvedMode = request.thinkingMode ? resolveThinkingMode(request.thinkingMode) : {};
|
|
389
|
-
const effectiveEffort = request.reasoningEffort ?? resolvedMode.reasoningEffort;
|
|
390
|
-
const effectiveDepth = request.analysisDepth ?? resolvedMode.analysisDepth;
|
|
391
|
-
// The budget is a CEILING, not a target — a fast answer returns immediately;
|
|
392
|
-
// the larger tiers only matter when the build genuinely needs the time.
|
|
393
|
-
const scale = (ms) => Math.min(600_000, Math.round(ms * reasoningEffortBudgetMultiplier(effectiveEffort)));
|
|
394
|
-
if (request.requestedMode === 'research' || effectiveDepth === 'deep') {
|
|
395
|
-
return scale(researchDeadline);
|
|
396
|
-
}
|
|
397
|
-
// A structured clarification chip displays a compact metric/path label, but
|
|
398
|
-
// the engine restores and executes the original analytical question. Budget
|
|
399
|
-
// from that same original question as well. Otherwise a complex ranking or
|
|
400
|
-
// cross-model request can incorrectly inherit the 45s scalar-lookup budget
|
|
401
|
-
// merely because the selected label is short, then time out after the user
|
|
402
|
-
// already supplied the exact governed meaning.
|
|
403
|
-
const deadlineQuestion = request.selectedEvidenceId
|
|
404
|
-
? request.clarificationSourceQuestion?.trim() || request.question
|
|
405
|
-
: request.question;
|
|
406
|
-
const plan = buildAnalysisQuestionPlan(deadlineQuestion);
|
|
407
|
-
if (plan.needsResearchWorkspace || isAnalyticalBuildQuestion(plan)) {
|
|
408
|
-
return scale(researchDeadline);
|
|
409
|
-
}
|
|
410
|
-
return scale(lookupDeadline);
|
|
411
|
-
}
|
|
412
|
-
export function shouldSynthesizeAgentRunAnswer(governedAnswer) {
|
|
396
|
+
export function planAgentRunNarration(governedAnswer, context) {
|
|
397
|
+
// A refusal is not narrated. It owes the user a reason or a clarifying
|
|
398
|
+
// question, which is the clarification lane's job, not the narrator's.
|
|
413
399
|
if (governedAnswer.kind === 'no_answer')
|
|
414
|
-
return
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
//
|
|
400
|
+
return { mode: 'skip', reason: 'no_answer' };
|
|
401
|
+
if (!context.providerAvailable)
|
|
402
|
+
return { mode: 'skip', reason: 'no_provider' };
|
|
403
|
+
const maxRows = Math.max(0, context.rowEgress.maxNarrationRows);
|
|
404
|
+
// The fact set is the strongest grounding available: every sentence can be
|
|
405
|
+
// tied back to a fact id and rejected when it is not.
|
|
406
|
+
if (governedAnswer.analyticalFacts)
|
|
407
|
+
return { mode: 'verified_facts', maxRows };
|
|
419
408
|
if (governedAnswer.result)
|
|
420
|
-
return
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
409
|
+
return { mode: 'preview_grounded', maxRows };
|
|
410
|
+
return { mode: 'skip', reason: 'nothing_to_narrate' };
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Boolean view of {@link planAgentRunNarration}, kept for callers and tests that
|
|
414
|
+
* only need "does this run narrate at all".
|
|
415
|
+
*/
|
|
416
|
+
export function shouldSynthesizeAgentRunAnswer(governedAnswer, requestedMode = 'ask') {
|
|
417
|
+
return planAgentRunNarration(governedAnswer, {
|
|
418
|
+
requestedMode,
|
|
419
|
+
providerAvailable: true,
|
|
420
|
+
rowEgress: DEFAULT_ASK_ROW_EGRESS_POLICY,
|
|
421
|
+
}).mode !== 'skip';
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* AGT-010 — the semantic route label is descriptive, while the exact
|
|
425
|
+
* route-specific aggregation proof is authoritative for governed trust.
|
|
426
|
+
* Missing proof remains blocked for legacy or malformed results.
|
|
427
|
+
*/
|
|
428
|
+
export function semanticAnswerHasPassedAggregationProof(governedAnswer) {
|
|
429
|
+
return governedAnswer.route?.tier === 'semantic_metric'
|
|
430
|
+
&& governedAnswer.aggregationSafetyProof?.status === 'safe';
|
|
433
431
|
}
|
|
434
432
|
export function agentAnswerHasExecutionFailure(governedAnswer) {
|
|
435
433
|
return typeof governedAnswer.executionError === 'string'
|
|
@@ -543,6 +541,194 @@ export function warehouseFailureFromAgentRun(run) {
|
|
|
543
541
|
}
|
|
544
542
|
return undefined;
|
|
545
543
|
}
|
|
544
|
+
const AUTOMATIC_ASK_REPAIR_FAILURE_CODES = new Set([
|
|
545
|
+
'COLUMN_NOT_FOUND',
|
|
546
|
+
'RELATION_NOT_FOUND',
|
|
547
|
+
'AMBIGUOUS_COLUMN',
|
|
548
|
+
'DIALECT_ERROR',
|
|
549
|
+
]);
|
|
550
|
+
/** Validate the immutable custom wrapper before any automatic repair is offered. */
|
|
551
|
+
export function validAskRepairDqlWrapper(source) {
|
|
552
|
+
try {
|
|
553
|
+
const program = new Parser(source, '<ask-repair-capability>').parse();
|
|
554
|
+
if (program.statements.length !== 1)
|
|
555
|
+
return false;
|
|
556
|
+
const block = program.statements[0];
|
|
557
|
+
return block.kind === NodeKind.BlockDecl
|
|
558
|
+
&& block.blockType === 'custom'
|
|
559
|
+
&& Boolean(extractBlockStudioSql(source)?.trim());
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/** Build retained automatic-repair authority from analytical failure state only. */
|
|
566
|
+
export function analyticalRepairCapabilityForAgentRun(run, resolvedTargetFingerprint) {
|
|
567
|
+
if (run.status !== 'blocked')
|
|
568
|
+
return undefined;
|
|
569
|
+
const failedRun = analyticalFailedRunFromAgentRun(run);
|
|
570
|
+
const failure = failedRun?.failure;
|
|
571
|
+
const dqlArtifact = run.artifacts.flatMap((artifact) => {
|
|
572
|
+
const payload = agentRunRecord(artifact.payload);
|
|
573
|
+
const candidate = normalizeDqlArtifactReference(payload?.dqlArtifact);
|
|
574
|
+
return candidate?.kind === 'sql_block' && candidate.source?.trim() ? [candidate] : [];
|
|
575
|
+
})[0];
|
|
576
|
+
const dqlSource = dqlArtifact?.source?.trim();
|
|
577
|
+
const sql = dqlArtifact?.compiledSql?.trim() || repairableSqlFromAgentRun(run);
|
|
578
|
+
const target = run.executionTarget;
|
|
579
|
+
const planFingerprint = failure?.planFingerprint?.trim();
|
|
580
|
+
const hash = (value) => `sha256:${createHash('sha256').update(stableExecutionValue(value)).digest('hex')}`;
|
|
581
|
+
const sourceArtifact = run.artifacts.find((artifact) => {
|
|
582
|
+
const payload = agentRunRecord(artifact.payload);
|
|
583
|
+
return Boolean(normalizeDqlArtifactReference(payload?.dqlArtifact)?.source?.trim());
|
|
584
|
+
});
|
|
585
|
+
const retainedPlanFingerprint = sourceArtifact
|
|
586
|
+
? agentRunString(agentRunRecord(agentRunRecord(sourceArtifact.payload)?.resolvedAnalyticalPlan)?.fingerprint)
|
|
587
|
+
: undefined;
|
|
588
|
+
const sourceFingerprint = sourceArtifact && dqlArtifact
|
|
589
|
+
? hash({
|
|
590
|
+
runId: run.id,
|
|
591
|
+
artifactId: sourceArtifact.id,
|
|
592
|
+
dqlArtifact: {
|
|
593
|
+
kind: dqlArtifact.kind,
|
|
594
|
+
name: dqlArtifact.name,
|
|
595
|
+
source: dqlArtifact.source?.trim(),
|
|
596
|
+
parameterValues: dqlArtifact.parameterValues ?? {},
|
|
597
|
+
trustState: dqlArtifact.trustState,
|
|
598
|
+
persistence: dqlArtifact.persistence,
|
|
599
|
+
},
|
|
600
|
+
})
|
|
601
|
+
: undefined;
|
|
602
|
+
const manualActions = (failure?.safeActions ?? []).flatMap((action) => {
|
|
603
|
+
if (action === 'edit_dql' || action === 'open_sql_notebook' || action === 'refresh_snapshot'
|
|
604
|
+
|| action === 'retry_same_plan' || action === 'change_authorized_connection' || action === 'request_access') {
|
|
605
|
+
return [action];
|
|
606
|
+
}
|
|
607
|
+
return [];
|
|
608
|
+
});
|
|
609
|
+
const ineligibilityReason = !failure
|
|
610
|
+
? 'missing_failure_authority'
|
|
611
|
+
: failure.phase !== 'execution' || !AUTOMATIC_ASK_REPAIR_FAILURE_CODES.has(failure.code)
|
|
612
|
+
? 'failure_not_eligible'
|
|
613
|
+
: !dqlSource
|
|
614
|
+
? 'missing_dql_wrapper'
|
|
615
|
+
: !validAskRepairDqlWrapper(dqlSource)
|
|
616
|
+
? 'invalid_dql_wrapper'
|
|
617
|
+
: !sql
|
|
618
|
+
? 'missing_compiled_sql'
|
|
619
|
+
: !planFingerprint || !retainedPlanFingerprint || retainedPlanFingerprint !== planFingerprint
|
|
620
|
+
? 'missing_plan_fingerprint'
|
|
621
|
+
: !sourceFingerprint
|
|
622
|
+
? 'missing_dql_wrapper'
|
|
623
|
+
: !target || !resolvedTargetFingerprint
|
|
624
|
+
? 'missing_execution_target'
|
|
625
|
+
: undefined;
|
|
626
|
+
const eligible = ineligibilityReason === undefined;
|
|
627
|
+
return {
|
|
628
|
+
version: 1,
|
|
629
|
+
automatic: {
|
|
630
|
+
eligible,
|
|
631
|
+
action: eligible ? 'repair_embedded_sql' : 'none',
|
|
632
|
+
correctionCode: eligible ? 'SQL_EXECUTION_REPAIR' : 'MANUAL_REVIEW_REQUIRED',
|
|
633
|
+
attemptsRemaining: eligible ? 1 : 0,
|
|
634
|
+
},
|
|
635
|
+
failureFingerprint: hash(failure ?? { unavailable: 'analytical_failure', runId: run.id }),
|
|
636
|
+
sourceFingerprint: sourceFingerprint ?? hash({ unavailable: 'source', runId: run.id }),
|
|
637
|
+
planFingerprint: retainedPlanFingerprint ?? planFingerprint ?? hash({ unavailable: 'plan', runId: run.id }),
|
|
638
|
+
dqlFingerprint: dqlSource ? hash(dqlSource) : hash({ unavailable: 'dql', runId: run.id }),
|
|
639
|
+
sqlFingerprint: sql ? hash(sql) : hash({ unavailable: 'sql', runId: run.id }),
|
|
640
|
+
targetFingerprint: resolvedTargetFingerprint ?? hash({ unavailable: 'target', runId: run.id }),
|
|
641
|
+
routeLocked: true,
|
|
642
|
+
targetLocked: true,
|
|
643
|
+
sourceImmutable: true,
|
|
644
|
+
manualActions: Array.from(new Set(manualActions)),
|
|
645
|
+
...(ineligibilityReason ? { ineligibilityReason } : {}),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function attachAnalyticalRepairCapability(run, resolvedTargetFingerprint) {
|
|
649
|
+
const capability = analyticalRepairCapabilityForAgentRun(run, resolvedTargetFingerprint);
|
|
650
|
+
if (!capability)
|
|
651
|
+
return run;
|
|
652
|
+
run.repairCapability = capability;
|
|
653
|
+
if (run.diagnosticReceipt)
|
|
654
|
+
run.diagnosticReceipt = { ...run.diagnosticReceipt, repairCapability: capability };
|
|
655
|
+
run.artifacts = run.artifacts.map((artifact) => {
|
|
656
|
+
const payload = agentRunRecord(artifact.payload);
|
|
657
|
+
if (!payload)
|
|
658
|
+
return artifact;
|
|
659
|
+
const diagnostic = agentRunRecord(payload.diagnosticReceipt);
|
|
660
|
+
return {
|
|
661
|
+
...artifact,
|
|
662
|
+
payload: {
|
|
663
|
+
...payload,
|
|
664
|
+
repairCapability: capability,
|
|
665
|
+
...(diagnostic ? { diagnosticReceipt: { ...diagnostic, repairCapability: capability } } : {}),
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
});
|
|
669
|
+
return run;
|
|
670
|
+
}
|
|
671
|
+
export function targetGenerationFingerprint(connection, connectionName) {
|
|
672
|
+
const identity = {
|
|
673
|
+
driver: connection.driver,
|
|
674
|
+
connectionName: connectionName ?? null,
|
|
675
|
+
host: connection.host ?? null,
|
|
676
|
+
port: connection.port ?? null,
|
|
677
|
+
username: connection.username ?? null,
|
|
678
|
+
authMethod: connection.authMethod ?? null,
|
|
679
|
+
authenticator: connection.authenticator ?? null,
|
|
680
|
+
profile: connection.profile ?? null,
|
|
681
|
+
ssl: connection.ssl ?? null,
|
|
682
|
+
account: connection.account ?? null,
|
|
683
|
+
database: connection.database ?? null,
|
|
684
|
+
schema: connection.schema ?? null,
|
|
685
|
+
warehouse: connection.warehouse ?? null,
|
|
686
|
+
role: connection.role ?? null,
|
|
687
|
+
catalog: connection.catalog ?? null,
|
|
688
|
+
httpPath: connection.httpPath ?? null,
|
|
689
|
+
filepath: connection.filepath ?? null,
|
|
690
|
+
projectId: connection.projectId ?? null,
|
|
691
|
+
region: connection.region ?? null,
|
|
692
|
+
workgroup: connection.workgroup ?? null,
|
|
693
|
+
location: connection.location ?? null,
|
|
694
|
+
outputLocation: connection.outputLocation ?? null,
|
|
695
|
+
accessUrl: connection.accessUrl ?? null,
|
|
696
|
+
application: connection.application ?? null,
|
|
697
|
+
queryTag: connection.queryTag ?? null,
|
|
698
|
+
workloadIdentityProvider: connection.workloadIdentityProvider ?? null,
|
|
699
|
+
workloadIdentityAzureClientId: connection.workloadIdentityAzureClientId ?? null,
|
|
700
|
+
workloadIdentityImpersonationPath: connection.workloadIdentityImpersonationPath ?? null,
|
|
701
|
+
oauthAuthorizationUrl: connection.oauthAuthorizationUrl ?? null,
|
|
702
|
+
oauthClientId: connection.oauthClientId ?? null,
|
|
703
|
+
oauthRedirectUri: connection.oauthRedirectUri ?? null,
|
|
704
|
+
oauthScope: connection.oauthScope ?? null,
|
|
705
|
+
oauthTokenRequestUrl: connection.oauthTokenRequestUrl ?? null,
|
|
706
|
+
keyFilename: connection.keyFilename ?? null,
|
|
707
|
+
privateKeyPath: connection.privateKeyPath ?? null,
|
|
708
|
+
accessKeyId: connection.accessKeyId ?? null,
|
|
709
|
+
proxyHost: connection.proxyHost ?? null,
|
|
710
|
+
proxyPort: connection.proxyPort ?? null,
|
|
711
|
+
proxyProtocol: connection.proxyProtocol ?? null,
|
|
712
|
+
proxyUser: connection.proxyUser ?? null,
|
|
713
|
+
noProxy: connection.noProxy ?? null,
|
|
714
|
+
credentialCacheDir: connection.credentialCacheDir ?? null,
|
|
715
|
+
moduleSearchPaths: connection.moduleSearchPaths ?? null,
|
|
716
|
+
clientRequestMFAToken: connection.clientRequestMFAToken ?? null,
|
|
717
|
+
clientStoreTemporaryCredential: connection.clientStoreTemporaryCredential ?? null,
|
|
718
|
+
clientSessionKeepAlive: connection.clientSessionKeepAlive ?? null,
|
|
719
|
+
clientSessionKeepAliveHeartbeatFrequency: connection.clientSessionKeepAliveHeartbeatFrequency ?? null,
|
|
720
|
+
keepAlive: connection.keepAlive ?? null,
|
|
721
|
+
passcodeInPassword: connection.passcodeInPassword ?? null,
|
|
722
|
+
browserActionTimeout: connection.browserActionTimeout ?? null,
|
|
723
|
+
timeout: connection.timeout ?? null,
|
|
724
|
+
waitTimeout: connection.waitTimeout ?? null,
|
|
725
|
+
byteLimit: connection.byteLimit ?? null,
|
|
726
|
+
};
|
|
727
|
+
return executionFingerprint(stableExecutionValue(identity));
|
|
728
|
+
}
|
|
729
|
+
function normalizeSqlForComparison(sql) {
|
|
730
|
+
return sql.trim().replace(/;\s*$/, '').replace(/\s+/g, ' ');
|
|
731
|
+
}
|
|
546
732
|
/**
|
|
547
733
|
* Retain planning and retrieval evidence for a successful derived repair while
|
|
548
734
|
* deliberately dropping the source failure and any result-bound facts. This
|
|
@@ -655,7 +841,7 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
655
841
|
});
|
|
656
842
|
}
|
|
657
843
|
return {
|
|
658
|
-
...(clientContext ?? {}),
|
|
844
|
+
...(sanitizeClientConversationContext(clientContext) ?? {}),
|
|
659
845
|
conversationStateVersion: 1,
|
|
660
846
|
threadId,
|
|
661
847
|
...(serverSnapshot ? { conversationEnvelope: serverSnapshot } : {}),
|
|
@@ -782,7 +968,9 @@ export function conversationTurnInputFromRun(run) {
|
|
|
782
968
|
?? run.artifacts.find((candidate) => candidate.kind === 'research_run')
|
|
783
969
|
?? run.artifacts[0];
|
|
784
970
|
const payload = agentRunRecord(artifact?.payload);
|
|
785
|
-
|
|
971
|
+
// A blocked run may retain diagnostic artifacts, but their result-shaped
|
|
972
|
+
// payload is never accepted conversation evidence or prose authority.
|
|
973
|
+
const result = run.status === 'blocked' ? undefined : agentRunRecord(payload?.result);
|
|
786
974
|
const columns = conversationResultColumns(result?.columns);
|
|
787
975
|
// The visual preview stays tiny, but member resolution needs a wider bounded
|
|
788
976
|
// value window. Deriving dimensions from only the eight preview rows caused a
|
|
@@ -923,6 +1111,230 @@ function apiErrorCode(error, fallback) {
|
|
|
923
1111
|
function apiErrorMessage(error) {
|
|
924
1112
|
return error instanceof Error ? error.message : String(error);
|
|
925
1113
|
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Slowest provider dispatch we assume before any have been observed, and the
|
|
1116
|
+
* settle time reserved after the last one. A subscription-CLI provider costs
|
|
1117
|
+
* roughly 10-15s per call, so these keep the first admission decision honest.
|
|
1118
|
+
*/
|
|
1119
|
+
const ASSUMED_PROVIDER_DISPATCH_MS = 12_000;
|
|
1120
|
+
const DISPATCH_SETTLE_MARGIN_MS = 4_000;
|
|
1121
|
+
export class RunScopedProviderDispatchEvidence {
|
|
1122
|
+
policy;
|
|
1123
|
+
runBudget;
|
|
1124
|
+
rowEgress;
|
|
1125
|
+
receipts = [];
|
|
1126
|
+
phaseCounts = new Map();
|
|
1127
|
+
currentRoute;
|
|
1128
|
+
/** Wall-clock start of the previous dispatch, used to learn this provider's cost. */
|
|
1129
|
+
lastDispatchStartedAtMs;
|
|
1130
|
+
observedDispatchDurations = [];
|
|
1131
|
+
/**
|
|
1132
|
+
* How long the NEXT dispatch is likely to take, learned from this run.
|
|
1133
|
+
*
|
|
1134
|
+
* Counting dispatches is not a time budget. A provider costing ~13s a call
|
|
1135
|
+
* will burn a 45s deadline in three calls and get killed mid-flight, so the
|
|
1136
|
+
* run ends with nothing — strictly worse than stopping early and answering
|
|
1137
|
+
* from what it already has.
|
|
1138
|
+
*/
|
|
1139
|
+
expectedDispatchMs() {
|
|
1140
|
+
if (this.observedDispatchDurations.length === 0)
|
|
1141
|
+
return ASSUMED_PROVIDER_DISPATCH_MS;
|
|
1142
|
+
const sorted = [...this.observedDispatchDurations].sort((left, right) => left - right);
|
|
1143
|
+
// The slowest observed call is the honest predictor: an optimistic median
|
|
1144
|
+
// still admits a dispatch that the deadline then kills.
|
|
1145
|
+
return sorted[sorted.length - 1];
|
|
1146
|
+
}
|
|
1147
|
+
/** True when the remaining wall clock cannot fit another provider call. */
|
|
1148
|
+
cannotFitAnotherDispatch() {
|
|
1149
|
+
if (!this.runBudget)
|
|
1150
|
+
return false;
|
|
1151
|
+
return this.runBudget.remainingMs() < this.expectedDispatchMs() + DISPATCH_SETTLE_MARGIN_MS;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* The run budget's own clock when there is one, so dispatch cost is measured
|
|
1155
|
+
* against the same timeline the deadline is enforced on (and stays injectable
|
|
1156
|
+
* for deterministic tests).
|
|
1157
|
+
*/
|
|
1158
|
+
nowMs() {
|
|
1159
|
+
return this.runBudget ? this.runBudget.elapsedMs() : Date.now();
|
|
1160
|
+
}
|
|
1161
|
+
recordDispatchStart() {
|
|
1162
|
+
const now = this.nowMs();
|
|
1163
|
+
if (this.lastDispatchStartedAtMs !== undefined) {
|
|
1164
|
+
this.observedDispatchDurations.push(Math.max(0, now - this.lastDispatchStartedAtMs));
|
|
1165
|
+
}
|
|
1166
|
+
this.lastDispatchStartedAtMs = now;
|
|
1167
|
+
}
|
|
1168
|
+
constructor(policy, runBudget, rowEgress = ZERO_ROW_EGRESS_POLICY) {
|
|
1169
|
+
this.policy = policy;
|
|
1170
|
+
this.runBudget = runBudget;
|
|
1171
|
+
this.rowEgress = rowEgress;
|
|
1172
|
+
this.currentRoute = runBudget?.mode === 'research' ? 'research' : 'generated_answer';
|
|
1173
|
+
}
|
|
1174
|
+
setRoute(route) {
|
|
1175
|
+
if (route)
|
|
1176
|
+
this.currentRoute = route;
|
|
1177
|
+
}
|
|
1178
|
+
mayStartToolCall() {
|
|
1179
|
+
return !this.runBudget || this.runBudget.mayStartDiscovery(this.currentRoute);
|
|
1180
|
+
}
|
|
1181
|
+
observe(event, context) {
|
|
1182
|
+
const softRoute = context.dispatchPhase === 'meaning_resolution' ? 'clarify' : this.currentRoute;
|
|
1183
|
+
// Admission control BEFORE the phase targets: starting a call that the hard
|
|
1184
|
+
// deadline will kill mid-flight wastes the remaining budget and ends the run
|
|
1185
|
+
// with nothing. Stopping here lets the caller answer from what it has.
|
|
1186
|
+
// Narration is exempt: it is the step that produces the answer, and it is
|
|
1187
|
+
// separately bounded by its own soft target below.
|
|
1188
|
+
if (context.dispatchPhase !== 'narration' && this.cannotFitAnotherDispatch()) {
|
|
1189
|
+
throw Object.assign(new Error(`Only ${Math.round((this.runBudget?.remainingMs() ?? 0) / 1_000)}s of the run deadline remain, which is not enough for another ~${Math.round(this.expectedDispatchMs() / 1_000)}s provider call. Answering from what has already been gathered.`), { code: 'RUN_DEADLINE_INSUFFICIENT' });
|
|
1190
|
+
}
|
|
1191
|
+
if (context.dispatchPhase === 'narration') {
|
|
1192
|
+
if (this.runBudget && !this.runBudget.mayStartNarration()) {
|
|
1193
|
+
throw Object.assign(new Error(`The ${Math.round(this.runBudget.narrationSoftTargetMs() / 1_000)}-second narration target elapsed before the answer could be written.`), { code: 'RUN_SOFT_TARGET_EXCEEDED' });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
else if (this.runBudget && !this.runBudget.mayStartDiscovery(softRoute)) {
|
|
1197
|
+
throw Object.assign(new Error(`The ${Math.round(this.runBudget.softTargetMs(softRoute) / 1_000)}-second soft target elapsed before this provider dispatch could start.`), { code: 'RUN_SOFT_TARGET_EXCEEDED' });
|
|
1198
|
+
}
|
|
1199
|
+
this.recordDispatchStart();
|
|
1200
|
+
if (this.receipts.length >= this.policy.total) {
|
|
1201
|
+
throw Object.assign(new Error(`Run-wide provider dispatch budget exhausted after ${this.policy.total} physical attempts.`), { code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED' });
|
|
1202
|
+
}
|
|
1203
|
+
const phaseCount = this.phaseCounts.get(context.dispatchPhase) ?? 0;
|
|
1204
|
+
const phaseLimit = context.dispatchPhase === 'meaning_resolution'
|
|
1205
|
+
? this.policy.meaningResolution
|
|
1206
|
+
: context.dispatchPhase === 'repair'
|
|
1207
|
+
? this.policy.repair
|
|
1208
|
+
: context.dispatchPhase === 'narration'
|
|
1209
|
+
? this.policy.narration
|
|
1210
|
+
: this.policy.generationGroup;
|
|
1211
|
+
// Planning and generation share one ledger because they compete for the
|
|
1212
|
+
// same "work out the query" budget. Narration does not: it is the step that
|
|
1213
|
+
// turns a settled result into an answer, and starving it produces a run
|
|
1214
|
+
// that computed the right numbers and then could not say them.
|
|
1215
|
+
const generationGroupCount = this.receipts.filter((receipt) => receipt.dispatchPhase === 'planning'
|
|
1216
|
+
|| receipt.dispatchPhase === 'generation').length;
|
|
1217
|
+
if (phaseCount >= phaseLimit || ((context.dispatchPhase === 'planning' || context.dispatchPhase === 'generation')
|
|
1218
|
+
&& generationGroupCount >= this.policy.generationGroup)) {
|
|
1219
|
+
throw Object.assign(new Error(`Provider dispatch budget exhausted for ${context.dispatchPhase} after ${phaseLimit} physical attempts.`), { code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED' });
|
|
1220
|
+
}
|
|
1221
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
|
|
1222
|
+
const projectedRowCount = context.serializedResultShape?.resultRowCount ?? 0;
|
|
1223
|
+
const projectedCumulativeRowCount = context.cumulativeResultRowCount ?? projectedRowCount;
|
|
1224
|
+
// Ordinary Ask narration reads its ceiling from the resolved project policy;
|
|
1225
|
+
// Research keeps its own explicit opt-in limits.
|
|
1226
|
+
const permittedRowLimit = context.purpose === 'answer_narration'
|
|
1227
|
+
? this.rowEgress.maxNarrationRows
|
|
1228
|
+
: context.optIn && context.purpose === 'research_narration'
|
|
1229
|
+
? 20
|
|
1230
|
+
: context.optIn && context.purpose === 'research_tool'
|
|
1231
|
+
? 200
|
|
1232
|
+
: 0;
|
|
1233
|
+
if (projectedRowCount > permittedRowLimit || projectedCumulativeRowCount > permittedRowLimit) {
|
|
1234
|
+
throw Object.assign(new Error(permittedRowLimit === 0
|
|
1235
|
+
? 'Provider egress blocked result rows without explicit Research run consent.'
|
|
1236
|
+
: `Provider egress blocked result rows beyond the cumulative Research limit of ${permittedRowLimit}.`), { code: permittedRowLimit === 0 ? 'PROVIDER_RESULT_ROWS_BLOCKED' : 'PROVIDER_RESULT_ROWS_LIMIT_EXCEEDED' });
|
|
1237
|
+
}
|
|
1238
|
+
assertProviderPayloadAllowed(envelope, {
|
|
1239
|
+
allowResultRows: false,
|
|
1240
|
+
maxResultRows: 0,
|
|
1241
|
+
purpose: context.purpose,
|
|
1242
|
+
});
|
|
1243
|
+
const rowCount = projectedRowCount;
|
|
1244
|
+
this.receipts.push(createProviderDispatchEgressReceipt({
|
|
1245
|
+
purpose: context.purpose,
|
|
1246
|
+
dispatchPhase: context.dispatchPhase,
|
|
1247
|
+
provider: event.provider,
|
|
1248
|
+
...(event.model ? { model: event.model } : {}),
|
|
1249
|
+
operation: event.operation,
|
|
1250
|
+
attemptIndex: event.attemptIndex,
|
|
1251
|
+
options: event.options,
|
|
1252
|
+
permittedCategories: rowCount > 0
|
|
1253
|
+
? ['instructions', 'question', 'schema_metadata', 'governed_context', 'result_rows']
|
|
1254
|
+
: ['instructions', 'question', 'schema_metadata', 'governed_context'],
|
|
1255
|
+
optIn: context.optIn && rowCount > 0,
|
|
1256
|
+
envelope,
|
|
1257
|
+
...(context.serializedResultShape ? { serializedResultShape: context.serializedResultShape } : {}),
|
|
1258
|
+
...(typeof context.cumulativeResultRowCount === 'number'
|
|
1259
|
+
? { cumulativeResultRowCount: context.cumulativeResultRowCount }
|
|
1260
|
+
: {}),
|
|
1261
|
+
}));
|
|
1262
|
+
this.phaseCounts.set(context.dispatchPhase, phaseCount + 1);
|
|
1263
|
+
return envelope;
|
|
1264
|
+
}
|
|
1265
|
+
snapshot(fallbackReason = 'none') {
|
|
1266
|
+
return {
|
|
1267
|
+
providerEgressReceipts: [...this.receipts],
|
|
1268
|
+
providerRoundTrips: this.receipts.length,
|
|
1269
|
+
toolCalls: 0,
|
|
1270
|
+
sqlExecutions: 0,
|
|
1271
|
+
repairs: 0,
|
|
1272
|
+
fallbackReason,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
const agentRunProviderEvidenceContext = new AsyncLocalStorage();
|
|
1277
|
+
function mergeRunScopedProviderDispatchEvidence(run, evidence) {
|
|
1278
|
+
const providerEgressReceipts = [...evidence.providerEgressReceipts];
|
|
1279
|
+
const elapsed = Math.max(0, Date.parse(run.completedAt) - Date.parse(run.startedAt));
|
|
1280
|
+
const telemetry = {
|
|
1281
|
+
version: 1,
|
|
1282
|
+
stageDurationsMs: {
|
|
1283
|
+
...(run.telemetry?.stageDurationsMs ?? {}),
|
|
1284
|
+
total: Number.isFinite(elapsed) ? Math.min(86_400_000, elapsed) : 0,
|
|
1285
|
+
},
|
|
1286
|
+
providerRoundTrips: evidence.providerRoundTrips,
|
|
1287
|
+
toolCalls: run.telemetry?.toolCalls ?? evidence.toolCalls,
|
|
1288
|
+
sqlExecutions: run.telemetry?.sqlExecutions ?? evidence.sqlExecutions,
|
|
1289
|
+
repairs: run.telemetry?.repairs ?? evidence.repairs,
|
|
1290
|
+
egressReceipts: providerEgressReceipts.length,
|
|
1291
|
+
...(run.telemetry?.warehouseDurationMs !== undefined
|
|
1292
|
+
? { warehouseDurationMs: run.telemetry.warehouseDurationMs }
|
|
1293
|
+
: {}),
|
|
1294
|
+
...(run.telemetry?.fallbackReason
|
|
1295
|
+
? { fallbackReason: run.telemetry.fallbackReason }
|
|
1296
|
+
: evidence.fallbackReason !== 'none'
|
|
1297
|
+
? { fallbackReason: evidence.fallbackReason }
|
|
1298
|
+
: {}),
|
|
1299
|
+
};
|
|
1300
|
+
const diagnosticReceipt = run.diagnosticReceipt
|
|
1301
|
+
? { ...run.diagnosticReceipt, providerEgressReceipts }
|
|
1302
|
+
: undefined;
|
|
1303
|
+
const diagnosticReceiptV2 = {
|
|
1304
|
+
version: 2,
|
|
1305
|
+
runId: run.id,
|
|
1306
|
+
route: run.route,
|
|
1307
|
+
status: run.status,
|
|
1308
|
+
telemetry,
|
|
1309
|
+
providerEgressReceiptFingerprints: providerEgressReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
|
|
1310
|
+
...(run.diagnosticReceiptV2?.repairCapabilityFingerprint
|
|
1311
|
+
? { repairCapabilityFingerprint: run.diagnosticReceiptV2.repairCapabilityFingerprint }
|
|
1312
|
+
: {}),
|
|
1313
|
+
};
|
|
1314
|
+
const artifacts = run.artifacts.map((artifact) => {
|
|
1315
|
+
if (!artifact.payload || typeof artifact.payload !== 'object' || Array.isArray(artifact.payload))
|
|
1316
|
+
return artifact;
|
|
1317
|
+
const payload = artifact.payload;
|
|
1318
|
+
if (!('diagnosticReceipt' in payload) && !('diagnosticReceiptV2' in payload))
|
|
1319
|
+
return artifact;
|
|
1320
|
+
return {
|
|
1321
|
+
...artifact,
|
|
1322
|
+
payload: {
|
|
1323
|
+
...payload,
|
|
1324
|
+
...(diagnosticReceipt ? { diagnosticReceipt } : {}),
|
|
1325
|
+
diagnosticReceiptV2,
|
|
1326
|
+
},
|
|
1327
|
+
};
|
|
1328
|
+
});
|
|
1329
|
+
return {
|
|
1330
|
+
...run,
|
|
1331
|
+
artifacts,
|
|
1332
|
+
providerEgressReceipts,
|
|
1333
|
+
telemetry,
|
|
1334
|
+
...(diagnosticReceipt ? { diagnosticReceipt } : {}),
|
|
1335
|
+
diagnosticReceiptV2,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
926
1338
|
export async function startLocalServer(opts) {
|
|
927
1339
|
const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
|
|
928
1340
|
const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
|
|
@@ -944,6 +1356,11 @@ export async function startLocalServer(opts) {
|
|
|
944
1356
|
if (gitRoot)
|
|
945
1357
|
ensureLocalRuntimeGitignore(projectRoot);
|
|
946
1358
|
let projectConfig = loadProjectConfig(projectRoot);
|
|
1359
|
+
const analyticalExecutionService = new ExecutionService({
|
|
1360
|
+
executor,
|
|
1361
|
+
projectRoot,
|
|
1362
|
+
projectConfig: () => projectConfig,
|
|
1363
|
+
});
|
|
947
1364
|
recordAgentRuntimeVersion(projectRoot, runtimeVersion);
|
|
948
1365
|
// A clone carries governed Hint Graph files in Git, never the rebuildable
|
|
949
1366
|
// `.dql/cache` database. Materialize that projection as soon as the Notebook
|
|
@@ -1565,23 +1982,81 @@ export async function startLocalServer(opts) {
|
|
|
1565
1982
|
};
|
|
1566
1983
|
// Provider-backed narration for stakeholder stories. Reuses the same provider
|
|
1567
1984
|
// adapter as the planner; narrateResult always returns (deterministic fallback).
|
|
1568
|
-
const narrateForAgentRun = async (input) =>
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1985
|
+
const narrateForAgentRun = async (input, allowProviderResultRows = false) => {
|
|
1986
|
+
// Without caller consent, keep narration deterministic and make no physical
|
|
1987
|
+
// provider dispatch. With consent, serialize only the bounded, redacted
|
|
1988
|
+
// sample the transport receipt will account for — bounded by the project's
|
|
1989
|
+
// own egress policy, so an admin kill-switch is not silently bypassed here.
|
|
1990
|
+
if (!allowProviderResultRows || !input.result)
|
|
1991
|
+
return narrateResult(input);
|
|
1992
|
+
const narrationRowEgress = resolveProviderResultRowEgressPolicy({
|
|
1993
|
+
projectSetting: projectConfig?.agent?.providerResultRowEgress,
|
|
1994
|
+
});
|
|
1995
|
+
if (narrationRowEgress.maxNarrationRows === 0)
|
|
1996
|
+
return narrateResult(input);
|
|
1997
|
+
const safeResult = {
|
|
1998
|
+
...input.result,
|
|
1999
|
+
rows: redactProviderResultRows(input.result.rows, narrationRowEgress.maxNarrationRows),
|
|
2000
|
+
};
|
|
2001
|
+
const safeInput = {
|
|
2002
|
+
...input,
|
|
2003
|
+
result: safeResult,
|
|
2004
|
+
};
|
|
2005
|
+
return narrateResult(safeInput, {
|
|
2006
|
+
complete: async ({ system, user, signal }) => {
|
|
2007
|
+
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
2008
|
+
if (!provider)
|
|
2009
|
+
throw new Error('No AI provider configured for narration.');
|
|
2010
|
+
return provider.generate([{ role: 'system', content: system }, { role: 'user', content: user }], {
|
|
2011
|
+
maxTokens: 600,
|
|
2012
|
+
temperature: 0.2,
|
|
2013
|
+
signal,
|
|
2014
|
+
maxProviderDispatches: 2,
|
|
2015
|
+
...(agentRunProviderEvidenceContext.getStore() ? {
|
|
2016
|
+
// This lane narrates App/notebook results, not Research. It used to
|
|
2017
|
+
// declare `research_narration` to clear the row check, which made
|
|
2018
|
+
// receipt analytics conflate two different lanes.
|
|
2019
|
+
onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
2020
|
+
purpose: 'answer_narration',
|
|
2021
|
+
dispatchPhase: 'narration',
|
|
2022
|
+
optIn: true,
|
|
2023
|
+
serializedResultShape: {
|
|
2024
|
+
resultRowCount: safeResult.rows.length,
|
|
2025
|
+
columnCount: safeResult.columns.length,
|
|
2026
|
+
},
|
|
2027
|
+
cumulativeResultRowCount: safeResult.rows.length,
|
|
2028
|
+
}),
|
|
2029
|
+
} : {}),
|
|
2030
|
+
});
|
|
2031
|
+
},
|
|
2032
|
+
});
|
|
2033
|
+
};
|
|
1576
2034
|
async function runGovernedAgentAnswerForRun(request, repair, route = 'generated_answer', onProgress, routeDecision) {
|
|
1577
2035
|
const governed = resolveGovernedAnswerRunner(projectRoot);
|
|
1578
|
-
|
|
1579
|
-
|
|
2036
|
+
let resolvedProvider = governed?.provider ?? null;
|
|
2037
|
+
let runner = governed?.runner ?? null;
|
|
2038
|
+
const exactProviderFreePlan = routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
2039
|
+
&& (routeDecision.resolvedAnalyticalPlan.capability === 'certified_execution'
|
|
2040
|
+
|| routeDecision.resolvedAnalyticalPlan.capability === 'semantic_execution');
|
|
2041
|
+
if ((!resolvedProvider || !runner) && exactProviderFreePlan) {
|
|
2042
|
+
const deterministicProvider = {
|
|
2043
|
+
name: 'ollama',
|
|
2044
|
+
available: async () => true,
|
|
2045
|
+
generate: async () => {
|
|
2046
|
+
throw Object.assign(new Error('Exact certified/semantic execution must not dispatch a provider.'), {
|
|
2047
|
+
code: 'EXACT_ROUTE_PROVIDER_DISPATCH_FORBIDDEN',
|
|
2048
|
+
});
|
|
2049
|
+
},
|
|
2050
|
+
};
|
|
2051
|
+
resolvedProvider = 'ollama';
|
|
2052
|
+
runner = createDqlAgentProviderRunner('ollama', deterministicProvider);
|
|
2053
|
+
}
|
|
1580
2054
|
if (!resolvedProvider || !runner) {
|
|
1581
2055
|
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.');
|
|
1582
2056
|
}
|
|
1583
2057
|
let governedAnswer;
|
|
1584
2058
|
let providerError;
|
|
2059
|
+
let providerDispatchEvidence;
|
|
1585
2060
|
const isRepair = (repair?.attempt ?? 0) > 0 && Boolean(repair?.repairHint);
|
|
1586
2061
|
// The chat composer sends a `thinkingMode` (auto/low/medium/high); resolve it
|
|
1587
2062
|
// into the effort+depth bundle it stands for. An explicit `reasoningEffort` /
|
|
@@ -1641,6 +2116,9 @@ export async function startLocalServer(opts) {
|
|
|
1641
2116
|
const semanticTableMapping = semanticLayer && semanticConnection
|
|
1642
2117
|
? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer, projectRoot, semanticConnectionName)
|
|
1643
2118
|
: undefined;
|
|
2119
|
+
const generatedProposalTargetIdentity = semanticConnection
|
|
2120
|
+
? await observeWarehouseTargetIdentity(executor, semanticConnection)
|
|
2121
|
+
: undefined;
|
|
1644
2122
|
const requestedDomain = normalizeAgentRunDomain(agentRunWorkspaceValue(request, 'domain'));
|
|
1645
2123
|
const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
|
|
1646
2124
|
const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
|
|
@@ -1658,6 +2136,9 @@ export async function startLocalServer(opts) {
|
|
|
1658
2136
|
: undefined;
|
|
1659
2137
|
await runner.run({
|
|
1660
2138
|
provider: resolvedProvider,
|
|
2139
|
+
...(agentRunProviderEvidenceContext.getStore()
|
|
2140
|
+
? { providerDispatchEvidenceSink: agentRunProviderEvidenceContext.getStore() }
|
|
2141
|
+
: {}),
|
|
1661
2142
|
messages: [
|
|
1662
2143
|
// No-thread fallback path: bound the raw client history so follow-ups
|
|
1663
2144
|
// cannot inflate every provider call (8 turns × 4k chars).
|
|
@@ -1677,6 +2158,8 @@ export async function startLocalServer(opts) {
|
|
|
1677
2158
|
},
|
|
1678
2159
|
reasoningEffort,
|
|
1679
2160
|
...(analysisDepth ? { analysisDepth } : {}),
|
|
2161
|
+
allowProviderSemanticMemberSelection: route === 'research',
|
|
2162
|
+
researchResultRowsOptIn: route === 'research' && request.researchResultRowsOptIn === true,
|
|
1680
2163
|
projectRoot,
|
|
1681
2164
|
preparedContextPack: preparedAgentContextPacks.get(request),
|
|
1682
2165
|
domainContext,
|
|
@@ -1798,9 +2281,20 @@ export async function startLocalServer(opts) {
|
|
|
1798
2281
|
: projectConfig.dbt?.projectDir;
|
|
1799
2282
|
throw new SemanticRuntimeRequiredError(await explainMissingSemanticRuntime(projectRoot, dbtProjectPath));
|
|
1800
2283
|
}
|
|
1801
|
-
|
|
2284
|
+
// The semantic bridge freezes the compiler output after trimming
|
|
2285
|
+
// transport-only surrounding whitespace. Bind the compiler receipt
|
|
2286
|
+
// and execution lookup to that exact consumed SQL identity.
|
|
2287
|
+
const governedCompiledSql = compiled.sql.trim();
|
|
2288
|
+
compiledSemanticQueries.set(executionFingerprint(governedCompiledSql), compiled);
|
|
2289
|
+
const aggregationCompilerReceipt = buildSemanticAggregationCompilerReceipt({
|
|
2290
|
+
semanticLayer: semanticLayer,
|
|
2291
|
+
metricNames: compiled.effectiveRequest.metrics,
|
|
2292
|
+
compiledSql: governedCompiledSql,
|
|
2293
|
+
plan: routeDecision?.resolvedAnalyticalPlan,
|
|
2294
|
+
targetFingerprint: targetBinding?.executionTarget.identityFingerprint,
|
|
2295
|
+
});
|
|
1802
2296
|
return {
|
|
1803
|
-
sql:
|
|
2297
|
+
sql: governedCompiledSql,
|
|
1804
2298
|
engine: compiled.engine,
|
|
1805
2299
|
selection: {
|
|
1806
2300
|
...selection,
|
|
@@ -1813,7 +2307,9 @@ export async function startLocalServer(opts) {
|
|
|
1813
2307
|
trace: {
|
|
1814
2308
|
...compiled.semanticTrace,
|
|
1815
2309
|
...(targetBinding ? { targetBinding } : {}),
|
|
2310
|
+
...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
|
|
1816
2311
|
},
|
|
2312
|
+
...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
|
|
1817
2313
|
};
|
|
1818
2314
|
},
|
|
1819
2315
|
} : {}),
|
|
@@ -1826,6 +2322,9 @@ export async function startLocalServer(opts) {
|
|
|
1826
2322
|
...(routeDecision?.resolvedAnalyticalPlan
|
|
1827
2323
|
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
1828
2324
|
: {}),
|
|
2325
|
+
...(generatedProposalTargetIdentity?.identityFingerprint
|
|
2326
|
+
? { generatedProposalTargetFingerprint: generatedProposalTargetIdentity.identityFingerprint }
|
|
2327
|
+
: {}),
|
|
1829
2328
|
analyticalReferenceInstant: new Date().toISOString(),
|
|
1830
2329
|
executeCertifiedBlock: (node, invocation) => executeCertifiedBlockForAgent(node, invocation, semanticConnection, semanticConnectionName),
|
|
1831
2330
|
executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName),
|
|
@@ -1842,10 +2341,11 @@ export async function startLocalServer(opts) {
|
|
|
1842
2341
|
}
|
|
1843
2342
|
if (turn.kind === 'error') {
|
|
1844
2343
|
providerError = turn.message;
|
|
2344
|
+
providerDispatchEvidence = turn.dispatchEvidence;
|
|
1845
2345
|
}
|
|
1846
2346
|
}, runSignal);
|
|
1847
2347
|
if (!governedAnswer) {
|
|
1848
|
-
throw new Error(providerError ?? 'The AI provider did not return a governed answer.');
|
|
2348
|
+
throw Object.assign(new Error(providerError ?? 'The AI provider did not return a governed answer.'), ...(providerDispatchEvidence ? [{ providerDispatchEvidence }] : []));
|
|
1849
2349
|
}
|
|
1850
2350
|
return governedAnswer;
|
|
1851
2351
|
}
|
|
@@ -2072,7 +2572,11 @@ export async function startLocalServer(opts) {
|
|
|
2072
2572
|
surface: 'ask_ai',
|
|
2073
2573
|
});
|
|
2074
2574
|
const certified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
2075
|
-
|
|
2575
|
+
// A semantic route is governed only when its route-specific aggregation
|
|
2576
|
+
// proof passed. The route label is not authority: treating a blocked or
|
|
2577
|
+
// missing proof as governed caused the CLI to persist a successful badge
|
|
2578
|
+
// even though the answer artifact recorded insufficient context.
|
|
2579
|
+
const semantic = semanticAnswerHasPassedAggregationProof(governedAnswer);
|
|
2076
2580
|
governedAnswer.dqlArtifact = {
|
|
2077
2581
|
...artifact,
|
|
2078
2582
|
...(invocation.parameters.length > 0 ? { parameters: invocation.parameters } : {}),
|
|
@@ -2081,11 +2585,19 @@ export async function startLocalServer(opts) {
|
|
|
2081
2585
|
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
2082
2586
|
...(governedAnswer.result?.sql ? { compiledSql: governedAnswer.result.sql } : originalSql ? { compiledSql: originalSql } : {}),
|
|
2083
2587
|
...(governedAnswer.result?.executionReceipt ? { executionReceipt: governedAnswer.result.executionReceipt } : {}),
|
|
2588
|
+
...(governedAnswer.result?.executableArtifact ? {
|
|
2589
|
+
executableArtifact: {
|
|
2590
|
+
...governedAnswer.result.executableArtifact,
|
|
2591
|
+
kind: artifact.kind,
|
|
2592
|
+
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
2593
|
+
},
|
|
2594
|
+
} : {}),
|
|
2084
2595
|
};
|
|
2085
2596
|
if (governedAnswer.result)
|
|
2086
2597
|
delete governedAnswer.result.dqlArtifact;
|
|
2087
2598
|
}
|
|
2088
2599
|
const answerRunExecutor = async ({ request, route, routeDecision, attempt, repairHint, emit }) => {
|
|
2600
|
+
const runStartedAtMs = Date.now();
|
|
2089
2601
|
let governedAnswer;
|
|
2090
2602
|
try {
|
|
2091
2603
|
governedAnswer = await runGovernedAgentAnswerForRun(request, { attempt, repairHint }, route, (message) => emit({ type: 'executor.started', message, route }), routeDecision);
|
|
@@ -2221,39 +2733,67 @@ export async function startLocalServer(opts) {
|
|
|
2221
2733
|
// Deadline/cancellation propagates to the engine, which renders the
|
|
2222
2734
|
// graceful "bounded execution deadline" outcome — never a provider error.
|
|
2223
2735
|
rethrowIfCancelled(error, request.signal);
|
|
2224
|
-
const
|
|
2736
|
+
const dispatchEvidence = providerDispatchTerminalEvidence(error);
|
|
2737
|
+
const dispatchBudgetExhausted = Boolean((error && typeof error === 'object'
|
|
2738
|
+
&& error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED')
|
|
2739
|
+
|| dispatchEvidence?.fallbackReason === 'orchestration_budget_exhausted');
|
|
2740
|
+
const message = dispatchBudgetExhausted
|
|
2741
|
+
? '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.'
|
|
2742
|
+
: formatAgentRunInfrastructureError(error, 'AI answer provider');
|
|
2225
2743
|
return {
|
|
2226
2744
|
summary: message,
|
|
2227
2745
|
status: 'blocked',
|
|
2228
2746
|
trustState: 'blocked',
|
|
2229
2747
|
stopReason: 'blocked',
|
|
2230
|
-
artifacts: [agentRunArtifact('answer', 'AI answer provider failed', {
|
|
2748
|
+
artifacts: [agentRunArtifact('answer', dispatchBudgetExhausted ? 'Internal orchestration budget exhausted' : 'AI answer provider failed', {
|
|
2231
2749
|
kind: 'no_answer',
|
|
2232
|
-
refusalCode: 'provider_error',
|
|
2750
|
+
refusalCode: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'provider_error',
|
|
2233
2751
|
text: message,
|
|
2234
2752
|
answer: message,
|
|
2235
2753
|
...(routeDecision?.resolvedAnalyticalPlan
|
|
2236
2754
|
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
2237
2755
|
: {}),
|
|
2238
2756
|
providerFailure: {
|
|
2239
|
-
code: 'AI_PROVIDER_FAILURE',
|
|
2757
|
+
code: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'AI_PROVIDER_FAILURE',
|
|
2240
2758
|
message,
|
|
2241
|
-
recoverable:
|
|
2759
|
+
recoverable: !dispatchBudgetExhausted,
|
|
2242
2760
|
},
|
|
2243
2761
|
}, undefined, 'blocked')],
|
|
2244
2762
|
evaluations: [
|
|
2245
2763
|
agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.'),
|
|
2246
|
-
agentRunEvaluation('ai-provider', 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error }),
|
|
2764
|
+
agentRunEvaluation(dispatchBudgetExhausted ? 'orchestration-budget' : 'ai-provider', dispatchBudgetExhausted ? 'Internal orchestration budget' : 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error }),
|
|
2247
2765
|
],
|
|
2248
2766
|
nextActions: [
|
|
2249
|
-
|
|
2250
|
-
|
|
2767
|
+
...(dispatchBudgetExhausted
|
|
2768
|
+
? [{ id: 'narrow-and-retry-ask', label: 'Narrow the metric or dimension and retry', route: 'generated_answer' }]
|
|
2769
|
+
: [
|
|
2770
|
+
{ id: 'retry-ask-after-provider', label: 'Retry after provider setup', route: 'generated_answer' },
|
|
2771
|
+
{ id: 'research-without-answer', label: 'Research with available metadata', route: 'research', artifactKind: 'research_run' },
|
|
2772
|
+
]),
|
|
2251
2773
|
],
|
|
2774
|
+
...(dispatchEvidence?.providerEgressReceipts.length
|
|
2775
|
+
? { providerEgressReceipts: dispatchEvidence.providerEgressReceipts }
|
|
2776
|
+
: {}),
|
|
2777
|
+
telemetry: {
|
|
2778
|
+
version: 1,
|
|
2779
|
+
stageDurationsMs: { total: Date.now() - runStartedAtMs },
|
|
2780
|
+
providerRoundTrips: dispatchEvidence?.providerRoundTrips ?? 0,
|
|
2781
|
+
toolCalls: dispatchEvidence?.toolCalls ?? 0,
|
|
2782
|
+
sqlExecutions: dispatchEvidence?.sqlExecutions ?? 0,
|
|
2783
|
+
repairs: Math.max(attempt, dispatchEvidence?.repairs ?? 0),
|
|
2784
|
+
egressReceipts: dispatchEvidence?.providerEgressReceipts.length ?? 0,
|
|
2785
|
+
fallbackReason: dispatchBudgetExhausted
|
|
2786
|
+
? 'provider_dispatch_budget_exhausted'
|
|
2787
|
+
: dispatchEvidence?.fallbackReason ?? 'provider_error',
|
|
2788
|
+
},
|
|
2252
2789
|
};
|
|
2253
2790
|
}
|
|
2254
2791
|
const answeredRoute = resolvedRunRouteFromAnswer(governedAnswer) ?? route;
|
|
2255
2792
|
const isCertified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
2256
|
-
const
|
|
2793
|
+
const semanticRouteClaimed = governedAnswer.route?.tier === 'semantic_metric';
|
|
2794
|
+
const semanticAggregationProofPassed = semanticAnswerHasPassedAggregationProof(governedAnswer);
|
|
2795
|
+
const semanticAggregationBlocked = semanticRouteClaimed && !semanticAggregationProofPassed;
|
|
2796
|
+
const isSemantic = semanticRouteClaimed && semanticAggregationProofPassed;
|
|
2257
2797
|
const requestedNotebookDataset = findMentionedNotebookDataset(request.question, datasetWorkspace.list());
|
|
2258
2798
|
const governedAssetMissesLocalSource = Boolean(requestedNotebookDataset && (isCertified
|
|
2259
2799
|
|| isSemantic
|
|
@@ -2270,7 +2810,8 @@ export async function startLocalServer(opts) {
|
|
|
2270
2810
|
// governance boundary, not an ambiguous user question and not a repairable
|
|
2271
2811
|
// retrieval miss. Keep the precise policy detail visible, but never burn two
|
|
2272
2812
|
// more provider calls retrying the same incompatible candidate.
|
|
2273
|
-
const isPolicyBlocked = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked'
|
|
2813
|
+
const isPolicyBlocked = (governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked')
|
|
2814
|
+
|| semanticAggregationBlocked;
|
|
2274
2815
|
const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure || isPolicyBlocked;
|
|
2275
2816
|
// The model tried to compose a governed query and declined despite having usable
|
|
2276
2817
|
// context (e.g. it wasn't confident about a multi-table join). The answer loop
|
|
@@ -2289,48 +2830,137 @@ export async function startLocalServer(opts) {
|
|
|
2289
2830
|
// reply ran context-free and was clarified again: the reported clarify loop.
|
|
2290
2831
|
const resolvedRoute = needsClarification ? 'clarify' : answeredRoute;
|
|
2291
2832
|
const sql = governedAnswer.proposedSql ?? governedAnswer.sql;
|
|
2292
|
-
const runnableSql = governedAnswer.kind === 'no_answer'
|
|
2833
|
+
const runnableSql = governedAnswer.kind === 'no_answer'
|
|
2834
|
+
|| isExecutionFailure
|
|
2835
|
+
|| semanticAggregationBlocked
|
|
2836
|
+
|| (isExploratory && !governedAnswer.result)
|
|
2293
2837
|
? undefined
|
|
2294
2838
|
: sql;
|
|
2295
|
-
//
|
|
2296
|
-
//
|
|
2297
|
-
//
|
|
2298
|
-
//
|
|
2839
|
+
// Narrate any run that actually produced values. This used to be gated on
|
|
2840
|
+
// `requestedMode === 'research'`, and because the UI sends ordinary Ask as
|
|
2841
|
+
// `'auto'`, every normal question skipped synthesis and shipped the answer
|
|
2842
|
+
// loop's deterministic fact-join as the primary answer — the reported
|
|
2843
|
+
// `column: value` dump. A model that never sees the result cannot describe
|
|
2844
|
+
// it, so a bounded, redacted row sample goes with the request unless a
|
|
2845
|
+
// project admin has switched row egress off.
|
|
2299
2846
|
let synthesizedAnswer;
|
|
2300
|
-
|
|
2847
|
+
const providerEgressReceipts = [
|
|
2848
|
+
...(governedAnswer.providerEgressReceipts ?? []),
|
|
2849
|
+
];
|
|
2850
|
+
let narrationDurationMs = 0;
|
|
2851
|
+
const researchRowsOptIn = route === 'research' && request.researchResultRowsOptIn === true;
|
|
2852
|
+
const rowEgress = resolveProviderResultRowEgressPolicy({
|
|
2853
|
+
projectSetting: projectConfig?.agent?.providerResultRowEgress,
|
|
2854
|
+
researchOptIn: researchRowsOptIn,
|
|
2855
|
+
});
|
|
2856
|
+
// `createBlockStudioAssistProvider` returns null when no AI provider is
|
|
2857
|
+
// configured; a narration failure must never take the governed answer down.
|
|
2858
|
+
const narrationProvider = await createBlockStudioAssistProvider(projectRoot).catch(() => null);
|
|
2859
|
+
const narrationPlan = planAgentRunNarration(governedAnswer, {
|
|
2860
|
+
requestedMode: request.requestedMode,
|
|
2861
|
+
providerAvailable: Boolean(narrationProvider),
|
|
2862
|
+
rowEgress,
|
|
2863
|
+
});
|
|
2864
|
+
const narrationMaxRows = narrationPlan.mode === 'skip' ? 0 : narrationPlan.maxRows;
|
|
2865
|
+
const preview = agentResultToSynthesisPreview(governedAnswer.result);
|
|
2866
|
+
const providerPreview = preview
|
|
2867
|
+
? { ...preview, rows: redactProviderResultRows(preview.rows, narrationMaxRows) }
|
|
2868
|
+
: undefined;
|
|
2869
|
+
let narrationSource;
|
|
2870
|
+
let narrationValidationFailures = [];
|
|
2871
|
+
if (narrationPlan.mode !== 'skip' && narrationProvider) {
|
|
2872
|
+
const narrationStartedAtMs = Date.now();
|
|
2873
|
+
const draft = governedAnswer.answer ?? governedAnswer.text;
|
|
2874
|
+
const observeNarrationDispatch = (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
2875
|
+
purpose: 'answer_narration',
|
|
2876
|
+
dispatchPhase: 'narration',
|
|
2877
|
+
optIn: narrationMaxRows > 0,
|
|
2878
|
+
serializedResultShape: {
|
|
2879
|
+
resultRowCount: providerPreview?.rows.length ?? 0,
|
|
2880
|
+
columnCount: providerPreview?.columns.length ?? 0,
|
|
2881
|
+
},
|
|
2882
|
+
});
|
|
2883
|
+
const narrationDispatchOptions = () => ({
|
|
2884
|
+
maxTokens: 350,
|
|
2885
|
+
temperature: 0.3,
|
|
2886
|
+
maxProviderDispatches: 2,
|
|
2887
|
+
...(agentRunProviderEvidenceContext.getStore()
|
|
2888
|
+
? { onProviderDispatch: observeNarrationDispatch }
|
|
2889
|
+
: {}),
|
|
2890
|
+
});
|
|
2301
2891
|
try {
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
:
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2892
|
+
const frame = governedAnswer.resolvedAnalyticalPlan?.analyticalFrame;
|
|
2893
|
+
if (narrationPlan.mode === 'verified_facts' && governedAnswer.analyticalFacts && frame) {
|
|
2894
|
+
// The fact set is the grounding contract: the model drafts, and every
|
|
2895
|
+
// claim is resolved against a fact id before it can reach the user.
|
|
2896
|
+
const composed = await composeVerifiedAnalyticalNarrative({
|
|
2897
|
+
frame,
|
|
2898
|
+
factSet: governedAnswer.analyticalFacts,
|
|
2899
|
+
question: request.question,
|
|
2900
|
+
maxRows: narrationPlan.maxRows,
|
|
2901
|
+
complete: async ({ system, user }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], narrationDispatchOptions(), () => { }),
|
|
2902
|
+
});
|
|
2903
|
+
narrationSource = composed.source;
|
|
2904
|
+
narrationValidationFailures = composed.validationFailures;
|
|
2905
|
+
synthesizedAnswer = composed.source === 'llm'
|
|
2906
|
+
? composed.narrative.text
|
|
2907
|
+
// A verification failure is not silent: the deterministic join is a
|
|
2908
|
+
// fallback, and the reader is told so rather than handed a dump that
|
|
2909
|
+
// looks like it was written on purpose.
|
|
2910
|
+
: `_Verified narration was unavailable, so this is the deterministic result record._\n\n${composed.narrative.text}`;
|
|
2911
|
+
}
|
|
2912
|
+
else {
|
|
2913
|
+
const result = await synthesizeAnswer({
|
|
2914
|
+
question: request.question,
|
|
2915
|
+
category: routeDecision?.category,
|
|
2916
|
+
// The primary Ask reply is always business-facing. Analysts keep
|
|
2917
|
+
// the full DQL, SQL, lineage, gates, and grain in the inspector.
|
|
2918
|
+
audience: 'stakeholder',
|
|
2919
|
+
resultPreview: providerPreview ?? preview,
|
|
2920
|
+
sql: sql,
|
|
2921
|
+
draftText: draft,
|
|
2922
|
+
gaps: businessNarrativeGaps(governedAnswer.validationWarnings),
|
|
2923
|
+
rankingDirection: governedAnswer.contextPack?.questionPlan.requestedShape.rankingDirection,
|
|
2924
|
+
// Governed display contract: metric/measure displayFormat metadata
|
|
2925
|
+
// beats column-name guessing, so $ and decimals stay consistent
|
|
2926
|
+
// between the narration and the table for names like total_bcm.
|
|
2927
|
+
...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
|
|
2928
|
+
}, {
|
|
2929
|
+
complete: ({ system, user, signal, onDelta }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], { ...narrationDispatchOptions(), signal }, onDelta ?? (() => { })),
|
|
2930
|
+
});
|
|
2931
|
+
narrationSource = result.source;
|
|
2932
|
+
if (result.text)
|
|
2933
|
+
synthesizedAnswer = result.text;
|
|
2934
|
+
}
|
|
2329
2935
|
}
|
|
2330
2936
|
catch {
|
|
2331
|
-
// Keep the governed draft on any
|
|
2937
|
+
// Keep the governed draft on any narration failure.
|
|
2332
2938
|
synthesizedAnswer = undefined;
|
|
2333
2939
|
}
|
|
2940
|
+
finally {
|
|
2941
|
+
narrationDurationMs = Date.now() - narrationStartedAtMs;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
// Every run accounts for its narration egress, including the runs that did
|
|
2945
|
+
// not narrate — "no receipt" must never be ambiguous between "no rows left
|
|
2946
|
+
// the host" and "nobody looked".
|
|
2947
|
+
{
|
|
2948
|
+
const narrated = narrationSource !== undefined;
|
|
2949
|
+
const rowsSent = narrated ? providerPreview?.rows.length ?? 0 : 0;
|
|
2950
|
+
providerEgressReceipts.push(createProviderEgressReceipt({
|
|
2951
|
+
purpose: 'answer_narration',
|
|
2952
|
+
provider: narrated ? narrationProvider?.name ?? 'none' : 'none',
|
|
2953
|
+
permittedCategories: rowsSent > 0
|
|
2954
|
+
? ['instructions', 'question', 'governed_context', 'result_rows']
|
|
2955
|
+
: ['instructions', 'question', 'governed_context'],
|
|
2956
|
+
optIn: rowsSent > 0,
|
|
2957
|
+
redactionPolicyId: rowEgress.policyId,
|
|
2958
|
+
payload: narrated
|
|
2959
|
+
? { question: request.question, resultPreview: rowsSent > 0 ? providerPreview : undefined }
|
|
2960
|
+
: { resultRows: 0, providerDisabled: !narrationProvider },
|
|
2961
|
+
resultRowCount: rowsSent,
|
|
2962
|
+
columnCount: narrated ? providerPreview?.columns.length ?? 0 : 0,
|
|
2963
|
+
}));
|
|
2334
2964
|
}
|
|
2335
2965
|
// A grounding/modeling gap is a REFUSAL. It was computed above but never
|
|
2336
2966
|
// reached this ternary, so it fell through to `needs_review` — the same
|
|
@@ -2414,9 +3044,21 @@ export async function startLocalServer(opts) {
|
|
|
2414
3044
|
artifactKind: 'sql_cell',
|
|
2415
3045
|
}] : []),
|
|
2416
3046
|
];
|
|
3047
|
+
const runDispatchEvidence = agentRunProviderEvidenceContext.getStore()?.snapshot(governedAnswer.refusalCode ?? 'none');
|
|
3048
|
+
const finalProviderEgressReceipts = runDispatchEvidence?.providerEgressReceipts
|
|
3049
|
+
?? providerEgressReceipts;
|
|
3050
|
+
const finalTelemetry = agentRunTelemetryForAnswer(governedAnswer, finalProviderEgressReceipts, Date.now() - runStartedAtMs, narrationDurationMs, attempt);
|
|
3051
|
+
if (runDispatchEvidence) {
|
|
3052
|
+
finalTelemetry.providerRoundTrips = runDispatchEvidence.providerRoundTrips;
|
|
3053
|
+
finalTelemetry.egressReceipts = finalProviderEgressReceipts.length;
|
|
3054
|
+
}
|
|
2417
3055
|
return {
|
|
2418
3056
|
resolvedRoute,
|
|
2419
|
-
answerRefusalCode: governedAnswer.kind === 'no_answer'
|
|
3057
|
+
answerRefusalCode: governedAnswer.kind === 'no_answer'
|
|
3058
|
+
? governedAnswer.refusalCode
|
|
3059
|
+
: semanticAggregationBlocked
|
|
3060
|
+
? 'policy_blocked'
|
|
3061
|
+
: undefined,
|
|
2420
3062
|
answerTier: governedAnswer.route?.tier,
|
|
2421
3063
|
summary: isTerminalFailure
|
|
2422
3064
|
? analyticalFailureSummary(governedAnswer.analyticalFailure)
|
|
@@ -2476,7 +3118,9 @@ export async function startLocalServer(opts) {
|
|
|
2476
3118
|
: needsClarification
|
|
2477
3119
|
? 'The answer loop needs more context before producing a governed answer.'
|
|
2478
3120
|
: isPolicyBlocked
|
|
2479
|
-
?
|
|
3121
|
+
? semanticAggregationBlocked
|
|
3122
|
+
? `DQL blocked semantic execution because its exact aggregation proof did not pass: ${governedAnswer.aggregationSafetyProof?.issueCodes.join(', ') || 'required proof evidence is missing.'}`
|
|
3123
|
+
: `DQL blocked the generated path under its relationship policy: ${governedAnswer.refusalDetails?.message ?? 'the relationship is not authorized for this analysis.'}`
|
|
2480
3124
|
: isExploratory
|
|
2481
3125
|
? 'The answer used bounded DBT/schema evidence because no certified modeled relationship path covered the request. It is review-required and was not certified.'
|
|
2482
3126
|
: 'The answer is generated or semantic-layer backed and remains review-required.', governedAnswer.route),
|
|
@@ -2502,10 +3146,13 @@ export async function startLocalServer(opts) {
|
|
|
2502
3146
|
},
|
|
2503
3147
|
] : []),
|
|
2504
3148
|
...(isPolicyBlocked ? [
|
|
2505
|
-
agentRunEvaluation('relationship-policy', 'Relationship policy', false, 'warning',
|
|
2506
|
-
|
|
3149
|
+
agentRunEvaluation(semanticAggregationBlocked ? 'aggregation-safety-proof' : 'relationship-policy', semanticAggregationBlocked ? 'Aggregation safety proof' : 'Relationship policy', false, semanticAggregationBlocked ? 'blocking' : 'warning', semanticAggregationBlocked
|
|
3150
|
+
? `Semantic execution was refused because the exact aggregation proof did not pass: ${governedAnswer.aggregationSafetyProof?.issueCodes.join(', ') || 'required proof evidence is missing.'}`
|
|
3151
|
+
: governedAnswer.refusalDetails?.message
|
|
3152
|
+
?? 'The selected relationship is not authorized for this analysis.', {
|
|
2507
3153
|
refusalCode: governedAnswer.refusalCode,
|
|
2508
3154
|
refusalDetails: governedAnswer.refusalDetails,
|
|
3155
|
+
aggregationSafetyProof: governedAnswer.aggregationSafetyProof,
|
|
2509
3156
|
validationWarnings: governedAnswer.validationWarnings,
|
|
2510
3157
|
route: governedAnswer.route,
|
|
2511
3158
|
}),
|
|
@@ -2515,6 +3162,8 @@ export async function startLocalServer(opts) {
|
|
|
2515
3162
|
] : []),
|
|
2516
3163
|
],
|
|
2517
3164
|
nextActions,
|
|
3165
|
+
providerEgressReceipts: finalProviderEgressReceipts,
|
|
3166
|
+
telemetry: finalTelemetry,
|
|
2518
3167
|
};
|
|
2519
3168
|
};
|
|
2520
3169
|
const conversationRunExecutor = async ({ request, routeDecision, emitAnswerDelta }) => {
|
|
@@ -2548,13 +3197,28 @@ export async function startLocalServer(opts) {
|
|
|
2548
3197
|
...(request.history ?? []).slice(-6).map((message) => ({ role: message.role, content: message.text })),
|
|
2549
3198
|
{ role: 'user', content: request.question },
|
|
2550
3199
|
];
|
|
2551
|
-
text = (await provider.generate(messages, {
|
|
3200
|
+
text = (await provider.generate(messages, {
|
|
3201
|
+
maxTokens: 320,
|
|
3202
|
+
temperature: 0.6,
|
|
3203
|
+
maxProviderDispatches: 2,
|
|
3204
|
+
...(agentRunProviderEvidenceContext.getStore() ? {
|
|
3205
|
+
onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
3206
|
+
purpose: 'answer_generation',
|
|
3207
|
+
dispatchPhase: 'generation',
|
|
3208
|
+
optIn: false,
|
|
3209
|
+
}),
|
|
3210
|
+
} : {}),
|
|
3211
|
+
})).trim();
|
|
2552
3212
|
// Perceived-latency: surface the reply as one delta for surfaces wired to stream.
|
|
2553
3213
|
if (text)
|
|
2554
3214
|
emitAnswerDelta?.(text);
|
|
2555
3215
|
}
|
|
2556
3216
|
}
|
|
2557
|
-
catch {
|
|
3217
|
+
catch (error) {
|
|
3218
|
+
if (error && typeof error === 'object'
|
|
3219
|
+
&& error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED') {
|
|
3220
|
+
throw error;
|
|
3221
|
+
}
|
|
2558
3222
|
// Provider unavailable / errored — fall through to the deterministic reply.
|
|
2559
3223
|
text = undefined;
|
|
2560
3224
|
}
|
|
@@ -3093,7 +3757,7 @@ export async function startLocalServer(opts) {
|
|
|
3093
3757
|
result: researchResultData,
|
|
3094
3758
|
evidence: plan.sources,
|
|
3095
3759
|
reviewRequired: true,
|
|
3096
|
-
})
|
|
3760
|
+
}, request.researchResultRowsOptIn === true)
|
|
3097
3761
|
: undefined;
|
|
3098
3762
|
const summary = needsClarification
|
|
3099
3763
|
? 'Needs clarification before running deeper research.'
|
|
@@ -3608,7 +4272,19 @@ export async function startLocalServer(opts) {
|
|
|
3608
4272
|
return provider.generate([
|
|
3609
4273
|
{ role: 'system', content: system },
|
|
3610
4274
|
{ role: 'user', content: user },
|
|
3611
|
-
], {
|
|
4275
|
+
], {
|
|
4276
|
+
maxTokens: 700,
|
|
4277
|
+
temperature: 0.1,
|
|
4278
|
+
signal,
|
|
4279
|
+
maxProviderDispatches: 2,
|
|
4280
|
+
...(agentRunProviderEvidenceContext.getStore() ? {
|
|
4281
|
+
onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
4282
|
+
purpose: 'answer_generation',
|
|
4283
|
+
dispatchPhase: 'planning',
|
|
4284
|
+
optIn: false,
|
|
4285
|
+
}),
|
|
4286
|
+
} : {}),
|
|
4287
|
+
});
|
|
3612
4288
|
},
|
|
3613
4289
|
getCatalogContext: buildRankedAgentRunCatalogContext,
|
|
3614
4290
|
});
|
|
@@ -3632,6 +4308,14 @@ export async function startLocalServer(opts) {
|
|
|
3632
4308
|
// instead of leaving the UI in "Generating and validating SQL" for a
|
|
3633
4309
|
// minute or more.
|
|
3634
4310
|
signal: boundedAgentMeaningSignal(signal),
|
|
4311
|
+
maxProviderDispatches: 1,
|
|
4312
|
+
...(agentRunProviderEvidenceContext.getStore() ? {
|
|
4313
|
+
onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
4314
|
+
purpose: 'answer_generation',
|
|
4315
|
+
dispatchPhase: 'meaning_resolution',
|
|
4316
|
+
optIn: false,
|
|
4317
|
+
}),
|
|
4318
|
+
} : {}),
|
|
3635
4319
|
});
|
|
3636
4320
|
},
|
|
3637
4321
|
getEvidence: buildAgentRunEvidence,
|
|
@@ -3648,6 +4332,8 @@ export async function startLocalServer(opts) {
|
|
|
3648
4332
|
// A run may outlive its streaming browser connection, so cancellation is
|
|
3649
4333
|
// server-owned and keyed by run id rather than relying on fetch abort alone.
|
|
3650
4334
|
const activeAgentRunControllers = new Map();
|
|
4335
|
+
const activeAnalyticalRepairReservations = new Set();
|
|
4336
|
+
const consumedAnalyticalRepairCapabilities = new Set();
|
|
3651
4337
|
// Server-side conversation threads: persisted multi-turn state (survives refresh).
|
|
3652
4338
|
// Lazy so a failed native-sqlite load never blocks the runtime; conversation
|
|
3653
4339
|
// persistence degrades to the per-request context instead.
|
|
@@ -3738,14 +4424,21 @@ export async function startLocalServer(opts) {
|
|
|
3738
4424
|
snapshotCells.push({ cellId, status: 'idle', executionCount: 0, executedAt });
|
|
3739
4425
|
continue;
|
|
3740
4426
|
}
|
|
3741
|
-
const prepared = prepareLocalExecution(executableSql, activeConnection, projectRoot, projectConfig);
|
|
3742
4427
|
assertAppAccess({ app, domain: resolved.domain ?? app.domain, level: 'execute' });
|
|
3743
|
-
const
|
|
3744
|
-
|
|
4428
|
+
const execution = await analyticalExecutionService.execute({
|
|
4429
|
+
sql: executableSql,
|
|
4430
|
+
subject: 'App notebook query',
|
|
4431
|
+
connection: activeConnection,
|
|
4432
|
+
sqlParams: plan?.sqlParams,
|
|
4433
|
+
variables: plan?.variables,
|
|
4434
|
+
});
|
|
4435
|
+
const result = execution.result;
|
|
3745
4436
|
snapshotCells.push({
|
|
3746
4437
|
cellId,
|
|
3747
4438
|
status: 'success',
|
|
3748
4439
|
result,
|
|
4440
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
4441
|
+
resultFingerprint: execution.resultFingerprint,
|
|
3749
4442
|
executionCount: 1,
|
|
3750
4443
|
executedAt,
|
|
3751
4444
|
});
|
|
@@ -3834,44 +4527,81 @@ export async function startLocalServer(opts) {
|
|
|
3834
4527
|
const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
|
|
3835
4528
|
throw analyticalError(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`, { origin: 'dql_compilation', stage: 'compile' });
|
|
3836
4529
|
}
|
|
3837
|
-
const
|
|
4530
|
+
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
4531
|
+
const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
|
|
4532
|
+
assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
|
|
4533
|
+
const pinnedSemanticCompile = semanticCompose?.sql
|
|
4534
|
+
? compiledSemanticQueries.get(executionFingerprint(semanticCompose.sql))
|
|
4535
|
+
: undefined;
|
|
4536
|
+
const semanticExecutionHolder = { value: null };
|
|
4537
|
+
const execution = await analyticalExecutionService.execute({
|
|
3838
4538
|
sql: semanticCompose?.sql ?? plan.sql,
|
|
3839
4539
|
subject: 'DQL artifact query',
|
|
3840
|
-
executor,
|
|
3841
4540
|
connection: activeConnection,
|
|
3842
|
-
projectRoot,
|
|
3843
|
-
projectConfig,
|
|
3844
4541
|
// Preserve the existing contract: previewed Ask artifacts are explicitly
|
|
3845
4542
|
// read-only and bounded; saved/manual DQL execution keeps its compiler-
|
|
3846
4543
|
// governed behavior when no preview bound was requested.
|
|
3847
4544
|
enforceReadOnly: invocationInput?.rowLimit !== undefined,
|
|
3848
4545
|
rowLimit: invocationInput?.rowLimit,
|
|
4546
|
+
sqlParams: plan?.sqlParams,
|
|
4547
|
+
variables: { ...(plan?.variables ?? {}), ...invocation.values },
|
|
4548
|
+
executePrepared: async (preparation) => {
|
|
4549
|
+
if (pinnedSemanticCompile) {
|
|
4550
|
+
const semanticExecution = await executeTargetBoundSemanticQuery({
|
|
4551
|
+
executor,
|
|
4552
|
+
connection: activeConnection,
|
|
4553
|
+
projectRoot,
|
|
4554
|
+
plannedAdapter: pinnedSemanticCompile.engine,
|
|
4555
|
+
metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
|
|
4556
|
+
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
4557
|
+
: undefined,
|
|
4558
|
+
compile: async () => pinnedSemanticCompile,
|
|
4559
|
+
prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
|
|
4560
|
+
rowBound: invocationInput?.rowLimit ?? pinnedSemanticCompile.effectiveRequest.limit,
|
|
4561
|
+
});
|
|
4562
|
+
if (semanticExecution) {
|
|
4563
|
+
semanticExecutionHolder.value = semanticExecution;
|
|
4564
|
+
return semanticExecution.result;
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
return executor.executeQuery(preparation.executedSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), preparation.connection);
|
|
4568
|
+
},
|
|
3849
4569
|
});
|
|
3850
|
-
const
|
|
4570
|
+
const semanticExecution = semanticExecutionHolder.value;
|
|
4571
|
+
const { preparation, result: normalized } = execution;
|
|
3851
4572
|
const executionSql = preparation.executedSql;
|
|
3852
|
-
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
3853
|
-
const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
|
|
3854
|
-
assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
|
|
3855
|
-
const pinnedSemanticCompile = semanticCompose?.sql
|
|
3856
|
-
? compiledSemanticQueries.get(executionFingerprint(semanticCompose.sql))
|
|
3857
|
-
: undefined;
|
|
3858
|
-
const semanticExecution = pinnedSemanticCompile
|
|
3859
|
-
? await executeTargetBoundSemanticQuery({
|
|
3860
|
-
executor,
|
|
3861
|
-
connection: activeConnection,
|
|
3862
|
-
projectRoot,
|
|
3863
|
-
plannedAdapter: pinnedSemanticCompile.engine,
|
|
3864
|
-
metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
|
|
3865
|
-
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
3866
|
-
: undefined,
|
|
3867
|
-
compile: async () => pinnedSemanticCompile,
|
|
3868
|
-
prepareSql: () => ({ sql: executionSql, connection: prepared.connection }),
|
|
3869
|
-
rowBound: invocationInput?.rowLimit ?? pinnedSemanticCompile.effectiveRequest.limit,
|
|
3870
|
-
})
|
|
3871
|
-
: null;
|
|
3872
|
-
const rawResult = semanticExecution?.result ?? await executor.executeQuery(executionSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), prepared.connection);
|
|
3873
|
-
const normalized = normalizeQueryResult(rawResult);
|
|
3874
4573
|
const executionReceipt = createDqlArtifactExecutionReceipt(source, executionSql, invocation.values, normalized);
|
|
4574
|
+
const executableArtifact = {
|
|
4575
|
+
version: 1,
|
|
4576
|
+
kind: precompiledSemanticQuery || semanticCompose?.sql ? 'semantic_block' : 'sql_block',
|
|
4577
|
+
dqlFingerprint: executionFingerprint(source),
|
|
4578
|
+
sourceFingerprint: executionFingerprint(stableExecutionValue({
|
|
4579
|
+
name: metadata.name ?? null,
|
|
4580
|
+
path: metadata.path ?? null,
|
|
4581
|
+
domain: sourceDomain ?? null,
|
|
4582
|
+
})),
|
|
4583
|
+
compiledSqlFingerprint: executionFingerprint(semanticCompose?.sql ?? plan?.sql ?? preparation.sourceSql),
|
|
4584
|
+
normalizedSqlFingerprint: executionFingerprint(preparation.decodedSql),
|
|
4585
|
+
parameterFingerprint: executionReceipt.parameterFingerprint,
|
|
4586
|
+
provenanceFingerprint: executionFingerprint(stableExecutionValue(invocation.resolvedParameters.map((parameter) => ({
|
|
4587
|
+
name: parameter.name,
|
|
4588
|
+
source: parameter.source,
|
|
4589
|
+
})))),
|
|
4590
|
+
targetFingerprint: targetGenerationFingerprint(activeConnection, executionConnectionName),
|
|
4591
|
+
snapshotFingerprint: executionFingerprint(projectSnapshot().snapshotId),
|
|
4592
|
+
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
4593
|
+
sqlFingerprint: executionFingerprint(semanticCompose?.sql ?? plan?.sql ?? preparation.sourceSql),
|
|
4594
|
+
parameterCount: plan?.sqlParams?.length ?? 0,
|
|
4595
|
+
variableNames: Object.keys(plan?.variables ?? {}).sort(),
|
|
4596
|
+
chartConfig: plan?.chartConfig ?? null,
|
|
4597
|
+
})),
|
|
4598
|
+
semanticAdapter: pinnedSemanticCompile?.engine ?? 'native',
|
|
4599
|
+
previewPolicy: invocationInput?.rowLimit === undefined
|
|
4600
|
+
? { mode: 'compiler_governed' }
|
|
4601
|
+
: { mode: 'read_only_bounded', rowLimit: invocationInput.rowLimit },
|
|
4602
|
+
trustState: 'review_required',
|
|
4603
|
+
receipt: executionReceipt,
|
|
4604
|
+
};
|
|
3875
4605
|
return {
|
|
3876
4606
|
columns: normalized.columns,
|
|
3877
4607
|
rows: normalized.rows,
|
|
@@ -3884,6 +4614,7 @@ export async function startLocalServer(opts) {
|
|
|
3884
4614
|
parameters: invocation.resolvedParameters,
|
|
3885
4615
|
auditId: invocation.auditId,
|
|
3886
4616
|
executionReceipt,
|
|
4617
|
+
executableArtifact,
|
|
3887
4618
|
...(semanticExecution ? {
|
|
3888
4619
|
semanticExecutionReceipt: semanticExecution.executionReceipt,
|
|
3889
4620
|
semanticTargetBinding: semanticExecution.targetBinding,
|
|
@@ -3918,6 +4649,9 @@ export async function startLocalServer(opts) {
|
|
|
3918
4649
|
trustState: 'certified',
|
|
3919
4650
|
compiledSql: result.sql,
|
|
3920
4651
|
executionReceipt: result.executionReceipt,
|
|
4652
|
+
...(result.executableArtifact ? {
|
|
4653
|
+
executableArtifact: { ...result.executableArtifact, kind: 'certified_block', trustState: 'certified' },
|
|
4654
|
+
} : {}),
|
|
3921
4655
|
...(invocationInput?.rowLimit ? { limit: invocationInput.rowLimit } : {}),
|
|
3922
4656
|
...(invocationInput?.parameters && Object.keys(invocationInput.parameters).length > 0
|
|
3923
4657
|
? { parameterValues: invocationInput.parameters }
|
|
@@ -3959,6 +4693,7 @@ export async function startLocalServer(opts) {
|
|
|
3959
4693
|
...(Object.keys(invocation.values).length > 0 ? { parameterValues: invocation.values } : {}),
|
|
3960
4694
|
compiledSql: result.sql,
|
|
3961
4695
|
executionReceipt: result.executionReceipt,
|
|
4696
|
+
...(result.executableArtifact ? { executableArtifact: result.executableArtifact } : {}),
|
|
3962
4697
|
},
|
|
3963
4698
|
};
|
|
3964
4699
|
};
|
|
@@ -4035,7 +4770,7 @@ export async function startLocalServer(opts) {
|
|
|
4035
4770
|
* string, so the only remaining reasons to differ are the connection and the
|
|
4036
4771
|
* governance gates, both of which report themselves honestly.
|
|
4037
4772
|
*/
|
|
4038
|
-
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings) => {
|
|
4773
|
+
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings, executionConnectionName) => {
|
|
4039
4774
|
const activeConnection = requireActiveConnection(executionConnection);
|
|
4040
4775
|
const rowBound = clampAnalyticalRowBound(seed?.limit ?? 200);
|
|
4041
4776
|
const trimmed = sql.trim().replace(/;\s*$/, '').trim();
|
|
@@ -4057,18 +4792,6 @@ export async function startLocalServer(opts) {
|
|
|
4057
4792
|
if (semantic.unresolvedRefs.length > 0) {
|
|
4058
4793
|
throw analyticalError(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`, { origin: 'dql_compilation', stage: 'compile', code: 'semantic_ref' });
|
|
4059
4794
|
}
|
|
4060
|
-
const preparation = await prepareAnalyticalExecutionSql({
|
|
4061
|
-
sql: semantic.sql,
|
|
4062
|
-
subject: 'Generated SQL',
|
|
4063
|
-
executor,
|
|
4064
|
-
connection: activeConnection,
|
|
4065
|
-
projectRoot,
|
|
4066
|
-
projectConfig,
|
|
4067
|
-
enforceReadOnly: true,
|
|
4068
|
-
rowLimit: rowBound,
|
|
4069
|
-
});
|
|
4070
|
-
const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
|
|
4071
|
-
const bounded = preparation.rowBound;
|
|
4072
4795
|
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
4073
4796
|
// Generated SQL is governed as 'uncategorized' unless its seed declares a
|
|
4074
4797
|
// domain — the same value `generatedSqlArtifactContract` stamps into the
|
|
@@ -4079,25 +4802,43 @@ export async function startLocalServer(opts) {
|
|
|
4079
4802
|
assertAppAccess({ app, domain: sourceDomain, level: 'execute' });
|
|
4080
4803
|
// A semantic query the loop already compiled (MetricFlow / dbt Cloud) must
|
|
4081
4804
|
// execute through its pinned target binding, not as loose SQL.
|
|
4082
|
-
const
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4805
|
+
const semanticExecutionHolder = { value: null };
|
|
4806
|
+
const execution = await analyticalExecutionService.execute({
|
|
4807
|
+
sql: semantic.sql,
|
|
4808
|
+
subject: 'Generated SQL',
|
|
4809
|
+
connection: activeConnection,
|
|
4810
|
+
enforceReadOnly: true,
|
|
4811
|
+
rowLimit: rowBound,
|
|
4812
|
+
sqlParams: bindings?.sqlParams,
|
|
4813
|
+
variables: bindings?.variables,
|
|
4814
|
+
semanticRefs: semantic.semanticRefs,
|
|
4815
|
+
executePrepared: async (preparation) => {
|
|
4816
|
+
const pinnedSemanticCompile = compiledSemanticQueries.get(executionFingerprint(preparation.preparedSql))
|
|
4817
|
+
?? compiledSemanticQueries.get(executionFingerprint(trimmed));
|
|
4818
|
+
if (pinnedSemanticCompile) {
|
|
4819
|
+
const semanticExecution = await executeTargetBoundSemanticQuery({
|
|
4820
|
+
executor,
|
|
4821
|
+
connection: activeConnection,
|
|
4822
|
+
projectRoot,
|
|
4823
|
+
plannedAdapter: pinnedSemanticCompile.engine,
|
|
4824
|
+
metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
|
|
4825
|
+
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
4826
|
+
: undefined,
|
|
4827
|
+
compile: async () => pinnedSemanticCompile,
|
|
4828
|
+
prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
|
|
4829
|
+
rowBound,
|
|
4830
|
+
});
|
|
4831
|
+
if (semanticExecution) {
|
|
4832
|
+
semanticExecutionHolder.value = semanticExecution;
|
|
4833
|
+
return semanticExecution.result;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
return executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
|
|
4837
|
+
},
|
|
4838
|
+
});
|
|
4839
|
+
const semanticExecution = semanticExecutionHolder.value;
|
|
4840
|
+
const { preparation, result: normalized } = execution;
|
|
4841
|
+
const bounded = preparation.rowBound;
|
|
4101
4842
|
// The authoritative bound. `buildRowBoundedSql` may legitimately decline to
|
|
4102
4843
|
// touch the statement, and only one of 15 drivers honours a maxRows hint, so
|
|
4103
4844
|
// the host is the only layer correctness may depend on.
|
|
@@ -4105,6 +4846,38 @@ export async function startLocalServer(opts) {
|
|
|
4105
4846
|
const rows = truncated ? normalized.rows.slice(0, rowBound) : normalized.rows;
|
|
4106
4847
|
const warnings = [];
|
|
4107
4848
|
const artifact = buildVerifiedGeneratedArtifact(question, trimmed, bounded.sql, seed, warnings, normalized.columns, activeConnection.driver);
|
|
4849
|
+
const receiptSource = artifact?.source ?? trimmed;
|
|
4850
|
+
const executionBindings = bindings && ((bindings.sqlParams?.length ?? 0) > 0 || Object.keys(bindings.variables ?? {}).length > 0)
|
|
4851
|
+
? { sqlParams: bindings.sqlParams ?? [], variables: bindings.variables ?? {} }
|
|
4852
|
+
: {};
|
|
4853
|
+
const executionReceipt = createDqlArtifactExecutionReceipt(receiptSource, bounded.sql, executionBindings, { columns: normalized.columns, rows, rowCount: rows.length });
|
|
4854
|
+
const executableArtifact = {
|
|
4855
|
+
version: 1,
|
|
4856
|
+
kind: 'sql_block',
|
|
4857
|
+
dqlFingerprint: executionFingerprint(receiptSource),
|
|
4858
|
+
sourceFingerprint: executionFingerprint(stableExecutionValue({
|
|
4859
|
+
name: artifact?.name ?? seed?.name ?? null,
|
|
4860
|
+
path: artifact?.sourcePath ?? seed?.sourcePath ?? null,
|
|
4861
|
+
})),
|
|
4862
|
+
compiledSqlFingerprint: executionFingerprint(semantic.sql),
|
|
4863
|
+
normalizedSqlFingerprint: executionFingerprint(preparation.decodedSql),
|
|
4864
|
+
parameterFingerprint: executionReceipt.parameterFingerprint,
|
|
4865
|
+
provenanceFingerprint: executionFingerprint(stableExecutionValue({
|
|
4866
|
+
sqlParamCount: bindings?.sqlParams?.length ?? 0,
|
|
4867
|
+
variableNames: Object.keys(bindings?.variables ?? {}).sort(),
|
|
4868
|
+
})),
|
|
4869
|
+
targetFingerprint: targetGenerationFingerprint(activeConnection, executionConnectionName),
|
|
4870
|
+
snapshotFingerprint: executionFingerprint(projectSnapshot().snapshotId),
|
|
4871
|
+
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
4872
|
+
compiledSqlFingerprint: executionFingerprint(semantic.sql),
|
|
4873
|
+
executedSqlFingerprint: executionFingerprint(bounded.sql),
|
|
4874
|
+
rowBound,
|
|
4875
|
+
})),
|
|
4876
|
+
semanticAdapter: semanticExecution?.targetBinding.adapterId ?? 'native',
|
|
4877
|
+
previewPolicy: { mode: 'read_only_bounded', rowLimit: rowBound },
|
|
4878
|
+
trustState: 'review_required',
|
|
4879
|
+
receipt: executionReceipt,
|
|
4880
|
+
};
|
|
4108
4881
|
return {
|
|
4109
4882
|
columns: normalized.columns,
|
|
4110
4883
|
rows,
|
|
@@ -4112,9 +4885,10 @@ export async function startLocalServer(opts) {
|
|
|
4112
4885
|
executionTime: normalized.executionTime,
|
|
4113
4886
|
sql: bounded.sql,
|
|
4114
4887
|
...(truncated ? { truncated: true } : {}),
|
|
4115
|
-
...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound } } : {}),
|
|
4888
|
+
...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound, executionReceipt, executableArtifact } } : {}),
|
|
4116
4889
|
...(warnings.length > 0 ? { validationWarnings: warnings } : {}),
|
|
4117
|
-
executionReceipt
|
|
4890
|
+
executionReceipt,
|
|
4891
|
+
executableArtifact,
|
|
4118
4892
|
};
|
|
4119
4893
|
};
|
|
4120
4894
|
const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName) => {
|
|
@@ -4124,7 +4898,7 @@ export async function startLocalServer(opts) {
|
|
|
4124
4898
|
if (seed && seed.kind !== 'sql_block') {
|
|
4125
4899
|
return executeArtifactReferenceForAgent({ ...seed, limit: seed.limit ?? 200 }, question, executionConnection, executionConnectionName);
|
|
4126
4900
|
}
|
|
4127
|
-
return executeGeneratedSqlDirect(question, sql, seed, executionConnection);
|
|
4901
|
+
return executeGeneratedSqlDirect(question, sql, seed, executionConnection, undefined, executionConnectionName);
|
|
4128
4902
|
};
|
|
4129
4903
|
/**
|
|
4130
4904
|
* EXP-001: execution host for the deliberately narrow non-governed lane.
|
|
@@ -4431,12 +5205,33 @@ export async function startLocalServer(opts) {
|
|
|
4431
5205
|
* conversation, writes a DQL file, changes targets, or promotes trust.
|
|
4432
5206
|
*/
|
|
4433
5207
|
const executeBoundedSqlRepair = async (input) => {
|
|
5208
|
+
let sourceSql = input.sourceSql;
|
|
5209
|
+
let rewrites = [];
|
|
5210
|
+
if (internalRelationIdsInSql(sourceSql).length > 0) {
|
|
5211
|
+
const normalizedSource = await prepareAnalyticalExecutionSql({
|
|
5212
|
+
sql: sourceSql,
|
|
5213
|
+
subject: 'Repair source SQL',
|
|
5214
|
+
executor,
|
|
5215
|
+
connection: input.targetConnection,
|
|
5216
|
+
projectRoot,
|
|
5217
|
+
projectConfig,
|
|
5218
|
+
enforceReadOnly: true,
|
|
5219
|
+
});
|
|
5220
|
+
sourceSql = normalizedSource.decodedSql;
|
|
5221
|
+
rewrites = normalizedSource.rewrites;
|
|
5222
|
+
}
|
|
5223
|
+
const sourceSignature = buildSqlAnalyticalSignature(sourceSql, input.targetConnection.driver);
|
|
5224
|
+
if (!sourceSignature) {
|
|
5225
|
+
throw boundedRepairError('DQL could not prove an immutable analytical signature for the source SQL. Repair it manually.', 409);
|
|
5226
|
+
}
|
|
4434
5227
|
let result;
|
|
4435
|
-
let repairedSql =
|
|
5228
|
+
let repairedSql = sourceSql;
|
|
4436
5229
|
let repairMode = 'deterministic';
|
|
4437
5230
|
let firstFailure;
|
|
5231
|
+
const providerEgressReceipts = [];
|
|
5232
|
+
let providerRoundTrips = 0;
|
|
4438
5233
|
try {
|
|
4439
|
-
result = await executeGeneratedSqlDirect(input.question,
|
|
5234
|
+
result = await executeGeneratedSqlDirect(input.question, sourceSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables }, input.targetConnectionName);
|
|
4440
5235
|
}
|
|
4441
5236
|
catch (error) {
|
|
4442
5237
|
firstFailure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
@@ -4457,50 +5252,89 @@ export async function startLocalServer(opts) {
|
|
|
4457
5252
|
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
4458
5253
|
throw boundedRepairError(failure.redactedMessage, 422, failure);
|
|
4459
5254
|
}
|
|
4460
|
-
const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
|
|
5255
|
+
const boundedSchema = markProviderMetadataArray(schemaContext.slice(0, 16).map((table) => ({
|
|
4461
5256
|
relation: table.relation,
|
|
4462
|
-
columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
|
|
4463
|
-
}));
|
|
5257
|
+
columns: markProviderMetadataArray(table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type }))),
|
|
5258
|
+
})));
|
|
4464
5259
|
// CTX-008: a repair that re-derives joins from column names can undo a
|
|
4465
5260
|
// join the original query got right. Give it the authored key pairs.
|
|
4466
|
-
const governedJoins = governedJoinFactsForRelations(schemaContext.slice(0, 16).map((table) => table.relation));
|
|
5261
|
+
const governedJoins = markProviderMetadataArray(governedJoinFactsForRelations(schemaContext.slice(0, 16).map((table) => table.relation)));
|
|
5262
|
+
const repairPayload = {
|
|
5263
|
+
question: input.question,
|
|
5264
|
+
driver: input.targetConnection.driver,
|
|
5265
|
+
failure: firstFailure.redactedMessage,
|
|
5266
|
+
sql: sourceSql,
|
|
5267
|
+
parameters: input.sqlParams ? markProviderMetadataArray(input.sqlParams.map((parameter) => ({
|
|
5268
|
+
name: parameter.name,
|
|
5269
|
+
placeholder: `$${parameter.position}`,
|
|
5270
|
+
}))) : undefined,
|
|
5271
|
+
schema: boundedSchema,
|
|
5272
|
+
...(governedJoins.length ? { governedJoins } : {}),
|
|
5273
|
+
};
|
|
5274
|
+
const repairMessages = [
|
|
5275
|
+
{
|
|
5276
|
+
role: 'system',
|
|
5277
|
+
content: [
|
|
5278
|
+
'Repair one failed read-only analytical SQL statement.',
|
|
5279
|
+
'Return exactly one JSON object in a ```json fence with keys summary, sql, viz, outputs.',
|
|
5280
|
+
'Preserve the requested grain, filters, output columns, and aggregation semantics.',
|
|
5281
|
+
input.sqlParams?.length
|
|
5282
|
+
? 'Preserve every positional parameter placeholder exactly; do not add, remove, or renumber placeholders.'
|
|
5283
|
+
: '',
|
|
5284
|
+
'Use only relations and columns present in the supplied schema context.',
|
|
5285
|
+
'When governedJoins is present, join on those exact key pairs rather than inferring a join from column names.',
|
|
5286
|
+
'Never emit internal DQL relation identities such as source::, model::, seed::, or snapshot::.',
|
|
5287
|
+
'Do not add writes, DDL, multiple statements, or commentary outside the JSON fence.',
|
|
5288
|
+
].join(' '),
|
|
5289
|
+
},
|
|
5290
|
+
{
|
|
5291
|
+
role: 'user',
|
|
5292
|
+
content: JSON.stringify(repairPayload),
|
|
5293
|
+
},
|
|
5294
|
+
];
|
|
5295
|
+
const repairEnvelope = prepareProviderWireEnvelopeForDispatch(provider.name, { messages: repairMessages });
|
|
5296
|
+
assertProviderPayloadAllowed(repairEnvelope, {
|
|
5297
|
+
allowResultRows: false,
|
|
5298
|
+
maxResultRows: 0,
|
|
5299
|
+
purpose: 'repair_sql',
|
|
5300
|
+
});
|
|
4467
5301
|
let raw;
|
|
4468
5302
|
try {
|
|
4469
|
-
raw = await provider.generate(
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
failure: firstFailure.redactedMessage,
|
|
4491
|
-
sql: input.sourceSql,
|
|
4492
|
-
parameters: input.sqlParams?.map((parameter) => ({
|
|
4493
|
-
name: parameter.name,
|
|
4494
|
-
placeholder: `$${parameter.position}`,
|
|
4495
|
-
})),
|
|
4496
|
-
schema: boundedSchema,
|
|
4497
|
-
...(governedJoins.length ? { governedJoins } : {}),
|
|
4498
|
-
}),
|
|
5303
|
+
raw = await provider.generate(repairEnvelope.messages, {
|
|
5304
|
+
maxTokens: 1200,
|
|
5305
|
+
temperature: 0,
|
|
5306
|
+
maxProviderDispatches: 2,
|
|
5307
|
+
onProviderDispatch: (event) => {
|
|
5308
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(provider.name, event.envelope);
|
|
5309
|
+
assertProviderPayloadAllowed(envelope, {
|
|
5310
|
+
allowResultRows: false,
|
|
5311
|
+
maxResultRows: 0,
|
|
5312
|
+
purpose: 'repair_sql',
|
|
5313
|
+
});
|
|
5314
|
+
providerRoundTrips += 1;
|
|
5315
|
+
providerEgressReceipts.push(createProviderDispatchEgressReceipt({
|
|
5316
|
+
purpose: 'repair_sql',
|
|
5317
|
+
dispatchPhase: 'repair',
|
|
5318
|
+
provider: provider.name,
|
|
5319
|
+
permittedCategories: ['instructions', 'question', 'schema_metadata', 'governed_context'],
|
|
5320
|
+
optIn: false,
|
|
5321
|
+
envelope,
|
|
5322
|
+
}));
|
|
5323
|
+
return envelope;
|
|
4499
5324
|
},
|
|
4500
|
-
|
|
5325
|
+
});
|
|
4501
5326
|
}
|
|
4502
5327
|
catch {
|
|
4503
|
-
throw boundedRepairError('The AI provider could not complete this repair. Check Provider Settings and retry.', 502)
|
|
5328
|
+
throw Object.assign(boundedRepairError('The AI provider could not complete this repair. Check Provider Settings and retry.', 502), {
|
|
5329
|
+
providerDispatchEvidence: {
|
|
5330
|
+
providerEgressReceipts: [...providerEgressReceipts],
|
|
5331
|
+
providerRoundTrips,
|
|
5332
|
+
toolCalls: 0,
|
|
5333
|
+
sqlExecutions: 0,
|
|
5334
|
+
repairs: 1,
|
|
5335
|
+
fallbackReason: 'repair_provider_error',
|
|
5336
|
+
},
|
|
5337
|
+
});
|
|
4504
5338
|
}
|
|
4505
5339
|
const proposal = parseProposal(raw);
|
|
4506
5340
|
if (!proposal.sql?.trim()) {
|
|
@@ -4511,9 +5345,17 @@ export async function startLocalServer(opts) {
|
|
|
4511
5345
|
if (requiredPlaceholders.some((placeholder) => !new RegExp(`${placeholder.replace('$', '\\$')}\\b`).test(repairedSql))) {
|
|
4512
5346
|
throw boundedRepairError('The repair changed this cell\'s parameter contract, so DQL stopped it before execution.', 422);
|
|
4513
5347
|
}
|
|
5348
|
+
const repairedSignature = buildSqlAnalyticalSignature(repairedSql, input.targetConnection.driver);
|
|
5349
|
+
if (!repairedSignature || stableExecutionValue(repairedSignature) !== stableExecutionValue(sourceSignature)) {
|
|
5350
|
+
throw boundedRepairError('The provider repair changed or could not prove the immutable analytical plan, so DQL stopped before execution.', 409);
|
|
5351
|
+
}
|
|
5352
|
+
const aggregationProof = buildAggregationSafetyProof(repairedSql, undefined, input.targetConnection.driver);
|
|
5353
|
+
if (aggregationProof.status !== 'safe') {
|
|
5354
|
+
throw boundedRepairError(`The repaired SQL lacks complete aggregation-safety evidence (${aggregationProof.issueCodes.join(', ')}). Repair it manually.`, 409);
|
|
5355
|
+
}
|
|
4514
5356
|
repairMode = 'ai';
|
|
4515
5357
|
try {
|
|
4516
|
-
result = await executeGeneratedSqlDirect(input.question, repairedSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables });
|
|
5358
|
+
result = await executeGeneratedSqlDirect(input.question, repairedSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables }, input.targetConnectionName);
|
|
4517
5359
|
}
|
|
4518
5360
|
catch (error) {
|
|
4519
5361
|
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
@@ -4523,7 +5365,6 @@ export async function startLocalServer(opts) {
|
|
|
4523
5365
|
// Keep preview-only row bounds out of editable notebook source. The one
|
|
4524
5366
|
// deterministic source rewrite we do persist is decoding internal graph
|
|
4525
5367
|
// identities, because those can never execute as warehouse SQL.
|
|
4526
|
-
let rewrites = [];
|
|
4527
5368
|
if (internalRelationIdsInSql(repairedSql).length > 0) {
|
|
4528
5369
|
const normalizedSource = await prepareAnalyticalExecutionSql({
|
|
4529
5370
|
sql: repairedSql,
|
|
@@ -4535,9 +5376,9 @@ export async function startLocalServer(opts) {
|
|
|
4535
5376
|
enforceReadOnly: true,
|
|
4536
5377
|
});
|
|
4537
5378
|
repairedSql = normalizedSource.decodedSql;
|
|
4538
|
-
rewrites
|
|
5379
|
+
rewrites.push(...normalizedSource.rewrites);
|
|
4539
5380
|
}
|
|
4540
|
-
return { result, repairedSql, repairMode, firstFailure, rewrites };
|
|
5381
|
+
return { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips, rewrites };
|
|
4541
5382
|
};
|
|
4542
5383
|
/**
|
|
4543
5384
|
* Repair a DQL wrapper that cannot compile far enough to expose executable
|
|
@@ -4550,6 +5391,8 @@ export async function startLocalServer(opts) {
|
|
|
4550
5391
|
if (!provider) {
|
|
4551
5392
|
throw boundedRepairError('Configure an available AI provider in Settings to repair this DQL draft.', 409);
|
|
4552
5393
|
}
|
|
5394
|
+
const providerEgressReceipts = [];
|
|
5395
|
+
let providerRoundTrips = 0;
|
|
4553
5396
|
let schemaContext;
|
|
4554
5397
|
try {
|
|
4555
5398
|
// Include source identifiers in retrieval so a terse cell title still
|
|
@@ -4595,10 +5438,41 @@ export async function startLocalServer(opts) {
|
|
|
4595
5438
|
...(governedJoins.length ? { governedJoins } : {}),
|
|
4596
5439
|
}),
|
|
4597
5440
|
},
|
|
4598
|
-
], {
|
|
5441
|
+
], {
|
|
5442
|
+
maxTokens: 2400,
|
|
5443
|
+
temperature: 0,
|
|
5444
|
+
maxProviderDispatches: 2,
|
|
5445
|
+
onProviderDispatch: (event) => {
|
|
5446
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(provider.name, event.envelope);
|
|
5447
|
+
assertProviderPayloadAllowed(envelope, {
|
|
5448
|
+
allowResultRows: false,
|
|
5449
|
+
maxResultRows: 0,
|
|
5450
|
+
purpose: 'repair_sql',
|
|
5451
|
+
});
|
|
5452
|
+
providerRoundTrips += 1;
|
|
5453
|
+
providerEgressReceipts.push(createProviderDispatchEgressReceipt({
|
|
5454
|
+
purpose: 'repair_sql',
|
|
5455
|
+
dispatchPhase: 'repair',
|
|
5456
|
+
provider: provider.name,
|
|
5457
|
+
permittedCategories: ['instructions', 'question', 'schema_metadata', 'governed_context'],
|
|
5458
|
+
optIn: false,
|
|
5459
|
+
envelope,
|
|
5460
|
+
}));
|
|
5461
|
+
return envelope;
|
|
5462
|
+
},
|
|
5463
|
+
});
|
|
4599
5464
|
}
|
|
4600
5465
|
catch {
|
|
4601
|
-
throw boundedRepairError('The AI provider could not complete this DQL repair. Check Provider Settings and retry.', 502)
|
|
5466
|
+
throw Object.assign(boundedRepairError('The AI provider could not complete this DQL repair. Check Provider Settings and retry.', 502), {
|
|
5467
|
+
providerDispatchEvidence: {
|
|
5468
|
+
providerEgressReceipts: [...providerEgressReceipts],
|
|
5469
|
+
providerRoundTrips,
|
|
5470
|
+
toolCalls: 0,
|
|
5471
|
+
sqlExecutions: 0,
|
|
5472
|
+
repairs: 1,
|
|
5473
|
+
fallbackReason: 'repair_provider_error',
|
|
5474
|
+
},
|
|
5475
|
+
});
|
|
4602
5476
|
}
|
|
4603
5477
|
let repairedSource;
|
|
4604
5478
|
try {
|
|
@@ -4702,6 +5576,8 @@ export async function startLocalServer(opts) {
|
|
|
4702
5576
|
repairedSql: plan.sql,
|
|
4703
5577
|
repairedSource,
|
|
4704
5578
|
repairMode: 'ai',
|
|
5579
|
+
providerEgressReceipts,
|
|
5580
|
+
providerRoundTrips,
|
|
4705
5581
|
rewrites: [],
|
|
4706
5582
|
sqlParams: plan.sqlParams,
|
|
4707
5583
|
variables: { ...plan.variables, ...invocation.values },
|
|
@@ -5700,11 +6576,18 @@ export async function startLocalServer(opts) {
|
|
|
5700
6576
|
tableMapping,
|
|
5701
6577
|
semanticSql: semanticCompose?.sql ?? undefined,
|
|
5702
6578
|
});
|
|
5703
|
-
const
|
|
5704
|
-
|
|
6579
|
+
const execution = await analyticalExecutionService.execute({
|
|
6580
|
+
sql: semanticCompose?.sql ?? plan?.sql ?? executableSql,
|
|
6581
|
+
subject: 'Block Studio preview query',
|
|
6582
|
+
connection: activeConnection,
|
|
6583
|
+
sqlParams: plan?.sqlParams,
|
|
6584
|
+
variables: { ...(plan?.variables ?? {}), ...invocation.values },
|
|
6585
|
+
});
|
|
5705
6586
|
return {
|
|
5706
|
-
sql:
|
|
5707
|
-
result:
|
|
6587
|
+
sql: execution.preparation.executedSql,
|
|
6588
|
+
result: execution.result,
|
|
6589
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
6590
|
+
resultFingerprint: execution.resultFingerprint,
|
|
5708
6591
|
chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
|
|
5709
6592
|
invocation,
|
|
5710
6593
|
tests: plan?.tests ?? [],
|
|
@@ -5755,12 +6638,14 @@ export async function startLocalServer(opts) {
|
|
|
5755
6638
|
if (tests.length === 0) {
|
|
5756
6639
|
return { passed: 0, failed: 0, skipped: 0, duration: Date.now() - start, assertions: [], runAt: new Date() };
|
|
5757
6640
|
}
|
|
5758
|
-
const
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
:
|
|
6641
|
+
const execution = await analyticalExecutionService.execute({
|
|
6642
|
+
sql: semanticCompose?.sql ?? plan.sql,
|
|
6643
|
+
subject: 'Block Studio certification test query',
|
|
6644
|
+
connection: activeConnection,
|
|
6645
|
+
sqlParams: plan.sqlParams,
|
|
6646
|
+
variables: plan.variables,
|
|
6647
|
+
});
|
|
6648
|
+
const { rows, columns } = execution.result;
|
|
5764
6649
|
return evaluateBlockStudioTests(tests, rows, columns, start);
|
|
5765
6650
|
};
|
|
5766
6651
|
const saveDqlGenerationDraft = (importId, candidate) => saveDqlGenerationDraftForProject(projectRoot, importId, candidate);
|
|
@@ -5809,10 +6694,20 @@ export async function startLocalServer(opts) {
|
|
|
5809
6694
|
};
|
|
5810
6695
|
};
|
|
5811
6696
|
const certifyBlockStudioSource = async (source, blockPath, options = {}) => {
|
|
6697
|
+
options.reportProgress?.({
|
|
6698
|
+
phase: 'validating',
|
|
6699
|
+
progress: 10,
|
|
6700
|
+
message: 'Validating the saved DQL source.',
|
|
6701
|
+
});
|
|
5812
6702
|
let validation = validateBlockStudioSource(source, semanticLayer);
|
|
5813
6703
|
let preview = null;
|
|
5814
6704
|
let testResults = null;
|
|
5815
6705
|
const blockers = [];
|
|
6706
|
+
options.reportProgress?.({
|
|
6707
|
+
phase: 'previewing',
|
|
6708
|
+
progress: 30,
|
|
6709
|
+
message: 'Compiling and previewing the saved block.',
|
|
6710
|
+
});
|
|
5816
6711
|
try {
|
|
5817
6712
|
preview = await runBlockStudioPreviewSource(source);
|
|
5818
6713
|
validation = preview.validation;
|
|
@@ -5820,6 +6715,11 @@ export async function startLocalServer(opts) {
|
|
|
5820
6715
|
catch (error) {
|
|
5821
6716
|
blockers.push(error instanceof Error ? error.message : String(error));
|
|
5822
6717
|
}
|
|
6718
|
+
options.reportProgress?.({
|
|
6719
|
+
phase: 'testing',
|
|
6720
|
+
progress: 60,
|
|
6721
|
+
message: 'Evaluating declared block tests.',
|
|
6722
|
+
});
|
|
5823
6723
|
try {
|
|
5824
6724
|
testResults = preview
|
|
5825
6725
|
? await runBlockStudioTestSummary(source, undefined, preview)
|
|
@@ -5835,6 +6735,11 @@ export async function startLocalServer(opts) {
|
|
|
5835
6735
|
runAt: new Date(),
|
|
5836
6736
|
};
|
|
5837
6737
|
}
|
|
6738
|
+
options.reportProgress?.({
|
|
6739
|
+
phase: 'assessing',
|
|
6740
|
+
progress: 75,
|
|
6741
|
+
message: 'Assessing certification rules and evidence.',
|
|
6742
|
+
});
|
|
5838
6743
|
const parsed = parseBlockSourceMetadata(source);
|
|
5839
6744
|
const record = {
|
|
5840
6745
|
id: parsed.name || 'local',
|
|
@@ -7222,23 +8127,81 @@ export async function startLocalServer(opts) {
|
|
|
7222
8127
|
res.end(serializeJSON({ error: 'Agent run not found.' }));
|
|
7223
8128
|
return;
|
|
7224
8129
|
}
|
|
8130
|
+
const capability = normalizeAnalyticalRepairCapabilityV1(sourceRun.repairCapability);
|
|
8131
|
+
if (!capability?.automatic.eligible || capability.automatic.attemptsRemaining < 1) {
|
|
8132
|
+
const manualActions = capability?.manualActions?.length
|
|
8133
|
+
? capability.manualActions
|
|
8134
|
+
: ['edit_dql', 'open_sql_notebook'];
|
|
8135
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8136
|
+
res.end(serializeJSON({
|
|
8137
|
+
code: 'REPAIR_CAPABILITY_REQUIRED',
|
|
8138
|
+
error: 'This run does not retain eligible automatic execution-repair authority.',
|
|
8139
|
+
manualActions,
|
|
8140
|
+
...(capability?.ineligibilityReason ? { ineligibilityReason: capability.ineligibilityReason } : {}),
|
|
8141
|
+
}));
|
|
8142
|
+
return;
|
|
8143
|
+
}
|
|
8144
|
+
// Rebind every server-issued capability identity to the currently
|
|
8145
|
+
// persisted immutable source run before checking idempotence or reserving
|
|
8146
|
+
// an attempt. A copied/stale capability must never authorize provider or
|
|
8147
|
+
// SQL work against a mutated plan, failure, or source artifact.
|
|
8148
|
+
const canonicalCapability = analyticalRepairCapabilityForAgentRun(sourceRun, capability.targetFingerprint);
|
|
8149
|
+
const bindingMatches = canonicalCapability
|
|
8150
|
+
&& canonicalCapability.automatic.eligible
|
|
8151
|
+
&& canonicalCapability.planFingerprint === capability.planFingerprint
|
|
8152
|
+
&& canonicalCapability.failureFingerprint === capability.failureFingerprint
|
|
8153
|
+
&& canonicalCapability.sourceFingerprint === capability.sourceFingerprint;
|
|
8154
|
+
if (!bindingMatches) {
|
|
8155
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8156
|
+
res.end(serializeJSON({
|
|
8157
|
+
code: 'REPAIR_CAPABILITY_REQUIRED',
|
|
8158
|
+
error: 'The persisted source plan, failure, or artifact no longer matches its server-issued repair capability.',
|
|
8159
|
+
manualActions: capability.manualActions,
|
|
8160
|
+
}));
|
|
8161
|
+
return;
|
|
8162
|
+
}
|
|
7225
8163
|
const derivedRunId = `${sourceRun.id}:repair:1`;
|
|
8164
|
+
if (activeAnalyticalRepairReservations.has(derivedRunId)) {
|
|
8165
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8166
|
+
res.end(serializeJSON({
|
|
8167
|
+
code: 'REPAIR_IN_PROGRESS',
|
|
8168
|
+
error: 'The single permitted repair attempt is already in progress.',
|
|
8169
|
+
manualActions: capability.manualActions,
|
|
8170
|
+
}));
|
|
8171
|
+
return;
|
|
8172
|
+
}
|
|
7226
8173
|
const existing = await agentRunStore.get(derivedRunId);
|
|
7227
8174
|
if (existing) {
|
|
7228
|
-
|
|
7229
|
-
res.
|
|
8175
|
+
const succeeded = existing.status === 'needs_review' && existing.trustState === 'review_required';
|
|
8176
|
+
res.writeHead(succeeded ? 200 : 409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8177
|
+
res.end(serializeJSON(succeeded
|
|
8178
|
+
? { run: existing }
|
|
8179
|
+
: {
|
|
8180
|
+
code: 'REPAIR_ATTEMPT_EXHAUSTED',
|
|
8181
|
+
error: 'The single permitted repair attempt was already consumed and did not complete successfully.',
|
|
8182
|
+
manualActions: capability.manualActions,
|
|
8183
|
+
run: existing,
|
|
8184
|
+
}));
|
|
7230
8185
|
return;
|
|
7231
8186
|
}
|
|
7232
|
-
const sourceSql = repairableSqlFromAgentRun(sourceRun);
|
|
7233
8187
|
const sourceDqlArtifact = repairableDqlArtifactFromAgentRun(sourceRun);
|
|
7234
8188
|
const retainedFailure = warehouseFailureFromAgentRun(sourceRun);
|
|
7235
|
-
if (
|
|
8189
|
+
if (!sourceDqlArtifact || !validAskRepairDqlWrapper(sourceDqlArtifact.source)) {
|
|
8190
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8191
|
+
res.end(serializeJSON({
|
|
8192
|
+
code: 'REPAIR_CAPABILITY_REQUIRED',
|
|
8193
|
+
error: 'The retained DQL wrapper is unavailable or invalid. Keep it editable and repair it manually.',
|
|
8194
|
+
manualActions: capability.manualActions,
|
|
8195
|
+
}));
|
|
8196
|
+
return;
|
|
8197
|
+
}
|
|
8198
|
+
const capabilityHash = (value) => `sha256:${createHash('sha256').update(stableExecutionValue(value)).digest('hex')}`;
|
|
8199
|
+
if (capability.dqlFingerprint !== capabilityHash(sourceDqlArtifact.source.trim())) {
|
|
7236
8200
|
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7237
8201
|
res.end(serializeJSON({
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
...(retainedFailure ? { warehouseFailure: retainedFailure } : {}),
|
|
8202
|
+
code: 'REPAIR_CAPABILITY_REQUIRED',
|
|
8203
|
+
error: 'The retained DQL source no longer matches its immutable repair capability.',
|
|
8204
|
+
manualActions: capability.manualActions,
|
|
7242
8205
|
}));
|
|
7243
8206
|
return;
|
|
7244
8207
|
}
|
|
@@ -7261,32 +8224,250 @@ export async function startLocalServer(opts) {
|
|
|
7261
8224
|
: sourceRun.executionTarget?.target === 'local'
|
|
7262
8225
|
? 'local'
|
|
7263
8226
|
: projectConfig.defaultConnectionName;
|
|
8227
|
+
const resolvedTargetFingerprint = targetGenerationFingerprint(targetConnection, targetConnectionName);
|
|
8228
|
+
if (resolvedTargetFingerprint !== capability.targetFingerprint) {
|
|
8229
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8230
|
+
res.end(serializeJSON({
|
|
8231
|
+
code: 'REPAIR_TARGET_DRIFT',
|
|
8232
|
+
error: 'The named execution target now resolves to a different connection generation. Automatic repair was not attempted.',
|
|
8233
|
+
manualActions: capability.manualActions,
|
|
8234
|
+
}));
|
|
8235
|
+
return;
|
|
8236
|
+
}
|
|
8237
|
+
if (consumedAnalyticalRepairCapabilities.has(derivedRunId)) {
|
|
8238
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8239
|
+
res.end(serializeJSON({
|
|
8240
|
+
code: 'REPAIR_ATTEMPT_EXHAUSTED',
|
|
8241
|
+
error: 'The single permitted repair attempt has already been consumed.',
|
|
8242
|
+
manualActions: capability.manualActions,
|
|
8243
|
+
}));
|
|
8244
|
+
return;
|
|
8245
|
+
}
|
|
8246
|
+
activeAnalyticalRepairReservations.add(derivedRunId);
|
|
8247
|
+
consumedAnalyticalRepairCapabilities.add(derivedRunId);
|
|
8248
|
+
const releaseRepairReservation = () => { activeAnalyticalRepairReservations.delete(derivedRunId); };
|
|
8249
|
+
res.once('finish', releaseRepairReservation);
|
|
8250
|
+
res.once('close', releaseRepairReservation);
|
|
8251
|
+
const repairStartedAt = new Date().toISOString();
|
|
8252
|
+
const repairStartedAtMs = Date.now();
|
|
8253
|
+
const sourceFailureForReservation = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
|
|
8254
|
+
const reservationEvent = {
|
|
8255
|
+
id: `${derivedRunId}:repair`,
|
|
8256
|
+
runId: derivedRunId,
|
|
8257
|
+
type: 'repair.attempted',
|
|
8258
|
+
at: repairStartedAt,
|
|
8259
|
+
message: 'Reserved the single bounded repair attempt.',
|
|
8260
|
+
route: 'generated_answer',
|
|
8261
|
+
};
|
|
8262
|
+
const reservedRun = {
|
|
8263
|
+
id: derivedRunId,
|
|
8264
|
+
question: sourceRun.question,
|
|
8265
|
+
requestedMode: sourceRun.requestedMode,
|
|
8266
|
+
route: 'generated_answer',
|
|
8267
|
+
status: 'blocked',
|
|
8268
|
+
trustState: 'blocked',
|
|
8269
|
+
stopReason: 'blocked',
|
|
8270
|
+
startedAt: repairStartedAt,
|
|
8271
|
+
completedAt: repairStartedAt,
|
|
8272
|
+
selectedObject: sourceRun.selectedObject,
|
|
8273
|
+
executionTarget: sourceRun.executionTarget,
|
|
8274
|
+
routeDecision: sourceRun.routeDecision,
|
|
8275
|
+
plan: sourceRun.plan,
|
|
8276
|
+
steps: [],
|
|
8277
|
+
summary: 'The single repair attempt was reserved but has not completed successfully.',
|
|
8278
|
+
artifacts: [],
|
|
8279
|
+
evaluations: [],
|
|
8280
|
+
events: [reservationEvent],
|
|
8281
|
+
nextActions: sourceRun.nextActions,
|
|
8282
|
+
repairAttempts: 1,
|
|
8283
|
+
lifecycle: {
|
|
8284
|
+
version: 1,
|
|
8285
|
+
state: 'terminal',
|
|
8286
|
+
phase: 'repair.attempted',
|
|
8287
|
+
revision: 1,
|
|
8288
|
+
eventCursor: 1,
|
|
8289
|
+
startedAt: repairStartedAt,
|
|
8290
|
+
updatedAt: repairStartedAt,
|
|
8291
|
+
completedAt: repairStartedAt,
|
|
8292
|
+
},
|
|
8293
|
+
derivation: {
|
|
8294
|
+
version: 1,
|
|
8295
|
+
kind: 'analytical_repair',
|
|
8296
|
+
sourceRunId: sourceRun.id,
|
|
8297
|
+
...(sourceFailureForReservation?.failureId ? { sourceFailureId: sourceFailureForReservation.failureId } : {}),
|
|
8298
|
+
attempt: 1,
|
|
8299
|
+
},
|
|
8300
|
+
};
|
|
8301
|
+
try {
|
|
8302
|
+
await agentRunStore.save(reservedRun);
|
|
8303
|
+
}
|
|
8304
|
+
catch (error) {
|
|
8305
|
+
releaseRepairReservation();
|
|
8306
|
+
throw error;
|
|
8307
|
+
}
|
|
7264
8308
|
let repaired;
|
|
7265
8309
|
try {
|
|
7266
|
-
//
|
|
7267
|
-
//
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
:
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
8310
|
+
// Compile the immutable original wrapper first. A provider receives only
|
|
8311
|
+
// this embedded SQL, never the DQL wrapper or its surrounding fields.
|
|
8312
|
+
const invocation = prepareBlockInvocation({
|
|
8313
|
+
source: sourceDqlArtifact.source,
|
|
8314
|
+
parameters: sourceDqlArtifact.parameterValues,
|
|
8315
|
+
question: sourceRun.question,
|
|
8316
|
+
surface: 'ask_ai',
|
|
8317
|
+
});
|
|
8318
|
+
if (invocation.errors.length || invocation.unresolvedParameters.length) {
|
|
8319
|
+
throw boundedRepairError(invocation.errors.join(' ') || `Provide required parameters before retrying: ${invocation.unresolvedParameters.join(', ')}.`, 409);
|
|
8320
|
+
}
|
|
8321
|
+
const originalPlan = buildExecutionPlan({
|
|
8322
|
+
id: 'ask-execution-repair-source',
|
|
8323
|
+
type: 'dql',
|
|
8324
|
+
source: sourceDqlArtifact.source,
|
|
8325
|
+
title: sourceDqlArtifact.name,
|
|
8326
|
+
}, {
|
|
8327
|
+
semanticLayer,
|
|
8328
|
+
driver: targetConnection.driver,
|
|
8329
|
+
parameters: invocation.values,
|
|
8330
|
+
});
|
|
8331
|
+
if (!originalPlan?.sql?.trim())
|
|
8332
|
+
throw boundedRepairError('The original DQL wrapper did not compile to SQL.', 409);
|
|
8333
|
+
if (capability.sqlFingerprint !== capabilityHash(originalPlan.sql.trim())) {
|
|
8334
|
+
throw boundedRepairError('The original compiled SQL no longer matches its immutable repair capability.', 409);
|
|
8335
|
+
}
|
|
8336
|
+
const sqlRepair = await executeBoundedSqlRepair({
|
|
8337
|
+
question: sourceRun.question,
|
|
8338
|
+
sourceSql: originalPlan.sql,
|
|
8339
|
+
targetConnection,
|
|
8340
|
+
targetConnectionName,
|
|
8341
|
+
sqlParams: originalPlan.sqlParams,
|
|
8342
|
+
variables: { ...originalPlan.variables, ...invocation.values },
|
|
8343
|
+
});
|
|
8344
|
+
const embeddedSql = restoreNotebookDqlParameterInterpolations(sqlRepair.repairedSql, originalPlan.sqlParams);
|
|
8345
|
+
const repairedSource = replaceNotebookDqlQueryForRepair(sourceDqlArtifact.source, embeddedSql);
|
|
8346
|
+
if (!repairedSource)
|
|
8347
|
+
throw boundedRepairError('DQL could not reinsert the repaired SQL into the immutable wrapper.', 422);
|
|
8348
|
+
const repairedInvocation = prepareBlockInvocation({
|
|
8349
|
+
source: repairedSource,
|
|
8350
|
+
parameters: sourceDqlArtifact.parameterValues,
|
|
8351
|
+
question: sourceRun.question,
|
|
8352
|
+
surface: 'ask_ai',
|
|
8353
|
+
});
|
|
8354
|
+
if (repairedInvocation.errors.length || repairedInvocation.unresolvedParameters.length) {
|
|
8355
|
+
throw boundedRepairError('The SQL-only repair did not preserve a compilable DQL wrapper.', 422);
|
|
8356
|
+
}
|
|
8357
|
+
const repairedPlan = buildExecutionPlan({
|
|
8358
|
+
id: 'ask-execution-repair-derived',
|
|
8359
|
+
type: 'dql',
|
|
8360
|
+
source: repairedSource,
|
|
8361
|
+
title: sourceDqlArtifact.name,
|
|
8362
|
+
}, {
|
|
8363
|
+
semanticLayer,
|
|
8364
|
+
driver: targetConnection.driver,
|
|
8365
|
+
parameters: repairedInvocation.values,
|
|
8366
|
+
});
|
|
8367
|
+
if (!repairedPlan?.sql?.trim() || normalizeSqlForComparison(repairedPlan.sql) !== normalizeSqlForComparison(sqlRepair.repairedSql)) {
|
|
8368
|
+
throw boundedRepairError('The repaired wrapper recompiled to different SQL, so DQL stopped before publishing it.', 422);
|
|
8369
|
+
}
|
|
8370
|
+
const repairedExecutionReceipt = createDqlArtifactExecutionReceipt(repairedSource, sqlRepair.result.sql ?? repairedPlan.sql, repairedInvocation.values, sqlRepair.result);
|
|
8371
|
+
const repairedExecutableArtifact = sqlRepair.result.executableArtifact
|
|
8372
|
+
? {
|
|
8373
|
+
...sqlRepair.result.executableArtifact,
|
|
8374
|
+
kind: 'sql_block',
|
|
8375
|
+
dqlFingerprint: executionFingerprint(repairedSource),
|
|
8376
|
+
sourceFingerprint: executionFingerprint(stableExecutionValue({
|
|
8377
|
+
name: sourceDqlArtifact.name ?? null,
|
|
8378
|
+
path: sourceDqlArtifact.sourcePath ?? null,
|
|
8379
|
+
})),
|
|
8380
|
+
compiledSqlFingerprint: executionFingerprint(repairedPlan.sql),
|
|
8381
|
+
normalizedSqlFingerprint: executionFingerprint(sqlRepair.repairedSql),
|
|
8382
|
+
parameterFingerprint: repairedExecutionReceipt.parameterFingerprint,
|
|
8383
|
+
provenanceFingerprint: executionFingerprint(stableExecutionValue(repairedInvocation.resolvedParameters.map((parameter) => ({
|
|
8384
|
+
name: parameter.name,
|
|
8385
|
+
source: parameter.source,
|
|
8386
|
+
})))),
|
|
8387
|
+
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
8388
|
+
compiledSqlFingerprint: executionFingerprint(repairedPlan.sql),
|
|
8389
|
+
parameterCount: repairedPlan.sqlParams?.length ?? 0,
|
|
8390
|
+
variableNames: Object.keys(repairedPlan.variables ?? {}).sort(),
|
|
8391
|
+
})),
|
|
8392
|
+
trustState: 'review_required',
|
|
8393
|
+
receipt: repairedExecutionReceipt,
|
|
8394
|
+
}
|
|
8395
|
+
: undefined;
|
|
8396
|
+
const repairedArtifact = {
|
|
8397
|
+
...sourceDqlArtifact,
|
|
8398
|
+
source: repairedSource,
|
|
8399
|
+
compiledSql: repairedPlan.sql,
|
|
8400
|
+
trustState: 'review_required',
|
|
8401
|
+
persistence: 'transient',
|
|
8402
|
+
executionReceipt: repairedExecutionReceipt,
|
|
8403
|
+
...(repairedExecutableArtifact ? { executableArtifact: repairedExecutableArtifact } : {}),
|
|
8404
|
+
...(repairedInvocation.parameters.length ? { parameters: repairedInvocation.parameters } : {}),
|
|
8405
|
+
...(Object.keys(repairedInvocation.values).length ? { parameterValues: repairedInvocation.values } : {}),
|
|
8406
|
+
};
|
|
8407
|
+
repaired = {
|
|
8408
|
+
...sqlRepair,
|
|
8409
|
+
result: {
|
|
8410
|
+
...sqlRepair.result,
|
|
8411
|
+
executionReceipt: repairedExecutionReceipt,
|
|
8412
|
+
...(repairedExecutableArtifact ? { executableArtifact: repairedExecutableArtifact } : {}),
|
|
8413
|
+
dqlArtifact: repairedArtifact,
|
|
8414
|
+
},
|
|
8415
|
+
repairedSql: repairedPlan.sql,
|
|
8416
|
+
repairedSource,
|
|
8417
|
+
sqlParams: repairedPlan.sqlParams,
|
|
8418
|
+
variables: { ...repairedPlan.variables, ...repairedInvocation.values },
|
|
8419
|
+
};
|
|
7287
8420
|
}
|
|
7288
8421
|
catch (error) {
|
|
7289
8422
|
const repairError = error;
|
|
8423
|
+
const dispatchEvidence = providerDispatchTerminalEvidence(error);
|
|
8424
|
+
const failedAt = new Date().toISOString();
|
|
8425
|
+
const failedTelemetry = {
|
|
8426
|
+
version: 1,
|
|
8427
|
+
stageDurationsMs: { repair: Date.now() - repairStartedAtMs, total: Date.now() - repairStartedAtMs },
|
|
8428
|
+
providerRoundTrips: dispatchEvidence?.providerRoundTrips ?? 0,
|
|
8429
|
+
toolCalls: dispatchEvidence?.toolCalls ?? 0,
|
|
8430
|
+
sqlExecutions: dispatchEvidence?.sqlExecutions ?? 0,
|
|
8431
|
+
repairs: Math.max(1, dispatchEvidence?.repairs ?? 0),
|
|
8432
|
+
egressReceipts: dispatchEvidence?.providerEgressReceipts.length ?? 0,
|
|
8433
|
+
fallbackReason: dispatchEvidence?.fallbackReason ?? 'repair_failed',
|
|
8434
|
+
};
|
|
8435
|
+
const failedReceipts = dispatchEvidence?.providerEgressReceipts ?? [];
|
|
8436
|
+
await agentRunStore.save({
|
|
8437
|
+
...reservedRun,
|
|
8438
|
+
completedAt: failedAt,
|
|
8439
|
+
summary: repairError.message,
|
|
8440
|
+
lifecycle: reservedRun.lifecycle ? {
|
|
8441
|
+
...reservedRun.lifecycle,
|
|
8442
|
+
phase: 'run.failed',
|
|
8443
|
+
updatedAt: failedAt,
|
|
8444
|
+
completedAt: failedAt,
|
|
8445
|
+
} : undefined,
|
|
8446
|
+
events: [
|
|
8447
|
+
reservationEvent,
|
|
8448
|
+
{
|
|
8449
|
+
id: `${derivedRunId}:failed`,
|
|
8450
|
+
runId: derivedRunId,
|
|
8451
|
+
type: 'run.failed',
|
|
8452
|
+
at: failedAt,
|
|
8453
|
+
message: repairError.message,
|
|
8454
|
+
route: 'generated_answer',
|
|
8455
|
+
status: 'blocked',
|
|
8456
|
+
trustState: 'blocked',
|
|
8457
|
+
},
|
|
8458
|
+
],
|
|
8459
|
+
telemetry: failedTelemetry,
|
|
8460
|
+
...(failedReceipts.length ? { providerEgressReceipts: failedReceipts } : {}),
|
|
8461
|
+
diagnosticReceiptV2: {
|
|
8462
|
+
version: 2,
|
|
8463
|
+
runId: derivedRunId,
|
|
8464
|
+
route: 'generated_answer',
|
|
8465
|
+
status: 'blocked',
|
|
8466
|
+
telemetry: failedTelemetry,
|
|
8467
|
+
providerEgressReceiptFingerprints: failedReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
|
|
8468
|
+
repairCapabilityFingerprint: executionFingerprint(stableExecutionValue(capability)),
|
|
8469
|
+
},
|
|
8470
|
+
});
|
|
7290
8471
|
res.writeHead(repairError.status ?? 500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7291
8472
|
res.end(serializeJSON({
|
|
7292
8473
|
error: repairError.message,
|
|
@@ -7294,27 +8475,39 @@ export async function startLocalServer(opts) {
|
|
|
7294
8475
|
}));
|
|
7295
8476
|
return;
|
|
7296
8477
|
}
|
|
7297
|
-
const { result, repairedSql, repairMode, firstFailure } = repaired;
|
|
8478
|
+
const { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips } = repaired;
|
|
7298
8479
|
const completedAt = new Date().toISOString();
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
audience: 'stakeholder',
|
|
7303
|
-
resultPreview: preview,
|
|
7304
|
-
sql: repairedSql,
|
|
7305
|
-
draftText: `The repaired query returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}.`,
|
|
7306
|
-
...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
|
|
7307
|
-
});
|
|
8480
|
+
// Repair never grants a second answer/narration call and never sends rows
|
|
8481
|
+
// to a provider. Present the validated contract deterministically.
|
|
8482
|
+
const presentationText = `The repaired query returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}. Review the derived SQL before reuse.`;
|
|
7308
8483
|
const sourceFailure = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
|
|
7309
8484
|
const presentationContext = repairPresentationContextFromAgentRun(sourceRun);
|
|
7310
8485
|
const executionResultFingerprint = result.executionReceipt?.resultFingerprint;
|
|
8486
|
+
const repairTotalDurationMs = Date.now() - repairStartedAtMs;
|
|
8487
|
+
const repairTelemetry = {
|
|
8488
|
+
version: 1,
|
|
8489
|
+
stageDurationsMs: {
|
|
8490
|
+
retrieval: 0,
|
|
8491
|
+
meaning: 0,
|
|
8492
|
+
repair: repairTotalDurationMs,
|
|
8493
|
+
...(typeof result.executionTime === 'number' ? { execution: result.executionTime } : {}),
|
|
8494
|
+
narration: 0,
|
|
8495
|
+
total: repairTotalDurationMs,
|
|
8496
|
+
},
|
|
8497
|
+
providerRoundTrips,
|
|
8498
|
+
toolCalls: 0,
|
|
8499
|
+
sqlExecutions: firstFailure ? 2 : 1,
|
|
8500
|
+
repairs: 1,
|
|
8501
|
+
egressReceipts: providerEgressReceipts.length,
|
|
8502
|
+
...(typeof result.executionTime === 'number' ? { warehouseDurationMs: result.executionTime } : {}),
|
|
8503
|
+
};
|
|
7311
8504
|
const artifact = agentRunArtifact('answer', 'Repaired analytical answer', {
|
|
7312
8505
|
kind: 'uncertified',
|
|
7313
8506
|
certification: 'analyst_review_required',
|
|
7314
8507
|
reviewStatus: 'analyst_review_required',
|
|
7315
8508
|
trustLabel: 'AI-generated · review required',
|
|
7316
|
-
answer:
|
|
7317
|
-
text:
|
|
8509
|
+
answer: presentationText,
|
|
8510
|
+
text: presentationText,
|
|
7318
8511
|
...presentationContext,
|
|
7319
8512
|
result,
|
|
7320
8513
|
proposedSql: repairedSql,
|
|
@@ -7335,10 +8528,11 @@ export async function startLocalServer(opts) {
|
|
|
7335
8528
|
rowCount: result.rowCount,
|
|
7336
8529
|
...(executionResultFingerprint ? { resultFingerprint: executionResultFingerprint } : {}),
|
|
7337
8530
|
},
|
|
8531
|
+
providerEgressReceipts,
|
|
7338
8532
|
},
|
|
7339
8533
|
previousAttempt: {
|
|
7340
8534
|
sourceRunId: sourceRun.id,
|
|
7341
|
-
|
|
8535
|
+
sql: repairableSqlFromAgentRun(sourceRun),
|
|
7342
8536
|
...(sourceDqlArtifact?.source ? { dql: sourceDqlArtifact.source } : {}),
|
|
7343
8537
|
failure: retainedFailure ?? firstFailure,
|
|
7344
8538
|
},
|
|
@@ -7365,7 +8559,7 @@ export async function startLocalServer(opts) {
|
|
|
7365
8559
|
artifacts: [artifact],
|
|
7366
8560
|
};
|
|
7367
8561
|
const events = [
|
|
7368
|
-
{ id: `${derivedRunId}:repair`, runId: derivedRunId, type: 'repair.attempted', at:
|
|
8562
|
+
{ id: `${derivedRunId}:repair`, runId: derivedRunId, type: 'repair.attempted', at: repairStartedAt, message: 'Started one bounded query repair.', route: 'generated_answer' },
|
|
7369
8563
|
{ id: `${derivedRunId}:artifact`, runId: derivedRunId, type: 'artifact.created', at: completedAt, message: 'Created the repaired result artifact.', route: 'generated_answer', trustState: 'review_required' },
|
|
7370
8564
|
{ id: `${derivedRunId}:complete`, runId: derivedRunId, type: 'run.completed', at: completedAt, message: 'The repaired query executed successfully.', route: 'generated_answer', status: 'needs_review', trustState: 'review_required' },
|
|
7371
8565
|
];
|
|
@@ -7377,15 +8571,15 @@ export async function startLocalServer(opts) {
|
|
|
7377
8571
|
status: 'needs_review',
|
|
7378
8572
|
trustState: 'review_required',
|
|
7379
8573
|
stopReason: 'generated_review_required',
|
|
7380
|
-
startedAt:
|
|
8574
|
+
startedAt: repairStartedAt,
|
|
7381
8575
|
completedAt,
|
|
7382
8576
|
selectedObject: sourceRun.selectedObject,
|
|
7383
8577
|
executionTarget: sourceRun.executionTarget,
|
|
7384
8578
|
routeDecision: sourceRun.routeDecision,
|
|
7385
8579
|
plan: sourceRun.plan,
|
|
7386
8580
|
steps: [repairStep],
|
|
7387
|
-
summary:
|
|
7388
|
-
answer:
|
|
8581
|
+
summary: presentationText,
|
|
8582
|
+
answer: presentationText,
|
|
7389
8583
|
answerKind: 'governed',
|
|
7390
8584
|
artifacts: [artifact],
|
|
7391
8585
|
evaluations: [evaluation],
|
|
@@ -7398,7 +8592,7 @@ export async function startLocalServer(opts) {
|
|
|
7398
8592
|
phase: 'run.completed',
|
|
7399
8593
|
revision: 1,
|
|
7400
8594
|
eventCursor: events.length,
|
|
7401
|
-
startedAt:
|
|
8595
|
+
startedAt: repairStartedAt,
|
|
7402
8596
|
updatedAt: completedAt,
|
|
7403
8597
|
completedAt,
|
|
7404
8598
|
},
|
|
@@ -7411,7 +8605,19 @@ export async function startLocalServer(opts) {
|
|
|
7411
8605
|
steps: [repairStep],
|
|
7412
8606
|
artifacts: [artifact],
|
|
7413
8607
|
evaluations: [evaluation],
|
|
8608
|
+
providerEgressReceipts,
|
|
7414
8609
|
},
|
|
8610
|
+
diagnosticReceiptV2: {
|
|
8611
|
+
version: 2,
|
|
8612
|
+
runId: derivedRunId,
|
|
8613
|
+
route: 'generated_answer',
|
|
8614
|
+
status: 'needs_review',
|
|
8615
|
+
telemetry: repairTelemetry,
|
|
8616
|
+
providerEgressReceiptFingerprints: providerEgressReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
|
|
8617
|
+
repairCapabilityFingerprint: executionFingerprint(stableExecutionValue(capability)),
|
|
8618
|
+
},
|
|
8619
|
+
telemetry: repairTelemetry,
|
|
8620
|
+
providerEgressReceipts,
|
|
7415
8621
|
derivation: {
|
|
7416
8622
|
version: 1,
|
|
7417
8623
|
kind: 'analytical_repair',
|
|
@@ -7557,21 +8763,59 @@ export async function startLocalServer(opts) {
|
|
|
7557
8763
|
let runError;
|
|
7558
8764
|
try {
|
|
7559
8765
|
const executeRun = async (operationSignal, report) => {
|
|
7560
|
-
|
|
8766
|
+
const ingressSignal = AbortSignal.any([
|
|
7561
8767
|
runController.signal,
|
|
7562
8768
|
...(operationSignal ? [operationSignal] : []),
|
|
7563
|
-
AbortSignal.timeout(agentRunDeadlineMs(parsed.request, process.env, getActiveProvider(projectRoot))),
|
|
7564
8769
|
]);
|
|
8770
|
+
parsed.request.runBudget = createAgentRunBudget({
|
|
8771
|
+
requestedMode: parsed.request.requestedMode,
|
|
8772
|
+
inheritedSignal: ingressSignal,
|
|
8773
|
+
});
|
|
8774
|
+
parsed.request.signal = parsed.request.runBudget.hardSignal;
|
|
8775
|
+
// Ordinary Ask needs room to think, write, and recover: meaning (1)
|
|
8776
|
+
// + planning/generation (3) + narration (2) + one repair. The old
|
|
8777
|
+
// `total: 2` left nothing for narration once generation had run,
|
|
8778
|
+
// which is why the answer arrived as a deterministic fact-join.
|
|
8779
|
+
const runProviderEvidence = new RunScopedProviderDispatchEvidence(parsed.request.requestedMode === 'research'
|
|
8780
|
+
? { total: 14, meaningResolution: 1, generationGroup: 11, narration: 2, repair: 1 }
|
|
8781
|
+
: { total: 6, meaningResolution: 1, generationGroup: 3, narration: 2, repair: 1 }, parsed.request.runBudget, resolveProviderResultRowEgressPolicy({
|
|
8782
|
+
projectSetting: projectConfig?.agent?.providerResultRowEgress,
|
|
8783
|
+
researchOptIn: parsed.request.requestedMode === 'research'
|
|
8784
|
+
&& parsed.request.researchResultRowsOptIn === true,
|
|
8785
|
+
}));
|
|
7565
8786
|
report?.({ phase: 'reasoning', progress: 8, message: 'Grounding the request and selecting the governed route.' });
|
|
7566
8787
|
try {
|
|
7567
|
-
const
|
|
8788
|
+
const engineRun = await agentRunProviderEvidenceContext.run(runProviderEvidence, () => agentRunEngine.run(parsed.request, (event) => {
|
|
8789
|
+
runProviderEvidence.setRoute(event.route);
|
|
7568
8790
|
if (wantsStream)
|
|
7569
8791
|
writeStream('agent-run-event', event);
|
|
7570
8792
|
report?.({ phase: event.type, progress: 45, message: event.message });
|
|
7571
8793
|
}, (delta) => {
|
|
7572
8794
|
if (wantsStream)
|
|
7573
8795
|
writeStream('agent-run-answer-delta', { runId: parsed.request.runId, delta });
|
|
7574
|
-
});
|
|
8796
|
+
}));
|
|
8797
|
+
const rawRun = mergeRunScopedProviderDispatchEvidence(engineRun, runProviderEvidence.snapshot(engineRun.telemetry?.fallbackReason ?? engineRun.stopReason));
|
|
8798
|
+
let resolvedTargetFingerprint;
|
|
8799
|
+
try {
|
|
8800
|
+
const resolvedTarget = await resolveAgentRunExecutionConnection({
|
|
8801
|
+
question: rawRun.question,
|
|
8802
|
+
executionTarget: rawRun.executionTarget,
|
|
8803
|
+
});
|
|
8804
|
+
const resolvedTargetName = rawRun.executionTarget?.target === 'connection'
|
|
8805
|
+
? rawRun.executionTarget.connectionName
|
|
8806
|
+
: rawRun.executionTarget?.target === 'local'
|
|
8807
|
+
? 'local'
|
|
8808
|
+
: projectConfig.defaultConnectionName;
|
|
8809
|
+
resolvedTargetFingerprint = targetGenerationFingerprint(resolvedTarget, resolvedTargetName);
|
|
8810
|
+
}
|
|
8811
|
+
catch {
|
|
8812
|
+
// Missing target-generation evidence keeps automatic repair
|
|
8813
|
+
// ineligible; the failed run itself remains fully inspectable.
|
|
8814
|
+
}
|
|
8815
|
+
const run = attachAnalyticalRepairCapability(rawRun, resolvedTargetFingerprint);
|
|
8816
|
+
// The engine persists before the host can validate the DQL wrapper;
|
|
8817
|
+
// replace that row with the server-owned capability atomically.
|
|
8818
|
+
await agentRunStore.save(run);
|
|
7575
8819
|
completedRun = run;
|
|
7576
8820
|
recordConversationTurn(conversationStore, parsed.request.threadId, run);
|
|
7577
8821
|
return { runId: run.id, threadId: parsed.request.threadId, status: run.status };
|
|
@@ -9637,13 +10881,22 @@ export async function startLocalServer(opts) {
|
|
|
9637
10881
|
});
|
|
9638
10882
|
if (!composed)
|
|
9639
10883
|
throw new Error('The governed semantic query could not be composed.');
|
|
9640
|
-
const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
|
|
9641
10884
|
let normalizedResult;
|
|
9642
10885
|
let repair;
|
|
9643
10886
|
let executedSql = composed.sql;
|
|
10887
|
+
let executionFingerprints;
|
|
9644
10888
|
try {
|
|
9645
|
-
const
|
|
9646
|
-
|
|
10889
|
+
const execution = await analyticalExecutionService.execute({
|
|
10890
|
+
sql: composed.sql,
|
|
10891
|
+
subject: 'App tile semantic query',
|
|
10892
|
+
connection: targetConnection,
|
|
10893
|
+
});
|
|
10894
|
+
normalizedResult = execution.result;
|
|
10895
|
+
executedSql = execution.preparation.executedSql;
|
|
10896
|
+
executionFingerprints = {
|
|
10897
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
10898
|
+
resultFingerprint: execution.resultFingerprint,
|
|
10899
|
+
};
|
|
9647
10900
|
}
|
|
9648
10901
|
catch (executionError) {
|
|
9649
10902
|
const repaired = await repairFailedAppTileExecution({
|
|
@@ -9685,6 +10938,7 @@ export async function startLocalServer(opts) {
|
|
|
9685
10938
|
title: item.title ?? item.semantic.id,
|
|
9686
10939
|
viz: item.viz,
|
|
9687
10940
|
result: normalizedResult,
|
|
10941
|
+
...(executionFingerprints ?? {}),
|
|
9688
10942
|
trustState: 'review_required',
|
|
9689
10943
|
citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
|
|
9690
10944
|
...(repair ? { repair } : {}),
|
|
@@ -9818,8 +11072,13 @@ export async function startLocalServer(opts) {
|
|
|
9818
11072
|
tileId: item.i,
|
|
9819
11073
|
tileFilterBindings: item.filterBindings,
|
|
9820
11074
|
});
|
|
9821
|
-
const
|
|
9822
|
-
|
|
11075
|
+
const execution = await analyticalExecutionService.execute({
|
|
11076
|
+
sql: blockFilterApplication.sql,
|
|
11077
|
+
subject: 'App tile block query',
|
|
11078
|
+
connection: blockTargetConnection,
|
|
11079
|
+
sqlParams: blockFilterApplication.sqlParams,
|
|
11080
|
+
variables: { ...blockFilterApplication.variables, ...blockInvocation.values },
|
|
11081
|
+
});
|
|
9823
11082
|
tiles.push({
|
|
9824
11083
|
tileId: item.i,
|
|
9825
11084
|
status: 'ok',
|
|
@@ -9829,7 +11088,9 @@ export async function startLocalServer(opts) {
|
|
|
9829
11088
|
title: item.title ?? block.name,
|
|
9830
11089
|
viz: item.viz,
|
|
9831
11090
|
chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
|
|
9832
|
-
result:
|
|
11091
|
+
result: execution.result,
|
|
11092
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
11093
|
+
resultFingerprint: execution.resultFingerprint,
|
|
9833
11094
|
filters: {
|
|
9834
11095
|
applied: blockFilterApplication.appliedFilters,
|
|
9835
11096
|
skipped: blockFilterApplication.skippedFilters,
|
|
@@ -11689,7 +12950,7 @@ export async function startLocalServer(opts) {
|
|
|
11689
12950
|
return;
|
|
11690
12951
|
}
|
|
11691
12952
|
}
|
|
11692
|
-
const draftSource = setBlockStudioStatusInSource(source, 'draft');
|
|
12953
|
+
const draftSource = markBlockStudioSourceReusable(setBlockStudioStatusInSource(source, 'draft'));
|
|
11693
12954
|
const parsed = parseBlockSourceMetadata(draftSource);
|
|
11694
12955
|
const name = typeof suppliedMetadata.name === 'string' && suppliedMetadata.name.trim()
|
|
11695
12956
|
? suppliedMetadata.name.trim()
|
|
@@ -11710,7 +12971,10 @@ export async function startLocalServer(opts) {
|
|
|
11710
12971
|
domain: typeof suppliedMetadata.domain === 'string' ? suppliedMetadata.domain : parsed.domain,
|
|
11711
12972
|
description: typeof suppliedMetadata.description === 'string' ? suppliedMetadata.description : parsed.description,
|
|
11712
12973
|
owner: typeof suppliedMetadata.owner === 'string' ? suppliedMetadata.owner : parsed.owner,
|
|
11713
|
-
tags: Array.
|
|
12974
|
+
tags: Array.from(new Set([
|
|
12975
|
+
...(Array.isArray(suppliedMetadata.tags) ? suppliedMetadata.tags.map(String) : parsed.tags),
|
|
12976
|
+
APP_SOURCE_REUSABLE_TAG,
|
|
12977
|
+
])),
|
|
11714
12978
|
lineage: Array.isArray(suppliedMetadata.lineage) ? suppliedMetadata.lineage.map(String) : undefined,
|
|
11715
12979
|
};
|
|
11716
12980
|
// A saved draft already has a durable identity, even when it lives in
|
|
@@ -11747,8 +13011,14 @@ export async function startLocalServer(opts) {
|
|
|
11747
13011
|
}
|
|
11748
13012
|
}
|
|
11749
13013
|
operationCoordinator.run(operation.id, async (signal, report) => {
|
|
11750
|
-
|
|
11751
|
-
|
|
13014
|
+
const result = await certifyBlockStudioSource(draftSource, draftPath, {
|
|
13015
|
+
enterprise: body.enterprise === true,
|
|
13016
|
+
reportProgress: (progress) => {
|
|
13017
|
+
if (signal.aborted)
|
|
13018
|
+
throw Object.assign(new Error('Certification cancelled.'), { code: 'OPERATION_CANCELLED' });
|
|
13019
|
+
report(progress);
|
|
13020
|
+
},
|
|
13021
|
+
});
|
|
11752
13022
|
const blockers = Array.from(new Set(result.checklist.blockers));
|
|
11753
13023
|
if (signal.aborted)
|
|
11754
13024
|
throw Object.assign(new Error('Certification cancelled.'), { code: 'OPERATION_CANCELLED' });
|
|
@@ -11771,7 +13041,7 @@ export async function startLocalServer(opts) {
|
|
|
11771
13041
|
block: { ...openedDraft, validation: result.validation },
|
|
11772
13042
|
};
|
|
11773
13043
|
}
|
|
11774
|
-
report({ phase: 'publishing', progress:
|
|
13044
|
+
report({ phase: 'publishing', progress: 82, message: 'Publishing the certified block.' });
|
|
11775
13045
|
const certifiedSource = setBlockStudioStatusInSource(draftSource, 'certified');
|
|
11776
13046
|
const certifiedMetadata = parseBlockSourceMetadata(certifiedSource);
|
|
11777
13047
|
const savedPath = saveBlockStudioArtifacts(projectRoot, {
|
|
@@ -11813,7 +13083,9 @@ export async function startLocalServer(opts) {
|
|
|
11813
13083
|
catch {
|
|
11814
13084
|
/* best-effort: learning capture must never block certification */
|
|
11815
13085
|
}
|
|
13086
|
+
report({ phase: 'indexing', progress: 92, message: 'Queueing the certified source index refresh.' });
|
|
11816
13087
|
const refreshOperation = scheduleProjectRefresh('block-studio-certification');
|
|
13088
|
+
report({ phase: 'indexing', progress: 96, message: 'Certified source saved; index refresh queued.' });
|
|
11817
13089
|
if (result.preview)
|
|
11818
13090
|
writeBlockStudioRunSummary(projectRoot, savedPath, certifiedSource, result.preview.result);
|
|
11819
13091
|
const openedCertified = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
|
|
@@ -12493,13 +13765,14 @@ export async function startLocalServer(opts) {
|
|
|
12493
13765
|
}));
|
|
12494
13766
|
return;
|
|
12495
13767
|
}
|
|
13768
|
+
const reusableSource = markBlockStudioSourceReusable(readiness.candidate.dqlSource);
|
|
12496
13769
|
const savedPath = saveBlockStudioArtifacts(projectRoot, {
|
|
12497
|
-
source:
|
|
13770
|
+
source: reusableSource,
|
|
12498
13771
|
name: readiness.candidate.name,
|
|
12499
13772
|
domain: readiness.candidate.domain,
|
|
12500
13773
|
description: readiness.candidate.description,
|
|
12501
13774
|
owner: readiness.candidate.owner,
|
|
12502
|
-
tags: readiness.candidate.tags,
|
|
13775
|
+
tags: Array.from(new Set([...readiness.candidate.tags, APP_SOURCE_REUSABLE_TAG])),
|
|
12503
13776
|
lineage: readiness.candidate.lineage.sourceTables,
|
|
12504
13777
|
importMeta: {
|
|
12505
13778
|
importId,
|
|
@@ -12508,7 +13781,13 @@ export async function startLocalServer(opts) {
|
|
|
12508
13781
|
sourcePath: readiness.candidate.sourcePath,
|
|
12509
13782
|
},
|
|
12510
13783
|
});
|
|
12511
|
-
const next = {
|
|
13784
|
+
const next = {
|
|
13785
|
+
...readiness.candidate,
|
|
13786
|
+
dqlSource: reusableSource,
|
|
13787
|
+
tags: Array.from(new Set([...readiness.candidate.tags, APP_SOURCE_REUSABLE_TAG])),
|
|
13788
|
+
reviewStatus: 'saved',
|
|
13789
|
+
savedPath,
|
|
13790
|
+
};
|
|
12512
13791
|
writeBlockStudioImportCandidate(projectRoot, importId, next);
|
|
12513
13792
|
await refreshLocalMetadataCatalog(projectRoot);
|
|
12514
13793
|
const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
|
|
@@ -12751,7 +14030,7 @@ export async function startLocalServer(opts) {
|
|
|
12751
14030
|
try {
|
|
12752
14031
|
const body = await readJSON(req);
|
|
12753
14032
|
const source = typeof body.source === 'string'
|
|
12754
|
-
? sanitizeAgentBlockDraftSource(body.source)
|
|
14033
|
+
? markBlockStudioSourceReusable(sanitizeAgentBlockDraftSource(body.source))
|
|
12755
14034
|
: '';
|
|
12756
14035
|
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
12757
14036
|
if (!source.trim() || !name) {
|
|
@@ -12764,7 +14043,10 @@ export async function startLocalServer(opts) {
|
|
|
12764
14043
|
name,
|
|
12765
14044
|
domain: typeof body.domain === 'string' ? body.domain : undefined,
|
|
12766
14045
|
description: typeof body.description === 'string' ? body.description : undefined,
|
|
12767
|
-
tags: Array.
|
|
14046
|
+
tags: Array.from(new Set([
|
|
14047
|
+
...(Array.isArray(body.tags) ? body.tags.map(String) : ['ai-generated', 'review-required']),
|
|
14048
|
+
APP_SOURCE_REUSABLE_TAG,
|
|
14049
|
+
])),
|
|
12768
14050
|
stableSuffix: typeof body.runId === 'string' ? body.runId : undefined,
|
|
12769
14051
|
});
|
|
12770
14052
|
const payload = openBlockStudioDocument(projectRoot, savedPath, semanticLayer);
|
|
@@ -12780,7 +14062,9 @@ export async function startLocalServer(opts) {
|
|
|
12780
14062
|
if (req.method === 'POST' && path === '/api/block-studio/save') {
|
|
12781
14063
|
try {
|
|
12782
14064
|
const body = await readJSON(req);
|
|
12783
|
-
const source = typeof body.source === 'string'
|
|
14065
|
+
const source = typeof body.source === 'string'
|
|
14066
|
+
? markBlockStudioSourceReusable(body.source)
|
|
14067
|
+
: '';
|
|
12784
14068
|
const metadata = body.metadata && typeof body.metadata === 'object'
|
|
12785
14069
|
? body.metadata
|
|
12786
14070
|
: {};
|
|
@@ -12817,7 +14101,10 @@ export async function startLocalServer(opts) {
|
|
|
12817
14101
|
folderPath: metadata.folderPath,
|
|
12818
14102
|
description: metadata.description,
|
|
12819
14103
|
owner: metadata.owner,
|
|
12820
|
-
tags: Array.
|
|
14104
|
+
tags: Array.from(new Set([
|
|
14105
|
+
...(Array.isArray(metadata.tags) ? metadata.tags.map(String) : []),
|
|
14106
|
+
APP_SOURCE_REUSABLE_TAG,
|
|
14107
|
+
])),
|
|
12821
14108
|
lineage: Array.isArray(metadata.lineage) ? metadata.lineage.map(String) : undefined,
|
|
12822
14109
|
importMeta: metadata.sourceKind || metadata.sourcePath || metadata.importId || metadata.candidateId
|
|
12823
14110
|
? {
|
|
@@ -14714,20 +16001,18 @@ export async function startLocalServer(opts) {
|
|
|
14714
16001
|
}));
|
|
14715
16002
|
return;
|
|
14716
16003
|
}
|
|
14717
|
-
const
|
|
16004
|
+
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
16005
|
+
const domain = typeof body.domain === 'string' ? body.domain : app?.domain;
|
|
16006
|
+
assertAppAccess({ app, domain, level: 'execute' });
|
|
16007
|
+
const execution = await analyticalExecutionService.execute({
|
|
14718
16008
|
sql: semantic.sql,
|
|
14719
16009
|
subject: 'Notebook query',
|
|
14720
|
-
executor,
|
|
14721
16010
|
connection: executionConnection,
|
|
14722
|
-
|
|
14723
|
-
|
|
16011
|
+
sqlParams: Array.isArray(body.sqlParams) ? body.sqlParams : [],
|
|
16012
|
+
variables: body.variables && typeof body.variables === 'object' ? body.variables : {},
|
|
16013
|
+
semanticRefs: semantic.semanticRefs,
|
|
14724
16014
|
});
|
|
14725
|
-
const
|
|
14726
|
-
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
14727
|
-
const domain = typeof body.domain === 'string' ? body.domain : app?.domain;
|
|
14728
|
-
assertAppAccess({ app, domain, level: 'execute' });
|
|
14729
|
-
const result = await executor.executeQuery(prepared.sql, Array.isArray(body.sqlParams) ? body.sqlParams : [], runtimeVariables(body.variables && typeof body.variables === 'object' ? body.variables : {}), prepared.connection);
|
|
14730
|
-
const normalized = normalizeQueryResult(result, semantic.semanticRefs);
|
|
16015
|
+
const { preparation, result: normalized } = execution;
|
|
14731
16016
|
if (execContext?.notebookPath) {
|
|
14732
16017
|
recordNotebookQueryRun(projectRoot, {
|
|
14733
16018
|
notebookPath: execContext.notebookPath,
|
|
@@ -15401,7 +16686,7 @@ export async function startLocalServer(opts) {
|
|
|
15401
16686
|
const invocation = prepareBlockInvocation({ source, surface: 'ask_ai' });
|
|
15402
16687
|
if (invocation.errors.length > 0)
|
|
15403
16688
|
throw new Error(invocation.errors.join(' '));
|
|
15404
|
-
const { executionReceipt: _receipt, compiledSql: _compiled, ...unexecuted } = artifact;
|
|
16689
|
+
const { executionReceipt: _receipt, executableArtifact: _executable, compiledSql: _compiled, ...unexecuted } = artifact;
|
|
15405
16690
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
15406
16691
|
res.end(serializeJSON({
|
|
15407
16692
|
artifact: {
|
|
@@ -15517,6 +16802,13 @@ export async function startLocalServer(opts) {
|
|
|
15517
16802
|
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
15518
16803
|
compiledSql: result.sql,
|
|
15519
16804
|
executionReceipt: result.executionReceipt,
|
|
16805
|
+
...(result.executableArtifact ? {
|
|
16806
|
+
executableArtifact: {
|
|
16807
|
+
...result.executableArtifact,
|
|
16808
|
+
kind: certified ? 'certified_block' : semantic ? 'semantic_block' : 'sql_block',
|
|
16809
|
+
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
16810
|
+
},
|
|
16811
|
+
} : {}),
|
|
15520
16812
|
...(requestedArtifact?.limit ? { limit: requestedArtifact.limit } : {}),
|
|
15521
16813
|
},
|
|
15522
16814
|
}));
|
|
@@ -15639,37 +16931,43 @@ export async function startLocalServer(opts) {
|
|
|
15639
16931
|
res.end(serializeJSON({ cellType: cell.type, result: null }));
|
|
15640
16932
|
return;
|
|
15641
16933
|
}
|
|
15642
|
-
const
|
|
16934
|
+
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
16935
|
+
assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
|
|
16936
|
+
const semanticExecutionHolder = { value: null };
|
|
16937
|
+
const execution = await analyticalExecutionService.execute({
|
|
15643
16938
|
sql: executableSql,
|
|
15644
16939
|
subject: cell.type === 'dql' ? 'DQL query' : 'Notebook query',
|
|
15645
|
-
executor,
|
|
15646
16940
|
connection: cellConnection,
|
|
15647
|
-
|
|
15648
|
-
|
|
16941
|
+
sqlParams: plan?.sqlParams,
|
|
16942
|
+
variables: { ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) },
|
|
16943
|
+
executePrepared: semanticCompose?.compiled
|
|
16944
|
+
? async (preparation) => {
|
|
16945
|
+
const semanticExecution = await executeTargetBoundSemanticQuery({
|
|
16946
|
+
executor,
|
|
16947
|
+
connection: cellConnection,
|
|
16948
|
+
projectRoot,
|
|
16949
|
+
plannedAdapter: semanticCompose.compiled.engine,
|
|
16950
|
+
metricFlow: semanticCompose.compiled.engine === 'metricflow-cli'
|
|
16951
|
+
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
16952
|
+
: undefined,
|
|
16953
|
+
// Compilation already produced the exact pinned adapter result
|
|
16954
|
+
// used by the plan. The gateway validates target identity before
|
|
16955
|
+
// accepting that result for preflight and execution.
|
|
16956
|
+
compile: async () => semanticCompose.compiled,
|
|
16957
|
+
prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
|
|
16958
|
+
rowBound: semanticCompose.compiled.effectiveRequest.limit,
|
|
16959
|
+
});
|
|
16960
|
+
if (semanticExecution) {
|
|
16961
|
+
semanticExecutionHolder.value = semanticExecution;
|
|
16962
|
+
return semanticExecution.result;
|
|
16963
|
+
}
|
|
16964
|
+
return executor.executeQuery(preparation.executedSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) }), preparation.connection);
|
|
16965
|
+
}
|
|
16966
|
+
: undefined,
|
|
15649
16967
|
});
|
|
16968
|
+
const { preparation, result: normalized } = execution;
|
|
15650
16969
|
const decodedSql = preparation.decodedSql;
|
|
15651
|
-
const
|
|
15652
|
-
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
15653
|
-
assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
|
|
15654
|
-
const semanticExecution = semanticCompose?.compiled
|
|
15655
|
-
? await executeTargetBoundSemanticQuery({
|
|
15656
|
-
executor,
|
|
15657
|
-
connection: cellConnection,
|
|
15658
|
-
projectRoot,
|
|
15659
|
-
plannedAdapter: semanticCompose.compiled.engine,
|
|
15660
|
-
metricFlow: semanticCompose.compiled.engine === 'metricflow-cli'
|
|
15661
|
-
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
15662
|
-
: undefined,
|
|
15663
|
-
// Compilation already produced the exact pinned adapter result
|
|
15664
|
-
// used by the plan. The gateway validates target identity before
|
|
15665
|
-
// accepting that result for preflight and execution.
|
|
15666
|
-
compile: async () => semanticCompose.compiled,
|
|
15667
|
-
prepareSql: () => ({ sql: prepared.sql, connection: prepared.connection }),
|
|
15668
|
-
rowBound: semanticCompose.compiled.effectiveRequest.limit,
|
|
15669
|
-
})
|
|
15670
|
-
: null;
|
|
15671
|
-
const rawResult = semanticExecution?.result ?? await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) }), prepared.connection);
|
|
15672
|
-
const normalized = normalizeQueryResult(rawResult);
|
|
16970
|
+
const semanticExecution = semanticExecutionHolder.value;
|
|
15673
16971
|
// Enforce the block's declared invariants against the result set. This
|
|
15674
16972
|
// is additive: blocks without invariants produce `null` and the
|
|
15675
16973
|
// response is unchanged. The agent surface (`query_via_block`) reads
|
|
@@ -15920,6 +17218,26 @@ table: ${table}${tagList}
|
|
|
15920
17218
|
});
|
|
15921
17219
|
});
|
|
15922
17220
|
}
|
|
17221
|
+
function providerDispatchTerminalEvidence(value) {
|
|
17222
|
+
if (!value || typeof value !== 'object')
|
|
17223
|
+
return undefined;
|
|
17224
|
+
const candidate = value.providerDispatchEvidence;
|
|
17225
|
+
if (!candidate || typeof candidate !== 'object')
|
|
17226
|
+
return undefined;
|
|
17227
|
+
const evidence = candidate;
|
|
17228
|
+
if (!Array.isArray(evidence.providerEgressReceipts)
|
|
17229
|
+
|| !Number.isInteger(evidence.providerRoundTrips)
|
|
17230
|
+
|| (evidence.providerRoundTrips ?? -1) < 0)
|
|
17231
|
+
return undefined;
|
|
17232
|
+
return {
|
|
17233
|
+
providerEgressReceipts: evidence.providerEgressReceipts,
|
|
17234
|
+
providerRoundTrips: evidence.providerRoundTrips,
|
|
17235
|
+
toolCalls: Number.isInteger(evidence.toolCalls) && (evidence.toolCalls ?? -1) >= 0 ? evidence.toolCalls : 0,
|
|
17236
|
+
sqlExecutions: Number.isInteger(evidence.sqlExecutions) && (evidence.sqlExecutions ?? -1) >= 0 ? evidence.sqlExecutions : 0,
|
|
17237
|
+
repairs: Number.isInteger(evidence.repairs) && (evidence.repairs ?? -1) >= 0 ? evidence.repairs : 0,
|
|
17238
|
+
fallbackReason: typeof evidence.fallbackReason === 'string' ? evidence.fallbackReason : 'provider_error',
|
|
17239
|
+
};
|
|
17240
|
+
}
|
|
15923
17241
|
async function validateModelingRelationship(relationship, manifest, execute, quoteIdentifier = quoteSqlIdentifier) {
|
|
15924
17242
|
const fromEntity = manifest.modeling?.entities[relationship.from];
|
|
15925
17243
|
const toEntity = manifest.modeling?.entities[relationship.to];
|
|
@@ -18604,9 +19922,104 @@ function createDqlArtifactExecutionReceipt(source, compiledSql, parameters, resu
|
|
|
18604
19922
|
})),
|
|
18605
19923
|
};
|
|
18606
19924
|
}
|
|
19925
|
+
function agentRunTelemetryForAnswer(answer, egressReceipts, totalDurationMs, narrationDurationMs, engineRepairAttempts) {
|
|
19926
|
+
const timings = new Map((answer.evidence?.timings ?? []).map((timing) => [timing.phase, timing.durationMs]));
|
|
19927
|
+
const toolCalls = answer.evidence?.toolCalls ?? [];
|
|
19928
|
+
const toolDurationMs = toolCalls.reduce((sum, call) => sum + Math.max(0, call.durationMs ?? 0), 0);
|
|
19929
|
+
const warehouseDurationMs = answer.evidence?.execution?.executionTime ?? answer.result?.executionTime;
|
|
19930
|
+
const counters = answer.evidence?.runtimeCounters;
|
|
19931
|
+
const stageDurationsMs = {
|
|
19932
|
+
...(timings.has('project_state') ? { snapshot: timings.get('project_state') } : {}),
|
|
19933
|
+
...(timings.has('context_retrieval') ? { retrieval: timings.get('context_retrieval') } : {}),
|
|
19934
|
+
...(timings.has('runtime_schema') ? { schema: timings.get('runtime_schema') } : {}),
|
|
19935
|
+
...(timings.has('source_search') ? { meaning: timings.get('source_search') } : {}),
|
|
19936
|
+
...(timings.has('answer_resolution') ? { provider: timings.get('answer_resolution') } : {}),
|
|
19937
|
+
...(toolCalls.length > 0 ? { tools: toolDurationMs } : {}),
|
|
19938
|
+
...(typeof warehouseDurationMs === 'number' ? { execution: Math.max(0, warehouseDurationMs) } : {}),
|
|
19939
|
+
...(narrationDurationMs > 0 ? { narration: narrationDurationMs } : {}),
|
|
19940
|
+
total: Math.max(0, totalDurationMs),
|
|
19941
|
+
};
|
|
19942
|
+
return {
|
|
19943
|
+
version: 1,
|
|
19944
|
+
stageDurationsMs,
|
|
19945
|
+
providerRoundTrips: counters?.providerRoundTrips ?? 0,
|
|
19946
|
+
toolCalls: counters?.toolCalls ?? toolCalls.length,
|
|
19947
|
+
sqlExecutions: counters?.sqlExecutions ?? 0,
|
|
19948
|
+
repairs: Math.max(engineRepairAttempts, counters?.repairs ?? 0),
|
|
19949
|
+
egressReceipts: egressReceipts.length,
|
|
19950
|
+
...(typeof warehouseDurationMs === 'number' ? { warehouseDurationMs: Math.max(0, warehouseDurationMs) } : {}),
|
|
19951
|
+
...(answer.refusalCode ? { fallbackReason: answer.refusalCode } : {}),
|
|
19952
|
+
};
|
|
19953
|
+
}
|
|
18607
19954
|
function executionFingerprint(value) {
|
|
18608
19955
|
return createHash('sha256').update(value).digest('hex');
|
|
18609
19956
|
}
|
|
19957
|
+
function buildSemanticAggregationCompilerReceipt(input) {
|
|
19958
|
+
const plan = input.plan;
|
|
19959
|
+
const metricId = plan?.analyticalFrame?.metricConceptIds[0];
|
|
19960
|
+
if (input.metricNames.length !== 1 || !plan || !metricId || !plan.executionId
|
|
19961
|
+
|| !plan.selectedCapabilityFingerprint || !input.targetFingerprint)
|
|
19962
|
+
return undefined;
|
|
19963
|
+
const metric = input.semanticLayer.getMetric(input.metricNames[0]);
|
|
19964
|
+
if (!metric || (metric.aggregation ?? metric.metricType)?.toLowerCase() !== 'ratio')
|
|
19965
|
+
return undefined;
|
|
19966
|
+
const refName = (value) => {
|
|
19967
|
+
if (typeof value === 'string' && value.trim())
|
|
19968
|
+
return value.trim();
|
|
19969
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
19970
|
+
return undefined;
|
|
19971
|
+
const name = value.name;
|
|
19972
|
+
return typeof name === 'string' && name.trim() ? name.trim() : undefined;
|
|
19973
|
+
};
|
|
19974
|
+
const numeratorName = refName(metric.typeParams?.numerator);
|
|
19975
|
+
const denominatorName = refName(metric.typeParams?.denominator);
|
|
19976
|
+
if (!numeratorName || !denominatorName || !metric.sql.trim())
|
|
19977
|
+
return undefined;
|
|
19978
|
+
const measures = [numeratorName, denominatorName].map((name) => {
|
|
19979
|
+
const matches = input.semanticLayer.listMeasures().filter((measure) => measure.name === name
|
|
19980
|
+
&& (!metric.cube || !measure.cube || measure.cube === metric.cube));
|
|
19981
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
19982
|
+
});
|
|
19983
|
+
if (measures.some((measure) => !measure))
|
|
19984
|
+
return undefined;
|
|
19985
|
+
const relations = Array.from(new Set(measures.map((measure) => measure.table.trim()).filter(Boolean)));
|
|
19986
|
+
if (relations.length !== 1)
|
|
19987
|
+
return undefined;
|
|
19988
|
+
const outputAlias = metricId.split(/[:.]/).at(-1);
|
|
19989
|
+
if (!outputAlias)
|
|
19990
|
+
return undefined;
|
|
19991
|
+
const orderedMeasureIds = measures.map((measure) => `semantic:${measure.domain || 'uncategorized'}:measure:${measure.cube ? `${measure.cube}.` : ''}${measure.name}`);
|
|
19992
|
+
const compiledSqlFingerprint = semanticExecutionFingerprint(input.compiledSql);
|
|
19993
|
+
const compiledOutputExpression = buildSqlOutputExpressionSignature(input.compiledSql, outputAlias);
|
|
19994
|
+
if (!compiledOutputExpression)
|
|
19995
|
+
return undefined;
|
|
19996
|
+
const compiledExpressionFingerprint = semanticExecutionFingerprint(compiledOutputExpression.canonicalExpression);
|
|
19997
|
+
const compiledSignature = buildGeneratedAnalyticalSqlSignature(input.compiledSql);
|
|
19998
|
+
if (!compiledSignature)
|
|
19999
|
+
return undefined;
|
|
20000
|
+
const body = {
|
|
20001
|
+
version: 1,
|
|
20002
|
+
executionId: plan.executionId,
|
|
20003
|
+
capabilityFingerprint: plan.selectedCapabilityFingerprint,
|
|
20004
|
+
metricId,
|
|
20005
|
+
metricExpressionSql: metric.sql,
|
|
20006
|
+
operator: 'ratio',
|
|
20007
|
+
orderedMeasureIds,
|
|
20008
|
+
physicalRelation: relations[0],
|
|
20009
|
+
relationAliases: Array.from(new Set([metric.cube, ...measures.map((measure) => measure.cube)].filter((value) => Boolean(value)))),
|
|
20010
|
+
outputAlias,
|
|
20011
|
+
dimensionIds: [...new Set(plan.analyticalFrame?.dimensions.map((dimension) => dimension.dimensionId) ?? [])].sort(),
|
|
20012
|
+
relationshipPathIds: [...plan.relationshipPathIds].sort(),
|
|
20013
|
+
relationshipProofFingerprints: [...new Set(plan.relationshipProofs?.map((proof) => proof.authorityFingerprint) ?? [])].sort(),
|
|
20014
|
+
orderBy: compiledSignature.orderBy,
|
|
20015
|
+
compiledSqlFingerprint,
|
|
20016
|
+
compiledExpressionFingerprint,
|
|
20017
|
+
planFingerprint: plan.fingerprint,
|
|
20018
|
+
snapshotId: plan.snapshotId,
|
|
20019
|
+
targetFingerprint: input.targetFingerprint,
|
|
20020
|
+
};
|
|
20021
|
+
return { ...body, receiptFingerprint: semanticExecutionFingerprint(body) };
|
|
20022
|
+
}
|
|
18610
20023
|
/**
|
|
18611
20024
|
* Older clients normalized executable DQL with trim(), while the receipt was
|
|
18612
20025
|
* correctly created from the original renderer output ending in one newline.
|
|
@@ -19024,6 +20437,43 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
|
|
|
19024
20437
|
connection: normalizedConnection,
|
|
19025
20438
|
};
|
|
19026
20439
|
}
|
|
20440
|
+
/**
|
|
20441
|
+
* The single connector boundary for Ask, Notebook, and artifact execution.
|
|
20442
|
+
* Callers compile their route-specific source first; this service owns target
|
|
20443
|
+
* preparation, parameters, execution, and result normalization after that.
|
|
20444
|
+
*/
|
|
20445
|
+
export class ExecutionService {
|
|
20446
|
+
host;
|
|
20447
|
+
constructor(host) {
|
|
20448
|
+
this.host = host;
|
|
20449
|
+
}
|
|
20450
|
+
async execute(input) {
|
|
20451
|
+
const preparation = await prepareAnalyticalExecutionSql({
|
|
20452
|
+
sql: input.sql,
|
|
20453
|
+
subject: input.subject,
|
|
20454
|
+
executor: this.host.executor,
|
|
20455
|
+
connection: input.connection,
|
|
20456
|
+
projectRoot: this.host.projectRoot,
|
|
20457
|
+
projectConfig: this.host.projectConfig(),
|
|
20458
|
+
enforceReadOnly: input.enforceReadOnly,
|
|
20459
|
+
rowLimit: input.rowLimit,
|
|
20460
|
+
});
|
|
20461
|
+
const raw = input.executePrepared
|
|
20462
|
+
? await input.executePrepared(preparation)
|
|
20463
|
+
: await this.host.executor.executeQuery(preparation.executedSql, input.sqlParams ?? [], runtimeVariables(input.variables ?? {}), preparation.connection);
|
|
20464
|
+
const result = normalizeQueryResult(raw, input.semanticRefs);
|
|
20465
|
+
return {
|
|
20466
|
+
preparation,
|
|
20467
|
+
result,
|
|
20468
|
+
compiledSqlFingerprint: executionFingerprint(preparation.executedSql),
|
|
20469
|
+
resultFingerprint: executionFingerprint(stableExecutionValue({
|
|
20470
|
+
columns: result.columns,
|
|
20471
|
+
rows: result.rows,
|
|
20472
|
+
rowCount: result.rowCount,
|
|
20473
|
+
})),
|
|
20474
|
+
};
|
|
20475
|
+
}
|
|
20476
|
+
}
|
|
19027
20477
|
/**
|
|
19028
20478
|
* One host-owned SQL preparation boundary shared by Ask and Notebook.
|
|
19029
20479
|
*
|
|
@@ -21745,14 +23195,18 @@ export function validateBlockStudioSource(source, semanticLayer) {
|
|
|
21745
23195
|
};
|
|
21746
23196
|
}
|
|
21747
23197
|
export async function createDqlArtifactGenerationSessionForProject(projectRoot, options, semanticLayer) {
|
|
21748
|
-
const
|
|
21749
|
-
if (!
|
|
23198
|
+
const normalizedDqlArtifact = normalizeDqlArtifactReference(options.dqlArtifact);
|
|
23199
|
+
if (!normalizedDqlArtifact)
|
|
21750
23200
|
throw new Error('A DQL artifact with source is required for DQL draft promotion.');
|
|
23201
|
+
const dqlArtifact = {
|
|
23202
|
+
...normalizedDqlArtifact,
|
|
23203
|
+
source: markBlockStudioSourceReusable(normalizedDqlArtifact.source),
|
|
23204
|
+
};
|
|
21751
23205
|
const question = notebookResearchString(options.question) ?? dqlArtifact.name ?? 'Generated DQL artifact';
|
|
21752
23206
|
const slug = deriveGeneratedDraftSlug(dqlArtifact.name ?? question);
|
|
21753
23207
|
const domain = notebookResearchString(options.domain) ?? 'misc';
|
|
21754
23208
|
const owner = notebookResearchString(options.owner) ?? '';
|
|
21755
|
-
const tags = Array.from(new Set(['notebook-research', 'review-required', 'dql-artifact', ...(Array.isArray(options.tags) ? options.tags : [])]
|
|
23209
|
+
const tags = Array.from(new Set(['notebook-research', 'review-required', 'dql-artifact', APP_SOURCE_REUSABLE_TAG, ...(Array.isArray(options.tags) ? options.tags : [])]
|
|
21756
23210
|
.map((tag) => notebookResearchString(tag))
|
|
21757
23211
|
.filter((tag) => Boolean(tag))));
|
|
21758
23212
|
const proposedContractId = `${domain}.Unknown.${slug}`;
|
|
@@ -22646,6 +24100,23 @@ export function parseBlockSourceMetadata(source) {
|
|
|
22646
24100
|
metricsRef: extractStringArray('metrics'),
|
|
22647
24101
|
};
|
|
22648
24102
|
}
|
|
24103
|
+
/**
|
|
24104
|
+
* Persist the explicit App-source reuse decision in canonical DQL. Generated
|
|
24105
|
+
* execution stays transient; only Block Studio save/promotion routes call this
|
|
24106
|
+
* helper. The marker is a normal parser-supported tag, so refresh/reindex can
|
|
24107
|
+
* make the participation decision without relying on filenames or prose.
|
|
24108
|
+
*/
|
|
24109
|
+
export function markBlockStudioSourceReusable(source) {
|
|
24110
|
+
const parsed = parseBlockSourceMetadata(source);
|
|
24111
|
+
if (parsed.tags.some((tag) => tag.trim().toLowerCase() === APP_SOURCE_REUSABLE_TAG))
|
|
24112
|
+
return source;
|
|
24113
|
+
const tags = Array.from(new Set([...parsed.tags, APP_SOURCE_REUSABLE_TAG]));
|
|
24114
|
+
const rendered = tags.map((tag) => JSON.stringify(tag)).join(', ');
|
|
24115
|
+
if (/\btags\s*=\s*\[[\s\S]*?\]/i.test(source)) {
|
|
24116
|
+
return source.replace(/\btags\s*=\s*\[[\s\S]*?\]/i, `tags = [${rendered}]`);
|
|
24117
|
+
}
|
|
24118
|
+
return source.replace(/(\bblock\s+"[^"]+"\s*\{)/i, `$1\n tags = [${rendered}]`);
|
|
24119
|
+
}
|
|
22649
24120
|
function compareBlockStudioValues(actual, operator, expected) {
|
|
22650
24121
|
const expectedValue = normalizeBlockStudioExpected(expected);
|
|
22651
24122
|
if (operator === '==' || operator === '=')
|