@duckcodeailabs/dql-cli 1.13.4 → 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/assets/dql-notebook/assets/{AgentLogPage-DYmHO6_i.js → AgentLogPage-BzQOKjyV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-BfhaOd1h.js → AiBuildDialog-CTxha499.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-DLKFAe6R.js → AiBuildResult--MuD_I6g.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiSidePanel-C6Z3URGr.js → AiSidePanel-DcI4PMJ1.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AnalyticsHome-uG3xCP_s.js → AnalyticsHome-Dq54vjNz.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AppsView-DfieNjkh.js → AppsView-0SlwWYex.js} +4 -4
- package/dist/assets/dql-notebook/assets/{BlockStudio-90J_wvWL.js → BlockStudio-CxxXZD3M.js} +1 -1
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-_zSMae5q.js → BusinessArtifactView-DxdAmeUN.js} +1 -1
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-DoXiPssu.js → DbtFirstModelingPage-y81gFg_b.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GitPage-CUfAhS67.js → GitPage-GQtcwncb.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-ovtL7nQu.js → GlobalAiRail-DM4wkxR_.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-CaMiKAzA.js → GovernedContextPage-B8Ft6JES.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-CEGUx28b.js → HelpDocsPage-DnD4nMuF.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-2v3SRf8V.js → HomePage-Ehb0ITj-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDAG-AaqLEiBN.js → LineageDAG-Lvjc2AQX.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDetailView-CFc8pm91.js → LineageDetailView-DX32pfbp.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-DOkFjPq-.js → LineageDrawer-CFq3jPfk.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-BrDwCuMs.js → LineagePathBreadcrumb-CVHXuHls.js} +1 -1
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-CiHtQs3y.js → MiniLineageGraph-BKaeT5Kt.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-DAgJZbws.js → NewBlockModal-DI-JDzFH.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-xjG7nZ3O.js → NewNotebookModal-Csw6eF74.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-CXZaKfYT.js → NotebookEditor-FEqs3789.js} +1 -1
- package/dist/assets/dql-notebook/assets/{ReadinessPage-MuJBFwwg.js → ReadinessPage-Bk0S_MEA.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-vKFtKtjh.js → SetupOnboarding-DyLaPFGn.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SkillsPage-DCFVOV5f.js → SkillsPage-DLHyvhci.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-TcwXFFHK.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-DCFWlHB7.js → answer-to-notebook-CFEJHLvs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-BLnlJAtp.js → arrow-left-B-Zdcyvm.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-BmUAb1aW.js → arrow-right-Cn8TM7cp.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-bdSChSag.js → book-open-text-Bko2NNUs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-Cc6dEaf4.js → circle-x-AWJAmUBB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-CPebQKQ9.js → dagre.esm-Cl_ucrRh.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-rmXvubf3.js → external-link-DN57tb5f.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-Sg4zd5-Q.js → grip-vertical-BOWwFFva.js} +1 -1
- package/dist/assets/dql-notebook/assets/{index-mQd_pC-P.js → index-Ck-wqvV2.js} +127 -127
- package/dist/assets/dql-notebook/assets/{link-2-BLaPoFxW.js → link-2-xUhbfBs8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-j0s4asFK.js → list-tree-BDWcBr67.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-Cl4pef4s.js → minimize-2-CEeNoMTS.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-Clz-FtWi.js → panel-right-open-DnSeUvOY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{play-DFdazMg7.js → play-EoDj8-fa.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-C3tTaR7s.js → rotate-ccw-BAMxWT9A.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-DIAhTlfy.js → semantic-fields-BwLOs1kl.js} +1 -1
- package/dist/assets/dql-notebook/assets/{sliders-horizontal-BUiu2tfY.js → sliders-horizontal-BYkm8SWW.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star--Yq77aH4.js → star-sVexqNCs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-D5Ng-xZO.js → triangle-alert-D2njv4o4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{upload-L8BKQAxP.js → upload-CXTMxIA8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DBu97YaM.js → usePersistedAgentThreadId-CPDeaAgL.js} +1 -1
- package/dist/assets/dql-notebook/assets/{user-round-BK5N1cJW.js → user-round-zVsI_uud.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-D8XDQD9z.js → wand-sparkles-BMIwIzXT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-Bl9zwG0q.js → workflow-Dj9y1sNk.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-C-zLCMbW.js → wrench-DPQi6zrs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-stiJ-sP7.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-runtime.d.ts +176 -4
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +1775 -372
- 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/UnifiedAgentRunPanel-Cu-YgIe4.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';
|
|
@@ -291,6 +292,12 @@ export function parseAgentRunRequestBody(body) {
|
|
|
291
292
|
const workspaceContext = agentRunRecord(record.workspaceContext) ?? agentRunRecord(record.context);
|
|
292
293
|
const signals = agentRunRecord(record.signals);
|
|
293
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
|
+
}
|
|
294
301
|
const audience = record.audience === 'stakeholder' || record.audience === 'analyst'
|
|
295
302
|
? record.audience
|
|
296
303
|
: undefined;
|
|
@@ -306,29 +313,47 @@ export function parseAgentRunRequestBody(body) {
|
|
|
306
313
|
selectedObject,
|
|
307
314
|
executionTarget,
|
|
308
315
|
workspaceContext,
|
|
309
|
-
conversationContext: agentRunRecord(record.conversationContext),
|
|
316
|
+
conversationContext: sanitizeClientConversationContext(agentRunRecord(record.conversationContext)),
|
|
310
317
|
history: parseAgentRunHistory(record.history),
|
|
311
318
|
threadId: agentRunString(record.threadId),
|
|
312
319
|
runId: agentRunString(record.runId),
|
|
313
320
|
reasoningEffort: parseAgentRunReasoningEffort(record.reasoningEffort),
|
|
314
321
|
analysisDepth: parseAgentRunAnalysisDepth(record.analysisDepth) ?? parseAgentRunAnalysisDepth(record.depth),
|
|
315
322
|
thinkingMode: coerceThinkingMode(record.thinkingMode),
|
|
323
|
+
researchResultRowsOptIn: requestedMode === 'research' && record.researchResultRowsOptIn === true,
|
|
316
324
|
},
|
|
317
325
|
};
|
|
318
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
|
+
}
|
|
319
350
|
const AGENT_LOOKUP_DEADLINE_MS = 45_000;
|
|
320
351
|
const AGENT_RESEARCH_DEADLINE_MS = 120_000;
|
|
321
|
-
/** Env-tunable run deadline (e.g. slow subscription-CLI providers), clamped to sane bounds. */
|
|
322
|
-
function resolveAgentDeadlineMs(envKey, fallback, env = process.env) {
|
|
323
|
-
const configured = Number(env[envKey]);
|
|
324
|
-
if (!Number.isFinite(configured) || configured <= 0)
|
|
325
|
-
return fallback;
|
|
326
|
-
return Math.max(15_000, Math.min(600_000, Math.floor(configured)));
|
|
327
|
-
}
|
|
328
352
|
/**
|
|
329
353
|
* PERF-002: one wall-clock budget follows the request through routing, provider
|
|
330
354
|
* calls, repair, and execution. Ordinary Ask never inherits Research's budget
|
|
331
|
-
*
|
|
355
|
+
* because of question wording, depth, provider, or relational shape; only an
|
|
356
|
+
* explicit `requestedMode: research` activates the Research budget.
|
|
332
357
|
*/
|
|
333
358
|
/** Resolve governed display formats for result columns from the semantic layer (best-effort, cached per call). */
|
|
334
359
|
function agentColumnDisplayFormats(projectRoot, columns) {
|
|
@@ -346,91 +371,63 @@ function agentColumnDisplayFormats(projectRoot, columns) {
|
|
|
346
371
|
catch { /* formatting is presentation-only; never fail an answer over it */ }
|
|
347
372
|
return formats;
|
|
348
373
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
/** High reasoning effort makes every LLM call materially slower, so the same
|
|
356
|
-
* multi-step build that fits a budget at medium effort overruns it at high. The
|
|
357
|
-
* budget must scale with effort or "High · thorough" reliably times out. */
|
|
358
|
-
function reasoningEffortBudgetMultiplier(effort) {
|
|
359
|
-
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;
|
|
360
380
|
}
|
|
361
381
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
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.
|
|
371
395
|
*/
|
|
372
|
-
function
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return plan.requestedShape.dimensions.length > 0
|
|
376
|
-
|| plan.dimensionTerms.length > 0
|
|
377
|
-
|| plan.requestedShape.filters.length > 0
|
|
378
|
-
|| plan.timeTerms.length > 0
|
|
379
|
-
|| Boolean(plan.requestedShape.topN);
|
|
380
|
-
}
|
|
381
|
-
export function agentRunDeadlineMs(request, env = process.env, activeProviderId) {
|
|
382
|
-
const cliProvider = activeProviderId === 'claude-code' || activeProviderId === 'codex';
|
|
383
|
-
const lookupDeadline = resolveAgentDeadlineMs('DQL_AGENT_LOOKUP_DEADLINE_MS', cliProvider ? AGENT_CLI_LOOKUP_DEADLINE_MS : AGENT_LOOKUP_DEADLINE_MS, env);
|
|
384
|
-
const researchDeadline = resolveAgentDeadlineMs('DQL_AGENT_RESEARCH_DEADLINE_MS', cliProvider ? AGENT_CLI_RESEARCH_DEADLINE_MS : AGENT_RESEARCH_DEADLINE_MS, env);
|
|
385
|
-
// The "Thinking" control sends a thinkingMode; explicit reasoningEffort/
|
|
386
|
-
// analysisDepth (CLI flags) take precedence but the mode is the common case.
|
|
387
|
-
// The deadline MUST see the resolved values — "High" both slows every call
|
|
388
|
-
// AND means deep — or picking High reliably times out at the lookup budget.
|
|
389
|
-
const resolvedMode = request.thinkingMode ? resolveThinkingMode(request.thinkingMode) : {};
|
|
390
|
-
const effectiveEffort = request.reasoningEffort ?? resolvedMode.reasoningEffort;
|
|
391
|
-
const effectiveDepth = request.analysisDepth ?? resolvedMode.analysisDepth;
|
|
392
|
-
// The budget is a CEILING, not a target — a fast answer returns immediately;
|
|
393
|
-
// the larger tiers only matter when the build genuinely needs the time.
|
|
394
|
-
const scale = (ms) => Math.min(600_000, Math.round(ms * reasoningEffortBudgetMultiplier(effectiveEffort)));
|
|
395
|
-
if (request.requestedMode === 'research' || effectiveDepth === 'deep') {
|
|
396
|
-
return scale(researchDeadline);
|
|
397
|
-
}
|
|
398
|
-
// A structured clarification chip displays a compact metric/path label, but
|
|
399
|
-
// the engine restores and executes the original analytical question. Budget
|
|
400
|
-
// from that same original question as well. Otherwise a complex ranking or
|
|
401
|
-
// cross-model request can incorrectly inherit the 45s scalar-lookup budget
|
|
402
|
-
// merely because the selected label is short, then time out after the user
|
|
403
|
-
// already supplied the exact governed meaning.
|
|
404
|
-
const deadlineQuestion = request.selectedEvidenceId
|
|
405
|
-
? request.clarificationSourceQuestion?.trim() || request.question
|
|
406
|
-
: request.question;
|
|
407
|
-
const plan = buildAnalysisQuestionPlan(deadlineQuestion);
|
|
408
|
-
if (plan.needsResearchWorkspace || isAnalyticalBuildQuestion(plan)) {
|
|
409
|
-
return scale(researchDeadline);
|
|
410
|
-
}
|
|
411
|
-
return scale(lookupDeadline);
|
|
412
|
-
}
|
|
413
|
-
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.
|
|
414
399
|
if (governedAnswer.kind === 'no_answer')
|
|
415
|
-
return
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
//
|
|
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 };
|
|
420
408
|
if (governedAnswer.result)
|
|
421
|
-
return
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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';
|
|
434
431
|
}
|
|
435
432
|
export function agentAnswerHasExecutionFailure(governedAnswer) {
|
|
436
433
|
return typeof governedAnswer.executionError === 'string'
|
|
@@ -544,6 +541,194 @@ export function warehouseFailureFromAgentRun(run) {
|
|
|
544
541
|
}
|
|
545
542
|
return undefined;
|
|
546
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
|
+
}
|
|
547
732
|
/**
|
|
548
733
|
* Retain planning and retrieval evidence for a successful derived repair while
|
|
549
734
|
* deliberately dropping the source failure and any result-bound facts. This
|
|
@@ -656,7 +841,7 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
656
841
|
});
|
|
657
842
|
}
|
|
658
843
|
return {
|
|
659
|
-
...(clientContext ?? {}),
|
|
844
|
+
...(sanitizeClientConversationContext(clientContext) ?? {}),
|
|
660
845
|
conversationStateVersion: 1,
|
|
661
846
|
threadId,
|
|
662
847
|
...(serverSnapshot ? { conversationEnvelope: serverSnapshot } : {}),
|
|
@@ -783,7 +968,9 @@ export function conversationTurnInputFromRun(run) {
|
|
|
783
968
|
?? run.artifacts.find((candidate) => candidate.kind === 'research_run')
|
|
784
969
|
?? run.artifacts[0];
|
|
785
970
|
const payload = agentRunRecord(artifact?.payload);
|
|
786
|
-
|
|
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);
|
|
787
974
|
const columns = conversationResultColumns(result?.columns);
|
|
788
975
|
// The visual preview stays tiny, but member resolution needs a wider bounded
|
|
789
976
|
// value window. Deriving dimensions from only the eight preview rows caused a
|
|
@@ -924,6 +1111,230 @@ function apiErrorCode(error, fallback) {
|
|
|
924
1111
|
function apiErrorMessage(error) {
|
|
925
1112
|
return error instanceof Error ? error.message : String(error);
|
|
926
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
|
+
}
|
|
927
1338
|
export async function startLocalServer(opts) {
|
|
928
1339
|
const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
|
|
929
1340
|
const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
|
|
@@ -945,6 +1356,11 @@ export async function startLocalServer(opts) {
|
|
|
945
1356
|
if (gitRoot)
|
|
946
1357
|
ensureLocalRuntimeGitignore(projectRoot);
|
|
947
1358
|
let projectConfig = loadProjectConfig(projectRoot);
|
|
1359
|
+
const analyticalExecutionService = new ExecutionService({
|
|
1360
|
+
executor,
|
|
1361
|
+
projectRoot,
|
|
1362
|
+
projectConfig: () => projectConfig,
|
|
1363
|
+
});
|
|
948
1364
|
recordAgentRuntimeVersion(projectRoot, runtimeVersion);
|
|
949
1365
|
// A clone carries governed Hint Graph files in Git, never the rebuildable
|
|
950
1366
|
// `.dql/cache` database. Materialize that projection as soon as the Notebook
|
|
@@ -1566,23 +1982,81 @@ export async function startLocalServer(opts) {
|
|
|
1566
1982
|
};
|
|
1567
1983
|
// Provider-backed narration for stakeholder stories. Reuses the same provider
|
|
1568
1984
|
// adapter as the planner; narrateResult always returns (deterministic fallback).
|
|
1569
|
-
const narrateForAgentRun = async (input) =>
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
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
|
+
};
|
|
1577
2034
|
async function runGovernedAgentAnswerForRun(request, repair, route = 'generated_answer', onProgress, routeDecision) {
|
|
1578
2035
|
const governed = resolveGovernedAnswerRunner(projectRoot);
|
|
1579
|
-
|
|
1580
|
-
|
|
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
|
+
}
|
|
1581
2054
|
if (!resolvedProvider || !runner) {
|
|
1582
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.');
|
|
1583
2056
|
}
|
|
1584
2057
|
let governedAnswer;
|
|
1585
2058
|
let providerError;
|
|
2059
|
+
let providerDispatchEvidence;
|
|
1586
2060
|
const isRepair = (repair?.attempt ?? 0) > 0 && Boolean(repair?.repairHint);
|
|
1587
2061
|
// The chat composer sends a `thinkingMode` (auto/low/medium/high); resolve it
|
|
1588
2062
|
// into the effort+depth bundle it stands for. An explicit `reasoningEffort` /
|
|
@@ -1642,6 +2116,9 @@ export async function startLocalServer(opts) {
|
|
|
1642
2116
|
const semanticTableMapping = semanticLayer && semanticConnection
|
|
1643
2117
|
? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer, projectRoot, semanticConnectionName)
|
|
1644
2118
|
: undefined;
|
|
2119
|
+
const generatedProposalTargetIdentity = semanticConnection
|
|
2120
|
+
? await observeWarehouseTargetIdentity(executor, semanticConnection)
|
|
2121
|
+
: undefined;
|
|
1645
2122
|
const requestedDomain = normalizeAgentRunDomain(agentRunWorkspaceValue(request, 'domain'));
|
|
1646
2123
|
const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
|
|
1647
2124
|
const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
|
|
@@ -1659,6 +2136,9 @@ export async function startLocalServer(opts) {
|
|
|
1659
2136
|
: undefined;
|
|
1660
2137
|
await runner.run({
|
|
1661
2138
|
provider: resolvedProvider,
|
|
2139
|
+
...(agentRunProviderEvidenceContext.getStore()
|
|
2140
|
+
? { providerDispatchEvidenceSink: agentRunProviderEvidenceContext.getStore() }
|
|
2141
|
+
: {}),
|
|
1662
2142
|
messages: [
|
|
1663
2143
|
// No-thread fallback path: bound the raw client history so follow-ups
|
|
1664
2144
|
// cannot inflate every provider call (8 turns × 4k chars).
|
|
@@ -1678,6 +2158,8 @@ export async function startLocalServer(opts) {
|
|
|
1678
2158
|
},
|
|
1679
2159
|
reasoningEffort,
|
|
1680
2160
|
...(analysisDepth ? { analysisDepth } : {}),
|
|
2161
|
+
allowProviderSemanticMemberSelection: route === 'research',
|
|
2162
|
+
researchResultRowsOptIn: route === 'research' && request.researchResultRowsOptIn === true,
|
|
1681
2163
|
projectRoot,
|
|
1682
2164
|
preparedContextPack: preparedAgentContextPacks.get(request),
|
|
1683
2165
|
domainContext,
|
|
@@ -1799,9 +2281,20 @@ export async function startLocalServer(opts) {
|
|
|
1799
2281
|
: projectConfig.dbt?.projectDir;
|
|
1800
2282
|
throw new SemanticRuntimeRequiredError(await explainMissingSemanticRuntime(projectRoot, dbtProjectPath));
|
|
1801
2283
|
}
|
|
1802
|
-
|
|
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
|
+
});
|
|
1803
2296
|
return {
|
|
1804
|
-
sql:
|
|
2297
|
+
sql: governedCompiledSql,
|
|
1805
2298
|
engine: compiled.engine,
|
|
1806
2299
|
selection: {
|
|
1807
2300
|
...selection,
|
|
@@ -1814,7 +2307,9 @@ export async function startLocalServer(opts) {
|
|
|
1814
2307
|
trace: {
|
|
1815
2308
|
...compiled.semanticTrace,
|
|
1816
2309
|
...(targetBinding ? { targetBinding } : {}),
|
|
2310
|
+
...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
|
|
1817
2311
|
},
|
|
2312
|
+
...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
|
|
1818
2313
|
};
|
|
1819
2314
|
},
|
|
1820
2315
|
} : {}),
|
|
@@ -1827,6 +2322,9 @@ export async function startLocalServer(opts) {
|
|
|
1827
2322
|
...(routeDecision?.resolvedAnalyticalPlan
|
|
1828
2323
|
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
1829
2324
|
: {}),
|
|
2325
|
+
...(generatedProposalTargetIdentity?.identityFingerprint
|
|
2326
|
+
? { generatedProposalTargetFingerprint: generatedProposalTargetIdentity.identityFingerprint }
|
|
2327
|
+
: {}),
|
|
1830
2328
|
analyticalReferenceInstant: new Date().toISOString(),
|
|
1831
2329
|
executeCertifiedBlock: (node, invocation) => executeCertifiedBlockForAgent(node, invocation, semanticConnection, semanticConnectionName),
|
|
1832
2330
|
executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName),
|
|
@@ -1843,10 +2341,11 @@ export async function startLocalServer(opts) {
|
|
|
1843
2341
|
}
|
|
1844
2342
|
if (turn.kind === 'error') {
|
|
1845
2343
|
providerError = turn.message;
|
|
2344
|
+
providerDispatchEvidence = turn.dispatchEvidence;
|
|
1846
2345
|
}
|
|
1847
2346
|
}, runSignal);
|
|
1848
2347
|
if (!governedAnswer) {
|
|
1849
|
-
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 }] : []));
|
|
1850
2349
|
}
|
|
1851
2350
|
return governedAnswer;
|
|
1852
2351
|
}
|
|
@@ -2073,7 +2572,11 @@ export async function startLocalServer(opts) {
|
|
|
2073
2572
|
surface: 'ask_ai',
|
|
2074
2573
|
});
|
|
2075
2574
|
const certified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
2076
|
-
|
|
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);
|
|
2077
2580
|
governedAnswer.dqlArtifact = {
|
|
2078
2581
|
...artifact,
|
|
2079
2582
|
...(invocation.parameters.length > 0 ? { parameters: invocation.parameters } : {}),
|
|
@@ -2082,11 +2585,19 @@ export async function startLocalServer(opts) {
|
|
|
2082
2585
|
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
2083
2586
|
...(governedAnswer.result?.sql ? { compiledSql: governedAnswer.result.sql } : originalSql ? { compiledSql: originalSql } : {}),
|
|
2084
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
|
+
} : {}),
|
|
2085
2595
|
};
|
|
2086
2596
|
if (governedAnswer.result)
|
|
2087
2597
|
delete governedAnswer.result.dqlArtifact;
|
|
2088
2598
|
}
|
|
2089
2599
|
const answerRunExecutor = async ({ request, route, routeDecision, attempt, repairHint, emit }) => {
|
|
2600
|
+
const runStartedAtMs = Date.now();
|
|
2090
2601
|
let governedAnswer;
|
|
2091
2602
|
try {
|
|
2092
2603
|
governedAnswer = await runGovernedAgentAnswerForRun(request, { attempt, repairHint }, route, (message) => emit({ type: 'executor.started', message, route }), routeDecision);
|
|
@@ -2222,39 +2733,67 @@ export async function startLocalServer(opts) {
|
|
|
2222
2733
|
// Deadline/cancellation propagates to the engine, which renders the
|
|
2223
2734
|
// graceful "bounded execution deadline" outcome — never a provider error.
|
|
2224
2735
|
rethrowIfCancelled(error, request.signal);
|
|
2225
|
-
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');
|
|
2226
2743
|
return {
|
|
2227
2744
|
summary: message,
|
|
2228
2745
|
status: 'blocked',
|
|
2229
2746
|
trustState: 'blocked',
|
|
2230
2747
|
stopReason: 'blocked',
|
|
2231
|
-
artifacts: [agentRunArtifact('answer', 'AI answer provider failed', {
|
|
2748
|
+
artifacts: [agentRunArtifact('answer', dispatchBudgetExhausted ? 'Internal orchestration budget exhausted' : 'AI answer provider failed', {
|
|
2232
2749
|
kind: 'no_answer',
|
|
2233
|
-
refusalCode: 'provider_error',
|
|
2750
|
+
refusalCode: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'provider_error',
|
|
2234
2751
|
text: message,
|
|
2235
2752
|
answer: message,
|
|
2236
2753
|
...(routeDecision?.resolvedAnalyticalPlan
|
|
2237
2754
|
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
2238
2755
|
: {}),
|
|
2239
2756
|
providerFailure: {
|
|
2240
|
-
code: 'AI_PROVIDER_FAILURE',
|
|
2757
|
+
code: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'AI_PROVIDER_FAILURE',
|
|
2241
2758
|
message,
|
|
2242
|
-
recoverable:
|
|
2759
|
+
recoverable: !dispatchBudgetExhausted,
|
|
2243
2760
|
},
|
|
2244
2761
|
}, undefined, 'blocked')],
|
|
2245
2762
|
evaluations: [
|
|
2246
2763
|
agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.'),
|
|
2247
|
-
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 }),
|
|
2248
2765
|
],
|
|
2249
2766
|
nextActions: [
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
+
]),
|
|
2252
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
|
+
},
|
|
2253
2789
|
};
|
|
2254
2790
|
}
|
|
2255
2791
|
const answeredRoute = resolvedRunRouteFromAnswer(governedAnswer) ?? route;
|
|
2256
2792
|
const isCertified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
2257
|
-
const
|
|
2793
|
+
const semanticRouteClaimed = governedAnswer.route?.tier === 'semantic_metric';
|
|
2794
|
+
const semanticAggregationProofPassed = semanticAnswerHasPassedAggregationProof(governedAnswer);
|
|
2795
|
+
const semanticAggregationBlocked = semanticRouteClaimed && !semanticAggregationProofPassed;
|
|
2796
|
+
const isSemantic = semanticRouteClaimed && semanticAggregationProofPassed;
|
|
2258
2797
|
const requestedNotebookDataset = findMentionedNotebookDataset(request.question, datasetWorkspace.list());
|
|
2259
2798
|
const governedAssetMissesLocalSource = Boolean(requestedNotebookDataset && (isCertified
|
|
2260
2799
|
|| isSemantic
|
|
@@ -2271,7 +2810,8 @@ export async function startLocalServer(opts) {
|
|
|
2271
2810
|
// governance boundary, not an ambiguous user question and not a repairable
|
|
2272
2811
|
// retrieval miss. Keep the precise policy detail visible, but never burn two
|
|
2273
2812
|
// more provider calls retrying the same incompatible candidate.
|
|
2274
|
-
const isPolicyBlocked = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked'
|
|
2813
|
+
const isPolicyBlocked = (governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked')
|
|
2814
|
+
|| semanticAggregationBlocked;
|
|
2275
2815
|
const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure || isPolicyBlocked;
|
|
2276
2816
|
// The model tried to compose a governed query and declined despite having usable
|
|
2277
2817
|
// context (e.g. it wasn't confident about a multi-table join). The answer loop
|
|
@@ -2290,48 +2830,137 @@ export async function startLocalServer(opts) {
|
|
|
2290
2830
|
// reply ran context-free and was clarified again: the reported clarify loop.
|
|
2291
2831
|
const resolvedRoute = needsClarification ? 'clarify' : answeredRoute;
|
|
2292
2832
|
const sql = governedAnswer.proposedSql ?? governedAnswer.sql;
|
|
2293
|
-
const runnableSql = governedAnswer.kind === 'no_answer'
|
|
2833
|
+
const runnableSql = governedAnswer.kind === 'no_answer'
|
|
2834
|
+
|| isExecutionFailure
|
|
2835
|
+
|| semanticAggregationBlocked
|
|
2836
|
+
|| (isExploratory && !governedAnswer.result)
|
|
2294
2837
|
? undefined
|
|
2295
2838
|
: sql;
|
|
2296
|
-
//
|
|
2297
|
-
//
|
|
2298
|
-
//
|
|
2299
|
-
//
|
|
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.
|
|
2300
2846
|
let synthesizedAnswer;
|
|
2301
|
-
|
|
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
|
+
});
|
|
2302
2891
|
try {
|
|
2303
|
-
const
|
|
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
|
-
|
|
2329
|
-
|
|
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
|
+
}
|
|
2330
2935
|
}
|
|
2331
2936
|
catch {
|
|
2332
|
-
// Keep the governed draft on any
|
|
2937
|
+
// Keep the governed draft on any narration failure.
|
|
2333
2938
|
synthesizedAnswer = undefined;
|
|
2334
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
|
+
}));
|
|
2335
2964
|
}
|
|
2336
2965
|
// A grounding/modeling gap is a REFUSAL. It was computed above but never
|
|
2337
2966
|
// reached this ternary, so it fell through to `needs_review` — the same
|
|
@@ -2415,9 +3044,21 @@ export async function startLocalServer(opts) {
|
|
|
2415
3044
|
artifactKind: 'sql_cell',
|
|
2416
3045
|
}] : []),
|
|
2417
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
|
+
}
|
|
2418
3055
|
return {
|
|
2419
3056
|
resolvedRoute,
|
|
2420
|
-
answerRefusalCode: governedAnswer.kind === 'no_answer'
|
|
3057
|
+
answerRefusalCode: governedAnswer.kind === 'no_answer'
|
|
3058
|
+
? governedAnswer.refusalCode
|
|
3059
|
+
: semanticAggregationBlocked
|
|
3060
|
+
? 'policy_blocked'
|
|
3061
|
+
: undefined,
|
|
2421
3062
|
answerTier: governedAnswer.route?.tier,
|
|
2422
3063
|
summary: isTerminalFailure
|
|
2423
3064
|
? analyticalFailureSummary(governedAnswer.analyticalFailure)
|
|
@@ -2477,7 +3118,9 @@ export async function startLocalServer(opts) {
|
|
|
2477
3118
|
: needsClarification
|
|
2478
3119
|
? 'The answer loop needs more context before producing a governed answer.'
|
|
2479
3120
|
: isPolicyBlocked
|
|
2480
|
-
?
|
|
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.'}`
|
|
2481
3124
|
: isExploratory
|
|
2482
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.'
|
|
2483
3126
|
: 'The answer is generated or semantic-layer backed and remains review-required.', governedAnswer.route),
|
|
@@ -2503,10 +3146,13 @@ export async function startLocalServer(opts) {
|
|
|
2503
3146
|
},
|
|
2504
3147
|
] : []),
|
|
2505
3148
|
...(isPolicyBlocked ? [
|
|
2506
|
-
agentRunEvaluation('relationship-policy', 'Relationship policy', false, 'warning',
|
|
2507
|
-
|
|
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.', {
|
|
2508
3153
|
refusalCode: governedAnswer.refusalCode,
|
|
2509
3154
|
refusalDetails: governedAnswer.refusalDetails,
|
|
3155
|
+
aggregationSafetyProof: governedAnswer.aggregationSafetyProof,
|
|
2510
3156
|
validationWarnings: governedAnswer.validationWarnings,
|
|
2511
3157
|
route: governedAnswer.route,
|
|
2512
3158
|
}),
|
|
@@ -2516,6 +3162,8 @@ export async function startLocalServer(opts) {
|
|
|
2516
3162
|
] : []),
|
|
2517
3163
|
],
|
|
2518
3164
|
nextActions,
|
|
3165
|
+
providerEgressReceipts: finalProviderEgressReceipts,
|
|
3166
|
+
telemetry: finalTelemetry,
|
|
2519
3167
|
};
|
|
2520
3168
|
};
|
|
2521
3169
|
const conversationRunExecutor = async ({ request, routeDecision, emitAnswerDelta }) => {
|
|
@@ -2549,13 +3197,28 @@ export async function startLocalServer(opts) {
|
|
|
2549
3197
|
...(request.history ?? []).slice(-6).map((message) => ({ role: message.role, content: message.text })),
|
|
2550
3198
|
{ role: 'user', content: request.question },
|
|
2551
3199
|
];
|
|
2552
|
-
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();
|
|
2553
3212
|
// Perceived-latency: surface the reply as one delta for surfaces wired to stream.
|
|
2554
3213
|
if (text)
|
|
2555
3214
|
emitAnswerDelta?.(text);
|
|
2556
3215
|
}
|
|
2557
3216
|
}
|
|
2558
|
-
catch {
|
|
3217
|
+
catch (error) {
|
|
3218
|
+
if (error && typeof error === 'object'
|
|
3219
|
+
&& error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED') {
|
|
3220
|
+
throw error;
|
|
3221
|
+
}
|
|
2559
3222
|
// Provider unavailable / errored — fall through to the deterministic reply.
|
|
2560
3223
|
text = undefined;
|
|
2561
3224
|
}
|
|
@@ -3094,7 +3757,7 @@ export async function startLocalServer(opts) {
|
|
|
3094
3757
|
result: researchResultData,
|
|
3095
3758
|
evidence: plan.sources,
|
|
3096
3759
|
reviewRequired: true,
|
|
3097
|
-
})
|
|
3760
|
+
}, request.researchResultRowsOptIn === true)
|
|
3098
3761
|
: undefined;
|
|
3099
3762
|
const summary = needsClarification
|
|
3100
3763
|
? 'Needs clarification before running deeper research.'
|
|
@@ -3609,7 +4272,19 @@ export async function startLocalServer(opts) {
|
|
|
3609
4272
|
return provider.generate([
|
|
3610
4273
|
{ role: 'system', content: system },
|
|
3611
4274
|
{ role: 'user', content: user },
|
|
3612
|
-
], {
|
|
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
|
+
});
|
|
3613
4288
|
},
|
|
3614
4289
|
getCatalogContext: buildRankedAgentRunCatalogContext,
|
|
3615
4290
|
});
|
|
@@ -3633,6 +4308,14 @@ export async function startLocalServer(opts) {
|
|
|
3633
4308
|
// instead of leaving the UI in "Generating and validating SQL" for a
|
|
3634
4309
|
// minute or more.
|
|
3635
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
|
+
} : {}),
|
|
3636
4319
|
});
|
|
3637
4320
|
},
|
|
3638
4321
|
getEvidence: buildAgentRunEvidence,
|
|
@@ -3649,6 +4332,8 @@ export async function startLocalServer(opts) {
|
|
|
3649
4332
|
// A run may outlive its streaming browser connection, so cancellation is
|
|
3650
4333
|
// server-owned and keyed by run id rather than relying on fetch abort alone.
|
|
3651
4334
|
const activeAgentRunControllers = new Map();
|
|
4335
|
+
const activeAnalyticalRepairReservations = new Set();
|
|
4336
|
+
const consumedAnalyticalRepairCapabilities = new Set();
|
|
3652
4337
|
// Server-side conversation threads: persisted multi-turn state (survives refresh).
|
|
3653
4338
|
// Lazy so a failed native-sqlite load never blocks the runtime; conversation
|
|
3654
4339
|
// persistence degrades to the per-request context instead.
|
|
@@ -3739,14 +4424,21 @@ export async function startLocalServer(opts) {
|
|
|
3739
4424
|
snapshotCells.push({ cellId, status: 'idle', executionCount: 0, executedAt });
|
|
3740
4425
|
continue;
|
|
3741
4426
|
}
|
|
3742
|
-
const prepared = prepareLocalExecution(executableSql, activeConnection, projectRoot, projectConfig);
|
|
3743
4427
|
assertAppAccess({ app, domain: resolved.domain ?? app.domain, level: 'execute' });
|
|
3744
|
-
const
|
|
3745
|
-
|
|
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;
|
|
3746
4436
|
snapshotCells.push({
|
|
3747
4437
|
cellId,
|
|
3748
4438
|
status: 'success',
|
|
3749
4439
|
result,
|
|
4440
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
4441
|
+
resultFingerprint: execution.resultFingerprint,
|
|
3750
4442
|
executionCount: 1,
|
|
3751
4443
|
executedAt,
|
|
3752
4444
|
});
|
|
@@ -3835,44 +4527,81 @@ export async function startLocalServer(opts) {
|
|
|
3835
4527
|
const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
|
|
3836
4528
|
throw analyticalError(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`, { origin: 'dql_compilation', stage: 'compile' });
|
|
3837
4529
|
}
|
|
3838
|
-
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({
|
|
3839
4538
|
sql: semanticCompose?.sql ?? plan.sql,
|
|
3840
4539
|
subject: 'DQL artifact query',
|
|
3841
|
-
executor,
|
|
3842
4540
|
connection: activeConnection,
|
|
3843
|
-
projectRoot,
|
|
3844
|
-
projectConfig,
|
|
3845
4541
|
// Preserve the existing contract: previewed Ask artifacts are explicitly
|
|
3846
4542
|
// read-only and bounded; saved/manual DQL execution keeps its compiler-
|
|
3847
4543
|
// governed behavior when no preview bound was requested.
|
|
3848
4544
|
enforceReadOnly: invocationInput?.rowLimit !== undefined,
|
|
3849
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
|
+
},
|
|
3850
4569
|
});
|
|
3851
|
-
const
|
|
4570
|
+
const semanticExecution = semanticExecutionHolder.value;
|
|
4571
|
+
const { preparation, result: normalized } = execution;
|
|
3852
4572
|
const executionSql = preparation.executedSql;
|
|
3853
|
-
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
3854
|
-
const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
|
|
3855
|
-
assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
|
|
3856
|
-
const pinnedSemanticCompile = semanticCompose?.sql
|
|
3857
|
-
? compiledSemanticQueries.get(executionFingerprint(semanticCompose.sql))
|
|
3858
|
-
: undefined;
|
|
3859
|
-
const semanticExecution = pinnedSemanticCompile
|
|
3860
|
-
? await executeTargetBoundSemanticQuery({
|
|
3861
|
-
executor,
|
|
3862
|
-
connection: activeConnection,
|
|
3863
|
-
projectRoot,
|
|
3864
|
-
plannedAdapter: pinnedSemanticCompile.engine,
|
|
3865
|
-
metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
|
|
3866
|
-
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
3867
|
-
: undefined,
|
|
3868
|
-
compile: async () => pinnedSemanticCompile,
|
|
3869
|
-
prepareSql: () => ({ sql: executionSql, connection: prepared.connection }),
|
|
3870
|
-
rowBound: invocationInput?.rowLimit ?? pinnedSemanticCompile.effectiveRequest.limit,
|
|
3871
|
-
})
|
|
3872
|
-
: null;
|
|
3873
|
-
const rawResult = semanticExecution?.result ?? await executor.executeQuery(executionSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), prepared.connection);
|
|
3874
|
-
const normalized = normalizeQueryResult(rawResult);
|
|
3875
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
|
+
};
|
|
3876
4605
|
return {
|
|
3877
4606
|
columns: normalized.columns,
|
|
3878
4607
|
rows: normalized.rows,
|
|
@@ -3885,6 +4614,7 @@ export async function startLocalServer(opts) {
|
|
|
3885
4614
|
parameters: invocation.resolvedParameters,
|
|
3886
4615
|
auditId: invocation.auditId,
|
|
3887
4616
|
executionReceipt,
|
|
4617
|
+
executableArtifact,
|
|
3888
4618
|
...(semanticExecution ? {
|
|
3889
4619
|
semanticExecutionReceipt: semanticExecution.executionReceipt,
|
|
3890
4620
|
semanticTargetBinding: semanticExecution.targetBinding,
|
|
@@ -3919,6 +4649,9 @@ export async function startLocalServer(opts) {
|
|
|
3919
4649
|
trustState: 'certified',
|
|
3920
4650
|
compiledSql: result.sql,
|
|
3921
4651
|
executionReceipt: result.executionReceipt,
|
|
4652
|
+
...(result.executableArtifact ? {
|
|
4653
|
+
executableArtifact: { ...result.executableArtifact, kind: 'certified_block', trustState: 'certified' },
|
|
4654
|
+
} : {}),
|
|
3922
4655
|
...(invocationInput?.rowLimit ? { limit: invocationInput.rowLimit } : {}),
|
|
3923
4656
|
...(invocationInput?.parameters && Object.keys(invocationInput.parameters).length > 0
|
|
3924
4657
|
? { parameterValues: invocationInput.parameters }
|
|
@@ -3960,6 +4693,7 @@ export async function startLocalServer(opts) {
|
|
|
3960
4693
|
...(Object.keys(invocation.values).length > 0 ? { parameterValues: invocation.values } : {}),
|
|
3961
4694
|
compiledSql: result.sql,
|
|
3962
4695
|
executionReceipt: result.executionReceipt,
|
|
4696
|
+
...(result.executableArtifact ? { executableArtifact: result.executableArtifact } : {}),
|
|
3963
4697
|
},
|
|
3964
4698
|
};
|
|
3965
4699
|
};
|
|
@@ -4036,7 +4770,7 @@ export async function startLocalServer(opts) {
|
|
|
4036
4770
|
* string, so the only remaining reasons to differ are the connection and the
|
|
4037
4771
|
* governance gates, both of which report themselves honestly.
|
|
4038
4772
|
*/
|
|
4039
|
-
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings) => {
|
|
4773
|
+
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings, executionConnectionName) => {
|
|
4040
4774
|
const activeConnection = requireActiveConnection(executionConnection);
|
|
4041
4775
|
const rowBound = clampAnalyticalRowBound(seed?.limit ?? 200);
|
|
4042
4776
|
const trimmed = sql.trim().replace(/;\s*$/, '').trim();
|
|
@@ -4058,18 +4792,6 @@ export async function startLocalServer(opts) {
|
|
|
4058
4792
|
if (semantic.unresolvedRefs.length > 0) {
|
|
4059
4793
|
throw analyticalError(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`, { origin: 'dql_compilation', stage: 'compile', code: 'semantic_ref' });
|
|
4060
4794
|
}
|
|
4061
|
-
const preparation = await prepareAnalyticalExecutionSql({
|
|
4062
|
-
sql: semantic.sql,
|
|
4063
|
-
subject: 'Generated SQL',
|
|
4064
|
-
executor,
|
|
4065
|
-
connection: activeConnection,
|
|
4066
|
-
projectRoot,
|
|
4067
|
-
projectConfig,
|
|
4068
|
-
enforceReadOnly: true,
|
|
4069
|
-
rowLimit: rowBound,
|
|
4070
|
-
});
|
|
4071
|
-
const prepared = { sql: preparation.preparedSql, connection: preparation.connection };
|
|
4072
|
-
const bounded = preparation.rowBound;
|
|
4073
4795
|
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
4074
4796
|
// Generated SQL is governed as 'uncategorized' unless its seed declares a
|
|
4075
4797
|
// domain — the same value `generatedSqlArtifactContract` stamps into the
|
|
@@ -4080,25 +4802,43 @@ export async function startLocalServer(opts) {
|
|
|
4080
4802
|
assertAppAccess({ app, domain: sourceDomain, level: 'execute' });
|
|
4081
4803
|
// A semantic query the loop already compiled (MetricFlow / dbt Cloud) must
|
|
4082
4804
|
// execute through its pinned target binding, not as loose SQL.
|
|
4083
|
-
const
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
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;
|
|
4102
4842
|
// The authoritative bound. `buildRowBoundedSql` may legitimately decline to
|
|
4103
4843
|
// touch the statement, and only one of 15 drivers honours a maxRows hint, so
|
|
4104
4844
|
// the host is the only layer correctness may depend on.
|
|
@@ -4106,6 +4846,38 @@ export async function startLocalServer(opts) {
|
|
|
4106
4846
|
const rows = truncated ? normalized.rows.slice(0, rowBound) : normalized.rows;
|
|
4107
4847
|
const warnings = [];
|
|
4108
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
|
+
};
|
|
4109
4881
|
return {
|
|
4110
4882
|
columns: normalized.columns,
|
|
4111
4883
|
rows,
|
|
@@ -4113,9 +4885,10 @@ export async function startLocalServer(opts) {
|
|
|
4113
4885
|
executionTime: normalized.executionTime,
|
|
4114
4886
|
sql: bounded.sql,
|
|
4115
4887
|
...(truncated ? { truncated: true } : {}),
|
|
4116
|
-
...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound } } : {}),
|
|
4888
|
+
...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound, executionReceipt, executableArtifact } } : {}),
|
|
4117
4889
|
...(warnings.length > 0 ? { validationWarnings: warnings } : {}),
|
|
4118
|
-
executionReceipt
|
|
4890
|
+
executionReceipt,
|
|
4891
|
+
executableArtifact,
|
|
4119
4892
|
};
|
|
4120
4893
|
};
|
|
4121
4894
|
const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName) => {
|
|
@@ -4125,7 +4898,7 @@ export async function startLocalServer(opts) {
|
|
|
4125
4898
|
if (seed && seed.kind !== 'sql_block') {
|
|
4126
4899
|
return executeArtifactReferenceForAgent({ ...seed, limit: seed.limit ?? 200 }, question, executionConnection, executionConnectionName);
|
|
4127
4900
|
}
|
|
4128
|
-
return executeGeneratedSqlDirect(question, sql, seed, executionConnection);
|
|
4901
|
+
return executeGeneratedSqlDirect(question, sql, seed, executionConnection, undefined, executionConnectionName);
|
|
4129
4902
|
};
|
|
4130
4903
|
/**
|
|
4131
4904
|
* EXP-001: execution host for the deliberately narrow non-governed lane.
|
|
@@ -4432,12 +5205,33 @@ export async function startLocalServer(opts) {
|
|
|
4432
5205
|
* conversation, writes a DQL file, changes targets, or promotes trust.
|
|
4433
5206
|
*/
|
|
4434
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
|
+
}
|
|
4435
5227
|
let result;
|
|
4436
|
-
let repairedSql =
|
|
5228
|
+
let repairedSql = sourceSql;
|
|
4437
5229
|
let repairMode = 'deterministic';
|
|
4438
5230
|
let firstFailure;
|
|
5231
|
+
const providerEgressReceipts = [];
|
|
5232
|
+
let providerRoundTrips = 0;
|
|
4439
5233
|
try {
|
|
4440
|
-
result = await executeGeneratedSqlDirect(input.question,
|
|
5234
|
+
result = await executeGeneratedSqlDirect(input.question, sourceSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables }, input.targetConnectionName);
|
|
4441
5235
|
}
|
|
4442
5236
|
catch (error) {
|
|
4443
5237
|
firstFailure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
@@ -4458,50 +5252,89 @@ export async function startLocalServer(opts) {
|
|
|
4458
5252
|
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
4459
5253
|
throw boundedRepairError(failure.redactedMessage, 422, failure);
|
|
4460
5254
|
}
|
|
4461
|
-
const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
|
|
5255
|
+
const boundedSchema = markProviderMetadataArray(schemaContext.slice(0, 16).map((table) => ({
|
|
4462
5256
|
relation: table.relation,
|
|
4463
|
-
columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
|
|
4464
|
-
}));
|
|
5257
|
+
columns: markProviderMetadataArray(table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type }))),
|
|
5258
|
+
})));
|
|
4465
5259
|
// CTX-008: a repair that re-derives joins from column names can undo a
|
|
4466
5260
|
// join the original query got right. Give it the authored key pairs.
|
|
4467
|
-
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
|
+
});
|
|
4468
5301
|
let raw;
|
|
4469
5302
|
try {
|
|
4470
|
-
raw = await provider.generate(
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
failure: firstFailure.redactedMessage,
|
|
4492
|
-
sql: input.sourceSql,
|
|
4493
|
-
parameters: input.sqlParams?.map((parameter) => ({
|
|
4494
|
-
name: parameter.name,
|
|
4495
|
-
placeholder: `$${parameter.position}`,
|
|
4496
|
-
})),
|
|
4497
|
-
schema: boundedSchema,
|
|
4498
|
-
...(governedJoins.length ? { governedJoins } : {}),
|
|
4499
|
-
}),
|
|
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;
|
|
4500
5324
|
},
|
|
4501
|
-
|
|
5325
|
+
});
|
|
4502
5326
|
}
|
|
4503
5327
|
catch {
|
|
4504
|
-
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
|
+
});
|
|
4505
5338
|
}
|
|
4506
5339
|
const proposal = parseProposal(raw);
|
|
4507
5340
|
if (!proposal.sql?.trim()) {
|
|
@@ -4512,9 +5345,17 @@ export async function startLocalServer(opts) {
|
|
|
4512
5345
|
if (requiredPlaceholders.some((placeholder) => !new RegExp(`${placeholder.replace('$', '\\$')}\\b`).test(repairedSql))) {
|
|
4513
5346
|
throw boundedRepairError('The repair changed this cell\'s parameter contract, so DQL stopped it before execution.', 422);
|
|
4514
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
|
+
}
|
|
4515
5356
|
repairMode = 'ai';
|
|
4516
5357
|
try {
|
|
4517
|
-
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);
|
|
4518
5359
|
}
|
|
4519
5360
|
catch (error) {
|
|
4520
5361
|
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
@@ -4524,7 +5365,6 @@ export async function startLocalServer(opts) {
|
|
|
4524
5365
|
// Keep preview-only row bounds out of editable notebook source. The one
|
|
4525
5366
|
// deterministic source rewrite we do persist is decoding internal graph
|
|
4526
5367
|
// identities, because those can never execute as warehouse SQL.
|
|
4527
|
-
let rewrites = [];
|
|
4528
5368
|
if (internalRelationIdsInSql(repairedSql).length > 0) {
|
|
4529
5369
|
const normalizedSource = await prepareAnalyticalExecutionSql({
|
|
4530
5370
|
sql: repairedSql,
|
|
@@ -4536,9 +5376,9 @@ export async function startLocalServer(opts) {
|
|
|
4536
5376
|
enforceReadOnly: true,
|
|
4537
5377
|
});
|
|
4538
5378
|
repairedSql = normalizedSource.decodedSql;
|
|
4539
|
-
rewrites
|
|
5379
|
+
rewrites.push(...normalizedSource.rewrites);
|
|
4540
5380
|
}
|
|
4541
|
-
return { result, repairedSql, repairMode, firstFailure, rewrites };
|
|
5381
|
+
return { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips, rewrites };
|
|
4542
5382
|
};
|
|
4543
5383
|
/**
|
|
4544
5384
|
* Repair a DQL wrapper that cannot compile far enough to expose executable
|
|
@@ -4551,6 +5391,8 @@ export async function startLocalServer(opts) {
|
|
|
4551
5391
|
if (!provider) {
|
|
4552
5392
|
throw boundedRepairError('Configure an available AI provider in Settings to repair this DQL draft.', 409);
|
|
4553
5393
|
}
|
|
5394
|
+
const providerEgressReceipts = [];
|
|
5395
|
+
let providerRoundTrips = 0;
|
|
4554
5396
|
let schemaContext;
|
|
4555
5397
|
try {
|
|
4556
5398
|
// Include source identifiers in retrieval so a terse cell title still
|
|
@@ -4596,10 +5438,41 @@ export async function startLocalServer(opts) {
|
|
|
4596
5438
|
...(governedJoins.length ? { governedJoins } : {}),
|
|
4597
5439
|
}),
|
|
4598
5440
|
},
|
|
4599
|
-
], {
|
|
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
|
+
});
|
|
4600
5464
|
}
|
|
4601
5465
|
catch {
|
|
4602
|
-
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
|
+
});
|
|
4603
5476
|
}
|
|
4604
5477
|
let repairedSource;
|
|
4605
5478
|
try {
|
|
@@ -4703,6 +5576,8 @@ export async function startLocalServer(opts) {
|
|
|
4703
5576
|
repairedSql: plan.sql,
|
|
4704
5577
|
repairedSource,
|
|
4705
5578
|
repairMode: 'ai',
|
|
5579
|
+
providerEgressReceipts,
|
|
5580
|
+
providerRoundTrips,
|
|
4706
5581
|
rewrites: [],
|
|
4707
5582
|
sqlParams: plan.sqlParams,
|
|
4708
5583
|
variables: { ...plan.variables, ...invocation.values },
|
|
@@ -5701,11 +6576,18 @@ export async function startLocalServer(opts) {
|
|
|
5701
6576
|
tableMapping,
|
|
5702
6577
|
semanticSql: semanticCompose?.sql ?? undefined,
|
|
5703
6578
|
});
|
|
5704
|
-
const
|
|
5705
|
-
|
|
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
|
+
});
|
|
5706
6586
|
return {
|
|
5707
|
-
sql:
|
|
5708
|
-
result:
|
|
6587
|
+
sql: execution.preparation.executedSql,
|
|
6588
|
+
result: execution.result,
|
|
6589
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
6590
|
+
resultFingerprint: execution.resultFingerprint,
|
|
5709
6591
|
chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
|
|
5710
6592
|
invocation,
|
|
5711
6593
|
tests: plan?.tests ?? [],
|
|
@@ -5756,12 +6638,14 @@ export async function startLocalServer(opts) {
|
|
|
5756
6638
|
if (tests.length === 0) {
|
|
5757
6639
|
return { passed: 0, failed: 0, skipped: 0, duration: Date.now() - start, assertions: [], runAt: new Date() };
|
|
5758
6640
|
}
|
|
5759
|
-
const
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
:
|
|
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;
|
|
5765
6649
|
return evaluateBlockStudioTests(tests, rows, columns, start);
|
|
5766
6650
|
};
|
|
5767
6651
|
const saveDqlGenerationDraft = (importId, candidate) => saveDqlGenerationDraftForProject(projectRoot, importId, candidate);
|
|
@@ -7243,23 +8127,81 @@ export async function startLocalServer(opts) {
|
|
|
7243
8127
|
res.end(serializeJSON({ error: 'Agent run not found.' }));
|
|
7244
8128
|
return;
|
|
7245
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
|
+
}
|
|
7246
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
|
+
}
|
|
7247
8173
|
const existing = await agentRunStore.get(derivedRunId);
|
|
7248
8174
|
if (existing) {
|
|
7249
|
-
|
|
7250
|
-
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
|
+
}));
|
|
7251
8185
|
return;
|
|
7252
8186
|
}
|
|
7253
|
-
const sourceSql = repairableSqlFromAgentRun(sourceRun);
|
|
7254
8187
|
const sourceDqlArtifact = repairableDqlArtifactFromAgentRun(sourceRun);
|
|
7255
8188
|
const retainedFailure = warehouseFailureFromAgentRun(sourceRun);
|
|
7256
|
-
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())) {
|
|
7257
8200
|
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7258
8201
|
res.end(serializeJSON({
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
...(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,
|
|
7263
8205
|
}));
|
|
7264
8206
|
return;
|
|
7265
8207
|
}
|
|
@@ -7282,32 +8224,250 @@ export async function startLocalServer(opts) {
|
|
|
7282
8224
|
: sourceRun.executionTarget?.target === 'local'
|
|
7283
8225
|
? 'local'
|
|
7284
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
|
+
}
|
|
7285
8308
|
let repaired;
|
|
7286
8309
|
try {
|
|
7287
|
-
//
|
|
7288
|
-
//
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
:
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
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
|
+
};
|
|
7308
8420
|
}
|
|
7309
8421
|
catch (error) {
|
|
7310
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
|
+
});
|
|
7311
8471
|
res.writeHead(repairError.status ?? 500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7312
8472
|
res.end(serializeJSON({
|
|
7313
8473
|
error: repairError.message,
|
|
@@ -7315,27 +8475,39 @@ export async function startLocalServer(opts) {
|
|
|
7315
8475
|
}));
|
|
7316
8476
|
return;
|
|
7317
8477
|
}
|
|
7318
|
-
const { result, repairedSql, repairMode, firstFailure } = repaired;
|
|
8478
|
+
const { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips } = repaired;
|
|
7319
8479
|
const completedAt = new Date().toISOString();
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
audience: 'stakeholder',
|
|
7324
|
-
resultPreview: preview,
|
|
7325
|
-
sql: repairedSql,
|
|
7326
|
-
draftText: `The repaired query returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}.`,
|
|
7327
|
-
...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
|
|
7328
|
-
});
|
|
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.`;
|
|
7329
8483
|
const sourceFailure = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
|
|
7330
8484
|
const presentationContext = repairPresentationContextFromAgentRun(sourceRun);
|
|
7331
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
|
+
};
|
|
7332
8504
|
const artifact = agentRunArtifact('answer', 'Repaired analytical answer', {
|
|
7333
8505
|
kind: 'uncertified',
|
|
7334
8506
|
certification: 'analyst_review_required',
|
|
7335
8507
|
reviewStatus: 'analyst_review_required',
|
|
7336
8508
|
trustLabel: 'AI-generated · review required',
|
|
7337
|
-
answer:
|
|
7338
|
-
text:
|
|
8509
|
+
answer: presentationText,
|
|
8510
|
+
text: presentationText,
|
|
7339
8511
|
...presentationContext,
|
|
7340
8512
|
result,
|
|
7341
8513
|
proposedSql: repairedSql,
|
|
@@ -7356,10 +8528,11 @@ export async function startLocalServer(opts) {
|
|
|
7356
8528
|
rowCount: result.rowCount,
|
|
7357
8529
|
...(executionResultFingerprint ? { resultFingerprint: executionResultFingerprint } : {}),
|
|
7358
8530
|
},
|
|
8531
|
+
providerEgressReceipts,
|
|
7359
8532
|
},
|
|
7360
8533
|
previousAttempt: {
|
|
7361
8534
|
sourceRunId: sourceRun.id,
|
|
7362
|
-
|
|
8535
|
+
sql: repairableSqlFromAgentRun(sourceRun),
|
|
7363
8536
|
...(sourceDqlArtifact?.source ? { dql: sourceDqlArtifact.source } : {}),
|
|
7364
8537
|
failure: retainedFailure ?? firstFailure,
|
|
7365
8538
|
},
|
|
@@ -7386,7 +8559,7 @@ export async function startLocalServer(opts) {
|
|
|
7386
8559
|
artifacts: [artifact],
|
|
7387
8560
|
};
|
|
7388
8561
|
const events = [
|
|
7389
|
-
{ 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' },
|
|
7390
8563
|
{ id: `${derivedRunId}:artifact`, runId: derivedRunId, type: 'artifact.created', at: completedAt, message: 'Created the repaired result artifact.', route: 'generated_answer', trustState: 'review_required' },
|
|
7391
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' },
|
|
7392
8565
|
];
|
|
@@ -7398,15 +8571,15 @@ export async function startLocalServer(opts) {
|
|
|
7398
8571
|
status: 'needs_review',
|
|
7399
8572
|
trustState: 'review_required',
|
|
7400
8573
|
stopReason: 'generated_review_required',
|
|
7401
|
-
startedAt:
|
|
8574
|
+
startedAt: repairStartedAt,
|
|
7402
8575
|
completedAt,
|
|
7403
8576
|
selectedObject: sourceRun.selectedObject,
|
|
7404
8577
|
executionTarget: sourceRun.executionTarget,
|
|
7405
8578
|
routeDecision: sourceRun.routeDecision,
|
|
7406
8579
|
plan: sourceRun.plan,
|
|
7407
8580
|
steps: [repairStep],
|
|
7408
|
-
summary:
|
|
7409
|
-
answer:
|
|
8581
|
+
summary: presentationText,
|
|
8582
|
+
answer: presentationText,
|
|
7410
8583
|
answerKind: 'governed',
|
|
7411
8584
|
artifacts: [artifact],
|
|
7412
8585
|
evaluations: [evaluation],
|
|
@@ -7419,7 +8592,7 @@ export async function startLocalServer(opts) {
|
|
|
7419
8592
|
phase: 'run.completed',
|
|
7420
8593
|
revision: 1,
|
|
7421
8594
|
eventCursor: events.length,
|
|
7422
|
-
startedAt:
|
|
8595
|
+
startedAt: repairStartedAt,
|
|
7423
8596
|
updatedAt: completedAt,
|
|
7424
8597
|
completedAt,
|
|
7425
8598
|
},
|
|
@@ -7432,7 +8605,19 @@ export async function startLocalServer(opts) {
|
|
|
7432
8605
|
steps: [repairStep],
|
|
7433
8606
|
artifacts: [artifact],
|
|
7434
8607
|
evaluations: [evaluation],
|
|
8608
|
+
providerEgressReceipts,
|
|
7435
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,
|
|
7436
8621
|
derivation: {
|
|
7437
8622
|
version: 1,
|
|
7438
8623
|
kind: 'analytical_repair',
|
|
@@ -7578,21 +8763,59 @@ export async function startLocalServer(opts) {
|
|
|
7578
8763
|
let runError;
|
|
7579
8764
|
try {
|
|
7580
8765
|
const executeRun = async (operationSignal, report) => {
|
|
7581
|
-
|
|
8766
|
+
const ingressSignal = AbortSignal.any([
|
|
7582
8767
|
runController.signal,
|
|
7583
8768
|
...(operationSignal ? [operationSignal] : []),
|
|
7584
|
-
AbortSignal.timeout(agentRunDeadlineMs(parsed.request, process.env, getActiveProvider(projectRoot))),
|
|
7585
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
|
+
}));
|
|
7586
8786
|
report?.({ phase: 'reasoning', progress: 8, message: 'Grounding the request and selecting the governed route.' });
|
|
7587
8787
|
try {
|
|
7588
|
-
const
|
|
8788
|
+
const engineRun = await agentRunProviderEvidenceContext.run(runProviderEvidence, () => agentRunEngine.run(parsed.request, (event) => {
|
|
8789
|
+
runProviderEvidence.setRoute(event.route);
|
|
7589
8790
|
if (wantsStream)
|
|
7590
8791
|
writeStream('agent-run-event', event);
|
|
7591
8792
|
report?.({ phase: event.type, progress: 45, message: event.message });
|
|
7592
8793
|
}, (delta) => {
|
|
7593
8794
|
if (wantsStream)
|
|
7594
8795
|
writeStream('agent-run-answer-delta', { runId: parsed.request.runId, delta });
|
|
7595
|
-
});
|
|
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);
|
|
7596
8819
|
completedRun = run;
|
|
7597
8820
|
recordConversationTurn(conversationStore, parsed.request.threadId, run);
|
|
7598
8821
|
return { runId: run.id, threadId: parsed.request.threadId, status: run.status };
|
|
@@ -9658,13 +10881,22 @@ export async function startLocalServer(opts) {
|
|
|
9658
10881
|
});
|
|
9659
10882
|
if (!composed)
|
|
9660
10883
|
throw new Error('The governed semantic query could not be composed.');
|
|
9661
|
-
const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
|
|
9662
10884
|
let normalizedResult;
|
|
9663
10885
|
let repair;
|
|
9664
10886
|
let executedSql = composed.sql;
|
|
10887
|
+
let executionFingerprints;
|
|
9665
10888
|
try {
|
|
9666
|
-
const
|
|
9667
|
-
|
|
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
|
+
};
|
|
9668
10900
|
}
|
|
9669
10901
|
catch (executionError) {
|
|
9670
10902
|
const repaired = await repairFailedAppTileExecution({
|
|
@@ -9706,6 +10938,7 @@ export async function startLocalServer(opts) {
|
|
|
9706
10938
|
title: item.title ?? item.semantic.id,
|
|
9707
10939
|
viz: item.viz,
|
|
9708
10940
|
result: normalizedResult,
|
|
10941
|
+
...(executionFingerprints ?? {}),
|
|
9709
10942
|
trustState: 'review_required',
|
|
9710
10943
|
citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
|
|
9711
10944
|
...(repair ? { repair } : {}),
|
|
@@ -9839,8 +11072,13 @@ export async function startLocalServer(opts) {
|
|
|
9839
11072
|
tileId: item.i,
|
|
9840
11073
|
tileFilterBindings: item.filterBindings,
|
|
9841
11074
|
});
|
|
9842
|
-
const
|
|
9843
|
-
|
|
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
|
+
});
|
|
9844
11082
|
tiles.push({
|
|
9845
11083
|
tileId: item.i,
|
|
9846
11084
|
status: 'ok',
|
|
@@ -9850,7 +11088,9 @@ export async function startLocalServer(opts) {
|
|
|
9850
11088
|
title: item.title ?? block.name,
|
|
9851
11089
|
viz: item.viz,
|
|
9852
11090
|
chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
|
|
9853
|
-
result:
|
|
11091
|
+
result: execution.result,
|
|
11092
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
11093
|
+
resultFingerprint: execution.resultFingerprint,
|
|
9854
11094
|
filters: {
|
|
9855
11095
|
applied: blockFilterApplication.appliedFilters,
|
|
9856
11096
|
skipped: blockFilterApplication.skippedFilters,
|
|
@@ -14761,20 +16001,18 @@ export async function startLocalServer(opts) {
|
|
|
14761
16001
|
}));
|
|
14762
16002
|
return;
|
|
14763
16003
|
}
|
|
14764
|
-
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({
|
|
14765
16008
|
sql: semantic.sql,
|
|
14766
16009
|
subject: 'Notebook query',
|
|
14767
|
-
executor,
|
|
14768
16010
|
connection: executionConnection,
|
|
14769
|
-
|
|
14770
|
-
|
|
16011
|
+
sqlParams: Array.isArray(body.sqlParams) ? body.sqlParams : [],
|
|
16012
|
+
variables: body.variables && typeof body.variables === 'object' ? body.variables : {},
|
|
16013
|
+
semanticRefs: semantic.semanticRefs,
|
|
14771
16014
|
});
|
|
14772
|
-
const
|
|
14773
|
-
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
14774
|
-
const domain = typeof body.domain === 'string' ? body.domain : app?.domain;
|
|
14775
|
-
assertAppAccess({ app, domain, level: 'execute' });
|
|
14776
|
-
const result = await executor.executeQuery(prepared.sql, Array.isArray(body.sqlParams) ? body.sqlParams : [], runtimeVariables(body.variables && typeof body.variables === 'object' ? body.variables : {}), prepared.connection);
|
|
14777
|
-
const normalized = normalizeQueryResult(result, semantic.semanticRefs);
|
|
16015
|
+
const { preparation, result: normalized } = execution;
|
|
14778
16016
|
if (execContext?.notebookPath) {
|
|
14779
16017
|
recordNotebookQueryRun(projectRoot, {
|
|
14780
16018
|
notebookPath: execContext.notebookPath,
|
|
@@ -15448,7 +16686,7 @@ export async function startLocalServer(opts) {
|
|
|
15448
16686
|
const invocation = prepareBlockInvocation({ source, surface: 'ask_ai' });
|
|
15449
16687
|
if (invocation.errors.length > 0)
|
|
15450
16688
|
throw new Error(invocation.errors.join(' '));
|
|
15451
|
-
const { executionReceipt: _receipt, compiledSql: _compiled, ...unexecuted } = artifact;
|
|
16689
|
+
const { executionReceipt: _receipt, executableArtifact: _executable, compiledSql: _compiled, ...unexecuted } = artifact;
|
|
15452
16690
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
15453
16691
|
res.end(serializeJSON({
|
|
15454
16692
|
artifact: {
|
|
@@ -15564,6 +16802,13 @@ export async function startLocalServer(opts) {
|
|
|
15564
16802
|
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
15565
16803
|
compiledSql: result.sql,
|
|
15566
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
|
+
} : {}),
|
|
15567
16812
|
...(requestedArtifact?.limit ? { limit: requestedArtifact.limit } : {}),
|
|
15568
16813
|
},
|
|
15569
16814
|
}));
|
|
@@ -15686,37 +16931,43 @@ export async function startLocalServer(opts) {
|
|
|
15686
16931
|
res.end(serializeJSON({ cellType: cell.type, result: null }));
|
|
15687
16932
|
return;
|
|
15688
16933
|
}
|
|
15689
|
-
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({
|
|
15690
16938
|
sql: executableSql,
|
|
15691
16939
|
subject: cell.type === 'dql' ? 'DQL query' : 'Notebook query',
|
|
15692
|
-
executor,
|
|
15693
16940
|
connection: cellConnection,
|
|
15694
|
-
|
|
15695
|
-
|
|
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,
|
|
15696
16967
|
});
|
|
16968
|
+
const { preparation, result: normalized } = execution;
|
|
15697
16969
|
const decodedSql = preparation.decodedSql;
|
|
15698
|
-
const
|
|
15699
|
-
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
15700
|
-
assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
|
|
15701
|
-
const semanticExecution = semanticCompose?.compiled
|
|
15702
|
-
? await executeTargetBoundSemanticQuery({
|
|
15703
|
-
executor,
|
|
15704
|
-
connection: cellConnection,
|
|
15705
|
-
projectRoot,
|
|
15706
|
-
plannedAdapter: semanticCompose.compiled.engine,
|
|
15707
|
-
metricFlow: semanticCompose.compiled.engine === 'metricflow-cli'
|
|
15708
|
-
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
15709
|
-
: undefined,
|
|
15710
|
-
// Compilation already produced the exact pinned adapter result
|
|
15711
|
-
// used by the plan. The gateway validates target identity before
|
|
15712
|
-
// accepting that result for preflight and execution.
|
|
15713
|
-
compile: async () => semanticCompose.compiled,
|
|
15714
|
-
prepareSql: () => ({ sql: prepared.sql, connection: prepared.connection }),
|
|
15715
|
-
rowBound: semanticCompose.compiled.effectiveRequest.limit,
|
|
15716
|
-
})
|
|
15717
|
-
: null;
|
|
15718
|
-
const rawResult = semanticExecution?.result ?? await executor.executeQuery(prepared.sql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) }), prepared.connection);
|
|
15719
|
-
const normalized = normalizeQueryResult(rawResult);
|
|
16970
|
+
const semanticExecution = semanticExecutionHolder.value;
|
|
15720
16971
|
// Enforce the block's declared invariants against the result set. This
|
|
15721
16972
|
// is additive: blocks without invariants produce `null` and the
|
|
15722
16973
|
// response is unchanged. The agent surface (`query_via_block`) reads
|
|
@@ -15967,6 +17218,26 @@ table: ${table}${tagList}
|
|
|
15967
17218
|
});
|
|
15968
17219
|
});
|
|
15969
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
|
+
}
|
|
15970
17241
|
async function validateModelingRelationship(relationship, manifest, execute, quoteIdentifier = quoteSqlIdentifier) {
|
|
15971
17242
|
const fromEntity = manifest.modeling?.entities[relationship.from];
|
|
15972
17243
|
const toEntity = manifest.modeling?.entities[relationship.to];
|
|
@@ -18651,9 +19922,104 @@ function createDqlArtifactExecutionReceipt(source, compiledSql, parameters, resu
|
|
|
18651
19922
|
})),
|
|
18652
19923
|
};
|
|
18653
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
|
+
}
|
|
18654
19954
|
function executionFingerprint(value) {
|
|
18655
19955
|
return createHash('sha256').update(value).digest('hex');
|
|
18656
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
|
+
}
|
|
18657
20023
|
/**
|
|
18658
20024
|
* Older clients normalized executable DQL with trim(), while the receipt was
|
|
18659
20025
|
* correctly created from the original renderer output ending in one newline.
|
|
@@ -19071,6 +20437,43 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
|
|
|
19071
20437
|
connection: normalizedConnection,
|
|
19072
20438
|
};
|
|
19073
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
|
+
}
|
|
19074
20477
|
/**
|
|
19075
20478
|
* One host-owned SQL preparation boundary shared by Ask and Notebook.
|
|
19076
20479
|
*
|