@duckcodeailabs/dql-cli 1.13.4 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.d.ts +2 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +5 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{AgentLogPage-DYmHO6_i.js → AgentLogPage-Ch7VK20X.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-BfhaOd1h.js → AiBuildDialog-DBr5TmyM.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-DLKFAe6R.js → AiBuildResult-jLPzQO7O.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiSidePanel-C6Z3URGr.js → AiSidePanel-CdlGsiVC.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AnalyticsHome-uG3xCP_s.js → AnalyticsHome-C0DXbOwY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AppsView-DfieNjkh.js → AppsView-CcpwjApv.js} +4 -4
- package/dist/assets/dql-notebook/assets/{BlockStudio-90J_wvWL.js → BlockStudio-CFYxafw-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-_zSMae5q.js → BusinessArtifactView-C0kLYg2p.js} +1 -1
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-DoXiPssu.js → DbtFirstModelingPage-CMwElXD_.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GitPage-CUfAhS67.js → GitPage-JjhRDeWY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-ovtL7nQu.js → GlobalAiRail-DakE4NdR.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-CaMiKAzA.js → GovernedContextPage-BokDqG6a.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-CEGUx28b.js → HelpDocsPage-CjOv6_gz.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-2v3SRf8V.js → HomePage-nGaNcwdw.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDAG-AaqLEiBN.js → LineageDAG-CO6CFJRg.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDetailView-CFc8pm91.js → LineageDetailView-BnGb7OF7.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-DOkFjPq-.js → LineageDrawer-C5Y0Ht0b.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-BrDwCuMs.js → LineagePathBreadcrumb-Cgh3GR3F.js} +1 -1
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-CiHtQs3y.js → MiniLineageGraph-Kla9PYuj.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-DAgJZbws.js → NewBlockModal-BR2SnmPT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-xjG7nZ3O.js → NewNotebookModal-BaCHMYeB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-CXZaKfYT.js → NotebookEditor-CBLqY8cE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{ReadinessPage-MuJBFwwg.js → ReadinessPage-CiN0IWSS.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-vKFtKtjh.js → SetupOnboarding-BhiCsYF-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SkillsPage-DCFVOV5f.js → SkillsPage-CCSf8VMm.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-TcwXFFHK.js → TrustBadge-BgQmFe_x.js} +1 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-Bw5AXNDB.js +88 -0
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-DCFWlHB7.js → answer-to-notebook-AeDYUDla.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-BLnlJAtp.js → arrow-left-C55x2hq_.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-BmUAb1aW.js → arrow-right-C1cJrhOm.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-bdSChSag.js → book-open-text-CQf_sdv2.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-Cc6dEaf4.js → circle-x-X8-Z2yLY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-CPebQKQ9.js → dagre.esm-BjjNYKyY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-rmXvubf3.js → external-link-C2zrz5DH.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-Sg4zd5-Q.js → grip-vertical-qGV_PYGU.js} +1 -1
- package/dist/assets/dql-notebook/assets/{index-mQd_pC-P.js → index-ByTDPDaH.js} +127 -127
- package/dist/assets/dql-notebook/assets/{link-2-BLaPoFxW.js → link-2-VpyOxQXG.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-j0s4asFK.js → list-tree-CH2Jhwms.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-Cl4pef4s.js → minimize-2-B2TZJ8BT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-Clz-FtWi.js → panel-right-open-BunF88lt.js} +1 -1
- package/dist/assets/dql-notebook/assets/{play-DFdazMg7.js → play-DAFVF4_G.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-C3tTaR7s.js → rotate-ccw-DGCrrqtY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-DIAhTlfy.js → semantic-fields-Ci-9QL9F.js} +1 -1
- package/dist/assets/dql-notebook/assets/{sliders-horizontal-BUiu2tfY.js → sliders-horizontal-BOAlXXbn.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star--Yq77aH4.js → star-CkksZXHt.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-D5Ng-xZO.js → triangle-alert-D3mjyJZE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{upload-L8BKQAxP.js → upload-CTNOAVEO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-DBu97YaM.js → usePersistedAgentThreadId-DiQjc7x-.js} +4 -4
- package/dist/assets/dql-notebook/assets/{user-round-BK5N1cJW.js → user-round-BWd5tQRg.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-D8XDQD9z.js → wand-sparkles-CqsAv8P-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-Bl9zwG0q.js → workflow-CSqsj-sC.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-C-zLCMbW.js → wrench-DWqzqlX8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-stiJ-sP7.js → x-65M5rLCE.js} +1 -1
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/commands/eval.d.ts +15 -1
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +32 -3
- package/dist/commands/eval.js.map +1 -1
- package/dist/index.js +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 +315 -22
- 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 +200 -5
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +2280 -457
- 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, buildCoverageGap, capResearchBranches, buildResearchEvidenceLedger, buildAnalyticalTurnPlan, DEFAULT_ASK_ROW_EGRESS_POLICY, ZERO_ROW_EGRESS_POLICY, resolveProviderResultRowEgressPolicy, normalizeCanonicalQueryResult, normalizeAnalyticalExecutionFingerprint, normalizeAnalyticalExecutionReceipt, createAgentRunCancellationError, } 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' || run.stopReason !== '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 } : {}),
|
|
@@ -778,12 +963,43 @@ export function slimAgentRunForTransport(run) {
|
|
|
778
963
|
: {}),
|
|
779
964
|
};
|
|
780
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* INDEX projection for `GET /api/agent-runs` — strictly lighter than the
|
|
968
|
+
* presentation projection above.
|
|
969
|
+
*
|
|
970
|
+
* A run history list renders one ROW per run: question, route, status, trust,
|
|
971
|
+
* timing, summary. It never renders an answer body, an event stream, or a step
|
|
972
|
+
* trace. Shipping those anyway dominated the response: on 300 real stored runs
|
|
973
|
+
* a 20-run page was 47.61 MB whole and still 6.80 MB under the presentation
|
|
974
|
+
* projection, of which 6.35 MB was `artifacts[].payload` alone.
|
|
975
|
+
*
|
|
976
|
+
* Artifact identity (`id`/`kind`/`title`/`trustState`) is kept so a row can say
|
|
977
|
+
* what it produced; only the payload body is dropped. The complete immutable
|
|
978
|
+
* record stays available from `GET /api/agent-runs/:id`.
|
|
979
|
+
*
|
|
980
|
+
* Acceptance: PERF-003, E2E-022.
|
|
981
|
+
*/
|
|
982
|
+
export function agentRunListEntryForTransport(run) {
|
|
983
|
+
const slim = slimAgentRunForTransport(run);
|
|
984
|
+
const artifacts = (slim.artifacts ?? []).map((artifact) => {
|
|
985
|
+
const record = agentRunRecord(artifact);
|
|
986
|
+
if (!record)
|
|
987
|
+
return artifact;
|
|
988
|
+
const { payload: _payload, ...rest } = record;
|
|
989
|
+
return rest;
|
|
990
|
+
});
|
|
991
|
+
return { ...slim, artifacts, steps: [], events: [] };
|
|
992
|
+
}
|
|
781
993
|
export function conversationTurnInputFromRun(run) {
|
|
782
994
|
const artifact = run.artifacts.find((candidate) => candidate.kind === 'answer')
|
|
783
995
|
?? run.artifacts.find((candidate) => candidate.kind === 'research_run')
|
|
784
996
|
?? run.artifacts[0];
|
|
785
997
|
const payload = agentRunRecord(artifact?.payload);
|
|
786
|
-
|
|
998
|
+
// A blocked run may retain diagnostic artifacts, but their result-shaped
|
|
999
|
+
// payload is never accepted conversation evidence or prose authority.
|
|
1000
|
+
const result = run.status === 'blocked' || run.status === 'cancelled'
|
|
1001
|
+
? undefined
|
|
1002
|
+
: agentRunRecord(payload?.result);
|
|
787
1003
|
const columns = conversationResultColumns(result?.columns);
|
|
788
1004
|
// The visual preview stays tiny, but member resolution needs a wider bounded
|
|
789
1005
|
// value window. Deriving dimensions from only the eight preview rows caused a
|
|
@@ -924,6 +1140,230 @@ function apiErrorCode(error, fallback) {
|
|
|
924
1140
|
function apiErrorMessage(error) {
|
|
925
1141
|
return error instanceof Error ? error.message : String(error);
|
|
926
1142
|
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Slowest provider dispatch we assume before any have been observed, and the
|
|
1145
|
+
* settle time reserved after the last one. A subscription-CLI provider costs
|
|
1146
|
+
* roughly 10-15s per call, so these keep the first admission decision honest.
|
|
1147
|
+
*/
|
|
1148
|
+
const ASSUMED_PROVIDER_DISPATCH_MS = 12_000;
|
|
1149
|
+
const DISPATCH_SETTLE_MARGIN_MS = 4_000;
|
|
1150
|
+
export class RunScopedProviderDispatchEvidence {
|
|
1151
|
+
policy;
|
|
1152
|
+
runBudget;
|
|
1153
|
+
rowEgress;
|
|
1154
|
+
receipts = [];
|
|
1155
|
+
phaseCounts = new Map();
|
|
1156
|
+
currentRoute;
|
|
1157
|
+
/** Wall-clock start of the previous dispatch, used to learn this provider's cost. */
|
|
1158
|
+
lastDispatchStartedAtMs;
|
|
1159
|
+
observedDispatchDurations = [];
|
|
1160
|
+
/**
|
|
1161
|
+
* How long the NEXT dispatch is likely to take, learned from this run.
|
|
1162
|
+
*
|
|
1163
|
+
* Counting dispatches is not a time budget. A provider costing ~13s a call
|
|
1164
|
+
* will burn a 45s deadline in three calls and get killed mid-flight, so the
|
|
1165
|
+
* run ends with nothing — strictly worse than stopping early and answering
|
|
1166
|
+
* from what it already has.
|
|
1167
|
+
*/
|
|
1168
|
+
expectedDispatchMs() {
|
|
1169
|
+
if (this.observedDispatchDurations.length === 0)
|
|
1170
|
+
return ASSUMED_PROVIDER_DISPATCH_MS;
|
|
1171
|
+
const sorted = [...this.observedDispatchDurations].sort((left, right) => left - right);
|
|
1172
|
+
// The slowest observed call is the honest predictor: an optimistic median
|
|
1173
|
+
// still admits a dispatch that the deadline then kills.
|
|
1174
|
+
return sorted[sorted.length - 1];
|
|
1175
|
+
}
|
|
1176
|
+
/** True when the remaining wall clock cannot fit another provider call. */
|
|
1177
|
+
cannotFitAnotherDispatch() {
|
|
1178
|
+
if (!this.runBudget)
|
|
1179
|
+
return false;
|
|
1180
|
+
return this.runBudget.remainingMs() < this.expectedDispatchMs() + DISPATCH_SETTLE_MARGIN_MS;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* The run budget's own clock when there is one, so dispatch cost is measured
|
|
1184
|
+
* against the same timeline the deadline is enforced on (and stays injectable
|
|
1185
|
+
* for deterministic tests).
|
|
1186
|
+
*/
|
|
1187
|
+
nowMs() {
|
|
1188
|
+
return this.runBudget ? this.runBudget.elapsedMs() : Date.now();
|
|
1189
|
+
}
|
|
1190
|
+
recordDispatchStart() {
|
|
1191
|
+
const now = this.nowMs();
|
|
1192
|
+
if (this.lastDispatchStartedAtMs !== undefined) {
|
|
1193
|
+
this.observedDispatchDurations.push(Math.max(0, now - this.lastDispatchStartedAtMs));
|
|
1194
|
+
}
|
|
1195
|
+
this.lastDispatchStartedAtMs = now;
|
|
1196
|
+
}
|
|
1197
|
+
constructor(policy, runBudget, rowEgress = ZERO_ROW_EGRESS_POLICY) {
|
|
1198
|
+
this.policy = policy;
|
|
1199
|
+
this.runBudget = runBudget;
|
|
1200
|
+
this.rowEgress = rowEgress;
|
|
1201
|
+
this.currentRoute = runBudget?.mode === 'research' ? 'research' : 'generated_answer';
|
|
1202
|
+
}
|
|
1203
|
+
setRoute(route) {
|
|
1204
|
+
if (route)
|
|
1205
|
+
this.currentRoute = route;
|
|
1206
|
+
}
|
|
1207
|
+
mayStartToolCall() {
|
|
1208
|
+
return !this.runBudget || this.runBudget.mayStartDiscovery(this.currentRoute);
|
|
1209
|
+
}
|
|
1210
|
+
observe(event, context) {
|
|
1211
|
+
const softRoute = context.dispatchPhase === 'meaning_resolution' ? 'clarify' : this.currentRoute;
|
|
1212
|
+
// Admission control BEFORE the phase targets: starting a call that the hard
|
|
1213
|
+
// deadline will kill mid-flight wastes the remaining budget and ends the run
|
|
1214
|
+
// with nothing. Stopping here lets the caller answer from what it has.
|
|
1215
|
+
// Narration is exempt: it is the step that produces the answer, and it is
|
|
1216
|
+
// separately bounded by its own soft target below.
|
|
1217
|
+
if (context.dispatchPhase !== 'narration' && this.cannotFitAnotherDispatch()) {
|
|
1218
|
+
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' });
|
|
1219
|
+
}
|
|
1220
|
+
if (context.dispatchPhase === 'narration') {
|
|
1221
|
+
if (this.runBudget && !this.runBudget.mayStartNarration()) {
|
|
1222
|
+
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' });
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
else if (this.runBudget && !this.runBudget.mayStartDiscovery(softRoute)) {
|
|
1226
|
+
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' });
|
|
1227
|
+
}
|
|
1228
|
+
this.recordDispatchStart();
|
|
1229
|
+
if (this.receipts.length >= this.policy.total) {
|
|
1230
|
+
throw Object.assign(new Error(`Run-wide provider dispatch budget exhausted after ${this.policy.total} physical attempts.`), { code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED' });
|
|
1231
|
+
}
|
|
1232
|
+
const phaseCount = this.phaseCounts.get(context.dispatchPhase) ?? 0;
|
|
1233
|
+
const phaseLimit = context.dispatchPhase === 'meaning_resolution'
|
|
1234
|
+
? this.policy.meaningResolution
|
|
1235
|
+
: context.dispatchPhase === 'repair'
|
|
1236
|
+
? this.policy.repair
|
|
1237
|
+
: context.dispatchPhase === 'narration'
|
|
1238
|
+
? this.policy.narration
|
|
1239
|
+
: this.policy.generationGroup;
|
|
1240
|
+
// Planning and generation share one ledger because they compete for the
|
|
1241
|
+
// same "work out the query" budget. Narration does not: it is the step that
|
|
1242
|
+
// turns a settled result into an answer, and starving it produces a run
|
|
1243
|
+
// that computed the right numbers and then could not say them.
|
|
1244
|
+
const generationGroupCount = this.receipts.filter((receipt) => receipt.dispatchPhase === 'planning'
|
|
1245
|
+
|| receipt.dispatchPhase === 'generation').length;
|
|
1246
|
+
if (phaseCount >= phaseLimit || ((context.dispatchPhase === 'planning' || context.dispatchPhase === 'generation')
|
|
1247
|
+
&& generationGroupCount >= this.policy.generationGroup)) {
|
|
1248
|
+
throw Object.assign(new Error(`Provider dispatch budget exhausted for ${context.dispatchPhase} after ${phaseLimit} physical attempts.`), { code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED' });
|
|
1249
|
+
}
|
|
1250
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
|
|
1251
|
+
const projectedRowCount = context.serializedResultShape?.resultRowCount ?? 0;
|
|
1252
|
+
const projectedCumulativeRowCount = context.cumulativeResultRowCount ?? projectedRowCount;
|
|
1253
|
+
// Ordinary Ask narration reads its ceiling from the resolved project policy;
|
|
1254
|
+
// Research keeps its own explicit opt-in limits.
|
|
1255
|
+
const permittedRowLimit = context.purpose === 'answer_narration'
|
|
1256
|
+
? this.rowEgress.maxNarrationRows
|
|
1257
|
+
: context.optIn && context.purpose === 'research_narration'
|
|
1258
|
+
? 20
|
|
1259
|
+
: context.optIn && context.purpose === 'research_tool'
|
|
1260
|
+
? 200
|
|
1261
|
+
: 0;
|
|
1262
|
+
if (projectedRowCount > permittedRowLimit || projectedCumulativeRowCount > permittedRowLimit) {
|
|
1263
|
+
throw Object.assign(new Error(permittedRowLimit === 0
|
|
1264
|
+
? 'Provider egress blocked result rows without explicit Research run consent.'
|
|
1265
|
+
: `Provider egress blocked result rows beyond the cumulative Research limit of ${permittedRowLimit}.`), { code: permittedRowLimit === 0 ? 'PROVIDER_RESULT_ROWS_BLOCKED' : 'PROVIDER_RESULT_ROWS_LIMIT_EXCEEDED' });
|
|
1266
|
+
}
|
|
1267
|
+
assertProviderPayloadAllowed(envelope, {
|
|
1268
|
+
allowResultRows: false,
|
|
1269
|
+
maxResultRows: 0,
|
|
1270
|
+
purpose: context.purpose,
|
|
1271
|
+
});
|
|
1272
|
+
const rowCount = projectedRowCount;
|
|
1273
|
+
this.receipts.push(createProviderDispatchEgressReceipt({
|
|
1274
|
+
purpose: context.purpose,
|
|
1275
|
+
dispatchPhase: context.dispatchPhase,
|
|
1276
|
+
provider: event.provider,
|
|
1277
|
+
...(event.model ? { model: event.model } : {}),
|
|
1278
|
+
operation: event.operation,
|
|
1279
|
+
attemptIndex: event.attemptIndex,
|
|
1280
|
+
options: event.options,
|
|
1281
|
+
permittedCategories: rowCount > 0
|
|
1282
|
+
? ['instructions', 'question', 'schema_metadata', 'governed_context', 'result_rows']
|
|
1283
|
+
: ['instructions', 'question', 'schema_metadata', 'governed_context'],
|
|
1284
|
+
optIn: context.optIn && rowCount > 0,
|
|
1285
|
+
envelope,
|
|
1286
|
+
...(context.serializedResultShape ? { serializedResultShape: context.serializedResultShape } : {}),
|
|
1287
|
+
...(typeof context.cumulativeResultRowCount === 'number'
|
|
1288
|
+
? { cumulativeResultRowCount: context.cumulativeResultRowCount }
|
|
1289
|
+
: {}),
|
|
1290
|
+
}));
|
|
1291
|
+
this.phaseCounts.set(context.dispatchPhase, phaseCount + 1);
|
|
1292
|
+
return envelope;
|
|
1293
|
+
}
|
|
1294
|
+
snapshot(fallbackReason = 'none') {
|
|
1295
|
+
return {
|
|
1296
|
+
providerEgressReceipts: [...this.receipts],
|
|
1297
|
+
providerRoundTrips: this.receipts.length,
|
|
1298
|
+
toolCalls: 0,
|
|
1299
|
+
sqlExecutions: 0,
|
|
1300
|
+
repairs: 0,
|
|
1301
|
+
fallbackReason,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const agentRunProviderEvidenceContext = new AsyncLocalStorage();
|
|
1306
|
+
function mergeRunScopedProviderDispatchEvidence(run, evidence) {
|
|
1307
|
+
const providerEgressReceipts = [...evidence.providerEgressReceipts];
|
|
1308
|
+
const elapsed = Math.max(0, Date.parse(run.completedAt) - Date.parse(run.startedAt));
|
|
1309
|
+
const telemetry = {
|
|
1310
|
+
version: 1,
|
|
1311
|
+
stageDurationsMs: {
|
|
1312
|
+
...(run.telemetry?.stageDurationsMs ?? {}),
|
|
1313
|
+
total: Number.isFinite(elapsed) ? Math.min(86_400_000, elapsed) : 0,
|
|
1314
|
+
},
|
|
1315
|
+
providerRoundTrips: evidence.providerRoundTrips,
|
|
1316
|
+
toolCalls: run.telemetry?.toolCalls ?? evidence.toolCalls,
|
|
1317
|
+
sqlExecutions: run.telemetry?.sqlExecutions ?? evidence.sqlExecutions,
|
|
1318
|
+
repairs: run.telemetry?.repairs ?? evidence.repairs,
|
|
1319
|
+
egressReceipts: providerEgressReceipts.length,
|
|
1320
|
+
...(run.telemetry?.warehouseDurationMs !== undefined
|
|
1321
|
+
? { warehouseDurationMs: run.telemetry.warehouseDurationMs }
|
|
1322
|
+
: {}),
|
|
1323
|
+
...(run.telemetry?.fallbackReason
|
|
1324
|
+
? { fallbackReason: run.telemetry.fallbackReason }
|
|
1325
|
+
: evidence.fallbackReason !== 'none'
|
|
1326
|
+
? { fallbackReason: evidence.fallbackReason }
|
|
1327
|
+
: {}),
|
|
1328
|
+
};
|
|
1329
|
+
const diagnosticReceipt = run.diagnosticReceipt
|
|
1330
|
+
? { ...run.diagnosticReceipt, providerEgressReceipts }
|
|
1331
|
+
: undefined;
|
|
1332
|
+
const diagnosticReceiptV2 = {
|
|
1333
|
+
version: 2,
|
|
1334
|
+
runId: run.id,
|
|
1335
|
+
route: run.route,
|
|
1336
|
+
status: run.status,
|
|
1337
|
+
telemetry,
|
|
1338
|
+
providerEgressReceiptFingerprints: providerEgressReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
|
|
1339
|
+
...(run.diagnosticReceiptV2?.repairCapabilityFingerprint
|
|
1340
|
+
? { repairCapabilityFingerprint: run.diagnosticReceiptV2.repairCapabilityFingerprint }
|
|
1341
|
+
: {}),
|
|
1342
|
+
};
|
|
1343
|
+
const artifacts = run.artifacts.map((artifact) => {
|
|
1344
|
+
if (!artifact.payload || typeof artifact.payload !== 'object' || Array.isArray(artifact.payload))
|
|
1345
|
+
return artifact;
|
|
1346
|
+
const payload = artifact.payload;
|
|
1347
|
+
if (!('diagnosticReceipt' in payload) && !('diagnosticReceiptV2' in payload))
|
|
1348
|
+
return artifact;
|
|
1349
|
+
return {
|
|
1350
|
+
...artifact,
|
|
1351
|
+
payload: {
|
|
1352
|
+
...payload,
|
|
1353
|
+
...(diagnosticReceipt ? { diagnosticReceipt } : {}),
|
|
1354
|
+
diagnosticReceiptV2,
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
});
|
|
1358
|
+
return {
|
|
1359
|
+
...run,
|
|
1360
|
+
artifacts,
|
|
1361
|
+
providerEgressReceipts,
|
|
1362
|
+
telemetry,
|
|
1363
|
+
...(diagnosticReceipt ? { diagnosticReceipt } : {}),
|
|
1364
|
+
diagnosticReceiptV2,
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
927
1367
|
export async function startLocalServer(opts) {
|
|
928
1368
|
const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
|
|
929
1369
|
const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
|
|
@@ -945,6 +1385,11 @@ export async function startLocalServer(opts) {
|
|
|
945
1385
|
if (gitRoot)
|
|
946
1386
|
ensureLocalRuntimeGitignore(projectRoot);
|
|
947
1387
|
let projectConfig = loadProjectConfig(projectRoot);
|
|
1388
|
+
const analyticalExecutionService = new ExecutionService({
|
|
1389
|
+
executor,
|
|
1390
|
+
projectRoot,
|
|
1391
|
+
projectConfig: () => projectConfig,
|
|
1392
|
+
});
|
|
948
1393
|
recordAgentRuntimeVersion(projectRoot, runtimeVersion);
|
|
949
1394
|
// A clone carries governed Hint Graph files in Git, never the rebuildable
|
|
950
1395
|
// `.dql/cache` database. Materialize that projection as soon as the Notebook
|
|
@@ -1566,23 +2011,81 @@ export async function startLocalServer(opts) {
|
|
|
1566
2011
|
};
|
|
1567
2012
|
// Provider-backed narration for stakeholder stories. Reuses the same provider
|
|
1568
2013
|
// adapter as the planner; narrateResult always returns (deterministic fallback).
|
|
1569
|
-
const narrateForAgentRun = async (input) =>
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2014
|
+
const narrateForAgentRun = async (input, allowProviderResultRows = false) => {
|
|
2015
|
+
// Without caller consent, keep narration deterministic and make no physical
|
|
2016
|
+
// provider dispatch. With consent, serialize only the bounded, redacted
|
|
2017
|
+
// sample the transport receipt will account for — bounded by the project's
|
|
2018
|
+
// own egress policy, so an admin kill-switch is not silently bypassed here.
|
|
2019
|
+
if (!allowProviderResultRows || !input.result)
|
|
2020
|
+
return narrateResult(input);
|
|
2021
|
+
const narrationRowEgress = resolveProviderResultRowEgressPolicy({
|
|
2022
|
+
projectSetting: projectConfig?.agent?.providerResultRowEgress,
|
|
2023
|
+
});
|
|
2024
|
+
if (narrationRowEgress.maxNarrationRows === 0)
|
|
2025
|
+
return narrateResult(input);
|
|
2026
|
+
const safeResult = {
|
|
2027
|
+
...input.result,
|
|
2028
|
+
rows: redactProviderResultRows(input.result.rows, narrationRowEgress.maxNarrationRows),
|
|
2029
|
+
};
|
|
2030
|
+
const safeInput = {
|
|
2031
|
+
...input,
|
|
2032
|
+
result: safeResult,
|
|
2033
|
+
};
|
|
2034
|
+
return narrateResult(safeInput, {
|
|
2035
|
+
complete: async ({ system, user, signal }) => {
|
|
2036
|
+
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
2037
|
+
if (!provider)
|
|
2038
|
+
throw new Error('No AI provider configured for narration.');
|
|
2039
|
+
return provider.generate([{ role: 'system', content: system }, { role: 'user', content: user }], {
|
|
2040
|
+
maxTokens: 600,
|
|
2041
|
+
temperature: 0.2,
|
|
2042
|
+
signal,
|
|
2043
|
+
maxProviderDispatches: 2,
|
|
2044
|
+
...(agentRunProviderEvidenceContext.getStore() ? {
|
|
2045
|
+
// This lane narrates App/notebook results, not Research. It used to
|
|
2046
|
+
// declare `research_narration` to clear the row check, which made
|
|
2047
|
+
// receipt analytics conflate two different lanes.
|
|
2048
|
+
onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
2049
|
+
purpose: 'answer_narration',
|
|
2050
|
+
dispatchPhase: 'narration',
|
|
2051
|
+
optIn: true,
|
|
2052
|
+
serializedResultShape: {
|
|
2053
|
+
resultRowCount: safeResult.rows.length,
|
|
2054
|
+
columnCount: safeResult.columns.length,
|
|
2055
|
+
},
|
|
2056
|
+
cumulativeResultRowCount: safeResult.rows.length,
|
|
2057
|
+
}),
|
|
2058
|
+
} : {}),
|
|
2059
|
+
});
|
|
2060
|
+
},
|
|
2061
|
+
});
|
|
2062
|
+
};
|
|
1577
2063
|
async function runGovernedAgentAnswerForRun(request, repair, route = 'generated_answer', onProgress, routeDecision) {
|
|
1578
2064
|
const governed = resolveGovernedAnswerRunner(projectRoot);
|
|
1579
|
-
|
|
1580
|
-
|
|
2065
|
+
let resolvedProvider = governed?.provider ?? null;
|
|
2066
|
+
let runner = governed?.runner ?? null;
|
|
2067
|
+
const exactProviderFreePlan = routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
2068
|
+
&& (routeDecision.resolvedAnalyticalPlan.capability === 'certified_execution'
|
|
2069
|
+
|| routeDecision.resolvedAnalyticalPlan.capability === 'semantic_execution');
|
|
2070
|
+
if ((!resolvedProvider || !runner) && exactProviderFreePlan) {
|
|
2071
|
+
const deterministicProvider = {
|
|
2072
|
+
name: 'ollama',
|
|
2073
|
+
available: async () => true,
|
|
2074
|
+
generate: async () => {
|
|
2075
|
+
throw Object.assign(new Error('Exact certified/semantic execution must not dispatch a provider.'), {
|
|
2076
|
+
code: 'EXACT_ROUTE_PROVIDER_DISPATCH_FORBIDDEN',
|
|
2077
|
+
});
|
|
2078
|
+
},
|
|
2079
|
+
};
|
|
2080
|
+
resolvedProvider = 'ollama';
|
|
2081
|
+
runner = createDqlAgentProviderRunner('ollama', deterministicProvider);
|
|
2082
|
+
}
|
|
1581
2083
|
if (!resolvedProvider || !runner) {
|
|
1582
2084
|
throw new Error('No AI provider is configured. Configure a subscription (Claude Code / Codex), OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.');
|
|
1583
2085
|
}
|
|
1584
2086
|
let governedAnswer;
|
|
1585
2087
|
let providerError;
|
|
2088
|
+
let providerDispatchEvidence;
|
|
1586
2089
|
const isRepair = (repair?.attempt ?? 0) > 0 && Boolean(repair?.repairHint);
|
|
1587
2090
|
// The chat composer sends a `thinkingMode` (auto/low/medium/high); resolve it
|
|
1588
2091
|
// into the effort+depth bundle it stands for. An explicit `reasoningEffort` /
|
|
@@ -1642,6 +2145,9 @@ export async function startLocalServer(opts) {
|
|
|
1642
2145
|
const semanticTableMapping = semanticLayer && semanticConnection
|
|
1643
2146
|
? await resolveSemanticTableMapping(executor, semanticConnection, semanticLayer, projectRoot, semanticConnectionName)
|
|
1644
2147
|
: undefined;
|
|
2148
|
+
const generatedProposalTargetIdentity = semanticConnection
|
|
2149
|
+
? await observeWarehouseTargetIdentity(executor, semanticConnection)
|
|
2150
|
+
: undefined;
|
|
1645
2151
|
const requestedDomain = normalizeAgentRunDomain(agentRunWorkspaceValue(request, 'domain'));
|
|
1646
2152
|
const requestedPurpose = agentRunWorkspaceValue(request, 'purpose');
|
|
1647
2153
|
const requestedModelAreaId = agentRunWorkspaceValue(request, 'modelAreaId');
|
|
@@ -1659,6 +2165,9 @@ export async function startLocalServer(opts) {
|
|
|
1659
2165
|
: undefined;
|
|
1660
2166
|
await runner.run({
|
|
1661
2167
|
provider: resolvedProvider,
|
|
2168
|
+
...(agentRunProviderEvidenceContext.getStore()
|
|
2169
|
+
? { providerDispatchEvidenceSink: agentRunProviderEvidenceContext.getStore() }
|
|
2170
|
+
: {}),
|
|
1662
2171
|
messages: [
|
|
1663
2172
|
// No-thread fallback path: bound the raw client history so follow-ups
|
|
1664
2173
|
// cannot inflate every provider call (8 turns × 4k chars).
|
|
@@ -1678,6 +2187,8 @@ export async function startLocalServer(opts) {
|
|
|
1678
2187
|
},
|
|
1679
2188
|
reasoningEffort,
|
|
1680
2189
|
...(analysisDepth ? { analysisDepth } : {}),
|
|
2190
|
+
allowProviderSemanticMemberSelection: route === 'research',
|
|
2191
|
+
researchResultRowsOptIn: route === 'research' && request.researchResultRowsOptIn === true,
|
|
1681
2192
|
projectRoot,
|
|
1682
2193
|
preparedContextPack: preparedAgentContextPacks.get(request),
|
|
1683
2194
|
domainContext,
|
|
@@ -1799,9 +2310,20 @@ export async function startLocalServer(opts) {
|
|
|
1799
2310
|
: projectConfig.dbt?.projectDir;
|
|
1800
2311
|
throw new SemanticRuntimeRequiredError(await explainMissingSemanticRuntime(projectRoot, dbtProjectPath));
|
|
1801
2312
|
}
|
|
1802
|
-
|
|
2313
|
+
// The semantic bridge freezes the compiler output after trimming
|
|
2314
|
+
// transport-only surrounding whitespace. Bind the compiler receipt
|
|
2315
|
+
// and execution lookup to that exact consumed SQL identity.
|
|
2316
|
+
const governedCompiledSql = compiled.sql.trim();
|
|
2317
|
+
compiledSemanticQueries.set(executionFingerprint(governedCompiledSql), compiled);
|
|
2318
|
+
const aggregationCompilerReceipt = buildSemanticAggregationCompilerReceipt({
|
|
2319
|
+
semanticLayer: semanticLayer,
|
|
2320
|
+
metricNames: compiled.effectiveRequest.metrics,
|
|
2321
|
+
compiledSql: governedCompiledSql,
|
|
2322
|
+
plan: routeDecision?.resolvedAnalyticalPlan,
|
|
2323
|
+
targetFingerprint: targetBinding?.executionTarget.identityFingerprint,
|
|
2324
|
+
});
|
|
1803
2325
|
return {
|
|
1804
|
-
sql:
|
|
2326
|
+
sql: governedCompiledSql,
|
|
1805
2327
|
engine: compiled.engine,
|
|
1806
2328
|
selection: {
|
|
1807
2329
|
...selection,
|
|
@@ -1814,7 +2336,9 @@ export async function startLocalServer(opts) {
|
|
|
1814
2336
|
trace: {
|
|
1815
2337
|
...compiled.semanticTrace,
|
|
1816
2338
|
...(targetBinding ? { targetBinding } : {}),
|
|
2339
|
+
...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
|
|
1817
2340
|
},
|
|
2341
|
+
...(aggregationCompilerReceipt ? { aggregationCompilerReceipt } : {}),
|
|
1818
2342
|
};
|
|
1819
2343
|
},
|
|
1820
2344
|
} : {}),
|
|
@@ -1827,6 +2351,9 @@ export async function startLocalServer(opts) {
|
|
|
1827
2351
|
...(routeDecision?.resolvedAnalyticalPlan
|
|
1828
2352
|
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
1829
2353
|
: {}),
|
|
2354
|
+
...(generatedProposalTargetIdentity?.identityFingerprint
|
|
2355
|
+
? { generatedProposalTargetFingerprint: generatedProposalTargetIdentity.identityFingerprint }
|
|
2356
|
+
: {}),
|
|
1830
2357
|
analyticalReferenceInstant: new Date().toISOString(),
|
|
1831
2358
|
executeCertifiedBlock: (node, invocation) => executeCertifiedBlockForAgent(node, invocation, semanticConnection, semanticConnectionName),
|
|
1832
2359
|
executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName),
|
|
@@ -1843,10 +2370,11 @@ export async function startLocalServer(opts) {
|
|
|
1843
2370
|
}
|
|
1844
2371
|
if (turn.kind === 'error') {
|
|
1845
2372
|
providerError = turn.message;
|
|
2373
|
+
providerDispatchEvidence = turn.dispatchEvidence;
|
|
1846
2374
|
}
|
|
1847
2375
|
}, runSignal);
|
|
1848
2376
|
if (!governedAnswer) {
|
|
1849
|
-
throw new Error(providerError ?? 'The AI provider did not return a governed answer.');
|
|
2377
|
+
throw Object.assign(new Error(providerError ?? 'The AI provider did not return a governed answer.'), ...(providerDispatchEvidence ? [{ providerDispatchEvidence }] : []));
|
|
1850
2378
|
}
|
|
1851
2379
|
return governedAnswer;
|
|
1852
2380
|
}
|
|
@@ -2073,7 +2601,11 @@ export async function startLocalServer(opts) {
|
|
|
2073
2601
|
surface: 'ask_ai',
|
|
2074
2602
|
});
|
|
2075
2603
|
const certified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
2076
|
-
|
|
2604
|
+
// A semantic route is governed only when its route-specific aggregation
|
|
2605
|
+
// proof passed. The route label is not authority: treating a blocked or
|
|
2606
|
+
// missing proof as governed caused the CLI to persist a successful badge
|
|
2607
|
+
// even though the answer artifact recorded insufficient context.
|
|
2608
|
+
const semantic = semanticAnswerHasPassedAggregationProof(governedAnswer);
|
|
2077
2609
|
governedAnswer.dqlArtifact = {
|
|
2078
2610
|
...artifact,
|
|
2079
2611
|
...(invocation.parameters.length > 0 ? { parameters: invocation.parameters } : {}),
|
|
@@ -2082,14 +2614,156 @@ export async function startLocalServer(opts) {
|
|
|
2082
2614
|
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
2083
2615
|
...(governedAnswer.result?.sql ? { compiledSql: governedAnswer.result.sql } : originalSql ? { compiledSql: originalSql } : {}),
|
|
2084
2616
|
...(governedAnswer.result?.executionReceipt ? { executionReceipt: governedAnswer.result.executionReceipt } : {}),
|
|
2617
|
+
...(governedAnswer.result?.executableArtifact ? {
|
|
2618
|
+
executableArtifact: {
|
|
2619
|
+
...governedAnswer.result.executableArtifact,
|
|
2620
|
+
kind: artifact.kind,
|
|
2621
|
+
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
2622
|
+
},
|
|
2623
|
+
} : {}),
|
|
2085
2624
|
};
|
|
2086
2625
|
if (governedAnswer.result)
|
|
2087
2626
|
delete governedAnswer.result.dqlArtifact;
|
|
2088
2627
|
}
|
|
2089
2628
|
const answerRunExecutor = async ({ request, route, routeDecision, attempt, repairHint, emit }) => {
|
|
2629
|
+
const runStartedAtMs = Date.now();
|
|
2630
|
+
const turnPlan = buildAnalyticalTurnPlan({
|
|
2631
|
+
question: request.question,
|
|
2632
|
+
turnId: request.runId,
|
|
2633
|
+
candidateIds: routeDecision?.retrievalEvidence?.candidateIds ?? [],
|
|
2634
|
+
frozen: routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative',
|
|
2635
|
+
snapshotId: routeDecision?.resolvedAnalyticalPlan?.snapshotId ?? routeDecision?.retrievalEvidence?.snapshotId,
|
|
2636
|
+
sourceFingerprint: routeDecision?.resolvedAnalyticalPlan?.sourceFingerprint ?? routeDecision?.retrievalEvidence?.sourceFingerprint,
|
|
2637
|
+
});
|
|
2638
|
+
const childTurn = Boolean(request.workspaceContext && typeof request.workspaceContext === 'object'
|
|
2639
|
+
&& request.workspaceContext.analyticalTaskChild === true);
|
|
2640
|
+
// Compound questions are a bounded task graph, not one query whose answer
|
|
2641
|
+
// is copied into several labels. Independent children share the parent's
|
|
2642
|
+
// signal/deadline and return truthful partial success.
|
|
2643
|
+
if (turnPlan.tasks.length > 1 && !childTurn && (attempt ?? 0) === 0) {
|
|
2644
|
+
const childResults = await Promise.all(turnPlan.tasks.slice(0, 6).map(async (task) => {
|
|
2645
|
+
if (request.signal?.aborted)
|
|
2646
|
+
rethrowIfCancelled(request.signal.reason, request.signal);
|
|
2647
|
+
try {
|
|
2648
|
+
const childRequest = {
|
|
2649
|
+
...request,
|
|
2650
|
+
question: task.question,
|
|
2651
|
+
workspaceContext: {
|
|
2652
|
+
...(request.workspaceContext && typeof request.workspaceContext === 'object' ? request.workspaceContext : {}),
|
|
2653
|
+
analyticalTaskChild: true,
|
|
2654
|
+
analyticalParentRunId: request.runId,
|
|
2655
|
+
analyticalTaskId: task.id,
|
|
2656
|
+
},
|
|
2657
|
+
};
|
|
2658
|
+
const answer = await runGovernedAgentAnswerForRun(childRequest, { attempt: 0, repairHint }, route, (message) => emit({ type: 'executor.started', message: `Task ${task.id}: ${message}`, route }), undefined);
|
|
2659
|
+
if (answer.result) {
|
|
2660
|
+
const canonical = normalizeCanonicalQueryResult({
|
|
2661
|
+
...answer.result,
|
|
2662
|
+
resultFingerprint: answer.result.resultFingerprint ?? answer.result.executionReceipt?.resultFingerprint,
|
|
2663
|
+
executionReceipt: answer.result.executionReceipt,
|
|
2664
|
+
answerTier: answer.route?.tier ?? answer.sourceTier,
|
|
2665
|
+
});
|
|
2666
|
+
answer.result = {
|
|
2667
|
+
...answer.result,
|
|
2668
|
+
columns: canonical.columns,
|
|
2669
|
+
rows: canonical.rows,
|
|
2670
|
+
rowCount: canonical.rowCount,
|
|
2671
|
+
resultFingerprint: canonical.resultFingerprint,
|
|
2672
|
+
...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
|
|
2673
|
+
...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
return { task, answer };
|
|
2677
|
+
}
|
|
2678
|
+
catch (error) {
|
|
2679
|
+
rethrowIfCancelled(error, request.signal);
|
|
2680
|
+
return { task, error: error instanceof Error ? error.message : String(error) };
|
|
2681
|
+
}
|
|
2682
|
+
}));
|
|
2683
|
+
const outcomes = childResults.map(({ task, answer, error }) => ({
|
|
2684
|
+
version: 1,
|
|
2685
|
+
taskId: task.id,
|
|
2686
|
+
status: error || answer?.kind === 'no_answer' ? 'gap' : 'completed',
|
|
2687
|
+
...(answer?.answer || answer?.text ? { summary: answer.answer ?? answer.text } : {}),
|
|
2688
|
+
...(answer?.result?.resultFingerprint ? { resultFingerprint: answer.result.resultFingerprint } : {}),
|
|
2689
|
+
...(error || answer?.kind === 'no_answer' ? {
|
|
2690
|
+
gap: buildCoverageGap({
|
|
2691
|
+
code: answer?.refusalCode === 'ambiguous' ? 'AMBIGUOUS_MEANING' : 'EXECUTION_FAILED',
|
|
2692
|
+
phase: answer?.executionError ? 'execution' : 'meaning',
|
|
2693
|
+
message: error ?? answer?.answer ?? answer?.text ?? 'The task did not produce an accepted analytical result.',
|
|
2694
|
+
searchedSources: routeDecision?.retrievalEvidence?.candidateIds ?? [],
|
|
2695
|
+
attemptedRoutes: ['certified', 'semantic', 'governed_relational', 'generated'],
|
|
2696
|
+
missing: [],
|
|
2697
|
+
recoverable: false,
|
|
2698
|
+
planFrozen: turnPlan.frozen,
|
|
2699
|
+
nextActions: ['Review the task context and retry the clause.'],
|
|
2700
|
+
}),
|
|
2701
|
+
} : {}),
|
|
2702
|
+
}));
|
|
2703
|
+
const completedCount = outcomes.filter((outcome) => outcome.status === 'completed').length;
|
|
2704
|
+
const tasks = turnPlan.tasks.map((task) => ({
|
|
2705
|
+
...task,
|
|
2706
|
+
status: outcomes.find((outcome) => outcome.taskId === task.id)?.status === 'completed' ? 'completed' : 'gap',
|
|
2707
|
+
}));
|
|
2708
|
+
const answerText = childResults.map(({ task, answer, error }) => `${task.question}: ${error ?? answer?.answer ?? answer?.text ?? 'No accepted result was produced.'}`).join('\n\n');
|
|
2709
|
+
return {
|
|
2710
|
+
summary: completedCount === outcomes.length
|
|
2711
|
+
? `Answered ${completedCount} independent analytical clauses.`
|
|
2712
|
+
: `Answered ${completedCount} of ${outcomes.length} analytical clauses; the remaining clauses need review.`,
|
|
2713
|
+
answer: answerText,
|
|
2714
|
+
status: completedCount === outcomes.length ? 'completed' : completedCount > 0 ? 'needs_review' : 'needs_clarification',
|
|
2715
|
+
trustState: completedCount === outcomes.length ? 'governed' : 'review_required',
|
|
2716
|
+
stopReason: completedCount === outcomes.length ? 'governed_semantic_answer' : 'human_review_required',
|
|
2717
|
+
artifacts: childResults.map(({ task, answer, error }) => agentRunArtifact('answer', `Task: ${task.question}`, {
|
|
2718
|
+
taskId: task.id,
|
|
2719
|
+
question: task.question,
|
|
2720
|
+
answer: answer?.answer ?? answer?.text,
|
|
2721
|
+
resultFingerprint: answer?.result?.resultFingerprint,
|
|
2722
|
+
error,
|
|
2723
|
+
})),
|
|
2724
|
+
evaluations: outcomes.map((outcome) => agentRunEvaluation(`analytical-task-${outcome.taskId}`, `Analytical task ${outcome.taskId}`, outcome.status === 'completed', outcome.status === 'completed' ? 'info' : 'warning', outcome.summary ?? outcome.gap?.message ?? 'Task outcome recorded.', outcome)),
|
|
2725
|
+
analyticalTurnPlan: { ...turnPlan, tasks, frozen: true },
|
|
2726
|
+
analyticalTaskOutcomes: outcomes,
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2090
2729
|
let governedAnswer;
|
|
2091
2730
|
try {
|
|
2092
2731
|
governedAnswer = await runGovernedAgentAnswerForRun(request, { attempt, repairHint }, route, (message) => emit({ type: 'executor.started', message, route }), routeDecision);
|
|
2732
|
+
// Keep the canonical result contract on the answer itself, not only on
|
|
2733
|
+
// the narration preview. Conversation persistence, follow-up member
|
|
2734
|
+
// resolution, Apply, and the notebook table all read this payload; if a
|
|
2735
|
+
// connector returned positional rows, dropping normalization here makes
|
|
2736
|
+
// the next question lose its customer/product/region binding (AGT-032).
|
|
2737
|
+
if (governedAnswer.result) {
|
|
2738
|
+
const canonical = normalizeCanonicalQueryResult({
|
|
2739
|
+
...governedAnswer.result,
|
|
2740
|
+
resultFingerprint: governedAnswer.result.resultFingerprint
|
|
2741
|
+
?? governedAnswer.result.executionReceipt?.resultFingerprint,
|
|
2742
|
+
executionReceipt: governedAnswer.result.executionReceipt,
|
|
2743
|
+
answerTier: governedAnswer.route?.tier ?? governedAnswer.sourceTier,
|
|
2744
|
+
trustState: governedAnswer.certification === 'certified'
|
|
2745
|
+
? 'certified'
|
|
2746
|
+
: governedAnswer.kind === 'no_answer'
|
|
2747
|
+
? 'blocked'
|
|
2748
|
+
: governedAnswer.reviewStatus === 'analyst_review_required'
|
|
2749
|
+
? 'review_required'
|
|
2750
|
+
: 'governed',
|
|
2751
|
+
});
|
|
2752
|
+
governedAnswer.result = {
|
|
2753
|
+
...governedAnswer.result,
|
|
2754
|
+
columns: canonical.columns,
|
|
2755
|
+
rows: canonical.rows,
|
|
2756
|
+
rowCount: canonical.rowCount,
|
|
2757
|
+
// Preserve the execution-service/receipt fingerprint. A local UI
|
|
2758
|
+
// digest is only a legacy fallback in normalizeCanonicalQueryResult;
|
|
2759
|
+
// it must never replace the cryptographic identity of this run.
|
|
2760
|
+
resultFingerprint: canonical.resultFingerprint,
|
|
2761
|
+
...(canonical.executionTime !== undefined ? { executionTime: canonical.executionTime } : {}),
|
|
2762
|
+
...(canonical.truncated ? { truncated: true } : {}),
|
|
2763
|
+
...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
|
|
2764
|
+
...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2093
2767
|
// Surface the approved Hint-Graph corrections that shaped this answer so the
|
|
2094
2768
|
// UI can show an "applied learnings" chip (memoryContext is already on the answer).
|
|
2095
2769
|
if (!governedAnswer.appliedHints) {
|
|
@@ -2222,39 +2896,67 @@ export async function startLocalServer(opts) {
|
|
|
2222
2896
|
// Deadline/cancellation propagates to the engine, which renders the
|
|
2223
2897
|
// graceful "bounded execution deadline" outcome — never a provider error.
|
|
2224
2898
|
rethrowIfCancelled(error, request.signal);
|
|
2225
|
-
const
|
|
2899
|
+
const dispatchEvidence = providerDispatchTerminalEvidence(error);
|
|
2900
|
+
const dispatchBudgetExhausted = Boolean((error && typeof error === 'object'
|
|
2901
|
+
&& error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED')
|
|
2902
|
+
|| dispatchEvidence?.fallbackReason === 'orchestration_budget_exhausted');
|
|
2903
|
+
const message = dispatchBudgetExhausted
|
|
2904
|
+
? 'Ask reached its internal provider-dispatch limit before it froze one executable analytical plan. Nothing was run. Narrow the metric or dimension and retry; this is an orchestration-budget diagnostic, not a provider outage.'
|
|
2905
|
+
: formatAgentRunInfrastructureError(error, 'AI answer provider');
|
|
2226
2906
|
return {
|
|
2227
2907
|
summary: message,
|
|
2228
2908
|
status: 'blocked',
|
|
2229
2909
|
trustState: 'blocked',
|
|
2230
2910
|
stopReason: 'blocked',
|
|
2231
|
-
artifacts: [agentRunArtifact('answer', 'AI answer provider failed', {
|
|
2911
|
+
artifacts: [agentRunArtifact('answer', dispatchBudgetExhausted ? 'Internal orchestration budget exhausted' : 'AI answer provider failed', {
|
|
2232
2912
|
kind: 'no_answer',
|
|
2233
|
-
refusalCode: 'provider_error',
|
|
2913
|
+
refusalCode: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'provider_error',
|
|
2234
2914
|
text: message,
|
|
2235
2915
|
answer: message,
|
|
2236
2916
|
...(routeDecision?.resolvedAnalyticalPlan
|
|
2237
2917
|
? { resolvedAnalyticalPlan: routeDecision.resolvedAnalyticalPlan }
|
|
2238
2918
|
: {}),
|
|
2239
2919
|
providerFailure: {
|
|
2240
|
-
code: 'AI_PROVIDER_FAILURE',
|
|
2920
|
+
code: dispatchBudgetExhausted ? 'orchestration_budget_exhausted' : 'AI_PROVIDER_FAILURE',
|
|
2241
2921
|
message,
|
|
2242
|
-
recoverable:
|
|
2922
|
+
recoverable: !dispatchBudgetExhausted,
|
|
2243
2923
|
},
|
|
2244
2924
|
}, undefined, 'blocked')],
|
|
2245
2925
|
evaluations: [
|
|
2246
2926
|
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 }),
|
|
2927
|
+
agentRunEvaluation(dispatchBudgetExhausted ? 'orchestration-budget' : 'ai-provider', dispatchBudgetExhausted ? 'Internal orchestration budget' : 'AI provider', false, 'blocking', message, { originalErrorType: error instanceof Error ? error.name : typeof error }),
|
|
2248
2928
|
],
|
|
2249
2929
|
nextActions: [
|
|
2250
|
-
|
|
2251
|
-
|
|
2930
|
+
...(dispatchBudgetExhausted
|
|
2931
|
+
? [{ id: 'narrow-and-retry-ask', label: 'Narrow the metric or dimension and retry', route: 'generated_answer' }]
|
|
2932
|
+
: [
|
|
2933
|
+
{ id: 'retry-ask-after-provider', label: 'Retry after provider setup', route: 'generated_answer' },
|
|
2934
|
+
{ id: 'research-without-answer', label: 'Research with available metadata', route: 'research', artifactKind: 'research_run' },
|
|
2935
|
+
]),
|
|
2252
2936
|
],
|
|
2937
|
+
...(dispatchEvidence?.providerEgressReceipts.length
|
|
2938
|
+
? { providerEgressReceipts: dispatchEvidence.providerEgressReceipts }
|
|
2939
|
+
: {}),
|
|
2940
|
+
telemetry: {
|
|
2941
|
+
version: 1,
|
|
2942
|
+
stageDurationsMs: { total: Date.now() - runStartedAtMs },
|
|
2943
|
+
providerRoundTrips: dispatchEvidence?.providerRoundTrips ?? 0,
|
|
2944
|
+
toolCalls: dispatchEvidence?.toolCalls ?? 0,
|
|
2945
|
+
sqlExecutions: dispatchEvidence?.sqlExecutions ?? 0,
|
|
2946
|
+
repairs: Math.max(attempt, dispatchEvidence?.repairs ?? 0),
|
|
2947
|
+
egressReceipts: dispatchEvidence?.providerEgressReceipts.length ?? 0,
|
|
2948
|
+
fallbackReason: dispatchBudgetExhausted
|
|
2949
|
+
? 'provider_dispatch_budget_exhausted'
|
|
2950
|
+
: dispatchEvidence?.fallbackReason ?? 'provider_error',
|
|
2951
|
+
},
|
|
2253
2952
|
};
|
|
2254
2953
|
}
|
|
2255
2954
|
const answeredRoute = resolvedRunRouteFromAnswer(governedAnswer) ?? route;
|
|
2256
2955
|
const isCertified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
2257
|
-
const
|
|
2956
|
+
const semanticRouteClaimed = governedAnswer.route?.tier === 'semantic_metric';
|
|
2957
|
+
const semanticAggregationProofPassed = semanticAnswerHasPassedAggregationProof(governedAnswer);
|
|
2958
|
+
const semanticAggregationBlocked = semanticRouteClaimed && !semanticAggregationProofPassed;
|
|
2959
|
+
const isSemantic = semanticRouteClaimed && semanticAggregationProofPassed;
|
|
2258
2960
|
const requestedNotebookDataset = findMentionedNotebookDataset(request.question, datasetWorkspace.list());
|
|
2259
2961
|
const governedAssetMissesLocalSource = Boolean(requestedNotebookDataset && (isCertified
|
|
2260
2962
|
|| isSemantic
|
|
@@ -2271,13 +2973,46 @@ export async function startLocalServer(opts) {
|
|
|
2271
2973
|
// governance boundary, not an ambiguous user question and not a repairable
|
|
2272
2974
|
// retrieval miss. Keep the precise policy detail visible, but never burn two
|
|
2273
2975
|
// more provider calls retrying the same incompatible candidate.
|
|
2274
|
-
const isPolicyBlocked = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked'
|
|
2976
|
+
const isPolicyBlocked = (governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'policy_blocked')
|
|
2977
|
+
|| semanticAggregationBlocked;
|
|
2275
2978
|
const isTerminalFailure = isProviderError || isExecutionFailure || isAnalyticalFailure || isPolicyBlocked;
|
|
2276
2979
|
// The model tried to compose a governed query and declined despite having usable
|
|
2277
2980
|
// context (e.g. it wasn't confident about a multi-table join). The answer loop
|
|
2278
2981
|
// has already spent its one evidence-aware repair. Keep this terminal and
|
|
2279
2982
|
// inspectable; an ordinary Ask must never silently become a second Research run.
|
|
2280
2983
|
const isModelDeclined = governedAnswer.kind === 'no_answer' && governedAnswer.refusalCode === 'model_declined';
|
|
2984
|
+
// Before an analytical plan is frozen, a generated Ask gap is a recoverable
|
|
2985
|
+
// coverage result rather than a terminal governed error. Let the run engine
|
|
2986
|
+
// consume the typed evaluation and make one bounded cascade decision. Frozen
|
|
2987
|
+
// certified/semantic plans and provider/policy/execution failures remain
|
|
2988
|
+
// fail-closed.
|
|
2989
|
+
const canRecoverPreFreezeGap = (isGroundingGap || isModelDeclined)
|
|
2990
|
+
&& route === 'generated_answer'
|
|
2991
|
+
&& (request.requestedMode === undefined || request.requestedMode === 'auto' || request.requestedMode === 'ask')
|
|
2992
|
+
&& routeDecision?.resolvedAnalyticalPlan?.mode !== 'authoritative';
|
|
2993
|
+
const typedCoverageGap = (isGroundingGap || isModelDeclined)
|
|
2994
|
+
? buildCoverageGap({
|
|
2995
|
+
code: governedAnswer.refusalCode === 'modeling_gap' ? 'MISSING_RELATIONSHIP' : 'MISSING_RUNTIME_CAPABILITY',
|
|
2996
|
+
phase: 'planning',
|
|
2997
|
+
message: governedAnswer.refusalDetails?.message
|
|
2998
|
+
?? (isModelDeclined
|
|
2999
|
+
? 'The available context did not produce a safe analytical tuple.'
|
|
3000
|
+
: 'The retrieved context did not prove the metadata required for this analytical question.'),
|
|
3001
|
+
searchedSources: ['certified_blocks', 'semantic_metrics', 'dbt_manifest', 'relationship_graph', 'warehouse_metadata'],
|
|
3002
|
+
attemptedRoutes: ['certified', 'semantic', 'governed_relational', 'generated'],
|
|
3003
|
+
missing: governedAnswer.refusalDetails?.offending
|
|
3004
|
+
? [
|
|
3005
|
+
governedAnswer.refusalDetails.offending.relation,
|
|
3006
|
+
governedAnswer.refusalDetails.offending.column,
|
|
3007
|
+
].filter((value) => Boolean(value))
|
|
3008
|
+
: ['an executable metric/dimension/relationship tuple'],
|
|
3009
|
+
recoverable: canRecoverPreFreezeGap,
|
|
3010
|
+
planFrozen: routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative',
|
|
3011
|
+
nextActions: canRecoverPreFreezeGap
|
|
3012
|
+
? ['continue through DBT-grounded relational context', 'run review-required generated SQL', 'start bounded Research if coverage remains incomplete']
|
|
3013
|
+
: ['review the retained metadata gap', 'select a governed metric or dimension', 'repair modeling if the relationship is missing'],
|
|
3014
|
+
})
|
|
3015
|
+
: undefined;
|
|
2281
3016
|
// Only a genuinely AMBIGUOUS question is surfaced as "needs clarification".
|
|
2282
3017
|
// Grounding/compiler gaps are terminal review states with their evidence trace;
|
|
2283
3018
|
// provider outages are blocked so the UI can offer an explicit retry.
|
|
@@ -2290,48 +3025,137 @@ export async function startLocalServer(opts) {
|
|
|
2290
3025
|
// reply ran context-free and was clarified again: the reported clarify loop.
|
|
2291
3026
|
const resolvedRoute = needsClarification ? 'clarify' : answeredRoute;
|
|
2292
3027
|
const sql = governedAnswer.proposedSql ?? governedAnswer.sql;
|
|
2293
|
-
const runnableSql = governedAnswer.kind === 'no_answer'
|
|
3028
|
+
const runnableSql = governedAnswer.kind === 'no_answer'
|
|
3029
|
+
|| isExecutionFailure
|
|
3030
|
+
|| semanticAggregationBlocked
|
|
3031
|
+
|| (isExploratory && !governedAnswer.result)
|
|
2294
3032
|
? undefined
|
|
2295
3033
|
: sql;
|
|
2296
|
-
//
|
|
2297
|
-
//
|
|
2298
|
-
//
|
|
2299
|
-
//
|
|
3034
|
+
// Narrate any run that actually produced values. This used to be gated on
|
|
3035
|
+
// `requestedMode === 'research'`, and because the UI sends ordinary Ask as
|
|
3036
|
+
// `'auto'`, every normal question skipped synthesis and shipped the answer
|
|
3037
|
+
// loop's deterministic fact-join as the primary answer — the reported
|
|
3038
|
+
// `column: value` dump. A model that never sees the result cannot describe
|
|
3039
|
+
// it, so a bounded, redacted row sample goes with the request unless a
|
|
3040
|
+
// project admin has switched row egress off.
|
|
2300
3041
|
let synthesizedAnswer;
|
|
2301
|
-
|
|
3042
|
+
const providerEgressReceipts = [
|
|
3043
|
+
...(governedAnswer.providerEgressReceipts ?? []),
|
|
3044
|
+
];
|
|
3045
|
+
let narrationDurationMs = 0;
|
|
3046
|
+
const researchRowsOptIn = route === 'research' && request.researchResultRowsOptIn === true;
|
|
3047
|
+
const rowEgress = resolveProviderResultRowEgressPolicy({
|
|
3048
|
+
projectSetting: projectConfig?.agent?.providerResultRowEgress,
|
|
3049
|
+
researchOptIn: researchRowsOptIn,
|
|
3050
|
+
});
|
|
3051
|
+
// `createBlockStudioAssistProvider` returns null when no AI provider is
|
|
3052
|
+
// configured; a narration failure must never take the governed answer down.
|
|
3053
|
+
const narrationProvider = await createBlockStudioAssistProvider(projectRoot).catch(() => null);
|
|
3054
|
+
const narrationPlan = planAgentRunNarration(governedAnswer, {
|
|
3055
|
+
requestedMode: request.requestedMode,
|
|
3056
|
+
providerAvailable: Boolean(narrationProvider),
|
|
3057
|
+
rowEgress,
|
|
3058
|
+
});
|
|
3059
|
+
const narrationMaxRows = narrationPlan.mode === 'skip' ? 0 : narrationPlan.maxRows;
|
|
3060
|
+
const preview = agentResultToSynthesisPreview(governedAnswer.result);
|
|
3061
|
+
const providerPreview = preview
|
|
3062
|
+
? { ...preview, rows: redactProviderResultRows(preview.rows, narrationMaxRows) }
|
|
3063
|
+
: undefined;
|
|
3064
|
+
let narrationSource;
|
|
3065
|
+
let narrationValidationFailures = [];
|
|
3066
|
+
if (narrationPlan.mode !== 'skip' && narrationProvider) {
|
|
3067
|
+
const narrationStartedAtMs = Date.now();
|
|
3068
|
+
const draft = governedAnswer.answer ?? governedAnswer.text;
|
|
3069
|
+
const observeNarrationDispatch = (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
3070
|
+
purpose: 'answer_narration',
|
|
3071
|
+
dispatchPhase: 'narration',
|
|
3072
|
+
optIn: narrationMaxRows > 0,
|
|
3073
|
+
serializedResultShape: {
|
|
3074
|
+
resultRowCount: providerPreview?.rows.length ?? 0,
|
|
3075
|
+
columnCount: providerPreview?.columns.length ?? 0,
|
|
3076
|
+
},
|
|
3077
|
+
});
|
|
3078
|
+
const narrationDispatchOptions = () => ({
|
|
3079
|
+
maxTokens: 350,
|
|
3080
|
+
temperature: 0.3,
|
|
3081
|
+
maxProviderDispatches: 2,
|
|
3082
|
+
...(agentRunProviderEvidenceContext.getStore()
|
|
3083
|
+
? { onProviderDispatch: observeNarrationDispatch }
|
|
3084
|
+
: {}),
|
|
3085
|
+
});
|
|
2302
3086
|
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
|
-
|
|
3087
|
+
const frame = governedAnswer.resolvedAnalyticalPlan?.analyticalFrame;
|
|
3088
|
+
if (narrationPlan.mode === 'verified_facts' && governedAnswer.analyticalFacts && frame) {
|
|
3089
|
+
// The fact set is the grounding contract: the model drafts, and every
|
|
3090
|
+
// claim is resolved against a fact id before it can reach the user.
|
|
3091
|
+
const composed = await composeVerifiedAnalyticalNarrative({
|
|
3092
|
+
frame,
|
|
3093
|
+
factSet: governedAnswer.analyticalFacts,
|
|
3094
|
+
question: request.question,
|
|
3095
|
+
maxRows: narrationPlan.maxRows,
|
|
3096
|
+
complete: async ({ system, user }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], narrationDispatchOptions(), () => { }),
|
|
3097
|
+
});
|
|
3098
|
+
narrationSource = composed.source;
|
|
3099
|
+
narrationValidationFailures = composed.validationFailures;
|
|
3100
|
+
synthesizedAnswer = composed.source === 'llm'
|
|
3101
|
+
? composed.narrative.text
|
|
3102
|
+
// A verification failure is not silent: the deterministic join is a
|
|
3103
|
+
// fallback, and the reader is told so rather than handed a dump that
|
|
3104
|
+
// looks like it was written on purpose.
|
|
3105
|
+
: `_Verified narration was unavailable, so this is the deterministic result record._\n\n${composed.narrative.text}`;
|
|
3106
|
+
}
|
|
3107
|
+
else {
|
|
3108
|
+
const result = await synthesizeAnswer({
|
|
3109
|
+
question: request.question,
|
|
3110
|
+
category: routeDecision?.category,
|
|
3111
|
+
// The primary Ask reply is always business-facing. Analysts keep
|
|
3112
|
+
// the full DQL, SQL, lineage, gates, and grain in the inspector.
|
|
3113
|
+
audience: 'stakeholder',
|
|
3114
|
+
resultPreview: providerPreview ?? preview,
|
|
3115
|
+
sql: sql,
|
|
3116
|
+
draftText: draft,
|
|
3117
|
+
gaps: businessNarrativeGaps(governedAnswer.validationWarnings),
|
|
3118
|
+
rankingDirection: governedAnswer.contextPack?.questionPlan.requestedShape.rankingDirection,
|
|
3119
|
+
// Governed display contract: metric/measure displayFormat metadata
|
|
3120
|
+
// beats column-name guessing, so $ and decimals stay consistent
|
|
3121
|
+
// between the narration and the table for names like total_bcm.
|
|
3122
|
+
...(preview ? { columnFormats: agentColumnDisplayFormats(projectRoot, preview.columns) } : {}),
|
|
3123
|
+
}, {
|
|
3124
|
+
complete: ({ system, user, signal, onDelta }) => streamOrGenerate(narrationProvider, [{ role: 'system', content: system }, { role: 'user', content: user }], { ...narrationDispatchOptions(), signal }, onDelta ?? (() => { })),
|
|
3125
|
+
});
|
|
3126
|
+
narrationSource = result.source;
|
|
3127
|
+
if (result.text)
|
|
3128
|
+
synthesizedAnswer = result.text;
|
|
3129
|
+
}
|
|
2330
3130
|
}
|
|
2331
3131
|
catch {
|
|
2332
|
-
// Keep the governed draft on any
|
|
3132
|
+
// Keep the governed draft on any narration failure.
|
|
2333
3133
|
synthesizedAnswer = undefined;
|
|
2334
3134
|
}
|
|
3135
|
+
finally {
|
|
3136
|
+
narrationDurationMs = Date.now() - narrationStartedAtMs;
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
// Every run accounts for its narration egress, including the runs that did
|
|
3140
|
+
// not narrate — "no receipt" must never be ambiguous between "no rows left
|
|
3141
|
+
// the host" and "nobody looked".
|
|
3142
|
+
{
|
|
3143
|
+
const narrated = narrationSource !== undefined;
|
|
3144
|
+
const rowsSent = narrated ? providerPreview?.rows.length ?? 0 : 0;
|
|
3145
|
+
providerEgressReceipts.push(createProviderEgressReceipt({
|
|
3146
|
+
purpose: 'answer_narration',
|
|
3147
|
+
provider: narrated ? narrationProvider?.name ?? 'none' : 'none',
|
|
3148
|
+
permittedCategories: rowsSent > 0
|
|
3149
|
+
? ['instructions', 'question', 'governed_context', 'result_rows']
|
|
3150
|
+
: ['instructions', 'question', 'governed_context'],
|
|
3151
|
+
optIn: rowsSent > 0,
|
|
3152
|
+
redactionPolicyId: rowEgress.policyId,
|
|
3153
|
+
payload: narrated
|
|
3154
|
+
? { question: request.question, resultPreview: rowsSent > 0 ? providerPreview : undefined }
|
|
3155
|
+
: { resultRows: 0, providerDisabled: !narrationProvider },
|
|
3156
|
+
resultRowCount: rowsSent,
|
|
3157
|
+
columnCount: narrated ? providerPreview?.columns.length ?? 0 : 0,
|
|
3158
|
+
}));
|
|
2335
3159
|
}
|
|
2336
3160
|
// A grounding/modeling gap is a REFUSAL. It was computed above but never
|
|
2337
3161
|
// reached this ternary, so it fell through to `needs_review` — the same
|
|
@@ -2342,7 +3166,7 @@ export async function startLocalServer(opts) {
|
|
|
2342
3166
|
? 'blocked'
|
|
2343
3167
|
: needsClarification
|
|
2344
3168
|
? 'needs_clarification'
|
|
2345
|
-
: isGroundingGap || isModelDeclined
|
|
3169
|
+
: (isGroundingGap || isModelDeclined) && !canRecoverPreFreezeGap
|
|
2346
3170
|
? 'blocked'
|
|
2347
3171
|
: isCertified || isSemantic
|
|
2348
3172
|
? 'completed'
|
|
@@ -2351,7 +3175,7 @@ export async function startLocalServer(opts) {
|
|
|
2351
3175
|
? 'blocked'
|
|
2352
3176
|
: needsClarification
|
|
2353
3177
|
? 'not_applicable'
|
|
2354
|
-
: isGroundingGap || isModelDeclined
|
|
3178
|
+
: (isGroundingGap || isModelDeclined) && !canRecoverPreFreezeGap
|
|
2355
3179
|
? 'blocked'
|
|
2356
3180
|
: isCertified
|
|
2357
3181
|
? 'certified'
|
|
@@ -2365,7 +3189,7 @@ export async function startLocalServer(opts) {
|
|
|
2365
3189
|
// A gap is terminal, but it is NOT a provider outage: it still owes the
|
|
2366
3190
|
// user its evidence trace, and its next-actions below stay the
|
|
2367
3191
|
// grounding-gap set rather than "retry the provider".
|
|
2368
|
-
: isGroundingGap || isModelDeclined
|
|
3192
|
+
: (isGroundingGap || isModelDeclined) && !canRecoverPreFreezeGap
|
|
2369
3193
|
? 'human_review_required'
|
|
2370
3194
|
: isCertified
|
|
2371
3195
|
? 'certified_answer_found'
|
|
@@ -2415,9 +3239,21 @@ export async function startLocalServer(opts) {
|
|
|
2415
3239
|
artifactKind: 'sql_cell',
|
|
2416
3240
|
}] : []),
|
|
2417
3241
|
];
|
|
3242
|
+
const runDispatchEvidence = agentRunProviderEvidenceContext.getStore()?.snapshot(governedAnswer.refusalCode ?? 'none');
|
|
3243
|
+
const finalProviderEgressReceipts = runDispatchEvidence?.providerEgressReceipts
|
|
3244
|
+
?? providerEgressReceipts;
|
|
3245
|
+
const finalTelemetry = agentRunTelemetryForAnswer(governedAnswer, finalProviderEgressReceipts, Date.now() - runStartedAtMs, narrationDurationMs, attempt);
|
|
3246
|
+
if (runDispatchEvidence) {
|
|
3247
|
+
finalTelemetry.providerRoundTrips = runDispatchEvidence.providerRoundTrips;
|
|
3248
|
+
finalTelemetry.egressReceipts = finalProviderEgressReceipts.length;
|
|
3249
|
+
}
|
|
2418
3250
|
return {
|
|
2419
3251
|
resolvedRoute,
|
|
2420
|
-
answerRefusalCode: governedAnswer.kind === 'no_answer'
|
|
3252
|
+
answerRefusalCode: governedAnswer.kind === 'no_answer'
|
|
3253
|
+
? governedAnswer.refusalCode
|
|
3254
|
+
: semanticAggregationBlocked
|
|
3255
|
+
? 'policy_blocked'
|
|
3256
|
+
: undefined,
|
|
2421
3257
|
answerTier: governedAnswer.route?.tier,
|
|
2422
3258
|
summary: isTerminalFailure
|
|
2423
3259
|
? analyticalFailureSummary(governedAnswer.analyticalFailure)
|
|
@@ -2442,7 +3278,7 @@ export async function startLocalServer(opts) {
|
|
|
2442
3278
|
? (governedAnswer.semanticExecutionTrace
|
|
2443
3279
|
? [agentRunArtifact('answer', governedAnswer.refusalCode === 'ambiguous'
|
|
2444
3280
|
? 'Semantic path selection required'
|
|
2445
|
-
: 'Semantic compilation details', governedAnswer, undefined, needsClarification ? 'not_applicable' : 'review_required')]
|
|
3281
|
+
: 'Semantic compilation details', typedCoverageGap ? { ...governedAnswer, coverageGap: typedCoverageGap } : governedAnswer, undefined, needsClarification ? 'not_applicable' : 'review_required')]
|
|
2446
3282
|
: governedAnswer.dqlArtifact && !isProviderError && !isGroundingGap && !isModelDeclined && !isPolicyBlocked
|
|
2447
3283
|
? [agentRunArtifact('dql_block_draft', 'DQL draft (review required)', governedAnswer.dqlArtifact, undefined, 'review_required')]
|
|
2448
3284
|
// Every refusal still owes the user an account of itself. Emitting no
|
|
@@ -2451,7 +3287,7 @@ export async function startLocalServer(opts) {
|
|
|
2451
3287
|
// answered", the DQL draft and the compiled SQL all become
|
|
2452
3288
|
// unreachable exactly when the user most needs to see why it stopped
|
|
2453
3289
|
// and carry the query into a notebook.
|
|
2454
|
-
: [agentRunArtifact('answer', 'No answer was accepted', governedAnswer, undefined, needsClarification ? 'not_applicable' : 'blocked')])
|
|
3290
|
+
: [agentRunArtifact('answer', 'No answer was accepted', typedCoverageGap ? { ...governedAnswer, coverageGap: typedCoverageGap } : governedAnswer, undefined, needsClarification ? 'not_applicable' : 'blocked')])
|
|
2455
3291
|
: [agentRunArtifact('answer', isCertified ? 'Certified answer' : isSemantic ? 'Governed semantic answer' : isExploratory ? 'Exploratory DBT-grounded answer' : 'Review-required answer', governedAnswer, governedAnswer.sourceCertifiedBlock ?? governedAnswer.block?.name, isCertified ? 'certified' : isSemantic ? 'governed' : 'review_required')],
|
|
2456
3292
|
evaluations: [
|
|
2457
3293
|
agentRunEvaluation('route-decision', 'Route decision', true, 'info', routeDecision?.reason ?? 'Routed request to governed answer.', {
|
|
@@ -2477,36 +3313,53 @@ export async function startLocalServer(opts) {
|
|
|
2477
3313
|
: needsClarification
|
|
2478
3314
|
? 'The answer loop needs more context before producing a governed answer.'
|
|
2479
3315
|
: isPolicyBlocked
|
|
2480
|
-
?
|
|
3316
|
+
? semanticAggregationBlocked
|
|
3317
|
+
? `DQL blocked semantic execution because its exact aggregation proof did not pass: ${governedAnswer.aggregationSafetyProof?.issueCodes.join(', ') || 'required proof evidence is missing.'}`
|
|
3318
|
+
: `DQL blocked the generated path under its relationship policy: ${governedAnswer.refusalDetails?.message ?? 'the relationship is not authorized for this analysis.'}`
|
|
2481
3319
|
: isExploratory
|
|
2482
3320
|
? '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
3321
|
: 'The answer is generated or semantic-layer backed and remains review-required.', governedAnswer.route),
|
|
2484
3322
|
...(isExecutionFailure ? [agentRunEvaluation('query-execution', 'Query execution', false, 'blocking', `The governed query failed before it produced a result: ${governedAnswer.executionError}`)] : []),
|
|
2485
3323
|
...(isGroundingGap ? [
|
|
2486
3324
|
{
|
|
2487
|
-
...agentRunEvaluation('grounding-gap', 'Metadata grounding', false, 'warning',
|
|
3325
|
+
...agentRunEvaluation('grounding-gap', 'Metadata grounding', false, 'warning', canRecoverPreFreezeGap
|
|
3326
|
+
? 'The first governed lookup did not prove the required metadata grounding. Continue through the bounded relational/generated cascade before asking the user to repair modeling.'
|
|
3327
|
+
: 'The bounded lookup could not prove the required metadata grounding. This plan is already frozen, so no automatic Research escalation was started.', {
|
|
2488
3328
|
refusalCode: governedAnswer.refusalCode,
|
|
2489
3329
|
refusalDetails: governedAnswer.refusalDetails,
|
|
2490
3330
|
validationWarnings: governedAnswer.validationWarnings,
|
|
2491
3331
|
route: governedAnswer.route,
|
|
3332
|
+
coverageGap: typedCoverageGap,
|
|
2492
3333
|
}),
|
|
3334
|
+
...(canRecoverPreFreezeGap ? {
|
|
3335
|
+
suggestedRepair: 'Continue the unfrozen Ask cascade through DBT-grounded relational context and review-required generated SQL.',
|
|
3336
|
+
} : {}),
|
|
2493
3337
|
},
|
|
2494
3338
|
] : []),
|
|
2495
3339
|
...(isModelDeclined ? [
|
|
2496
3340
|
{
|
|
2497
|
-
...agentRunEvaluation('declined-despite-context', 'Answer grounding', false, 'blocking',
|
|
3341
|
+
...agentRunEvaluation('declined-despite-context', 'Answer grounding', false, 'blocking', canRecoverPreFreezeGap
|
|
3342
|
+
? 'The governed lookup could not compose a query after its bounded in-lane repair. Continue with the Research context ledger before returning a typed gap.'
|
|
3343
|
+
: 'The bounded lookup could not compose a governed query after its in-lane repair. Start Research explicitly to investigate beyond this lookup budget.', {
|
|
2498
3344
|
refusalCode: governedAnswer.refusalCode,
|
|
2499
3345
|
refusalDetails: governedAnswer.refusalDetails,
|
|
2500
3346
|
validationWarnings: governedAnswer.validationWarnings,
|
|
2501
3347
|
route: governedAnswer.route,
|
|
3348
|
+
coverageGap: typedCoverageGap,
|
|
2502
3349
|
}),
|
|
3350
|
+
...(canRecoverPreFreezeGap ? {
|
|
3351
|
+
suggestedRepair: 'Search the surrounding metadata and relationship context before giving up on the analytical turn.',
|
|
3352
|
+
} : {}),
|
|
2503
3353
|
},
|
|
2504
3354
|
] : []),
|
|
2505
3355
|
...(isPolicyBlocked ? [
|
|
2506
|
-
agentRunEvaluation('relationship-policy', 'Relationship policy', false, 'warning',
|
|
2507
|
-
|
|
3356
|
+
agentRunEvaluation(semanticAggregationBlocked ? 'aggregation-safety-proof' : 'relationship-policy', semanticAggregationBlocked ? 'Aggregation safety proof' : 'Relationship policy', false, semanticAggregationBlocked ? 'blocking' : 'warning', semanticAggregationBlocked
|
|
3357
|
+
? `Semantic execution was refused because the exact aggregation proof did not pass: ${governedAnswer.aggregationSafetyProof?.issueCodes.join(', ') || 'required proof evidence is missing.'}`
|
|
3358
|
+
: governedAnswer.refusalDetails?.message
|
|
3359
|
+
?? 'The selected relationship is not authorized for this analysis.', {
|
|
2508
3360
|
refusalCode: governedAnswer.refusalCode,
|
|
2509
3361
|
refusalDetails: governedAnswer.refusalDetails,
|
|
3362
|
+
aggregationSafetyProof: governedAnswer.aggregationSafetyProof,
|
|
2510
3363
|
validationWarnings: governedAnswer.validationWarnings,
|
|
2511
3364
|
route: governedAnswer.route,
|
|
2512
3365
|
}),
|
|
@@ -2514,8 +3367,18 @@ export async function startLocalServer(opts) {
|
|
|
2514
3367
|
...(governedAnswer.executionError ? [
|
|
2515
3368
|
agentRunEvaluation('execution-error', 'Execution error', false, 'warning', governedAnswer.executionError),
|
|
2516
3369
|
] : []),
|
|
3370
|
+
// A rejected narration must say WHY it was rejected. The failures were
|
|
3371
|
+
// computed and then dropped, so a reader saw "Verified narration was
|
|
3372
|
+
// unavailable" with no way to tell whether the model invented a number,
|
|
3373
|
+
// cited a fact id that does not exist, or the provider simply failed.
|
|
3374
|
+
// The fallback itself stays: this only makes its reason inspectable.
|
|
3375
|
+
...(narrationSource === 'deterministic' && narrationValidationFailures.length > 0 ? [
|
|
3376
|
+
agentRunEvaluation('narration-verification', 'Narration verification', false, 'warning', `The drafted narration was rejected against the result fact set, so the deterministic record was shown instead: ${narrationValidationFailures.join('; ')}`, { narrationSource, validationFailures: narrationValidationFailures }),
|
|
3377
|
+
] : []),
|
|
2517
3378
|
],
|
|
2518
3379
|
nextActions,
|
|
3380
|
+
providerEgressReceipts: finalProviderEgressReceipts,
|
|
3381
|
+
telemetry: finalTelemetry,
|
|
2519
3382
|
};
|
|
2520
3383
|
};
|
|
2521
3384
|
const conversationRunExecutor = async ({ request, routeDecision, emitAnswerDelta }) => {
|
|
@@ -2549,13 +3412,28 @@ export async function startLocalServer(opts) {
|
|
|
2549
3412
|
...(request.history ?? []).slice(-6).map((message) => ({ role: message.role, content: message.text })),
|
|
2550
3413
|
{ role: 'user', content: request.question },
|
|
2551
3414
|
];
|
|
2552
|
-
text = (await provider.generate(messages, {
|
|
3415
|
+
text = (await provider.generate(messages, {
|
|
3416
|
+
maxTokens: 320,
|
|
3417
|
+
temperature: 0.6,
|
|
3418
|
+
maxProviderDispatches: 2,
|
|
3419
|
+
...(agentRunProviderEvidenceContext.getStore() ? {
|
|
3420
|
+
onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
3421
|
+
purpose: 'answer_generation',
|
|
3422
|
+
dispatchPhase: 'generation',
|
|
3423
|
+
optIn: false,
|
|
3424
|
+
}),
|
|
3425
|
+
} : {}),
|
|
3426
|
+
})).trim();
|
|
2553
3427
|
// Perceived-latency: surface the reply as one delta for surfaces wired to stream.
|
|
2554
3428
|
if (text)
|
|
2555
3429
|
emitAnswerDelta?.(text);
|
|
2556
3430
|
}
|
|
2557
3431
|
}
|
|
2558
|
-
catch {
|
|
3432
|
+
catch (error) {
|
|
3433
|
+
if (error && typeof error === 'object'
|
|
3434
|
+
&& error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED') {
|
|
3435
|
+
throw error;
|
|
3436
|
+
}
|
|
2559
3437
|
// Provider unavailable / errored — fall through to the deterministic reply.
|
|
2560
3438
|
text = undefined;
|
|
2561
3439
|
}
|
|
@@ -3024,6 +3902,7 @@ export async function startLocalServer(opts) {
|
|
|
3024
3902
|
plan,
|
|
3025
3903
|
};
|
|
3026
3904
|
let researchRun;
|
|
3905
|
+
const researchRuns = [];
|
|
3027
3906
|
let researchWorkspaceError;
|
|
3028
3907
|
if (!needsClarification) {
|
|
3029
3908
|
try {
|
|
@@ -3048,26 +3927,106 @@ export async function startLocalServer(opts) {
|
|
|
3048
3927
|
});
|
|
3049
3928
|
emit({
|
|
3050
3929
|
type: 'artifact.created',
|
|
3051
|
-
message: 'Saved
|
|
3930
|
+
message: 'Saved the immutable root research plan; executing bounded child branches.',
|
|
3052
3931
|
route: 'research',
|
|
3053
3932
|
trustState: 'review_required',
|
|
3054
|
-
payload: { researchRunId: created.id, notebookPath },
|
|
3933
|
+
payload: { researchRunId: created.id, notebookPath, branchCap: 6 },
|
|
3055
3934
|
});
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3935
|
+
// The root record is a plan/dossier parent. Only child runs are
|
|
3936
|
+
// observed research executions. This prevents one root result from
|
|
3937
|
+
// being copied into six fabricated ledger entries (AGT-016/033).
|
|
3938
|
+
// An explicit Research request still gets one real child when the
|
|
3939
|
+
// catalog planner has no grounded step (for example, an empty
|
|
3940
|
+
// starter project). The child is an observed metadata/baseline
|
|
3941
|
+
// attempt, not a fabricated successful finding; its durable status
|
|
3942
|
+
// and receipt determine the ledger entry.
|
|
3943
|
+
const fallbackBranch = {
|
|
3944
|
+
thought: 'Inspect the requested analytical question against the frozen root context.',
|
|
3945
|
+
action: {
|
|
3946
|
+
kind: 'lookup_metric',
|
|
3947
|
+
target: routeDecision?.resolvedAnalyticalPlan?.executionId ?? request.question,
|
|
3948
|
+
},
|
|
3949
|
+
expectation: 'Whether the frozen context contains enough evidence for a bounded answer.',
|
|
3950
|
+
};
|
|
3951
|
+
const branches = capResearchBranches(plan.steps.length > 0 ? plan.steps : [fallbackBranch], 6);
|
|
3952
|
+
for (let index = 0; index < branches.length; index += 1) {
|
|
3953
|
+
const step = branches[index];
|
|
3954
|
+
if (request.signal?.aborted)
|
|
3955
|
+
rethrowIfCancelled(request.signal.reason, request.signal);
|
|
3956
|
+
const branchId = `${step.action.kind}:${step.action.target}`;
|
|
3957
|
+
const branchQuestion = `${request.question}\nResearch branch ${index + 1} (${branchId}): ${step.expectation}`;
|
|
3958
|
+
const childId = `${created.id}:research:${index + 1}`;
|
|
3959
|
+
const child = storage.createRun({
|
|
3960
|
+
id: childId,
|
|
3961
|
+
notebookPath,
|
|
3962
|
+
title: `${agentRunTitle(request.question, 'Agent research')} · branch ${index + 1}`,
|
|
3963
|
+
question: branchQuestion,
|
|
3964
|
+
sourceCell,
|
|
3965
|
+
sourceCellId,
|
|
3966
|
+
sourceCellName,
|
|
3967
|
+
sourceCellFingerprint,
|
|
3968
|
+
intent: researchIntent,
|
|
3969
|
+
domain: agentRunWorkspaceValue(request, 'domain'),
|
|
3970
|
+
owner: agentRunWorkspaceValue(request, 'owner'),
|
|
3971
|
+
context: {
|
|
3972
|
+
...researchContextEnvelope,
|
|
3973
|
+
rootRunId: created.id,
|
|
3974
|
+
rootPlanId: plan.rootPlanId,
|
|
3975
|
+
branch: {
|
|
3976
|
+
id: branchId,
|
|
3977
|
+
index: index + 1,
|
|
3978
|
+
expectation: step.expectation,
|
|
3979
|
+
action: step.action,
|
|
3980
|
+
},
|
|
3981
|
+
},
|
|
3982
|
+
});
|
|
3983
|
+
emit({
|
|
3984
|
+
type: 'artifact.created',
|
|
3985
|
+
message: `Started research branch ${index + 1} of ${branches.length}.`,
|
|
3986
|
+
route: 'research',
|
|
3987
|
+
trustState: 'review_required',
|
|
3988
|
+
payload: { researchRunId: child.id, parentResearchRunId: created.id, branchId },
|
|
3989
|
+
});
|
|
3990
|
+
try {
|
|
3991
|
+
const executed = await runNotebookResearch(storage, child, {
|
|
3992
|
+
domain: agentRunWorkspaceValue(request, 'domain'),
|
|
3993
|
+
owner: agentRunWorkspaceValue(request, 'owner'),
|
|
3994
|
+
sourceCellFingerprint,
|
|
3995
|
+
question: branchQuestion,
|
|
3996
|
+
intent: researchIntent,
|
|
3997
|
+
context: {
|
|
3998
|
+
...researchContextEnvelope,
|
|
3999
|
+
rootRunId: created.id,
|
|
4000
|
+
rootPlanId: plan.rootPlanId,
|
|
4001
|
+
branch: { id: branchId, index: index + 1, expectation: step.expectation, action: step.action },
|
|
4002
|
+
},
|
|
4003
|
+
executionConnection: researchExecutionConnection,
|
|
4004
|
+
executionConnectionName: researchExecutionConnectionName,
|
|
4005
|
+
signal: request.signal,
|
|
4006
|
+
baselineSql: agentRunString(researchSource?.sql),
|
|
4007
|
+
baselineDqlArtifact: researchSource?.dqlArtifact,
|
|
4008
|
+
baselineRunId: agentRunString(researchSource?.runId),
|
|
4009
|
+
});
|
|
4010
|
+
researchRuns.push(withNotebookResearchChecklist(executed));
|
|
4011
|
+
}
|
|
4012
|
+
catch (error) {
|
|
4013
|
+
// A child is a real durable run even when cancellation stops the
|
|
4014
|
+
// shared branch budget. Persist the truthful stop before
|
|
4015
|
+
// propagating cancellation to the parent run.
|
|
4016
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4017
|
+
storage.updateRun(child.id, {
|
|
4018
|
+
status: 'error',
|
|
4019
|
+
error: message,
|
|
4020
|
+
summary: 'Research branch stopped before producing a result.',
|
|
4021
|
+
reviewStatus: 'needs_review',
|
|
4022
|
+
});
|
|
4023
|
+
const stopped = storage.getRun(child.id);
|
|
4024
|
+
if (stopped)
|
|
4025
|
+
researchRuns.push(withNotebookResearchChecklist(stopped));
|
|
4026
|
+
rethrowIfCancelled(error, request.signal);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
researchRun = researchRuns[0];
|
|
3071
4030
|
}
|
|
3072
4031
|
finally {
|
|
3073
4032
|
storage.close();
|
|
@@ -3078,15 +4037,81 @@ export async function startLocalServer(opts) {
|
|
|
3078
4037
|
researchWorkspaceError = formatNotebookResearchStorageError(error);
|
|
3079
4038
|
}
|
|
3080
4039
|
}
|
|
3081
|
-
const researchResultPreview =
|
|
4040
|
+
const researchResultPreview = researchRuns
|
|
4041
|
+
.map((run) => run.resultPreview)
|
|
4042
|
+
// Keep zero-row executions in the proof path. `coerceNarrateResultData`
|
|
4043
|
+
// intentionally omits empty row sets for prose, but an empty result is
|
|
4044
|
+
// still an executed result when it carries its execution identity.
|
|
4045
|
+
.find((preview) => {
|
|
4046
|
+
const record = agentRunRecord(preview);
|
|
4047
|
+
return Boolean(record && Array.isArray(record.rows));
|
|
4048
|
+
})
|
|
4049
|
+
?? researchRun?.resultPreview;
|
|
3082
4050
|
const researchResultData = coerceNarrateResultData(researchResultPreview);
|
|
3083
4051
|
const researchResultRecord = agentRunRecord(researchResultPreview);
|
|
4052
|
+
// Keep each bounded research branch inspectable as an evidence ledger.
|
|
4053
|
+
// The notebook workspace remains the durable execution record; this
|
|
4054
|
+
// additive projection gives Ask/Research narration a stable list of
|
|
4055
|
+
// observations, failures, facts, and receipts without exposing provider
|
|
4056
|
+
// chain-of-thought. Six is the hard branch budget for one root question.
|
|
4057
|
+
const researchLedger = buildResearchEvidenceLedger({
|
|
4058
|
+
rootQuestion: request.question,
|
|
4059
|
+
planId: plan.rootPlanId,
|
|
4060
|
+
snapshotId: routeDecision?.resolvedAnalyticalPlan?.snapshotId,
|
|
4061
|
+
entries: researchRuns.slice(0, 6).map((branch, index) => {
|
|
4062
|
+
const branchContext = agentRunRecord(branch.context)?.branch;
|
|
4063
|
+
const branchPreviewRecord = agentRunRecord(branch.resultPreview);
|
|
4064
|
+
const branchPreview = coerceNarrateResultData(branchPreviewRecord);
|
|
4065
|
+
const previewRecord = branchPreviewRecord;
|
|
4066
|
+
const executionReceipt = normalizeAnalyticalExecutionReceipt(previewRecord?.executionReceipt);
|
|
4067
|
+
const resultFingerprint = normalizeAnalyticalExecutionFingerprint(previewRecord?.resultFingerprint)
|
|
4068
|
+
?? executionReceipt?.resultFingerprint;
|
|
4069
|
+
// A child run ID or context-pack ID proves that planning happened,
|
|
4070
|
+
// not that a query executed. Only the canonical result fingerprint
|
|
4071
|
+
// (on the result or its execution receipt) can make a branch
|
|
4072
|
+
// observed (AGT-016/033).
|
|
4073
|
+
const executionProof = resultFingerprint;
|
|
4074
|
+
const observed = branch.status === 'ready' && Boolean(executionProof);
|
|
4075
|
+
return {
|
|
4076
|
+
id: branch.id,
|
|
4077
|
+
branchId: agentRunString(branchContext?.id) ?? `branch:${index + 1}`,
|
|
4078
|
+
question: branch.question,
|
|
4079
|
+
status: observed ? 'observed' : branch.status === 'error' ? 'failed' : 'skipped',
|
|
4080
|
+
...(branchPreviewRecord && Array.isArray(branchPreviewRecord.rows)
|
|
4081
|
+
? { rowCount: branchPreviewRecord.rows.length }
|
|
4082
|
+
: {}),
|
|
4083
|
+
...(resultFingerprint ? { resultFingerprint } : {}),
|
|
4084
|
+
...(executionReceipt ? { executionReceipt } : {}),
|
|
4085
|
+
facts: [agentRunString(branchContext?.expectation) ?? branch.question, ...(branch.evidence?.citations ?? []).flatMap((item) => typeof item === 'string' ? [item] : [])].slice(0, 8),
|
|
4086
|
+
// A context-pack ID or child run ID is not execution evidence. Keep
|
|
4087
|
+
// the receipt list empty until the execution service supplies a
|
|
4088
|
+
// receipt/fingerprint (AGT-016/033).
|
|
4089
|
+
receipts: executionProof ? [executionProof] : [],
|
|
4090
|
+
...(!observed ? {
|
|
4091
|
+
error: branch.error
|
|
4092
|
+
?? (branch.status === 'error'
|
|
4093
|
+
? branch.summary
|
|
4094
|
+
: 'Research branch did not produce an execution receipt or result fingerprint.'),
|
|
4095
|
+
} : {}),
|
|
4096
|
+
};
|
|
4097
|
+
}),
|
|
4098
|
+
stoppingReason: needsClarification
|
|
4099
|
+
? 'not_started'
|
|
4100
|
+
: researchRuns.some((run) => run.status === 'error')
|
|
4101
|
+
? 'insufficient_evidence'
|
|
4102
|
+
: plan.steps.length > 6
|
|
4103
|
+
? 'budget'
|
|
4104
|
+
: 'completed',
|
|
4105
|
+
});
|
|
3084
4106
|
// A query that ran and matched 0 rows STILL executed — treat it as a clean,
|
|
3085
4107
|
// grounded execution (not "no result"), so an empty answer is surfaced as
|
|
3086
4108
|
// "0 rows matched" rather than silently downgraded to review-required.
|
|
3087
4109
|
const researchDidExecute = Boolean(researchResultData) || Boolean(researchResultRecord && Array.isArray(researchResultRecord.rows));
|
|
3088
4110
|
const researchZeroRows = !researchResultData && researchDidExecute;
|
|
3089
|
-
const researchExecutedCleanly = researchDidExecute
|
|
4111
|
+
const researchExecutedCleanly = researchDidExecute
|
|
4112
|
+
&& !researchWorkspaceError
|
|
4113
|
+
&& researchRuns.length > 0
|
|
4114
|
+
&& researchRuns.every((run) => run.status === 'ready');
|
|
3090
4115
|
const narration = !needsClarification && researchResultData
|
|
3091
4116
|
? await narrateForAgentRun({
|
|
3092
4117
|
question: request.question,
|
|
@@ -3094,7 +4119,7 @@ export async function startLocalServer(opts) {
|
|
|
3094
4119
|
result: researchResultData,
|
|
3095
4120
|
evidence: plan.sources,
|
|
3096
4121
|
reviewRequired: true,
|
|
3097
|
-
})
|
|
4122
|
+
}, request.researchResultRowsOptIn === true)
|
|
3098
4123
|
: undefined;
|
|
3099
4124
|
const summary = needsClarification
|
|
3100
4125
|
? 'Needs clarification before running deeper research.'
|
|
@@ -3122,8 +4147,11 @@ export async function startLocalServer(opts) {
|
|
|
3122
4147
|
? []
|
|
3123
4148
|
: [agentRunArtifact('research_run', 'Research plan', {
|
|
3124
4149
|
plan,
|
|
4150
|
+
researchLedger,
|
|
3125
4151
|
researchRun,
|
|
4152
|
+
researchRuns,
|
|
3126
4153
|
researchRunId: researchRun?.id,
|
|
4154
|
+
researchRunIds: researchRuns.map((run) => run.id),
|
|
3127
4155
|
notebookPath,
|
|
3128
4156
|
workspaceError: researchWorkspaceError,
|
|
3129
4157
|
routeDecision,
|
|
@@ -3154,7 +4182,7 @@ export async function startLocalServer(opts) {
|
|
|
3154
4182
|
: [
|
|
3155
4183
|
...(researchRun?.id ? [{ id: 'open-research', label: 'Open research dossier', artifactKind: 'research_run' }] : []),
|
|
3156
4184
|
{ id: 'create-block', label: 'Review DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
|
|
3157
|
-
...(
|
|
4185
|
+
...(researchRuns.some((run) => run.generatedSql || run.reviewedSql) ? [{ id: 'insert-sql', label: 'Insert SQL preview', route: 'sql_cell', artifactKind: 'sql_cell' }] : []),
|
|
3158
4186
|
],
|
|
3159
4187
|
};
|
|
3160
4188
|
},
|
|
@@ -3609,15 +4637,28 @@ export async function startLocalServer(opts) {
|
|
|
3609
4637
|
return provider.generate([
|
|
3610
4638
|
{ role: 'system', content: system },
|
|
3611
4639
|
{ role: 'user', content: user },
|
|
3612
|
-
], {
|
|
4640
|
+
], {
|
|
4641
|
+
maxTokens: 700,
|
|
4642
|
+
temperature: 0.1,
|
|
4643
|
+
signal,
|
|
4644
|
+
maxProviderDispatches: 2,
|
|
4645
|
+
...(agentRunProviderEvidenceContext.getStore() ? {
|
|
4646
|
+
onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
4647
|
+
purpose: 'answer_generation',
|
|
4648
|
+
dispatchPhase: 'planning',
|
|
4649
|
+
optIn: false,
|
|
4650
|
+
}),
|
|
4651
|
+
} : {}),
|
|
4652
|
+
});
|
|
3613
4653
|
},
|
|
3614
4654
|
getCatalogContext: buildRankedAgentRunCatalogContext,
|
|
3615
4655
|
});
|
|
3616
|
-
//
|
|
3617
|
-
//
|
|
3618
|
-
//
|
|
3619
|
-
//
|
|
4656
|
+
// Explicit selections and conversation-only turns remain deterministic;
|
|
4657
|
+
// each fresh natural-language analytical turn gets one bounded candidate-ID
|
|
4658
|
+
// interpretation call by default. The rollback is host-owned and cannot be
|
|
4659
|
+
// supplied by an HTTP/MCP client.
|
|
3620
4660
|
const agentRunRouter = createHybridRouter({
|
|
4661
|
+
requireMeaningCallForNaturalLanguage: opts.requireMeaningCallForNaturalLanguage ?? true,
|
|
3621
4662
|
complete: async ({ system, user, signal }) => {
|
|
3622
4663
|
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
3623
4664
|
if (!provider)
|
|
@@ -3633,6 +4674,14 @@ export async function startLocalServer(opts) {
|
|
|
3633
4674
|
// instead of leaving the UI in "Generating and validating SQL" for a
|
|
3634
4675
|
// minute or more.
|
|
3635
4676
|
signal: boundedAgentMeaningSignal(signal),
|
|
4677
|
+
maxProviderDispatches: 1,
|
|
4678
|
+
...(agentRunProviderEvidenceContext.getStore() ? {
|
|
4679
|
+
onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
4680
|
+
purpose: 'answer_generation',
|
|
4681
|
+
dispatchPhase: 'meaning_resolution',
|
|
4682
|
+
optIn: false,
|
|
4683
|
+
}),
|
|
4684
|
+
} : {}),
|
|
3636
4685
|
});
|
|
3637
4686
|
},
|
|
3638
4687
|
getEvidence: buildAgentRunEvidence,
|
|
@@ -3649,6 +4698,27 @@ export async function startLocalServer(opts) {
|
|
|
3649
4698
|
// A run may outlive its streaming browser connection, so cancellation is
|
|
3650
4699
|
// server-owned and keyed by run id rather than relying on fetch abort alone.
|
|
3651
4700
|
const activeAgentRunControllers = new Map();
|
|
4701
|
+
const cancelActiveAgentRun = (id) => {
|
|
4702
|
+
const controller = activeAgentRunControllers.get(id);
|
|
4703
|
+
if (!controller)
|
|
4704
|
+
return false;
|
|
4705
|
+
const progress = agentRunStore.getProgress(id);
|
|
4706
|
+
if (progress) {
|
|
4707
|
+
agentRunStore.saveProgress({
|
|
4708
|
+
...progress,
|
|
4709
|
+
lifecycle: {
|
|
4710
|
+
...progress.lifecycle,
|
|
4711
|
+
state: 'cancelling',
|
|
4712
|
+
revision: progress.lifecycle.revision + 1,
|
|
4713
|
+
updatedAt: new Date().toISOString(),
|
|
4714
|
+
},
|
|
4715
|
+
});
|
|
4716
|
+
}
|
|
4717
|
+
controller.abort(createAgentRunCancellationError());
|
|
4718
|
+
return true;
|
|
4719
|
+
};
|
|
4720
|
+
const activeAnalyticalRepairReservations = new Set();
|
|
4721
|
+
const consumedAnalyticalRepairCapabilities = new Set();
|
|
3652
4722
|
// Server-side conversation threads: persisted multi-turn state (survives refresh).
|
|
3653
4723
|
// Lazy so a failed native-sqlite load never blocks the runtime; conversation
|
|
3654
4724
|
// persistence degrades to the per-request context instead.
|
|
@@ -3739,14 +4809,21 @@ export async function startLocalServer(opts) {
|
|
|
3739
4809
|
snapshotCells.push({ cellId, status: 'idle', executionCount: 0, executedAt });
|
|
3740
4810
|
continue;
|
|
3741
4811
|
}
|
|
3742
|
-
const prepared = prepareLocalExecution(executableSql, activeConnection, projectRoot, projectConfig);
|
|
3743
4812
|
assertAppAccess({ app, domain: resolved.domain ?? app.domain, level: 'execute' });
|
|
3744
|
-
const
|
|
3745
|
-
|
|
4813
|
+
const execution = await analyticalExecutionService.execute({
|
|
4814
|
+
sql: executableSql,
|
|
4815
|
+
subject: 'App notebook query',
|
|
4816
|
+
connection: activeConnection,
|
|
4817
|
+
sqlParams: plan?.sqlParams,
|
|
4818
|
+
variables: plan?.variables,
|
|
4819
|
+
});
|
|
4820
|
+
const result = execution.result;
|
|
3746
4821
|
snapshotCells.push({
|
|
3747
4822
|
cellId,
|
|
3748
4823
|
status: 'success',
|
|
3749
4824
|
result,
|
|
4825
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
4826
|
+
resultFingerprint: execution.resultFingerprint,
|
|
3750
4827
|
executionCount: 1,
|
|
3751
4828
|
executedAt,
|
|
3752
4829
|
});
|
|
@@ -3835,44 +4912,81 @@ export async function startLocalServer(opts) {
|
|
|
3835
4912
|
const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
|
|
3836
4913
|
throw analyticalError(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`, { origin: 'dql_compilation', stage: 'compile' });
|
|
3837
4914
|
}
|
|
3838
|
-
const
|
|
4915
|
+
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
4916
|
+
const sourceDomain = metadata.domain ?? source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
|
|
4917
|
+
assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
|
|
4918
|
+
const pinnedSemanticCompile = semanticCompose?.sql
|
|
4919
|
+
? compiledSemanticQueries.get(executionFingerprint(semanticCompose.sql))
|
|
4920
|
+
: undefined;
|
|
4921
|
+
const semanticExecutionHolder = { value: null };
|
|
4922
|
+
const execution = await analyticalExecutionService.execute({
|
|
3839
4923
|
sql: semanticCompose?.sql ?? plan.sql,
|
|
3840
4924
|
subject: 'DQL artifact query',
|
|
3841
|
-
executor,
|
|
3842
4925
|
connection: activeConnection,
|
|
3843
|
-
projectRoot,
|
|
3844
|
-
projectConfig,
|
|
3845
4926
|
// Preserve the existing contract: previewed Ask artifacts are explicitly
|
|
3846
4927
|
// read-only and bounded; saved/manual DQL execution keeps its compiler-
|
|
3847
4928
|
// governed behavior when no preview bound was requested.
|
|
3848
4929
|
enforceReadOnly: invocationInput?.rowLimit !== undefined,
|
|
3849
4930
|
rowLimit: invocationInput?.rowLimit,
|
|
4931
|
+
sqlParams: plan?.sqlParams,
|
|
4932
|
+
variables: { ...(plan?.variables ?? {}), ...invocation.values },
|
|
4933
|
+
executePrepared: async (preparation) => {
|
|
4934
|
+
if (pinnedSemanticCompile) {
|
|
4935
|
+
const semanticExecution = await executeTargetBoundSemanticQuery({
|
|
4936
|
+
executor,
|
|
4937
|
+
connection: activeConnection,
|
|
4938
|
+
projectRoot,
|
|
4939
|
+
plannedAdapter: pinnedSemanticCompile.engine,
|
|
4940
|
+
metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
|
|
4941
|
+
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
4942
|
+
: undefined,
|
|
4943
|
+
compile: async () => pinnedSemanticCompile,
|
|
4944
|
+
prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
|
|
4945
|
+
rowBound: invocationInput?.rowLimit ?? pinnedSemanticCompile.effectiveRequest.limit,
|
|
4946
|
+
});
|
|
4947
|
+
if (semanticExecution) {
|
|
4948
|
+
semanticExecutionHolder.value = semanticExecution;
|
|
4949
|
+
return semanticExecution.result;
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
return executor.executeQuery(preparation.executedSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), preparation.connection);
|
|
4953
|
+
},
|
|
3850
4954
|
});
|
|
3851
|
-
const
|
|
4955
|
+
const semanticExecution = semanticExecutionHolder.value;
|
|
4956
|
+
const { preparation, result: normalized } = execution;
|
|
3852
4957
|
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
4958
|
const executionReceipt = createDqlArtifactExecutionReceipt(source, executionSql, invocation.values, normalized);
|
|
4959
|
+
const executableArtifact = {
|
|
4960
|
+
version: 1,
|
|
4961
|
+
kind: precompiledSemanticQuery || semanticCompose?.sql ? 'semantic_block' : 'sql_block',
|
|
4962
|
+
dqlFingerprint: executionFingerprint(source),
|
|
4963
|
+
sourceFingerprint: executionFingerprint(stableExecutionValue({
|
|
4964
|
+
name: metadata.name ?? null,
|
|
4965
|
+
path: metadata.path ?? null,
|
|
4966
|
+
domain: sourceDomain ?? null,
|
|
4967
|
+
})),
|
|
4968
|
+
compiledSqlFingerprint: executionFingerprint(semanticCompose?.sql ?? plan?.sql ?? preparation.sourceSql),
|
|
4969
|
+
normalizedSqlFingerprint: executionFingerprint(preparation.decodedSql),
|
|
4970
|
+
parameterFingerprint: executionReceipt.parameterFingerprint,
|
|
4971
|
+
provenanceFingerprint: executionFingerprint(stableExecutionValue(invocation.resolvedParameters.map((parameter) => ({
|
|
4972
|
+
name: parameter.name,
|
|
4973
|
+
source: parameter.source,
|
|
4974
|
+
})))),
|
|
4975
|
+
targetFingerprint: targetGenerationFingerprint(activeConnection, executionConnectionName),
|
|
4976
|
+
snapshotFingerprint: executionFingerprint(projectSnapshot().snapshotId),
|
|
4977
|
+
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
4978
|
+
sqlFingerprint: executionFingerprint(semanticCompose?.sql ?? plan?.sql ?? preparation.sourceSql),
|
|
4979
|
+
parameterCount: plan?.sqlParams?.length ?? 0,
|
|
4980
|
+
variableNames: Object.keys(plan?.variables ?? {}).sort(),
|
|
4981
|
+
chartConfig: plan?.chartConfig ?? null,
|
|
4982
|
+
})),
|
|
4983
|
+
semanticAdapter: pinnedSemanticCompile?.engine ?? 'native',
|
|
4984
|
+
previewPolicy: invocationInput?.rowLimit === undefined
|
|
4985
|
+
? { mode: 'compiler_governed' }
|
|
4986
|
+
: { mode: 'read_only_bounded', rowLimit: invocationInput.rowLimit },
|
|
4987
|
+
trustState: 'review_required',
|
|
4988
|
+
receipt: executionReceipt,
|
|
4989
|
+
};
|
|
3876
4990
|
return {
|
|
3877
4991
|
columns: normalized.columns,
|
|
3878
4992
|
rows: normalized.rows,
|
|
@@ -3885,6 +4999,7 @@ export async function startLocalServer(opts) {
|
|
|
3885
4999
|
parameters: invocation.resolvedParameters,
|
|
3886
5000
|
auditId: invocation.auditId,
|
|
3887
5001
|
executionReceipt,
|
|
5002
|
+
executableArtifact,
|
|
3888
5003
|
...(semanticExecution ? {
|
|
3889
5004
|
semanticExecutionReceipt: semanticExecution.executionReceipt,
|
|
3890
5005
|
semanticTargetBinding: semanticExecution.targetBinding,
|
|
@@ -3919,6 +5034,9 @@ export async function startLocalServer(opts) {
|
|
|
3919
5034
|
trustState: 'certified',
|
|
3920
5035
|
compiledSql: result.sql,
|
|
3921
5036
|
executionReceipt: result.executionReceipt,
|
|
5037
|
+
...(result.executableArtifact ? {
|
|
5038
|
+
executableArtifact: { ...result.executableArtifact, kind: 'certified_block', trustState: 'certified' },
|
|
5039
|
+
} : {}),
|
|
3922
5040
|
...(invocationInput?.rowLimit ? { limit: invocationInput.rowLimit } : {}),
|
|
3923
5041
|
...(invocationInput?.parameters && Object.keys(invocationInput.parameters).length > 0
|
|
3924
5042
|
? { parameterValues: invocationInput.parameters }
|
|
@@ -3960,6 +5078,7 @@ export async function startLocalServer(opts) {
|
|
|
3960
5078
|
...(Object.keys(invocation.values).length > 0 ? { parameterValues: invocation.values } : {}),
|
|
3961
5079
|
compiledSql: result.sql,
|
|
3962
5080
|
executionReceipt: result.executionReceipt,
|
|
5081
|
+
...(result.executableArtifact ? { executableArtifact: result.executableArtifact } : {}),
|
|
3963
5082
|
},
|
|
3964
5083
|
};
|
|
3965
5084
|
};
|
|
@@ -4036,7 +5155,7 @@ export async function startLocalServer(opts) {
|
|
|
4036
5155
|
* string, so the only remaining reasons to differ are the connection and the
|
|
4037
5156
|
* governance gates, both of which report themselves honestly.
|
|
4038
5157
|
*/
|
|
4039
|
-
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings) => {
|
|
5158
|
+
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection, bindings, executionConnectionName) => {
|
|
4040
5159
|
const activeConnection = requireActiveConnection(executionConnection);
|
|
4041
5160
|
const rowBound = clampAnalyticalRowBound(seed?.limit ?? 200);
|
|
4042
5161
|
const trimmed = sql.trim().replace(/;\s*$/, '').trim();
|
|
@@ -4058,18 +5177,6 @@ export async function startLocalServer(opts) {
|
|
|
4058
5177
|
if (semantic.unresolvedRefs.length > 0) {
|
|
4059
5178
|
throw analyticalError(`Unknown semantic reference${semantic.unresolvedRefs.length > 1 ? 's' : ''}: ${semantic.unresolvedRefs.join(', ')}`, { origin: 'dql_compilation', stage: 'compile', code: 'semantic_ref' });
|
|
4060
5179
|
}
|
|
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
5180
|
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
4074
5181
|
// Generated SQL is governed as 'uncategorized' unless its seed declares a
|
|
4075
5182
|
// domain — the same value `generatedSqlArtifactContract` stamps into the
|
|
@@ -4080,25 +5187,43 @@ export async function startLocalServer(opts) {
|
|
|
4080
5187
|
assertAppAccess({ app, domain: sourceDomain, level: 'execute' });
|
|
4081
5188
|
// A semantic query the loop already compiled (MetricFlow / dbt Cloud) must
|
|
4082
5189
|
// 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
|
-
|
|
5190
|
+
const semanticExecutionHolder = { value: null };
|
|
5191
|
+
const execution = await analyticalExecutionService.execute({
|
|
5192
|
+
sql: semantic.sql,
|
|
5193
|
+
subject: 'Generated SQL',
|
|
5194
|
+
connection: activeConnection,
|
|
5195
|
+
enforceReadOnly: true,
|
|
5196
|
+
rowLimit: rowBound,
|
|
5197
|
+
sqlParams: bindings?.sqlParams,
|
|
5198
|
+
variables: bindings?.variables,
|
|
5199
|
+
semanticRefs: semantic.semanticRefs,
|
|
5200
|
+
executePrepared: async (preparation) => {
|
|
5201
|
+
const pinnedSemanticCompile = compiledSemanticQueries.get(executionFingerprint(preparation.preparedSql))
|
|
5202
|
+
?? compiledSemanticQueries.get(executionFingerprint(trimmed));
|
|
5203
|
+
if (pinnedSemanticCompile) {
|
|
5204
|
+
const semanticExecution = await executeTargetBoundSemanticQuery({
|
|
5205
|
+
executor,
|
|
5206
|
+
connection: activeConnection,
|
|
5207
|
+
projectRoot,
|
|
5208
|
+
plannedAdapter: pinnedSemanticCompile.engine,
|
|
5209
|
+
metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
|
|
5210
|
+
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
5211
|
+
: undefined,
|
|
5212
|
+
compile: async () => pinnedSemanticCompile,
|
|
5213
|
+
prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
|
|
5214
|
+
rowBound,
|
|
5215
|
+
});
|
|
5216
|
+
if (semanticExecution) {
|
|
5217
|
+
semanticExecutionHolder.value = semanticExecution;
|
|
5218
|
+
return semanticExecution.result;
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
return executor.executeQuery(preparation.executedSql, bindings?.sqlParams ?? [], runtimeVariables(bindings?.variables ?? {}), preparation.connection);
|
|
5222
|
+
},
|
|
5223
|
+
});
|
|
5224
|
+
const semanticExecution = semanticExecutionHolder.value;
|
|
5225
|
+
const { preparation, result: normalized } = execution;
|
|
5226
|
+
const bounded = preparation.rowBound;
|
|
4102
5227
|
// The authoritative bound. `buildRowBoundedSql` may legitimately decline to
|
|
4103
5228
|
// touch the statement, and only one of 15 drivers honours a maxRows hint, so
|
|
4104
5229
|
// the host is the only layer correctness may depend on.
|
|
@@ -4106,6 +5231,38 @@ export async function startLocalServer(opts) {
|
|
|
4106
5231
|
const rows = truncated ? normalized.rows.slice(0, rowBound) : normalized.rows;
|
|
4107
5232
|
const warnings = [];
|
|
4108
5233
|
const artifact = buildVerifiedGeneratedArtifact(question, trimmed, bounded.sql, seed, warnings, normalized.columns, activeConnection.driver);
|
|
5234
|
+
const receiptSource = artifact?.source ?? trimmed;
|
|
5235
|
+
const executionBindings = bindings && ((bindings.sqlParams?.length ?? 0) > 0 || Object.keys(bindings.variables ?? {}).length > 0)
|
|
5236
|
+
? { sqlParams: bindings.sqlParams ?? [], variables: bindings.variables ?? {} }
|
|
5237
|
+
: {};
|
|
5238
|
+
const executionReceipt = createDqlArtifactExecutionReceipt(receiptSource, bounded.sql, executionBindings, { columns: normalized.columns, rows, rowCount: rows.length });
|
|
5239
|
+
const executableArtifact = {
|
|
5240
|
+
version: 1,
|
|
5241
|
+
kind: 'sql_block',
|
|
5242
|
+
dqlFingerprint: executionFingerprint(receiptSource),
|
|
5243
|
+
sourceFingerprint: executionFingerprint(stableExecutionValue({
|
|
5244
|
+
name: artifact?.name ?? seed?.name ?? null,
|
|
5245
|
+
path: artifact?.sourcePath ?? seed?.sourcePath ?? null,
|
|
5246
|
+
})),
|
|
5247
|
+
compiledSqlFingerprint: executionFingerprint(semantic.sql),
|
|
5248
|
+
normalizedSqlFingerprint: executionFingerprint(preparation.decodedSql),
|
|
5249
|
+
parameterFingerprint: executionReceipt.parameterFingerprint,
|
|
5250
|
+
provenanceFingerprint: executionFingerprint(stableExecutionValue({
|
|
5251
|
+
sqlParamCount: bindings?.sqlParams?.length ?? 0,
|
|
5252
|
+
variableNames: Object.keys(bindings?.variables ?? {}).sort(),
|
|
5253
|
+
})),
|
|
5254
|
+
targetFingerprint: targetGenerationFingerprint(activeConnection, executionConnectionName),
|
|
5255
|
+
snapshotFingerprint: executionFingerprint(projectSnapshot().snapshotId),
|
|
5256
|
+
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
5257
|
+
compiledSqlFingerprint: executionFingerprint(semantic.sql),
|
|
5258
|
+
executedSqlFingerprint: executionFingerprint(bounded.sql),
|
|
5259
|
+
rowBound,
|
|
5260
|
+
})),
|
|
5261
|
+
semanticAdapter: semanticExecution?.targetBinding.adapterId ?? 'native',
|
|
5262
|
+
previewPolicy: { mode: 'read_only_bounded', rowLimit: rowBound },
|
|
5263
|
+
trustState: 'review_required',
|
|
5264
|
+
receipt: executionReceipt,
|
|
5265
|
+
};
|
|
4109
5266
|
return {
|
|
4110
5267
|
columns: normalized.columns,
|
|
4111
5268
|
rows,
|
|
@@ -4113,9 +5270,10 @@ export async function startLocalServer(opts) {
|
|
|
4113
5270
|
executionTime: normalized.executionTime,
|
|
4114
5271
|
sql: bounded.sql,
|
|
4115
5272
|
...(truncated ? { truncated: true } : {}),
|
|
4116
|
-
...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound } } : {}),
|
|
5273
|
+
...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound, executionReceipt, executableArtifact } } : {}),
|
|
4117
5274
|
...(warnings.length > 0 ? { validationWarnings: warnings } : {}),
|
|
4118
|
-
executionReceipt
|
|
5275
|
+
executionReceipt,
|
|
5276
|
+
executableArtifact,
|
|
4119
5277
|
};
|
|
4120
5278
|
};
|
|
4121
5279
|
const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName) => {
|
|
@@ -4125,7 +5283,7 @@ export async function startLocalServer(opts) {
|
|
|
4125
5283
|
if (seed && seed.kind !== 'sql_block') {
|
|
4126
5284
|
return executeArtifactReferenceForAgent({ ...seed, limit: seed.limit ?? 200 }, question, executionConnection, executionConnectionName);
|
|
4127
5285
|
}
|
|
4128
|
-
return executeGeneratedSqlDirect(question, sql, seed, executionConnection);
|
|
5286
|
+
return executeGeneratedSqlDirect(question, sql, seed, executionConnection, undefined, executionConnectionName);
|
|
4129
5287
|
};
|
|
4130
5288
|
/**
|
|
4131
5289
|
* EXP-001: execution host for the deliberately narrow non-governed lane.
|
|
@@ -4432,12 +5590,33 @@ export async function startLocalServer(opts) {
|
|
|
4432
5590
|
* conversation, writes a DQL file, changes targets, or promotes trust.
|
|
4433
5591
|
*/
|
|
4434
5592
|
const executeBoundedSqlRepair = async (input) => {
|
|
5593
|
+
let sourceSql = input.sourceSql;
|
|
5594
|
+
let rewrites = [];
|
|
5595
|
+
if (internalRelationIdsInSql(sourceSql).length > 0) {
|
|
5596
|
+
const normalizedSource = await prepareAnalyticalExecutionSql({
|
|
5597
|
+
sql: sourceSql,
|
|
5598
|
+
subject: 'Repair source SQL',
|
|
5599
|
+
executor,
|
|
5600
|
+
connection: input.targetConnection,
|
|
5601
|
+
projectRoot,
|
|
5602
|
+
projectConfig,
|
|
5603
|
+
enforceReadOnly: true,
|
|
5604
|
+
});
|
|
5605
|
+
sourceSql = normalizedSource.decodedSql;
|
|
5606
|
+
rewrites = normalizedSource.rewrites;
|
|
5607
|
+
}
|
|
5608
|
+
const sourceSignature = buildSqlAnalyticalSignature(sourceSql, input.targetConnection.driver);
|
|
5609
|
+
if (!sourceSignature) {
|
|
5610
|
+
throw boundedRepairError('DQL could not prove an immutable analytical signature for the source SQL. Repair it manually.', 409);
|
|
5611
|
+
}
|
|
4435
5612
|
let result;
|
|
4436
|
-
let repairedSql =
|
|
5613
|
+
let repairedSql = sourceSql;
|
|
4437
5614
|
let repairMode = 'deterministic';
|
|
4438
5615
|
let firstFailure;
|
|
5616
|
+
const providerEgressReceipts = [];
|
|
5617
|
+
let providerRoundTrips = 0;
|
|
4439
5618
|
try {
|
|
4440
|
-
result = await executeGeneratedSqlDirect(input.question,
|
|
5619
|
+
result = await executeGeneratedSqlDirect(input.question, sourceSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables }, input.targetConnectionName);
|
|
4441
5620
|
}
|
|
4442
5621
|
catch (error) {
|
|
4443
5622
|
firstFailure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
@@ -4458,50 +5637,89 @@ export async function startLocalServer(opts) {
|
|
|
4458
5637
|
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
4459
5638
|
throw boundedRepairError(failure.redactedMessage, 422, failure);
|
|
4460
5639
|
}
|
|
4461
|
-
const boundedSchema = schemaContext.slice(0, 16).map((table) => ({
|
|
5640
|
+
const boundedSchema = markProviderMetadataArray(schemaContext.slice(0, 16).map((table) => ({
|
|
4462
5641
|
relation: table.relation,
|
|
4463
|
-
columns: table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type })),
|
|
4464
|
-
}));
|
|
5642
|
+
columns: markProviderMetadataArray(table.columns.slice(0, 40).map((column) => ({ name: column.name, type: column.type }))),
|
|
5643
|
+
})));
|
|
4465
5644
|
// CTX-008: a repair that re-derives joins from column names can undo a
|
|
4466
5645
|
// join the original query got right. Give it the authored key pairs.
|
|
4467
|
-
const governedJoins = governedJoinFactsForRelations(schemaContext.slice(0, 16).map((table) => table.relation));
|
|
5646
|
+
const governedJoins = markProviderMetadataArray(governedJoinFactsForRelations(schemaContext.slice(0, 16).map((table) => table.relation)));
|
|
5647
|
+
const repairPayload = {
|
|
5648
|
+
question: input.question,
|
|
5649
|
+
driver: input.targetConnection.driver,
|
|
5650
|
+
failure: firstFailure.redactedMessage,
|
|
5651
|
+
sql: sourceSql,
|
|
5652
|
+
parameters: input.sqlParams ? markProviderMetadataArray(input.sqlParams.map((parameter) => ({
|
|
5653
|
+
name: parameter.name,
|
|
5654
|
+
placeholder: `$${parameter.position}`,
|
|
5655
|
+
}))) : undefined,
|
|
5656
|
+
schema: boundedSchema,
|
|
5657
|
+
...(governedJoins.length ? { governedJoins } : {}),
|
|
5658
|
+
};
|
|
5659
|
+
const repairMessages = [
|
|
5660
|
+
{
|
|
5661
|
+
role: 'system',
|
|
5662
|
+
content: [
|
|
5663
|
+
'Repair one failed read-only analytical SQL statement.',
|
|
5664
|
+
'Return exactly one JSON object in a ```json fence with keys summary, sql, viz, outputs.',
|
|
5665
|
+
'Preserve the requested grain, filters, output columns, and aggregation semantics.',
|
|
5666
|
+
input.sqlParams?.length
|
|
5667
|
+
? 'Preserve every positional parameter placeholder exactly; do not add, remove, or renumber placeholders.'
|
|
5668
|
+
: '',
|
|
5669
|
+
'Use only relations and columns present in the supplied schema context.',
|
|
5670
|
+
'When governedJoins is present, join on those exact key pairs rather than inferring a join from column names.',
|
|
5671
|
+
'Never emit internal DQL relation identities such as source::, model::, seed::, or snapshot::.',
|
|
5672
|
+
'Do not add writes, DDL, multiple statements, or commentary outside the JSON fence.',
|
|
5673
|
+
].join(' '),
|
|
5674
|
+
},
|
|
5675
|
+
{
|
|
5676
|
+
role: 'user',
|
|
5677
|
+
content: JSON.stringify(repairPayload),
|
|
5678
|
+
},
|
|
5679
|
+
];
|
|
5680
|
+
const repairEnvelope = prepareProviderWireEnvelopeForDispatch(provider.name, { messages: repairMessages });
|
|
5681
|
+
assertProviderPayloadAllowed(repairEnvelope, {
|
|
5682
|
+
allowResultRows: false,
|
|
5683
|
+
maxResultRows: 0,
|
|
5684
|
+
purpose: 'repair_sql',
|
|
5685
|
+
});
|
|
4468
5686
|
let raw;
|
|
4469
5687
|
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
|
-
}),
|
|
5688
|
+
raw = await provider.generate(repairEnvelope.messages, {
|
|
5689
|
+
maxTokens: 1200,
|
|
5690
|
+
temperature: 0,
|
|
5691
|
+
maxProviderDispatches: 2,
|
|
5692
|
+
onProviderDispatch: (event) => {
|
|
5693
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(provider.name, event.envelope);
|
|
5694
|
+
assertProviderPayloadAllowed(envelope, {
|
|
5695
|
+
allowResultRows: false,
|
|
5696
|
+
maxResultRows: 0,
|
|
5697
|
+
purpose: 'repair_sql',
|
|
5698
|
+
});
|
|
5699
|
+
providerRoundTrips += 1;
|
|
5700
|
+
providerEgressReceipts.push(createProviderDispatchEgressReceipt({
|
|
5701
|
+
purpose: 'repair_sql',
|
|
5702
|
+
dispatchPhase: 'repair',
|
|
5703
|
+
provider: provider.name,
|
|
5704
|
+
permittedCategories: ['instructions', 'question', 'schema_metadata', 'governed_context'],
|
|
5705
|
+
optIn: false,
|
|
5706
|
+
envelope,
|
|
5707
|
+
}));
|
|
5708
|
+
return envelope;
|
|
4500
5709
|
},
|
|
4501
|
-
|
|
5710
|
+
});
|
|
4502
5711
|
}
|
|
4503
5712
|
catch {
|
|
4504
|
-
throw boundedRepairError('The AI provider could not complete this repair. Check Provider Settings and retry.', 502)
|
|
5713
|
+
throw Object.assign(boundedRepairError('The AI provider could not complete this repair. Check Provider Settings and retry.', 502), {
|
|
5714
|
+
providerDispatchEvidence: {
|
|
5715
|
+
providerEgressReceipts: [...providerEgressReceipts],
|
|
5716
|
+
providerRoundTrips,
|
|
5717
|
+
toolCalls: 0,
|
|
5718
|
+
sqlExecutions: 0,
|
|
5719
|
+
repairs: 1,
|
|
5720
|
+
fallbackReason: 'repair_provider_error',
|
|
5721
|
+
},
|
|
5722
|
+
});
|
|
4505
5723
|
}
|
|
4506
5724
|
const proposal = parseProposal(raw);
|
|
4507
5725
|
if (!proposal.sql?.trim()) {
|
|
@@ -4512,9 +5730,17 @@ export async function startLocalServer(opts) {
|
|
|
4512
5730
|
if (requiredPlaceholders.some((placeholder) => !new RegExp(`${placeholder.replace('$', '\\$')}\\b`).test(repairedSql))) {
|
|
4513
5731
|
throw boundedRepairError('The repair changed this cell\'s parameter contract, so DQL stopped it before execution.', 422);
|
|
4514
5732
|
}
|
|
5733
|
+
const repairedSignature = buildSqlAnalyticalSignature(repairedSql, input.targetConnection.driver);
|
|
5734
|
+
if (!repairedSignature || stableExecutionValue(repairedSignature) !== stableExecutionValue(sourceSignature)) {
|
|
5735
|
+
throw boundedRepairError('The provider repair changed or could not prove the immutable analytical plan, so DQL stopped before execution.', 409);
|
|
5736
|
+
}
|
|
5737
|
+
const aggregationProof = buildAggregationSafetyProof(repairedSql, undefined, input.targetConnection.driver);
|
|
5738
|
+
if (aggregationProof.status !== 'safe') {
|
|
5739
|
+
throw boundedRepairError(`The repaired SQL lacks complete aggregation-safety evidence (${aggregationProof.issueCodes.join(', ')}). Repair it manually.`, 409);
|
|
5740
|
+
}
|
|
4515
5741
|
repairMode = 'ai';
|
|
4516
5742
|
try {
|
|
4517
|
-
result = await executeGeneratedSqlDirect(input.question, repairedSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables });
|
|
5743
|
+
result = await executeGeneratedSqlDirect(input.question, repairedSql, undefined, input.targetConnection, { sqlParams: input.sqlParams, variables: input.variables }, input.targetConnectionName);
|
|
4518
5744
|
}
|
|
4519
5745
|
catch (error) {
|
|
4520
5746
|
const failure = normalizeWarehouseSqlFailure(error, input.targetConnection.driver);
|
|
@@ -4524,7 +5750,6 @@ export async function startLocalServer(opts) {
|
|
|
4524
5750
|
// Keep preview-only row bounds out of editable notebook source. The one
|
|
4525
5751
|
// deterministic source rewrite we do persist is decoding internal graph
|
|
4526
5752
|
// identities, because those can never execute as warehouse SQL.
|
|
4527
|
-
let rewrites = [];
|
|
4528
5753
|
if (internalRelationIdsInSql(repairedSql).length > 0) {
|
|
4529
5754
|
const normalizedSource = await prepareAnalyticalExecutionSql({
|
|
4530
5755
|
sql: repairedSql,
|
|
@@ -4536,9 +5761,9 @@ export async function startLocalServer(opts) {
|
|
|
4536
5761
|
enforceReadOnly: true,
|
|
4537
5762
|
});
|
|
4538
5763
|
repairedSql = normalizedSource.decodedSql;
|
|
4539
|
-
rewrites
|
|
5764
|
+
rewrites.push(...normalizedSource.rewrites);
|
|
4540
5765
|
}
|
|
4541
|
-
return { result, repairedSql, repairMode, firstFailure, rewrites };
|
|
5766
|
+
return { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips, rewrites };
|
|
4542
5767
|
};
|
|
4543
5768
|
/**
|
|
4544
5769
|
* Repair a DQL wrapper that cannot compile far enough to expose executable
|
|
@@ -4551,6 +5776,8 @@ export async function startLocalServer(opts) {
|
|
|
4551
5776
|
if (!provider) {
|
|
4552
5777
|
throw boundedRepairError('Configure an available AI provider in Settings to repair this DQL draft.', 409);
|
|
4553
5778
|
}
|
|
5779
|
+
const providerEgressReceipts = [];
|
|
5780
|
+
let providerRoundTrips = 0;
|
|
4554
5781
|
let schemaContext;
|
|
4555
5782
|
try {
|
|
4556
5783
|
// Include source identifiers in retrieval so a terse cell title still
|
|
@@ -4596,10 +5823,41 @@ export async function startLocalServer(opts) {
|
|
|
4596
5823
|
...(governedJoins.length ? { governedJoins } : {}),
|
|
4597
5824
|
}),
|
|
4598
5825
|
},
|
|
4599
|
-
], {
|
|
5826
|
+
], {
|
|
5827
|
+
maxTokens: 2400,
|
|
5828
|
+
temperature: 0,
|
|
5829
|
+
maxProviderDispatches: 2,
|
|
5830
|
+
onProviderDispatch: (event) => {
|
|
5831
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(provider.name, event.envelope);
|
|
5832
|
+
assertProviderPayloadAllowed(envelope, {
|
|
5833
|
+
allowResultRows: false,
|
|
5834
|
+
maxResultRows: 0,
|
|
5835
|
+
purpose: 'repair_sql',
|
|
5836
|
+
});
|
|
5837
|
+
providerRoundTrips += 1;
|
|
5838
|
+
providerEgressReceipts.push(createProviderDispatchEgressReceipt({
|
|
5839
|
+
purpose: 'repair_sql',
|
|
5840
|
+
dispatchPhase: 'repair',
|
|
5841
|
+
provider: provider.name,
|
|
5842
|
+
permittedCategories: ['instructions', 'question', 'schema_metadata', 'governed_context'],
|
|
5843
|
+
optIn: false,
|
|
5844
|
+
envelope,
|
|
5845
|
+
}));
|
|
5846
|
+
return envelope;
|
|
5847
|
+
},
|
|
5848
|
+
});
|
|
4600
5849
|
}
|
|
4601
5850
|
catch {
|
|
4602
|
-
throw boundedRepairError('The AI provider could not complete this DQL repair. Check Provider Settings and retry.', 502)
|
|
5851
|
+
throw Object.assign(boundedRepairError('The AI provider could not complete this DQL repair. Check Provider Settings and retry.', 502), {
|
|
5852
|
+
providerDispatchEvidence: {
|
|
5853
|
+
providerEgressReceipts: [...providerEgressReceipts],
|
|
5854
|
+
providerRoundTrips,
|
|
5855
|
+
toolCalls: 0,
|
|
5856
|
+
sqlExecutions: 0,
|
|
5857
|
+
repairs: 1,
|
|
5858
|
+
fallbackReason: 'repair_provider_error',
|
|
5859
|
+
},
|
|
5860
|
+
});
|
|
4603
5861
|
}
|
|
4604
5862
|
let repairedSource;
|
|
4605
5863
|
try {
|
|
@@ -4703,6 +5961,8 @@ export async function startLocalServer(opts) {
|
|
|
4703
5961
|
repairedSql: plan.sql,
|
|
4704
5962
|
repairedSource,
|
|
4705
5963
|
repairMode: 'ai',
|
|
5964
|
+
providerEgressReceipts,
|
|
5965
|
+
providerRoundTrips,
|
|
4706
5966
|
rewrites: [],
|
|
4707
5967
|
sqlParams: plan.sqlParams,
|
|
4708
5968
|
variables: { ...plan.variables, ...invocation.values },
|
|
@@ -5238,6 +6498,19 @@ export async function startLocalServer(opts) {
|
|
|
5238
6498
|
: notebookResearchString(governedAnswer?.answer)
|
|
5239
6499
|
?? notebookResearchString(governedAnswer?.text)
|
|
5240
6500
|
?? notebookResearchSummary(question, resultPreview, previewError);
|
|
6501
|
+
const previewRecord = agentRunRecord(resultPreview);
|
|
6502
|
+
const executionReceipt = normalizeAnalyticalExecutionReceipt(previewRecord?.executionReceipt);
|
|
6503
|
+
// Do not treat a child run ID as execution evidence. The canonical
|
|
6504
|
+
// fingerprint is the only proof that this Research branch produced a
|
|
6505
|
+
// result; planning, SQL text, and a durable run record remain review
|
|
6506
|
+
// required until that proof exists (AGT-016/033).
|
|
6507
|
+
const executionProof = normalizeAnalyticalExecutionFingerprint(previewRecord?.resultFingerprint)
|
|
6508
|
+
?? executionReceipt?.resultFingerprint;
|
|
6509
|
+
const executionUnavailable = !previewError && !executionProof;
|
|
6510
|
+
const terminalError = previewError
|
|
6511
|
+
?? (executionUnavailable
|
|
6512
|
+
? 'Research did not produce an executed result or execution receipt; the branch remains review-required.'
|
|
6513
|
+
: undefined);
|
|
5241
6514
|
const recommendation = previewError
|
|
5242
6515
|
? 'Review the SQL, selected metadata, and connection context before rerunning.'
|
|
5243
6516
|
: dqlArtifact && !reviewedSql
|
|
@@ -5262,8 +6535,14 @@ export async function startLocalServer(opts) {
|
|
|
5262
6535
|
question,
|
|
5263
6536
|
intent,
|
|
5264
6537
|
context,
|
|
5265
|
-
|
|
5266
|
-
|
|
6538
|
+
// A plan, generated SQL, or DQL artifact is not an observed execution.
|
|
6539
|
+
// Only a result carrying an execution fingerprint/receipt may become
|
|
6540
|
+
// `ready`; otherwise persist a failed review state with no fabricated
|
|
6541
|
+
// evidence (AGT-016/033).
|
|
6542
|
+
status: terminalError ? 'error' : 'ready',
|
|
6543
|
+
summary: terminalError && executionUnavailable
|
|
6544
|
+
? 'Research branch stopped without an executed result; no observation was recorded.'
|
|
6545
|
+
: summary,
|
|
5267
6546
|
recommendation,
|
|
5268
6547
|
resultPreview,
|
|
5269
6548
|
evidence,
|
|
@@ -5283,7 +6562,7 @@ export async function startLocalServer(opts) {
|
|
|
5283
6562
|
...(display && display.ok ? display.warnings : []),
|
|
5284
6563
|
],
|
|
5285
6564
|
reviewStatus: 'needs_review',
|
|
5286
|
-
error:
|
|
6565
|
+
error: terminalError,
|
|
5287
6566
|
lastRunAt: startedAt,
|
|
5288
6567
|
}) ?? run;
|
|
5289
6568
|
}
|
|
@@ -5701,11 +6980,18 @@ export async function startLocalServer(opts) {
|
|
|
5701
6980
|
tableMapping,
|
|
5702
6981
|
semanticSql: semanticCompose?.sql ?? undefined,
|
|
5703
6982
|
});
|
|
5704
|
-
const
|
|
5705
|
-
|
|
6983
|
+
const execution = await analyticalExecutionService.execute({
|
|
6984
|
+
sql: semanticCompose?.sql ?? plan?.sql ?? executableSql,
|
|
6985
|
+
subject: 'Block Studio preview query',
|
|
6986
|
+
connection: activeConnection,
|
|
6987
|
+
sqlParams: plan?.sqlParams,
|
|
6988
|
+
variables: { ...(plan?.variables ?? {}), ...invocation.values },
|
|
6989
|
+
});
|
|
5706
6990
|
return {
|
|
5707
|
-
sql:
|
|
5708
|
-
result:
|
|
6991
|
+
sql: execution.preparation.executedSql,
|
|
6992
|
+
result: execution.result,
|
|
6993
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
6994
|
+
resultFingerprint: execution.resultFingerprint,
|
|
5709
6995
|
chartConfig: plan?.chartConfig ?? validation.chartConfig ?? null,
|
|
5710
6996
|
invocation,
|
|
5711
6997
|
tests: plan?.tests ?? [],
|
|
@@ -5756,12 +7042,14 @@ export async function startLocalServer(opts) {
|
|
|
5756
7042
|
if (tests.length === 0) {
|
|
5757
7043
|
return { passed: 0, failed: 0, skipped: 0, duration: Date.now() - start, assertions: [], runAt: new Date() };
|
|
5758
7044
|
}
|
|
5759
|
-
const
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
:
|
|
7045
|
+
const execution = await analyticalExecutionService.execute({
|
|
7046
|
+
sql: semanticCompose?.sql ?? plan.sql,
|
|
7047
|
+
subject: 'Block Studio certification test query',
|
|
7048
|
+
connection: activeConnection,
|
|
7049
|
+
sqlParams: plan.sqlParams,
|
|
7050
|
+
variables: plan.variables,
|
|
7051
|
+
});
|
|
7052
|
+
const { rows, columns } = execution.result;
|
|
5765
7053
|
return evaluateBlockStudioTests(tests, rows, columns, start);
|
|
5766
7054
|
};
|
|
5767
7055
|
const saveDqlGenerationDraft = (importId, candidate) => saveDqlGenerationDraftForProject(projectRoot, importId, candidate);
|
|
@@ -6101,7 +7389,12 @@ export async function startLocalServer(opts) {
|
|
|
6101
7389
|
return;
|
|
6102
7390
|
}
|
|
6103
7391
|
if (operationMatch && req.method === 'DELETE') {
|
|
6104
|
-
const
|
|
7392
|
+
const operationId = decodeURIComponent(operationMatch[1]);
|
|
7393
|
+
const existingOperation = operationCoordinator.get(operationId);
|
|
7394
|
+
if (existingOperation?.type === 'agent_run' && existingOperation.scope.startsWith('agent-run:')) {
|
|
7395
|
+
cancelActiveAgentRun(existingOperation.scope.slice('agent-run:'.length));
|
|
7396
|
+
}
|
|
7397
|
+
const operation = operationCoordinator.cancel(operationId);
|
|
6105
7398
|
res.writeHead(operation ? 200 : 404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
6106
7399
|
res.end(serializeJSON(operation ?? { error: 'Operation not found.' }));
|
|
6107
7400
|
return;
|
|
@@ -7214,12 +8507,17 @@ export async function startLocalServer(opts) {
|
|
|
7214
8507
|
const limit = Number.isFinite(rawLimit) && rawLimit > 0
|
|
7215
8508
|
? Math.min(200, Math.floor(rawLimit))
|
|
7216
8509
|
: 50;
|
|
7217
|
-
const
|
|
7218
|
-
|
|
8510
|
+
const stored = agentRunStore.list();
|
|
8511
|
+
// This is an INDEX payload: one row per run, no answer bodies. Shipping
|
|
8512
|
+
// stored runs whole meant a measured 47.61 MB for 20 real runs.
|
|
8513
|
+
// `GET /api/agent-runs/:id` still serves the complete immutable record.
|
|
8514
|
+
const runs = stored
|
|
8515
|
+
.slice()
|
|
7219
8516
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
|
|
7220
|
-
.slice(0, limit)
|
|
8517
|
+
.slice(0, limit)
|
|
8518
|
+
.map(agentRunListEntryForTransport);
|
|
7221
8519
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7222
|
-
res.end(serializeJSON({ runs, total:
|
|
8520
|
+
res.end(serializeJSON({ runs, total: stored.length, limit }));
|
|
7223
8521
|
return;
|
|
7224
8522
|
}
|
|
7225
8523
|
/**
|
|
@@ -7243,23 +8541,81 @@ export async function startLocalServer(opts) {
|
|
|
7243
8541
|
res.end(serializeJSON({ error: 'Agent run not found.' }));
|
|
7244
8542
|
return;
|
|
7245
8543
|
}
|
|
8544
|
+
const capability = normalizeAnalyticalRepairCapabilityV1(sourceRun.repairCapability);
|
|
8545
|
+
if (!capability?.automatic.eligible || capability.automatic.attemptsRemaining < 1) {
|
|
8546
|
+
const manualActions = capability?.manualActions?.length
|
|
8547
|
+
? capability.manualActions
|
|
8548
|
+
: ['edit_dql', 'open_sql_notebook'];
|
|
8549
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8550
|
+
res.end(serializeJSON({
|
|
8551
|
+
code: 'REPAIR_CAPABILITY_REQUIRED',
|
|
8552
|
+
error: 'This run does not retain eligible automatic execution-repair authority.',
|
|
8553
|
+
manualActions,
|
|
8554
|
+
...(capability?.ineligibilityReason ? { ineligibilityReason: capability.ineligibilityReason } : {}),
|
|
8555
|
+
}));
|
|
8556
|
+
return;
|
|
8557
|
+
}
|
|
8558
|
+
// Rebind every server-issued capability identity to the currently
|
|
8559
|
+
// persisted immutable source run before checking idempotence or reserving
|
|
8560
|
+
// an attempt. A copied/stale capability must never authorize provider or
|
|
8561
|
+
// SQL work against a mutated plan, failure, or source artifact.
|
|
8562
|
+
const canonicalCapability = analyticalRepairCapabilityForAgentRun(sourceRun, capability.targetFingerprint);
|
|
8563
|
+
const bindingMatches = canonicalCapability
|
|
8564
|
+
&& canonicalCapability.automatic.eligible
|
|
8565
|
+
&& canonicalCapability.planFingerprint === capability.planFingerprint
|
|
8566
|
+
&& canonicalCapability.failureFingerprint === capability.failureFingerprint
|
|
8567
|
+
&& canonicalCapability.sourceFingerprint === capability.sourceFingerprint;
|
|
8568
|
+
if (!bindingMatches) {
|
|
8569
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8570
|
+
res.end(serializeJSON({
|
|
8571
|
+
code: 'REPAIR_CAPABILITY_REQUIRED',
|
|
8572
|
+
error: 'The persisted source plan, failure, or artifact no longer matches its server-issued repair capability.',
|
|
8573
|
+
manualActions: capability.manualActions,
|
|
8574
|
+
}));
|
|
8575
|
+
return;
|
|
8576
|
+
}
|
|
7246
8577
|
const derivedRunId = `${sourceRun.id}:repair:1`;
|
|
8578
|
+
if (activeAnalyticalRepairReservations.has(derivedRunId)) {
|
|
8579
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8580
|
+
res.end(serializeJSON({
|
|
8581
|
+
code: 'REPAIR_IN_PROGRESS',
|
|
8582
|
+
error: 'The single permitted repair attempt is already in progress.',
|
|
8583
|
+
manualActions: capability.manualActions,
|
|
8584
|
+
}));
|
|
8585
|
+
return;
|
|
8586
|
+
}
|
|
7247
8587
|
const existing = await agentRunStore.get(derivedRunId);
|
|
7248
8588
|
if (existing) {
|
|
7249
|
-
|
|
7250
|
-
res.
|
|
8589
|
+
const succeeded = existing.status === 'needs_review' && existing.trustState === 'review_required';
|
|
8590
|
+
res.writeHead(succeeded ? 200 : 409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8591
|
+
res.end(serializeJSON(succeeded
|
|
8592
|
+
? { run: existing }
|
|
8593
|
+
: {
|
|
8594
|
+
code: 'REPAIR_ATTEMPT_EXHAUSTED',
|
|
8595
|
+
error: 'The single permitted repair attempt was already consumed and did not complete successfully.',
|
|
8596
|
+
manualActions: capability.manualActions,
|
|
8597
|
+
run: existing,
|
|
8598
|
+
}));
|
|
7251
8599
|
return;
|
|
7252
8600
|
}
|
|
7253
|
-
const sourceSql = repairableSqlFromAgentRun(sourceRun);
|
|
7254
8601
|
const sourceDqlArtifact = repairableDqlArtifactFromAgentRun(sourceRun);
|
|
7255
8602
|
const retainedFailure = warehouseFailureFromAgentRun(sourceRun);
|
|
7256
|
-
if (
|
|
8603
|
+
if (!sourceDqlArtifact || !validAskRepairDqlWrapper(sourceDqlArtifact.source)) {
|
|
8604
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8605
|
+
res.end(serializeJSON({
|
|
8606
|
+
code: 'REPAIR_CAPABILITY_REQUIRED',
|
|
8607
|
+
error: 'The retained DQL wrapper is unavailable or invalid. Keep it editable and repair it manually.',
|
|
8608
|
+
manualActions: capability.manualActions,
|
|
8609
|
+
}));
|
|
8610
|
+
return;
|
|
8611
|
+
}
|
|
8612
|
+
const capabilityHash = (value) => `sha256:${createHash('sha256').update(stableExecutionValue(value)).digest('hex')}`;
|
|
8613
|
+
if (capability.dqlFingerprint !== capabilityHash(sourceDqlArtifact.source.trim())) {
|
|
7257
8614
|
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7258
8615
|
res.end(serializeJSON({
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
...(retainedFailure ? { warehouseFailure: retainedFailure } : {}),
|
|
8616
|
+
code: 'REPAIR_CAPABILITY_REQUIRED',
|
|
8617
|
+
error: 'The retained DQL source no longer matches its immutable repair capability.',
|
|
8618
|
+
manualActions: capability.manualActions,
|
|
7263
8619
|
}));
|
|
7264
8620
|
return;
|
|
7265
8621
|
}
|
|
@@ -7282,32 +8638,250 @@ export async function startLocalServer(opts) {
|
|
|
7282
8638
|
: sourceRun.executionTarget?.target === 'local'
|
|
7283
8639
|
? 'local'
|
|
7284
8640
|
: projectConfig.defaultConnectionName;
|
|
8641
|
+
const resolvedTargetFingerprint = targetGenerationFingerprint(targetConnection, targetConnectionName);
|
|
8642
|
+
if (resolvedTargetFingerprint !== capability.targetFingerprint) {
|
|
8643
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8644
|
+
res.end(serializeJSON({
|
|
8645
|
+
code: 'REPAIR_TARGET_DRIFT',
|
|
8646
|
+
error: 'The named execution target now resolves to a different connection generation. Automatic repair was not attempted.',
|
|
8647
|
+
manualActions: capability.manualActions,
|
|
8648
|
+
}));
|
|
8649
|
+
return;
|
|
8650
|
+
}
|
|
8651
|
+
if (consumedAnalyticalRepairCapabilities.has(derivedRunId)) {
|
|
8652
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8653
|
+
res.end(serializeJSON({
|
|
8654
|
+
code: 'REPAIR_ATTEMPT_EXHAUSTED',
|
|
8655
|
+
error: 'The single permitted repair attempt has already been consumed.',
|
|
8656
|
+
manualActions: capability.manualActions,
|
|
8657
|
+
}));
|
|
8658
|
+
return;
|
|
8659
|
+
}
|
|
8660
|
+
activeAnalyticalRepairReservations.add(derivedRunId);
|
|
8661
|
+
consumedAnalyticalRepairCapabilities.add(derivedRunId);
|
|
8662
|
+
const releaseRepairReservation = () => { activeAnalyticalRepairReservations.delete(derivedRunId); };
|
|
8663
|
+
res.once('finish', releaseRepairReservation);
|
|
8664
|
+
res.once('close', releaseRepairReservation);
|
|
8665
|
+
const repairStartedAt = new Date().toISOString();
|
|
8666
|
+
const repairStartedAtMs = Date.now();
|
|
8667
|
+
const sourceFailureForReservation = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
|
|
8668
|
+
const reservationEvent = {
|
|
8669
|
+
id: `${derivedRunId}:repair`,
|
|
8670
|
+
runId: derivedRunId,
|
|
8671
|
+
type: 'repair.attempted',
|
|
8672
|
+
at: repairStartedAt,
|
|
8673
|
+
message: 'Reserved the single bounded repair attempt.',
|
|
8674
|
+
route: 'generated_answer',
|
|
8675
|
+
};
|
|
8676
|
+
const reservedRun = {
|
|
8677
|
+
id: derivedRunId,
|
|
8678
|
+
question: sourceRun.question,
|
|
8679
|
+
requestedMode: sourceRun.requestedMode,
|
|
8680
|
+
route: 'generated_answer',
|
|
8681
|
+
status: 'blocked',
|
|
8682
|
+
trustState: 'blocked',
|
|
8683
|
+
stopReason: 'blocked',
|
|
8684
|
+
startedAt: repairStartedAt,
|
|
8685
|
+
completedAt: repairStartedAt,
|
|
8686
|
+
selectedObject: sourceRun.selectedObject,
|
|
8687
|
+
executionTarget: sourceRun.executionTarget,
|
|
8688
|
+
routeDecision: sourceRun.routeDecision,
|
|
8689
|
+
plan: sourceRun.plan,
|
|
8690
|
+
steps: [],
|
|
8691
|
+
summary: 'The single repair attempt was reserved but has not completed successfully.',
|
|
8692
|
+
artifacts: [],
|
|
8693
|
+
evaluations: [],
|
|
8694
|
+
events: [reservationEvent],
|
|
8695
|
+
nextActions: sourceRun.nextActions,
|
|
8696
|
+
repairAttempts: 1,
|
|
8697
|
+
lifecycle: {
|
|
8698
|
+
version: 1,
|
|
8699
|
+
state: 'terminal',
|
|
8700
|
+
phase: 'repair.attempted',
|
|
8701
|
+
revision: 1,
|
|
8702
|
+
eventCursor: 1,
|
|
8703
|
+
startedAt: repairStartedAt,
|
|
8704
|
+
updatedAt: repairStartedAt,
|
|
8705
|
+
completedAt: repairStartedAt,
|
|
8706
|
+
},
|
|
8707
|
+
derivation: {
|
|
8708
|
+
version: 1,
|
|
8709
|
+
kind: 'analytical_repair',
|
|
8710
|
+
sourceRunId: sourceRun.id,
|
|
8711
|
+
...(sourceFailureForReservation?.failureId ? { sourceFailureId: sourceFailureForReservation.failureId } : {}),
|
|
8712
|
+
attempt: 1,
|
|
8713
|
+
},
|
|
8714
|
+
};
|
|
8715
|
+
try {
|
|
8716
|
+
await agentRunStore.save(reservedRun);
|
|
8717
|
+
}
|
|
8718
|
+
catch (error) {
|
|
8719
|
+
releaseRepairReservation();
|
|
8720
|
+
throw error;
|
|
8721
|
+
}
|
|
7285
8722
|
let repaired;
|
|
7286
8723
|
try {
|
|
7287
|
-
//
|
|
7288
|
-
//
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
:
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
8724
|
+
// Compile the immutable original wrapper first. A provider receives only
|
|
8725
|
+
// this embedded SQL, never the DQL wrapper or its surrounding fields.
|
|
8726
|
+
const invocation = prepareBlockInvocation({
|
|
8727
|
+
source: sourceDqlArtifact.source,
|
|
8728
|
+
parameters: sourceDqlArtifact.parameterValues,
|
|
8729
|
+
question: sourceRun.question,
|
|
8730
|
+
surface: 'ask_ai',
|
|
8731
|
+
});
|
|
8732
|
+
if (invocation.errors.length || invocation.unresolvedParameters.length) {
|
|
8733
|
+
throw boundedRepairError(invocation.errors.join(' ') || `Provide required parameters before retrying: ${invocation.unresolvedParameters.join(', ')}.`, 409);
|
|
8734
|
+
}
|
|
8735
|
+
const originalPlan = buildExecutionPlan({
|
|
8736
|
+
id: 'ask-execution-repair-source',
|
|
8737
|
+
type: 'dql',
|
|
8738
|
+
source: sourceDqlArtifact.source,
|
|
8739
|
+
title: sourceDqlArtifact.name,
|
|
8740
|
+
}, {
|
|
8741
|
+
semanticLayer,
|
|
8742
|
+
driver: targetConnection.driver,
|
|
8743
|
+
parameters: invocation.values,
|
|
8744
|
+
});
|
|
8745
|
+
if (!originalPlan?.sql?.trim())
|
|
8746
|
+
throw boundedRepairError('The original DQL wrapper did not compile to SQL.', 409);
|
|
8747
|
+
if (capability.sqlFingerprint !== capabilityHash(originalPlan.sql.trim())) {
|
|
8748
|
+
throw boundedRepairError('The original compiled SQL no longer matches its immutable repair capability.', 409);
|
|
8749
|
+
}
|
|
8750
|
+
const sqlRepair = await executeBoundedSqlRepair({
|
|
8751
|
+
question: sourceRun.question,
|
|
8752
|
+
sourceSql: originalPlan.sql,
|
|
8753
|
+
targetConnection,
|
|
8754
|
+
targetConnectionName,
|
|
8755
|
+
sqlParams: originalPlan.sqlParams,
|
|
8756
|
+
variables: { ...originalPlan.variables, ...invocation.values },
|
|
8757
|
+
});
|
|
8758
|
+
const embeddedSql = restoreNotebookDqlParameterInterpolations(sqlRepair.repairedSql, originalPlan.sqlParams);
|
|
8759
|
+
const repairedSource = replaceNotebookDqlQueryForRepair(sourceDqlArtifact.source, embeddedSql);
|
|
8760
|
+
if (!repairedSource)
|
|
8761
|
+
throw boundedRepairError('DQL could not reinsert the repaired SQL into the immutable wrapper.', 422);
|
|
8762
|
+
const repairedInvocation = prepareBlockInvocation({
|
|
8763
|
+
source: repairedSource,
|
|
8764
|
+
parameters: sourceDqlArtifact.parameterValues,
|
|
8765
|
+
question: sourceRun.question,
|
|
8766
|
+
surface: 'ask_ai',
|
|
8767
|
+
});
|
|
8768
|
+
if (repairedInvocation.errors.length || repairedInvocation.unresolvedParameters.length) {
|
|
8769
|
+
throw boundedRepairError('The SQL-only repair did not preserve a compilable DQL wrapper.', 422);
|
|
8770
|
+
}
|
|
8771
|
+
const repairedPlan = buildExecutionPlan({
|
|
8772
|
+
id: 'ask-execution-repair-derived',
|
|
8773
|
+
type: 'dql',
|
|
8774
|
+
source: repairedSource,
|
|
8775
|
+
title: sourceDqlArtifact.name,
|
|
8776
|
+
}, {
|
|
8777
|
+
semanticLayer,
|
|
8778
|
+
driver: targetConnection.driver,
|
|
8779
|
+
parameters: repairedInvocation.values,
|
|
8780
|
+
});
|
|
8781
|
+
if (!repairedPlan?.sql?.trim() || normalizeSqlForComparison(repairedPlan.sql) !== normalizeSqlForComparison(sqlRepair.repairedSql)) {
|
|
8782
|
+
throw boundedRepairError('The repaired wrapper recompiled to different SQL, so DQL stopped before publishing it.', 422);
|
|
8783
|
+
}
|
|
8784
|
+
const repairedExecutionReceipt = createDqlArtifactExecutionReceipt(repairedSource, sqlRepair.result.sql ?? repairedPlan.sql, repairedInvocation.values, sqlRepair.result);
|
|
8785
|
+
const repairedExecutableArtifact = sqlRepair.result.executableArtifact
|
|
8786
|
+
? {
|
|
8787
|
+
...sqlRepair.result.executableArtifact,
|
|
8788
|
+
kind: 'sql_block',
|
|
8789
|
+
dqlFingerprint: executionFingerprint(repairedSource),
|
|
8790
|
+
sourceFingerprint: executionFingerprint(stableExecutionValue({
|
|
8791
|
+
name: sourceDqlArtifact.name ?? null,
|
|
8792
|
+
path: sourceDqlArtifact.sourcePath ?? null,
|
|
8793
|
+
})),
|
|
8794
|
+
compiledSqlFingerprint: executionFingerprint(repairedPlan.sql),
|
|
8795
|
+
normalizedSqlFingerprint: executionFingerprint(sqlRepair.repairedSql),
|
|
8796
|
+
parameterFingerprint: repairedExecutionReceipt.parameterFingerprint,
|
|
8797
|
+
provenanceFingerprint: executionFingerprint(stableExecutionValue(repairedInvocation.resolvedParameters.map((parameter) => ({
|
|
8798
|
+
name: parameter.name,
|
|
8799
|
+
source: parameter.source,
|
|
8800
|
+
})))),
|
|
8801
|
+
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
8802
|
+
compiledSqlFingerprint: executionFingerprint(repairedPlan.sql),
|
|
8803
|
+
parameterCount: repairedPlan.sqlParams?.length ?? 0,
|
|
8804
|
+
variableNames: Object.keys(repairedPlan.variables ?? {}).sort(),
|
|
8805
|
+
})),
|
|
8806
|
+
trustState: 'review_required',
|
|
8807
|
+
receipt: repairedExecutionReceipt,
|
|
8808
|
+
}
|
|
8809
|
+
: undefined;
|
|
8810
|
+
const repairedArtifact = {
|
|
8811
|
+
...sourceDqlArtifact,
|
|
8812
|
+
source: repairedSource,
|
|
8813
|
+
compiledSql: repairedPlan.sql,
|
|
8814
|
+
trustState: 'review_required',
|
|
8815
|
+
persistence: 'transient',
|
|
8816
|
+
executionReceipt: repairedExecutionReceipt,
|
|
8817
|
+
...(repairedExecutableArtifact ? { executableArtifact: repairedExecutableArtifact } : {}),
|
|
8818
|
+
...(repairedInvocation.parameters.length ? { parameters: repairedInvocation.parameters } : {}),
|
|
8819
|
+
...(Object.keys(repairedInvocation.values).length ? { parameterValues: repairedInvocation.values } : {}),
|
|
8820
|
+
};
|
|
8821
|
+
repaired = {
|
|
8822
|
+
...sqlRepair,
|
|
8823
|
+
result: {
|
|
8824
|
+
...sqlRepair.result,
|
|
8825
|
+
executionReceipt: repairedExecutionReceipt,
|
|
8826
|
+
...(repairedExecutableArtifact ? { executableArtifact: repairedExecutableArtifact } : {}),
|
|
8827
|
+
dqlArtifact: repairedArtifact,
|
|
8828
|
+
},
|
|
8829
|
+
repairedSql: repairedPlan.sql,
|
|
8830
|
+
repairedSource,
|
|
8831
|
+
sqlParams: repairedPlan.sqlParams,
|
|
8832
|
+
variables: { ...repairedPlan.variables, ...repairedInvocation.values },
|
|
8833
|
+
};
|
|
7308
8834
|
}
|
|
7309
8835
|
catch (error) {
|
|
7310
8836
|
const repairError = error;
|
|
8837
|
+
const dispatchEvidence = providerDispatchTerminalEvidence(error);
|
|
8838
|
+
const failedAt = new Date().toISOString();
|
|
8839
|
+
const failedTelemetry = {
|
|
8840
|
+
version: 1,
|
|
8841
|
+
stageDurationsMs: { repair: Date.now() - repairStartedAtMs, total: Date.now() - repairStartedAtMs },
|
|
8842
|
+
providerRoundTrips: dispatchEvidence?.providerRoundTrips ?? 0,
|
|
8843
|
+
toolCalls: dispatchEvidence?.toolCalls ?? 0,
|
|
8844
|
+
sqlExecutions: dispatchEvidence?.sqlExecutions ?? 0,
|
|
8845
|
+
repairs: Math.max(1, dispatchEvidence?.repairs ?? 0),
|
|
8846
|
+
egressReceipts: dispatchEvidence?.providerEgressReceipts.length ?? 0,
|
|
8847
|
+
fallbackReason: dispatchEvidence?.fallbackReason ?? 'repair_failed',
|
|
8848
|
+
};
|
|
8849
|
+
const failedReceipts = dispatchEvidence?.providerEgressReceipts ?? [];
|
|
8850
|
+
await agentRunStore.save({
|
|
8851
|
+
...reservedRun,
|
|
8852
|
+
completedAt: failedAt,
|
|
8853
|
+
summary: repairError.message,
|
|
8854
|
+
lifecycle: reservedRun.lifecycle ? {
|
|
8855
|
+
...reservedRun.lifecycle,
|
|
8856
|
+
phase: 'run.failed',
|
|
8857
|
+
updatedAt: failedAt,
|
|
8858
|
+
completedAt: failedAt,
|
|
8859
|
+
} : undefined,
|
|
8860
|
+
events: [
|
|
8861
|
+
reservationEvent,
|
|
8862
|
+
{
|
|
8863
|
+
id: `${derivedRunId}:failed`,
|
|
8864
|
+
runId: derivedRunId,
|
|
8865
|
+
type: 'run.failed',
|
|
8866
|
+
at: failedAt,
|
|
8867
|
+
message: repairError.message,
|
|
8868
|
+
route: 'generated_answer',
|
|
8869
|
+
status: 'blocked',
|
|
8870
|
+
trustState: 'blocked',
|
|
8871
|
+
},
|
|
8872
|
+
],
|
|
8873
|
+
telemetry: failedTelemetry,
|
|
8874
|
+
...(failedReceipts.length ? { providerEgressReceipts: failedReceipts } : {}),
|
|
8875
|
+
diagnosticReceiptV2: {
|
|
8876
|
+
version: 2,
|
|
8877
|
+
runId: derivedRunId,
|
|
8878
|
+
route: 'generated_answer',
|
|
8879
|
+
status: 'blocked',
|
|
8880
|
+
telemetry: failedTelemetry,
|
|
8881
|
+
providerEgressReceiptFingerprints: failedReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
|
|
8882
|
+
repairCapabilityFingerprint: executionFingerprint(stableExecutionValue(capability)),
|
|
8883
|
+
},
|
|
8884
|
+
});
|
|
7311
8885
|
res.writeHead(repairError.status ?? 500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7312
8886
|
res.end(serializeJSON({
|
|
7313
8887
|
error: repairError.message,
|
|
@@ -7315,27 +8889,39 @@ export async function startLocalServer(opts) {
|
|
|
7315
8889
|
}));
|
|
7316
8890
|
return;
|
|
7317
8891
|
}
|
|
7318
|
-
const { result, repairedSql, repairMode, firstFailure } = repaired;
|
|
8892
|
+
const { result, repairedSql, repairMode, firstFailure, providerEgressReceipts, providerRoundTrips } = repaired;
|
|
7319
8893
|
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
|
-
});
|
|
8894
|
+
// Repair never grants a second answer/narration call and never sends rows
|
|
8895
|
+
// to a provider. Present the validated contract deterministically.
|
|
8896
|
+
const presentationText = `The repaired query returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}. Review the derived SQL before reuse.`;
|
|
7329
8897
|
const sourceFailure = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
|
|
7330
8898
|
const presentationContext = repairPresentationContextFromAgentRun(sourceRun);
|
|
7331
8899
|
const executionResultFingerprint = result.executionReceipt?.resultFingerprint;
|
|
8900
|
+
const repairTotalDurationMs = Date.now() - repairStartedAtMs;
|
|
8901
|
+
const repairTelemetry = {
|
|
8902
|
+
version: 1,
|
|
8903
|
+
stageDurationsMs: {
|
|
8904
|
+
retrieval: 0,
|
|
8905
|
+
meaning: 0,
|
|
8906
|
+
repair: repairTotalDurationMs,
|
|
8907
|
+
...(typeof result.executionTime === 'number' ? { execution: result.executionTime } : {}),
|
|
8908
|
+
narration: 0,
|
|
8909
|
+
total: repairTotalDurationMs,
|
|
8910
|
+
},
|
|
8911
|
+
providerRoundTrips,
|
|
8912
|
+
toolCalls: 0,
|
|
8913
|
+
sqlExecutions: firstFailure ? 2 : 1,
|
|
8914
|
+
repairs: 1,
|
|
8915
|
+
egressReceipts: providerEgressReceipts.length,
|
|
8916
|
+
...(typeof result.executionTime === 'number' ? { warehouseDurationMs: result.executionTime } : {}),
|
|
8917
|
+
};
|
|
7332
8918
|
const artifact = agentRunArtifact('answer', 'Repaired analytical answer', {
|
|
7333
8919
|
kind: 'uncertified',
|
|
7334
8920
|
certification: 'analyst_review_required',
|
|
7335
8921
|
reviewStatus: 'analyst_review_required',
|
|
7336
8922
|
trustLabel: 'AI-generated · review required',
|
|
7337
|
-
answer:
|
|
7338
|
-
text:
|
|
8923
|
+
answer: presentationText,
|
|
8924
|
+
text: presentationText,
|
|
7339
8925
|
...presentationContext,
|
|
7340
8926
|
result,
|
|
7341
8927
|
proposedSql: repairedSql,
|
|
@@ -7356,10 +8942,11 @@ export async function startLocalServer(opts) {
|
|
|
7356
8942
|
rowCount: result.rowCount,
|
|
7357
8943
|
...(executionResultFingerprint ? { resultFingerprint: executionResultFingerprint } : {}),
|
|
7358
8944
|
},
|
|
8945
|
+
providerEgressReceipts,
|
|
7359
8946
|
},
|
|
7360
8947
|
previousAttempt: {
|
|
7361
8948
|
sourceRunId: sourceRun.id,
|
|
7362
|
-
|
|
8949
|
+
sql: repairableSqlFromAgentRun(sourceRun),
|
|
7363
8950
|
...(sourceDqlArtifact?.source ? { dql: sourceDqlArtifact.source } : {}),
|
|
7364
8951
|
failure: retainedFailure ?? firstFailure,
|
|
7365
8952
|
},
|
|
@@ -7386,7 +8973,7 @@ export async function startLocalServer(opts) {
|
|
|
7386
8973
|
artifacts: [artifact],
|
|
7387
8974
|
};
|
|
7388
8975
|
const events = [
|
|
7389
|
-
{ id: `${derivedRunId}:repair`, runId: derivedRunId, type: 'repair.attempted', at:
|
|
8976
|
+
{ id: `${derivedRunId}:repair`, runId: derivedRunId, type: 'repair.attempted', at: repairStartedAt, message: 'Started one bounded query repair.', route: 'generated_answer' },
|
|
7390
8977
|
{ id: `${derivedRunId}:artifact`, runId: derivedRunId, type: 'artifact.created', at: completedAt, message: 'Created the repaired result artifact.', route: 'generated_answer', trustState: 'review_required' },
|
|
7391
8978
|
{ 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
8979
|
];
|
|
@@ -7398,15 +8985,15 @@ export async function startLocalServer(opts) {
|
|
|
7398
8985
|
status: 'needs_review',
|
|
7399
8986
|
trustState: 'review_required',
|
|
7400
8987
|
stopReason: 'generated_review_required',
|
|
7401
|
-
startedAt:
|
|
8988
|
+
startedAt: repairStartedAt,
|
|
7402
8989
|
completedAt,
|
|
7403
8990
|
selectedObject: sourceRun.selectedObject,
|
|
7404
8991
|
executionTarget: sourceRun.executionTarget,
|
|
7405
8992
|
routeDecision: sourceRun.routeDecision,
|
|
7406
8993
|
plan: sourceRun.plan,
|
|
7407
8994
|
steps: [repairStep],
|
|
7408
|
-
summary:
|
|
7409
|
-
answer:
|
|
8995
|
+
summary: presentationText,
|
|
8996
|
+
answer: presentationText,
|
|
7410
8997
|
answerKind: 'governed',
|
|
7411
8998
|
artifacts: [artifact],
|
|
7412
8999
|
evaluations: [evaluation],
|
|
@@ -7419,7 +9006,7 @@ export async function startLocalServer(opts) {
|
|
|
7419
9006
|
phase: 'run.completed',
|
|
7420
9007
|
revision: 1,
|
|
7421
9008
|
eventCursor: events.length,
|
|
7422
|
-
startedAt:
|
|
9009
|
+
startedAt: repairStartedAt,
|
|
7423
9010
|
updatedAt: completedAt,
|
|
7424
9011
|
completedAt,
|
|
7425
9012
|
},
|
|
@@ -7432,7 +9019,19 @@ export async function startLocalServer(opts) {
|
|
|
7432
9019
|
steps: [repairStep],
|
|
7433
9020
|
artifacts: [artifact],
|
|
7434
9021
|
evaluations: [evaluation],
|
|
9022
|
+
providerEgressReceipts,
|
|
7435
9023
|
},
|
|
9024
|
+
diagnosticReceiptV2: {
|
|
9025
|
+
version: 2,
|
|
9026
|
+
runId: derivedRunId,
|
|
9027
|
+
route: 'generated_answer',
|
|
9028
|
+
status: 'needs_review',
|
|
9029
|
+
telemetry: repairTelemetry,
|
|
9030
|
+
providerEgressReceiptFingerprints: providerEgressReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
|
|
9031
|
+
repairCapabilityFingerprint: executionFingerprint(stableExecutionValue(capability)),
|
|
9032
|
+
},
|
|
9033
|
+
telemetry: repairTelemetry,
|
|
9034
|
+
providerEgressReceipts,
|
|
7436
9035
|
derivation: {
|
|
7437
9036
|
version: 1,
|
|
7438
9037
|
kind: 'analytical_repair',
|
|
@@ -7456,6 +9055,14 @@ export async function startLocalServer(opts) {
|
|
|
7456
9055
|
res.end(serializeJSON({ error: 'Agent run not found.' }));
|
|
7457
9056
|
return;
|
|
7458
9057
|
}
|
|
9058
|
+
if (run.status !== 'blocked' || run.stopReason !== 'blocked') {
|
|
9059
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
9060
|
+
res.end(serializeJSON({
|
|
9061
|
+
code: 'REPAIR_CAPABILITY_REQUIRED',
|
|
9062
|
+
error: 'Only a terminal blocked run with a blocked stop reason may derive an analytical repair.',
|
|
9063
|
+
}));
|
|
9064
|
+
return;
|
|
9065
|
+
}
|
|
7459
9066
|
const source = analyticalFailedRunFromAgentRun(run);
|
|
7460
9067
|
if (!source) {
|
|
7461
9068
|
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -7494,25 +9101,11 @@ export async function startLocalServer(opts) {
|
|
|
7494
9101
|
if (req.method === 'POST' && /^\/api\/agent-runs\/[^/]+\/cancel$/.test(path)) {
|
|
7495
9102
|
const match = path.match(/^\/api\/agent-runs\/([^/]+)\/cancel$/);
|
|
7496
9103
|
const id = decodeURIComponent(match?.[1] ?? '');
|
|
7497
|
-
|
|
7498
|
-
if (!controller) {
|
|
9104
|
+
if (!cancelActiveAgentRun(id)) {
|
|
7499
9105
|
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7500
9106
|
res.end(serializeJSON({ ok: false, error: 'This run is no longer active.' }));
|
|
7501
9107
|
return;
|
|
7502
9108
|
}
|
|
7503
|
-
const progress = agentRunStore.getProgress(id);
|
|
7504
|
-
if (progress) {
|
|
7505
|
-
agentRunStore.saveProgress({
|
|
7506
|
-
...progress,
|
|
7507
|
-
lifecycle: {
|
|
7508
|
-
...progress.lifecycle,
|
|
7509
|
-
state: 'cancelling',
|
|
7510
|
-
revision: progress.lifecycle.revision + 1,
|
|
7511
|
-
updatedAt: new Date().toISOString(),
|
|
7512
|
-
},
|
|
7513
|
-
});
|
|
7514
|
-
}
|
|
7515
|
-
controller.abort(new Error('Stopped by user.'));
|
|
7516
9109
|
res.writeHead(202, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7517
9110
|
res.end(serializeJSON({ ok: true, id }));
|
|
7518
9111
|
return;
|
|
@@ -7578,21 +9171,59 @@ export async function startLocalServer(opts) {
|
|
|
7578
9171
|
let runError;
|
|
7579
9172
|
try {
|
|
7580
9173
|
const executeRun = async (operationSignal, report) => {
|
|
7581
|
-
|
|
9174
|
+
const ingressSignal = AbortSignal.any([
|
|
7582
9175
|
runController.signal,
|
|
7583
9176
|
...(operationSignal ? [operationSignal] : []),
|
|
7584
|
-
AbortSignal.timeout(agentRunDeadlineMs(parsed.request, process.env, getActiveProvider(projectRoot))),
|
|
7585
9177
|
]);
|
|
9178
|
+
parsed.request.runBudget = createAgentRunBudget({
|
|
9179
|
+
requestedMode: parsed.request.requestedMode,
|
|
9180
|
+
inheritedSignal: ingressSignal,
|
|
9181
|
+
});
|
|
9182
|
+
parsed.request.signal = parsed.request.runBudget.hardSignal;
|
|
9183
|
+
// Ordinary Ask needs room to think, write, and recover: meaning (1)
|
|
9184
|
+
// + planning/generation (3) + narration (2) + one repair. The old
|
|
9185
|
+
// `total: 2` left nothing for narration once generation had run,
|
|
9186
|
+
// which is why the answer arrived as a deterministic fact-join.
|
|
9187
|
+
const runProviderEvidence = new RunScopedProviderDispatchEvidence(parsed.request.requestedMode === 'research'
|
|
9188
|
+
? { total: 14, meaningResolution: 1, generationGroup: 11, narration: 2, repair: 1 }
|
|
9189
|
+
: { total: 6, meaningResolution: 1, generationGroup: 3, narration: 2, repair: 1 }, parsed.request.runBudget, resolveProviderResultRowEgressPolicy({
|
|
9190
|
+
projectSetting: projectConfig?.agent?.providerResultRowEgress,
|
|
9191
|
+
researchOptIn: parsed.request.requestedMode === 'research'
|
|
9192
|
+
&& parsed.request.researchResultRowsOptIn === true,
|
|
9193
|
+
}));
|
|
7586
9194
|
report?.({ phase: 'reasoning', progress: 8, message: 'Grounding the request and selecting the governed route.' });
|
|
7587
9195
|
try {
|
|
7588
|
-
const
|
|
9196
|
+
const engineRun = await agentRunProviderEvidenceContext.run(runProviderEvidence, () => agentRunEngine.run(parsed.request, (event) => {
|
|
9197
|
+
runProviderEvidence.setRoute(event.route);
|
|
7589
9198
|
if (wantsStream)
|
|
7590
9199
|
writeStream('agent-run-event', event);
|
|
7591
9200
|
report?.({ phase: event.type, progress: 45, message: event.message });
|
|
7592
9201
|
}, (delta) => {
|
|
7593
9202
|
if (wantsStream)
|
|
7594
9203
|
writeStream('agent-run-answer-delta', { runId: parsed.request.runId, delta });
|
|
7595
|
-
});
|
|
9204
|
+
}));
|
|
9205
|
+
const rawRun = mergeRunScopedProviderDispatchEvidence(engineRun, runProviderEvidence.snapshot(engineRun.telemetry?.fallbackReason ?? engineRun.stopReason));
|
|
9206
|
+
let resolvedTargetFingerprint;
|
|
9207
|
+
try {
|
|
9208
|
+
const resolvedTarget = await resolveAgentRunExecutionConnection({
|
|
9209
|
+
question: rawRun.question,
|
|
9210
|
+
executionTarget: rawRun.executionTarget,
|
|
9211
|
+
});
|
|
9212
|
+
const resolvedTargetName = rawRun.executionTarget?.target === 'connection'
|
|
9213
|
+
? rawRun.executionTarget.connectionName
|
|
9214
|
+
: rawRun.executionTarget?.target === 'local'
|
|
9215
|
+
? 'local'
|
|
9216
|
+
: projectConfig.defaultConnectionName;
|
|
9217
|
+
resolvedTargetFingerprint = targetGenerationFingerprint(resolvedTarget, resolvedTargetName);
|
|
9218
|
+
}
|
|
9219
|
+
catch {
|
|
9220
|
+
// Missing target-generation evidence keeps automatic repair
|
|
9221
|
+
// ineligible; the failed run itself remains fully inspectable.
|
|
9222
|
+
}
|
|
9223
|
+
const run = attachAnalyticalRepairCapability(rawRun, resolvedTargetFingerprint);
|
|
9224
|
+
// The engine persists before the host can validate the DQL wrapper;
|
|
9225
|
+
// replace that row with the server-owned capability atomically.
|
|
9226
|
+
await agentRunStore.save(run);
|
|
7596
9227
|
completedRun = run;
|
|
7597
9228
|
recordConversationTurn(conversationStore, parsed.request.threadId, run);
|
|
7598
9229
|
return { runId: run.id, threadId: parsed.request.threadId, status: run.status };
|
|
@@ -7619,7 +9250,10 @@ export async function startLocalServer(opts) {
|
|
|
7619
9250
|
return;
|
|
7620
9251
|
}
|
|
7621
9252
|
res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
7622
|
-
|
|
9253
|
+
// Same run, same reader as the stream above, so it ships the same
|
|
9254
|
+
// projection. Sending the stored record here instead made an ordinary
|
|
9255
|
+
// completed Ask a 4.66 MB reply.
|
|
9256
|
+
res.end(serializeJSON({ run: slimAgentRunForTransport(completedRun) }));
|
|
7623
9257
|
}
|
|
7624
9258
|
finally {
|
|
7625
9259
|
if (runId)
|
|
@@ -9658,13 +11292,22 @@ export async function startLocalServer(opts) {
|
|
|
9658
11292
|
});
|
|
9659
11293
|
if (!composed)
|
|
9660
11294
|
throw new Error('The governed semantic query could not be composed.');
|
|
9661
|
-
const prepared = prepareLocalExecution(composed.sql, targetConnection, projectRoot, projectConfig);
|
|
9662
11295
|
let normalizedResult;
|
|
9663
11296
|
let repair;
|
|
9664
11297
|
let executedSql = composed.sql;
|
|
11298
|
+
let executionFingerprints;
|
|
9665
11299
|
try {
|
|
9666
|
-
const
|
|
9667
|
-
|
|
11300
|
+
const execution = await analyticalExecutionService.execute({
|
|
11301
|
+
sql: composed.sql,
|
|
11302
|
+
subject: 'App tile semantic query',
|
|
11303
|
+
connection: targetConnection,
|
|
11304
|
+
});
|
|
11305
|
+
normalizedResult = execution.result;
|
|
11306
|
+
executedSql = execution.preparation.executedSql;
|
|
11307
|
+
executionFingerprints = {
|
|
11308
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
11309
|
+
resultFingerprint: execution.resultFingerprint,
|
|
11310
|
+
};
|
|
9668
11311
|
}
|
|
9669
11312
|
catch (executionError) {
|
|
9670
11313
|
const repaired = await repairFailedAppTileExecution({
|
|
@@ -9706,6 +11349,7 @@ export async function startLocalServer(opts) {
|
|
|
9706
11349
|
title: item.title ?? item.semantic.id,
|
|
9707
11350
|
viz: item.viz,
|
|
9708
11351
|
result: normalizedResult,
|
|
11352
|
+
...(executionFingerprints ?? {}),
|
|
9709
11353
|
trustState: 'review_required',
|
|
9710
11354
|
citation: { kind: repair ? 'semantic_query_repaired' : 'semantic_query', name: item.semantic.id },
|
|
9711
11355
|
...(repair ? { repair } : {}),
|
|
@@ -9839,8 +11483,13 @@ export async function startLocalServer(opts) {
|
|
|
9839
11483
|
tileId: item.i,
|
|
9840
11484
|
tileFilterBindings: item.filterBindings,
|
|
9841
11485
|
});
|
|
9842
|
-
const
|
|
9843
|
-
|
|
11486
|
+
const execution = await analyticalExecutionService.execute({
|
|
11487
|
+
sql: blockFilterApplication.sql,
|
|
11488
|
+
subject: 'App tile block query',
|
|
11489
|
+
connection: blockTargetConnection,
|
|
11490
|
+
sqlParams: blockFilterApplication.sqlParams,
|
|
11491
|
+
variables: { ...blockFilterApplication.variables, ...blockInvocation.values },
|
|
11492
|
+
});
|
|
9844
11493
|
tiles.push({
|
|
9845
11494
|
tileId: item.i,
|
|
9846
11495
|
status: 'ok',
|
|
@@ -9850,7 +11499,9 @@ export async function startLocalServer(opts) {
|
|
|
9850
11499
|
title: item.title ?? block.name,
|
|
9851
11500
|
viz: item.viz,
|
|
9852
11501
|
chartConfig: mergeDashboardChartConfig(blockPlan?.chartConfig, item),
|
|
9853
|
-
result:
|
|
11502
|
+
result: execution.result,
|
|
11503
|
+
compiledSqlFingerprint: execution.compiledSqlFingerprint,
|
|
11504
|
+
resultFingerprint: execution.resultFingerprint,
|
|
9854
11505
|
filters: {
|
|
9855
11506
|
applied: blockFilterApplication.appliedFilters,
|
|
9856
11507
|
skipped: blockFilterApplication.skippedFilters,
|
|
@@ -14761,20 +16412,18 @@ export async function startLocalServer(opts) {
|
|
|
14761
16412
|
}));
|
|
14762
16413
|
return;
|
|
14763
16414
|
}
|
|
14764
|
-
const
|
|
16415
|
+
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
16416
|
+
const domain = typeof body.domain === 'string' ? body.domain : app?.domain;
|
|
16417
|
+
assertAppAccess({ app, domain, level: 'execute' });
|
|
16418
|
+
const execution = await analyticalExecutionService.execute({
|
|
14765
16419
|
sql: semantic.sql,
|
|
14766
16420
|
subject: 'Notebook query',
|
|
14767
|
-
executor,
|
|
14768
16421
|
connection: executionConnection,
|
|
14769
|
-
|
|
14770
|
-
|
|
16422
|
+
sqlParams: Array.isArray(body.sqlParams) ? body.sqlParams : [],
|
|
16423
|
+
variables: body.variables && typeof body.variables === 'object' ? body.variables : {},
|
|
16424
|
+
semanticRefs: semantic.semanticRefs,
|
|
14771
16425
|
});
|
|
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);
|
|
16426
|
+
const { preparation, result: normalized } = execution;
|
|
14778
16427
|
if (execContext?.notebookPath) {
|
|
14779
16428
|
recordNotebookQueryRun(projectRoot, {
|
|
14780
16429
|
notebookPath: execContext.notebookPath,
|
|
@@ -15448,7 +17097,7 @@ export async function startLocalServer(opts) {
|
|
|
15448
17097
|
const invocation = prepareBlockInvocation({ source, surface: 'ask_ai' });
|
|
15449
17098
|
if (invocation.errors.length > 0)
|
|
15450
17099
|
throw new Error(invocation.errors.join(' '));
|
|
15451
|
-
const { executionReceipt: _receipt, compiledSql: _compiled, ...unexecuted } = artifact;
|
|
17100
|
+
const { executionReceipt: _receipt, executableArtifact: _executable, compiledSql: _compiled, ...unexecuted } = artifact;
|
|
15452
17101
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
15453
17102
|
res.end(serializeJSON({
|
|
15454
17103
|
artifact: {
|
|
@@ -15564,6 +17213,13 @@ export async function startLocalServer(opts) {
|
|
|
15564
17213
|
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
15565
17214
|
compiledSql: result.sql,
|
|
15566
17215
|
executionReceipt: result.executionReceipt,
|
|
17216
|
+
...(result.executableArtifact ? {
|
|
17217
|
+
executableArtifact: {
|
|
17218
|
+
...result.executableArtifact,
|
|
17219
|
+
kind: certified ? 'certified_block' : semantic ? 'semantic_block' : 'sql_block',
|
|
17220
|
+
trustState: certified ? 'certified' : semantic ? 'governed' : 'review_required',
|
|
17221
|
+
},
|
|
17222
|
+
} : {}),
|
|
15567
17223
|
...(requestedArtifact?.limit ? { limit: requestedArtifact.limit } : {}),
|
|
15568
17224
|
},
|
|
15569
17225
|
}));
|
|
@@ -15686,37 +17342,43 @@ export async function startLocalServer(opts) {
|
|
|
15686
17342
|
res.end(serializeJSON({ cellType: cell.type, result: null }));
|
|
15687
17343
|
return;
|
|
15688
17344
|
}
|
|
15689
|
-
const
|
|
17345
|
+
const app = loadRuntimeApp(projectRoot, typeof body.appId === 'string' ? body.appId : activePersonaAppId());
|
|
17346
|
+
assertAppAccess({ app, domain: resolved.domain ?? app?.domain, level: 'execute' });
|
|
17347
|
+
const semanticExecutionHolder = { value: null };
|
|
17348
|
+
const execution = await analyticalExecutionService.execute({
|
|
15690
17349
|
sql: executableSql,
|
|
15691
17350
|
subject: cell.type === 'dql' ? 'DQL query' : 'Notebook query',
|
|
15692
|
-
executor,
|
|
15693
17351
|
connection: cellConnection,
|
|
15694
|
-
|
|
15695
|
-
|
|
17352
|
+
sqlParams: plan?.sqlParams,
|
|
17353
|
+
variables: { ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) },
|
|
17354
|
+
executePrepared: semanticCompose?.compiled
|
|
17355
|
+
? async (preparation) => {
|
|
17356
|
+
const semanticExecution = await executeTargetBoundSemanticQuery({
|
|
17357
|
+
executor,
|
|
17358
|
+
connection: cellConnection,
|
|
17359
|
+
projectRoot,
|
|
17360
|
+
plannedAdapter: semanticCompose.compiled.engine,
|
|
17361
|
+
metricFlow: semanticCompose.compiled.engine === 'metricflow-cli'
|
|
17362
|
+
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
17363
|
+
: undefined,
|
|
17364
|
+
// Compilation already produced the exact pinned adapter result
|
|
17365
|
+
// used by the plan. The gateway validates target identity before
|
|
17366
|
+
// accepting that result for preflight and execution.
|
|
17367
|
+
compile: async () => semanticCompose.compiled,
|
|
17368
|
+
prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
|
|
17369
|
+
rowBound: semanticCompose.compiled.effectiveRequest.limit,
|
|
17370
|
+
});
|
|
17371
|
+
if (semanticExecution) {
|
|
17372
|
+
semanticExecutionHolder.value = semanticExecution;
|
|
17373
|
+
return semanticExecution.result;
|
|
17374
|
+
}
|
|
17375
|
+
return executor.executeQuery(preparation.executedSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...(invocation?.values ?? {}) }), preparation.connection);
|
|
17376
|
+
}
|
|
17377
|
+
: undefined,
|
|
15696
17378
|
});
|
|
17379
|
+
const { preparation, result: normalized } = execution;
|
|
15697
17380
|
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);
|
|
17381
|
+
const semanticExecution = semanticExecutionHolder.value;
|
|
15720
17382
|
// Enforce the block's declared invariants against the result set. This
|
|
15721
17383
|
// is additive: blocks without invariants produce `null` and the
|
|
15722
17384
|
// response is unchanged. The agent surface (`query_via_block`) reads
|
|
@@ -15967,6 +17629,26 @@ table: ${table}${tagList}
|
|
|
15967
17629
|
});
|
|
15968
17630
|
});
|
|
15969
17631
|
}
|
|
17632
|
+
function providerDispatchTerminalEvidence(value) {
|
|
17633
|
+
if (!value || typeof value !== 'object')
|
|
17634
|
+
return undefined;
|
|
17635
|
+
const candidate = value.providerDispatchEvidence;
|
|
17636
|
+
if (!candidate || typeof candidate !== 'object')
|
|
17637
|
+
return undefined;
|
|
17638
|
+
const evidence = candidate;
|
|
17639
|
+
if (!Array.isArray(evidence.providerEgressReceipts)
|
|
17640
|
+
|| !Number.isInteger(evidence.providerRoundTrips)
|
|
17641
|
+
|| (evidence.providerRoundTrips ?? -1) < 0)
|
|
17642
|
+
return undefined;
|
|
17643
|
+
return {
|
|
17644
|
+
providerEgressReceipts: evidence.providerEgressReceipts,
|
|
17645
|
+
providerRoundTrips: evidence.providerRoundTrips,
|
|
17646
|
+
toolCalls: Number.isInteger(evidence.toolCalls) && (evidence.toolCalls ?? -1) >= 0 ? evidence.toolCalls : 0,
|
|
17647
|
+
sqlExecutions: Number.isInteger(evidence.sqlExecutions) && (evidence.sqlExecutions ?? -1) >= 0 ? evidence.sqlExecutions : 0,
|
|
17648
|
+
repairs: Number.isInteger(evidence.repairs) && (evidence.repairs ?? -1) >= 0 ? evidence.repairs : 0,
|
|
17649
|
+
fallbackReason: typeof evidence.fallbackReason === 'string' ? evidence.fallbackReason : 'provider_error',
|
|
17650
|
+
};
|
|
17651
|
+
}
|
|
15970
17652
|
async function validateModelingRelationship(relationship, manifest, execute, quoteIdentifier = quoteSqlIdentifier) {
|
|
15971
17653
|
const fromEntity = manifest.modeling?.entities[relationship.from];
|
|
15972
17654
|
const toEntity = manifest.modeling?.entities[relationship.to];
|
|
@@ -16256,21 +17938,31 @@ function connectionDriverLabel(connection) {
|
|
|
16256
17938
|
* The notebook SPA expects columns as string[] (just names).
|
|
16257
17939
|
*/
|
|
16258
17940
|
function normalizeQueryResult(result, semanticRefs) {
|
|
16259
|
-
const
|
|
16260
|
-
|
|
17941
|
+
const canonical = normalizeCanonicalQueryResult({
|
|
17942
|
+
columns: result?.columns,
|
|
17943
|
+
rows: result?.rows,
|
|
17944
|
+
rowCount: result?.rowCount,
|
|
17945
|
+
executionTime: result?.executionTime,
|
|
17946
|
+
executionTimeMs: result?.executionTimeMs,
|
|
17947
|
+
truncated: result?.truncated,
|
|
17948
|
+
resultFingerprint: result?.resultFingerprint,
|
|
17949
|
+
executionReceipt: result?.executionReceipt,
|
|
17950
|
+
trustState: result?.trustState,
|
|
17951
|
+
answerTier: result?.answerTier,
|
|
17952
|
+
});
|
|
16261
17953
|
const rawRows = Array.isArray(result?.rows) ? result.rows : [];
|
|
16262
|
-
const rows =
|
|
17954
|
+
const rows = canonical.rows.slice(0, NOTEBOOK_EXECUTE_PREVIEW_ROW_LIMIT);
|
|
16263
17955
|
const hasRefs = semanticRefs && (semanticRefs.metrics.length > 0 || semanticRefs.dimensions.length > 0);
|
|
16264
17956
|
return {
|
|
16265
|
-
columns,
|
|
17957
|
+
columns: canonical.columns,
|
|
16266
17958
|
rows,
|
|
16267
|
-
rowCount:
|
|
16268
|
-
|
|
16269
|
-
|
|
16270
|
-
|
|
16271
|
-
|
|
16272
|
-
|
|
16273
|
-
...(
|
|
17959
|
+
rowCount: canonical.rowCount,
|
|
17960
|
+
resultFingerprint: canonical.resultFingerprint,
|
|
17961
|
+
executionTime: canonical.executionTime ?? 0,
|
|
17962
|
+
...(rawRows.length > rows.length || canonical.truncated ? { truncated: true } : {}),
|
|
17963
|
+
...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
|
|
17964
|
+
...(canonical.trustState ? { trustState: canonical.trustState } : {}),
|
|
17965
|
+
...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
|
|
16274
17966
|
...(hasRefs ? { semanticRefs } : {}),
|
|
16275
17967
|
};
|
|
16276
17968
|
}
|
|
@@ -18651,9 +20343,104 @@ function createDqlArtifactExecutionReceipt(source, compiledSql, parameters, resu
|
|
|
18651
20343
|
})),
|
|
18652
20344
|
};
|
|
18653
20345
|
}
|
|
20346
|
+
function agentRunTelemetryForAnswer(answer, egressReceipts, totalDurationMs, narrationDurationMs, engineRepairAttempts) {
|
|
20347
|
+
const timings = new Map((answer.evidence?.timings ?? []).map((timing) => [timing.phase, timing.durationMs]));
|
|
20348
|
+
const toolCalls = answer.evidence?.toolCalls ?? [];
|
|
20349
|
+
const toolDurationMs = toolCalls.reduce((sum, call) => sum + Math.max(0, call.durationMs ?? 0), 0);
|
|
20350
|
+
const warehouseDurationMs = answer.evidence?.execution?.executionTime ?? answer.result?.executionTime;
|
|
20351
|
+
const counters = answer.evidence?.runtimeCounters;
|
|
20352
|
+
const stageDurationsMs = {
|
|
20353
|
+
...(timings.has('project_state') ? { snapshot: timings.get('project_state') } : {}),
|
|
20354
|
+
...(timings.has('context_retrieval') ? { retrieval: timings.get('context_retrieval') } : {}),
|
|
20355
|
+
...(timings.has('runtime_schema') ? { schema: timings.get('runtime_schema') } : {}),
|
|
20356
|
+
...(timings.has('source_search') ? { meaning: timings.get('source_search') } : {}),
|
|
20357
|
+
...(timings.has('answer_resolution') ? { provider: timings.get('answer_resolution') } : {}),
|
|
20358
|
+
...(toolCalls.length > 0 ? { tools: toolDurationMs } : {}),
|
|
20359
|
+
...(typeof warehouseDurationMs === 'number' ? { execution: Math.max(0, warehouseDurationMs) } : {}),
|
|
20360
|
+
...(narrationDurationMs > 0 ? { narration: narrationDurationMs } : {}),
|
|
20361
|
+
total: Math.max(0, totalDurationMs),
|
|
20362
|
+
};
|
|
20363
|
+
return {
|
|
20364
|
+
version: 1,
|
|
20365
|
+
stageDurationsMs,
|
|
20366
|
+
providerRoundTrips: counters?.providerRoundTrips ?? 0,
|
|
20367
|
+
toolCalls: counters?.toolCalls ?? toolCalls.length,
|
|
20368
|
+
sqlExecutions: counters?.sqlExecutions ?? 0,
|
|
20369
|
+
repairs: Math.max(engineRepairAttempts, counters?.repairs ?? 0),
|
|
20370
|
+
egressReceipts: egressReceipts.length,
|
|
20371
|
+
...(typeof warehouseDurationMs === 'number' ? { warehouseDurationMs: Math.max(0, warehouseDurationMs) } : {}),
|
|
20372
|
+
...(answer.refusalCode ? { fallbackReason: answer.refusalCode } : {}),
|
|
20373
|
+
};
|
|
20374
|
+
}
|
|
18654
20375
|
function executionFingerprint(value) {
|
|
18655
20376
|
return createHash('sha256').update(value).digest('hex');
|
|
18656
20377
|
}
|
|
20378
|
+
function buildSemanticAggregationCompilerReceipt(input) {
|
|
20379
|
+
const plan = input.plan;
|
|
20380
|
+
const metricId = plan?.analyticalFrame?.metricConceptIds[0];
|
|
20381
|
+
if (input.metricNames.length !== 1 || !plan || !metricId || !plan.executionId
|
|
20382
|
+
|| !plan.selectedCapabilityFingerprint || !input.targetFingerprint)
|
|
20383
|
+
return undefined;
|
|
20384
|
+
const metric = input.semanticLayer.getMetric(input.metricNames[0]);
|
|
20385
|
+
if (!metric || (metric.aggregation ?? metric.metricType)?.toLowerCase() !== 'ratio')
|
|
20386
|
+
return undefined;
|
|
20387
|
+
const refName = (value) => {
|
|
20388
|
+
if (typeof value === 'string' && value.trim())
|
|
20389
|
+
return value.trim();
|
|
20390
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
20391
|
+
return undefined;
|
|
20392
|
+
const name = value.name;
|
|
20393
|
+
return typeof name === 'string' && name.trim() ? name.trim() : undefined;
|
|
20394
|
+
};
|
|
20395
|
+
const numeratorName = refName(metric.typeParams?.numerator);
|
|
20396
|
+
const denominatorName = refName(metric.typeParams?.denominator);
|
|
20397
|
+
if (!numeratorName || !denominatorName || !metric.sql.trim())
|
|
20398
|
+
return undefined;
|
|
20399
|
+
const measures = [numeratorName, denominatorName].map((name) => {
|
|
20400
|
+
const matches = input.semanticLayer.listMeasures().filter((measure) => measure.name === name
|
|
20401
|
+
&& (!metric.cube || !measure.cube || measure.cube === metric.cube));
|
|
20402
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
20403
|
+
});
|
|
20404
|
+
if (measures.some((measure) => !measure))
|
|
20405
|
+
return undefined;
|
|
20406
|
+
const relations = Array.from(new Set(measures.map((measure) => measure.table.trim()).filter(Boolean)));
|
|
20407
|
+
if (relations.length !== 1)
|
|
20408
|
+
return undefined;
|
|
20409
|
+
const outputAlias = metricId.split(/[:.]/).at(-1);
|
|
20410
|
+
if (!outputAlias)
|
|
20411
|
+
return undefined;
|
|
20412
|
+
const orderedMeasureIds = measures.map((measure) => `semantic:${measure.domain || 'uncategorized'}:measure:${measure.cube ? `${measure.cube}.` : ''}${measure.name}`);
|
|
20413
|
+
const compiledSqlFingerprint = semanticExecutionFingerprint(input.compiledSql);
|
|
20414
|
+
const compiledOutputExpression = buildSqlOutputExpressionSignature(input.compiledSql, outputAlias);
|
|
20415
|
+
if (!compiledOutputExpression)
|
|
20416
|
+
return undefined;
|
|
20417
|
+
const compiledExpressionFingerprint = semanticExecutionFingerprint(compiledOutputExpression.canonicalExpression);
|
|
20418
|
+
const compiledSignature = buildGeneratedAnalyticalSqlSignature(input.compiledSql);
|
|
20419
|
+
if (!compiledSignature)
|
|
20420
|
+
return undefined;
|
|
20421
|
+
const body = {
|
|
20422
|
+
version: 1,
|
|
20423
|
+
executionId: plan.executionId,
|
|
20424
|
+
capabilityFingerprint: plan.selectedCapabilityFingerprint,
|
|
20425
|
+
metricId,
|
|
20426
|
+
metricExpressionSql: metric.sql,
|
|
20427
|
+
operator: 'ratio',
|
|
20428
|
+
orderedMeasureIds,
|
|
20429
|
+
physicalRelation: relations[0],
|
|
20430
|
+
relationAliases: Array.from(new Set([metric.cube, ...measures.map((measure) => measure.cube)].filter((value) => Boolean(value)))),
|
|
20431
|
+
outputAlias,
|
|
20432
|
+
dimensionIds: [...new Set(plan.analyticalFrame?.dimensions.map((dimension) => dimension.dimensionId) ?? [])].sort(),
|
|
20433
|
+
relationshipPathIds: [...plan.relationshipPathIds].sort(),
|
|
20434
|
+
relationshipProofFingerprints: [...new Set(plan.relationshipProofs?.map((proof) => proof.authorityFingerprint) ?? [])].sort(),
|
|
20435
|
+
orderBy: compiledSignature.orderBy,
|
|
20436
|
+
compiledSqlFingerprint,
|
|
20437
|
+
compiledExpressionFingerprint,
|
|
20438
|
+
planFingerprint: plan.fingerprint,
|
|
20439
|
+
snapshotId: plan.snapshotId,
|
|
20440
|
+
targetFingerprint: input.targetFingerprint,
|
|
20441
|
+
};
|
|
20442
|
+
return { ...body, receiptFingerprint: semanticExecutionFingerprint(body) };
|
|
20443
|
+
}
|
|
18657
20444
|
/**
|
|
18658
20445
|
* Older clients normalized executable DQL with trim(), while the receipt was
|
|
18659
20446
|
* correctly created from the original renderer output ending in one newline.
|
|
@@ -19071,6 +20858,43 @@ export function prepareLocalExecution(sql, connection, projectRoot, projectConfi
|
|
|
19071
20858
|
connection: normalizedConnection,
|
|
19072
20859
|
};
|
|
19073
20860
|
}
|
|
20861
|
+
/**
|
|
20862
|
+
* The single connector boundary for Ask, Notebook, and artifact execution.
|
|
20863
|
+
* Callers compile their route-specific source first; this service owns target
|
|
20864
|
+
* preparation, parameters, execution, and result normalization after that.
|
|
20865
|
+
*/
|
|
20866
|
+
export class ExecutionService {
|
|
20867
|
+
host;
|
|
20868
|
+
constructor(host) {
|
|
20869
|
+
this.host = host;
|
|
20870
|
+
}
|
|
20871
|
+
async execute(input) {
|
|
20872
|
+
const preparation = await prepareAnalyticalExecutionSql({
|
|
20873
|
+
sql: input.sql,
|
|
20874
|
+
subject: input.subject,
|
|
20875
|
+
executor: this.host.executor,
|
|
20876
|
+
connection: input.connection,
|
|
20877
|
+
projectRoot: this.host.projectRoot,
|
|
20878
|
+
projectConfig: this.host.projectConfig(),
|
|
20879
|
+
enforceReadOnly: input.enforceReadOnly,
|
|
20880
|
+
rowLimit: input.rowLimit,
|
|
20881
|
+
});
|
|
20882
|
+
const raw = input.executePrepared
|
|
20883
|
+
? await input.executePrepared(preparation)
|
|
20884
|
+
: await this.host.executor.executeQuery(preparation.executedSql, input.sqlParams ?? [], runtimeVariables(input.variables ?? {}), preparation.connection);
|
|
20885
|
+
const result = normalizeQueryResult(raw, input.semanticRefs);
|
|
20886
|
+
return {
|
|
20887
|
+
preparation,
|
|
20888
|
+
result,
|
|
20889
|
+
compiledSqlFingerprint: executionFingerprint(preparation.executedSql),
|
|
20890
|
+
resultFingerprint: executionFingerprint(stableExecutionValue({
|
|
20891
|
+
columns: result.columns,
|
|
20892
|
+
rows: result.rows,
|
|
20893
|
+
rowCount: result.rowCount,
|
|
20894
|
+
})),
|
|
20895
|
+
};
|
|
20896
|
+
}
|
|
20897
|
+
}
|
|
19074
20898
|
/**
|
|
19075
20899
|
* One host-owned SQL preparation boundary shared by Ask and Notebook.
|
|
19076
20900
|
*
|
|
@@ -27485,26 +29309,25 @@ function notebookResearchContextPreview(contextPack) {
|
|
|
27485
29309
|
};
|
|
27486
29310
|
}
|
|
27487
29311
|
function normalizeNotebookAgentResult(result) {
|
|
27488
|
-
const
|
|
27489
|
-
|
|
27490
|
-
|
|
27491
|
-
|
|
27492
|
-
|
|
27493
|
-
|
|
27494
|
-
|
|
27495
|
-
|
|
27496
|
-
|
|
27497
|
-
|
|
27498
|
-
const rows = Array.isArray(result.rows)
|
|
27499
|
-
? result.rows
|
|
27500
|
-
.filter((row) => Boolean(row && typeof row === 'object' && !Array.isArray(row)))
|
|
27501
|
-
.map((row) => row)
|
|
27502
|
-
: [];
|
|
29312
|
+
const canonical = normalizeCanonicalQueryResult({
|
|
29313
|
+
columns: result.columns,
|
|
29314
|
+
rows: result.rows,
|
|
29315
|
+
rowCount: result.rowCount,
|
|
29316
|
+
executionTime: result.executionTime,
|
|
29317
|
+
resultFingerprint: result.resultFingerprint,
|
|
29318
|
+
executionReceipt: result.executionReceipt,
|
|
29319
|
+
trustState: result.executableArtifact?.trustState,
|
|
29320
|
+
answerTier: result.answerTier,
|
|
29321
|
+
});
|
|
27503
29322
|
return {
|
|
27504
|
-
columns,
|
|
27505
|
-
rows,
|
|
27506
|
-
rowCount:
|
|
27507
|
-
|
|
29323
|
+
columns: canonical.columns,
|
|
29324
|
+
rows: canonical.rows,
|
|
29325
|
+
rowCount: canonical.rowCount,
|
|
29326
|
+
resultFingerprint: canonical.resultFingerprint,
|
|
29327
|
+
executionTime: canonical.executionTime ?? 0,
|
|
29328
|
+
...(canonical.truncated ? { truncated: true } : {}),
|
|
29329
|
+
...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
|
|
29330
|
+
...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
|
|
27508
29331
|
};
|
|
27509
29332
|
}
|
|
27510
29333
|
function notebookResearchSummary(question, result, error) {
|