@duckcodeailabs/dql-cli 1.14.2 → 1.14.3-rc.1
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 +4 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +16 -0
- package/dist/args.js.map +1 -1
- package/dist/assets/dql-notebook/assets/{AgentLogPage-DKbGpRQS.js → AgentLogPage-Jbyb4so-.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-DPSu0Mly.js → AiBuildDialog-Du07X1qO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-1uaGnpi1.js → AiBuildResult-xKtGthWv.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiSidePanel-BwMREwa7.js → AiSidePanel-CE_L04dK.js} +1 -1
- package/dist/assets/dql-notebook/assets/AnalyticsHome-CiUE10uF.js +6 -0
- package/dist/assets/dql-notebook/assets/{AppsView-CM1tPywy.js → AppsView-DlNaTuwD.js} +30 -35
- package/dist/assets/dql-notebook/assets/AskObservabilityPage-DracZPI9.js +1 -0
- package/dist/assets/dql-notebook/assets/AskTracePage-CSMoMw9a.js +8 -0
- package/dist/assets/dql-notebook/assets/{BlockStudio--S6WFVO4.js → BlockStudio-DQS5Hcq9.js} +9 -14
- package/dist/assets/dql-notebook/assets/BusinessArtifactView-CqV4i9l_.js +1 -0
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-CNyU5MBX.js → DbtFirstModelingPage-JKhB_Y2H.js} +3 -3
- package/dist/assets/dql-notebook/assets/{GitPage-IcydAai3.js → GitPage-DjaNp0o3.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-CSKW-5eD.js → GlobalAiRail-CsV3zlc8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-CFen0eFT.js → GovernedContextPage-uXUHUOIJ.js} +3 -3
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-D0D9UCIz.js → HelpDocsPage-Hbh9IeYB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-CmSxapR3.js → HomePage-CRRl-R_v.js} +1 -1
- package/dist/assets/dql-notebook/assets/LineageDAG-OMUP2sbl.js +1 -0
- package/dist/assets/dql-notebook/assets/LineageDetailView-C964BLuV.js +1 -0
- package/dist/assets/dql-notebook/assets/LineageDrawer-CzY0nfpD.js +1 -0
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-CTZJp4_r.js → LineagePathBreadcrumb-0Gi-Fm1B.js} +1 -1
- package/dist/assets/dql-notebook/assets/MiniLineageGraph-wn0wLc9m.js +1 -0
- package/dist/assets/dql-notebook/assets/{NewBlockModal-DMBFB7nE.js → NewBlockModal-kMAzk91d.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-DsC9CWlW.js → NewNotebookModal-D_cWd_6M.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-hs-kw8v9.js → NotebookEditor-Bax0lxaz.js} +25 -30
- package/dist/assets/dql-notebook/assets/{ReadinessPage-DJ83cMik.js → ReadinessPage-DEUIXQQY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-B1Pu_Bvv.js → SetupOnboarding-CyE-Ex41.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SkillsPage-BHKDay8n.js → SkillsPage-QgvKGPxE.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-BkyGgob2.js → TrustBadge-D5CGp0cu.js} +1 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-BjsTE6yf.js +89 -0
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-DmNiuLQA.js → answer-to-notebook-tACdCsTV.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left--1rsrxm8.js → arrow-left-CF6wxXU5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-D5TdqY1G.js → arrow-right-C7G3M_S8.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-Bw7nHbzg.js → book-open-text-DDBO8G7L.js} +1 -1
- package/dist/assets/dql-notebook/assets/chevron-left-DxajrX2v.js +6 -0
- package/dist/assets/dql-notebook/assets/{circle-x-DLe6NNM4.js → circle-x-HBvXjnT6.js} +1 -1
- package/dist/assets/dql-notebook/assets/clock-3-CJIdsPLt.js +6 -0
- package/dist/assets/dql-notebook/assets/dagre.esm-B6nvU4OB.js +1 -0
- package/dist/assets/dql-notebook/assets/{external-link-C9Q97sA3.js → external-link-WciAU8Bu.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-CztvkIgo.js → grip-vertical-DVu7ok6x.js} +1 -1
- package/dist/assets/dql-notebook/assets/index-B_kaoARS.css +1 -0
- package/dist/assets/dql-notebook/assets/{index-zHHzDn6l.js → index-D3wnucQC.js} +133 -128
- package/dist/assets/dql-notebook/assets/{link-2-CiKAvumL.js → link-2-C6uQx50X.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-BtnP2nQ5.js → list-tree-DapsIuQB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-TSFGxcCP.js → minimize-2-BlVoipnT.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-BfXIUWy0.js → panel-right-open-DcnKWxfx.js} +1 -1
- package/dist/assets/dql-notebook/assets/{play-DVbSFJHD.js → play-CNuRuaAs.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-D_cesDcX.js → rotate-ccw-B2p953Zh.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-CoVStdYB.js → semantic-fields-2fWgkTJc.js} +1 -1
- package/dist/assets/dql-notebook/assets/{sliders-horizontal-l7xV9K5A.js → sliders-horizontal-DwRTu1w4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{star-CSBS0H3b.js → star-Cbaj4F3i.js} +1 -1
- package/dist/assets/dql-notebook/assets/style-NlN9F6JE.js +23 -0
- package/dist/assets/dql-notebook/assets/{triangle-alert-BTrnyY4q.js → triangle-alert-BPHvH1wA.js} +1 -1
- package/dist/assets/dql-notebook/assets/{upload-sySLq9zb.js → upload-DIqK0KE1.js} +1 -1
- package/dist/assets/dql-notebook/assets/{usePersistedAgentThreadId-CzwgGdus.js → usePersistedAgentThreadId-DpQXOEDw.js} +1 -1
- package/dist/assets/dql-notebook/assets/{user-round-ChlgXi9j.js → user-round-DukNupB_.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-CGw0ytyT.js → wand-sparkles-CAwLX0b9.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-C_RltiK5.js → workflow-bOT-NqdO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-D-wLfeu0.js → wrench-1m0_kcza.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-B28hIJIC.js → x-fp82BETM.js} +1 -1
- package/dist/assets/dql-notebook/index.html +2 -2
- package/dist/commands/agent-eval-runtime.d.ts +9 -0
- package/dist/commands/agent-eval-runtime.d.ts.map +1 -1
- package/dist/commands/agent-eval-runtime.js +7 -0
- package/dist/commands/agent-eval-runtime.js.map +1 -1
- package/dist/commands/agent-trace.d.ts +3 -0
- package/dist/commands/agent-trace.d.ts.map +1 -0
- package/dist/commands/agent-trace.js +169 -0
- package/dist/commands/agent-trace.js.map +1 -0
- package/dist/commands/agent.d.ts +28 -2
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +408 -35
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/notebook.d.ts +2 -0
- package/dist/commands/notebook.d.ts.map +1 -1
- package/dist/commands/notebook.js +4 -0
- package/dist/commands/notebook.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +759 -145
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +21 -2
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +256 -33
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +2844 -490
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/dist/providers/oauth/claude-oauth.d.ts.map +1 -1
- package/dist/providers/oauth/claude-oauth.js +52 -18
- package/dist/providers/oauth/claude-oauth.js.map +1 -1
- package/dist/providers/oauth/codex-oauth.d.ts.map +1 -1
- package/dist/providers/oauth/codex-oauth.js +72 -44
- package/dist/providers/oauth/codex-oauth.js.map +1 -1
- package/dist/providers/subscription-cli.d.ts +20 -0
- package/dist/providers/subscription-cli.d.ts.map +1 -1
- package/dist/providers/subscription-cli.js +69 -11
- package/dist/providers/subscription-cli.js.map +1 -1
- package/package.json +10 -10
- package/dist/assets/dql-notebook/assets/AnalyticsHome-BGfey_ve.js +0 -6
- package/dist/assets/dql-notebook/assets/BusinessArtifactView-BEAJ-yNW.js +0 -1
- package/dist/assets/dql-notebook/assets/LineageDAG-BGUcIt1B.js +0 -1
- package/dist/assets/dql-notebook/assets/LineageDetailView-Dx48Zfzb.js +0 -1
- package/dist/assets/dql-notebook/assets/LineageDrawer-zaBfSLZO.js +0 -1
- package/dist/assets/dql-notebook/assets/MiniLineageGraph-CdivNR1S.js +0 -1
- package/dist/assets/dql-notebook/assets/UnifiedAgentRunPanel-BYdrTaEW.js +0 -89
- package/dist/assets/dql-notebook/assets/dagre.esm-CW5QZdBt.js +0 -23
- package/dist/assets/dql-notebook/assets/index-B3shyZsg.css +0 -1
- /package/dist/assets/dql-notebook/assets/{dagre-BZV40eAE.css → style-BZV40eAE.css} +0 -0
package/dist/local-runtime.js
CHANGED
|
@@ -16,17 +16,17 @@ import { buildMixedSourceWarehouseFallbackSql, findMentionedNotebookDataset, pla
|
|
|
16
16
|
import { resolveNpmInvocation } from './npm-runtime.js';
|
|
17
17
|
import { ensureDqlGitignore, isGovernedSourceFile, isLegacyBroadDqlIgnore, } from './git-contract.js';
|
|
18
18
|
import { buildExecutionPlan, createWelcomeNotebook, deserializeNotebook, getConnectorFormSchemas, hasSemanticRefs, hasStandaloneSemanticRef, resolveSemanticRefs, } from '@duckcodeailabs/dql-notebook';
|
|
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
|
+
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, normalizeProviderEgressReceiptV1, relationshipValidationProofFingerprint, renderSemanticBlockSource, discoverDbtDomains, collectDbtRelationshipTests, renderDomainDeclaration, ProjectSnapshotService, analyzeSqlReferences, buildSqlAnalyticalSignature, buildGeneratedAnalyticalSqlSignature, buildSqlOutputExpressionSignature, semanticDimensionReference, semanticExecutionFingerprint, modelAreaLocalId, DEFAULT_MODEL_AREA_ID, } from '@duckcodeailabs/dql-core';
|
|
20
20
|
import { load as loadYaml } from 'js-yaml';
|
|
21
21
|
import { listBlockTemplates } from './block-templates.js';
|
|
22
22
|
import { getRunner as getLLMRunner } from './llm/index.js';
|
|
23
23
|
import { rethrowIfCancelled } from './llm/cancellation.js';
|
|
24
24
|
import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from './version-status.js';
|
|
25
25
|
import { resolveRetrievalHealthStatus } from './retrieval-health.js';
|
|
26
|
-
import { applyFinding, createResearchState, narrationMaxTokensForFacts, nextHypothesis,
|
|
26
|
+
import { applyFinding, createResearchState, narrationMaxTokensForFacts, nextHypothesis, synthesizeResearchNarrative, AgenticExecutionCapabilityGate, createAgenticSqlExecutionCapability, mintFinalSqlAuthorization, verifyAgenticSqlExecutionCapability, qualifyAuthorizationReferences, scopeContextPackToExploratoryCandidateClosure, validateFrozenRequiredOutputProjection, validateSqlAgainstLocalContext as validateAuthorizedSqlReferences, verifyFinalSql, attachAskTraceObserverV1, askTraceObserverForV1, } from '@duckcodeailabs/dql-agent';
|
|
27
27
|
import { applyEvalCassette, createDqlAgentProviderRunner, createEvalCassetteReplayProvider, createGovernedTextProvider, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
|
|
28
28
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
29
|
-
import { composeBusinessExplanation, ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildAggregationSafetyProof, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, previewSkillChange, buildContextAuthoringProposal, contextAuthoringDependencyClosure, FileContextAuthoringProposalStore, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, createAgentRunBudget, isProbeSafeColumn, deadlineScale, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, assertProviderPayloadAllowed, createProviderDispatchEgressReceipt, prepareProviderWireEnvelopeForDispatch, markProviderMetadataArray,
|
|
29
|
+
import { composeBusinessExplanation, ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildAggregationSafetyProof, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, normalizeWarehouseSqlFailure, parseProposal, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, AskTraceSqliteStoreV1, createAskTraceObserverV1, defaultAskTraceSqlitePath, createAskTracePortableBundleV1, 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, selectRoute, isProbeSafeColumn, deadlineScale, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, resolveInternalRelationIds, analyticalError, analyticalErrorDetail, tagAnalyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, assertProviderPayloadAllowed, createProviderDispatchEgressReceipt, prepareProviderWireEnvelopeForDispatch, markProviderMetadataArray, redactProviderResultRows, composeVerifiedAnalyticalNarrative, classifyProviderFailure, buildCoverageGap, capResearchBranches, buildResearchEvidenceLedger, buildResearchEvidenceLedgerV2, buildResearchHypothesisPlanV2, inferResearchValidatorKind, buildAnalyticalTurnPlan, buildAnalyticalRequirementSeedV1, buildAnalyticalRequirementSet, resolveTopRankedRegionDependency, DEFAULT_ASK_ROW_EGRESS_POLICY, ZERO_ROW_EGRESS_POLICY, resolveProviderResultRowEgressPolicy, normalizeCanonicalQueryResult, normalizeAnalyticalExecutionFingerprint, normalizeAnalyticalExecutionReceipt, createAgentRunCancellationError, isAgentRunUserCancellation, } from '@duckcodeailabs/dql-agent';
|
|
30
30
|
import { addSqlResultFilter, dashboardFilterableResultColumns, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
|
|
31
31
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
32
32
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization, } from './apps-api.js';
|
|
@@ -90,6 +90,56 @@ function hasDbtSemanticArtifacts(projectRoot, dbtProjectDir, configuredManifestP
|
|
|
90
90
|
}
|
|
91
91
|
return false;
|
|
92
92
|
}
|
|
93
|
+
/** One-time local capability lifetime. It is never persisted or sent to a provider. */
|
|
94
|
+
export const CLI_ASK_TRACE_CAPABILITY_TTL_MS = 30_000;
|
|
95
|
+
function isLoopbackRemoteAddress(value) {
|
|
96
|
+
return value === '127.0.0.1'
|
|
97
|
+
|| value === '::1'
|
|
98
|
+
|| value === '::ffff:127.0.0.1';
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Host-owned, one-shot attribution capabilities for an already-local runtime.
|
|
102
|
+
* A plain client header is never enough: it must be minted by this process,
|
|
103
|
+
* unexpired, scoped to AgentRun admission, and arrive over loopback.
|
|
104
|
+
*/
|
|
105
|
+
export function createLocalCliAskTraceCapabilityRegistryV1(options = {}) {
|
|
106
|
+
const records = new Map();
|
|
107
|
+
const now = options.now ?? Date.now;
|
|
108
|
+
const mint = options.mint ?? randomUUID;
|
|
109
|
+
const purge = (at) => {
|
|
110
|
+
for (const [capability, record] of records) {
|
|
111
|
+
if (record.expiresAtMs <= at)
|
|
112
|
+
records.delete(capability);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
issue(input = {}) {
|
|
117
|
+
const issuedAt = input.nowMs ?? now();
|
|
118
|
+
purge(issuedAt);
|
|
119
|
+
const capability = input.capability ?? mint();
|
|
120
|
+
const expiresAtMs = issuedAt + Math.max(1, Math.min(CLI_ASK_TRACE_CAPABILITY_TTL_MS, input.ttlMs ?? CLI_ASK_TRACE_CAPABILITY_TTL_MS));
|
|
121
|
+
records.set(capability, { expiresAtMs, scope: 'agent-runs' });
|
|
122
|
+
return {
|
|
123
|
+
capability,
|
|
124
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
125
|
+
scope: 'agent-runs',
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
consume(input) {
|
|
129
|
+
const at = input.nowMs ?? now();
|
|
130
|
+
purge(at);
|
|
131
|
+
if (!input.loopbackServer || !isLoopbackRemoteAddress(input.remoteAddress) || typeof input.capability !== 'string')
|
|
132
|
+
return undefined;
|
|
133
|
+
const record = records.get(input.capability);
|
|
134
|
+
if (!record || record.scope !== input.scope || record.expiresAtMs <= at)
|
|
135
|
+
return undefined;
|
|
136
|
+
// A capability represents one concrete request admission. Deleting it
|
|
137
|
+
// prevents copied local headers from relabelling later browser requests.
|
|
138
|
+
records.delete(input.capability);
|
|
139
|
+
return 'cli';
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
93
143
|
// Every member of `AgentRunRequestedMode`. The `Record` (rather than a bare
|
|
94
144
|
// Set literal) is deliberate: a `Set<AgentRunRequestedMode>` happily accepts a
|
|
95
145
|
// subset, which is how 'modeling' and 'skill' went missing here — the parser
|
|
@@ -172,6 +222,101 @@ function agentRunRecord(value) {
|
|
|
172
222
|
function agentRunString(value) {
|
|
173
223
|
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
174
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Catalog previews are display-only run-store joins; the trace store and
|
|
227
|
+
* strict exports remain prompt-free. Returning a partially redacted arbitrary
|
|
228
|
+
* prompt is not safe: member values, SQL literals, URLs, filesystem paths, and
|
|
229
|
+
* secrets are all meaningful diagnostics in an analytics system. Therefore a
|
|
230
|
+
* preview is optional and all-or-nothing: it is returned only for a short
|
|
231
|
+
* generic analytic question composed entirely of this deliberately small
|
|
232
|
+
* vocabulary. Every other question is represented by its typed scenario label.
|
|
233
|
+
*/
|
|
234
|
+
const ASK_TRACE_PREVIEW_MAX_CHARS = 160;
|
|
235
|
+
const ASK_TRACE_PREVIEW_ALLOWED_WORDS = new Set([
|
|
236
|
+
'a', 'account', 'accounts', 'all', 'amount', 'an', 'and', 'are', 'average',
|
|
237
|
+
'by', 'categories', 'category', 'compare', 'count', 'current', 'customer',
|
|
238
|
+
'customers', 'daily', 'data', 'date', 'dates', 'dimension', 'dimensions',
|
|
239
|
+
'fiscal', 'for', 'from', 'growth', 'have', 'highest', 'how', 'is', 'last',
|
|
240
|
+
'lowest', 'me', 'metric', 'metrics', 'month', 'monthly', 'of', 'order',
|
|
241
|
+
'orders', 'our', 'previous', 'product', 'products', 'quarter', 'quarterly',
|
|
242
|
+
'region', 'regions', 'result', 'results', 'revenue', 'sales', 'show',
|
|
243
|
+
'spend', 'the', 'their', 'these', 'this', 'to', 'top', 'total', 'trend',
|
|
244
|
+
'trends', 'what', 'when', 'where', 'which', 'who', 'why', 'with', 'year',
|
|
245
|
+
]);
|
|
246
|
+
const ASK_TRACE_PREVIEW_SENSITIVE_PATTERNS = [
|
|
247
|
+
/\b(?:select|insert|update|delete|drop|alter|create|grant|revoke|truncate|merge|with)\b/i,
|
|
248
|
+
/(?:--|\/\*|\*\/|;)/,
|
|
249
|
+
/(?:https?|ftp):\/\/|\bwww\./i,
|
|
250
|
+
/(?:^|[\s"'`])(?:~\/|\/(?:Users|home|var|tmp|private|etc|opt|Volumes)\/|[A-Za-z]:[\\/])/,
|
|
251
|
+
/\b\d{3}-\d{2}-\d{4}\b/,
|
|
252
|
+
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
|
|
253
|
+
/(?:\+?\d[\d().\-\s]{7,}\d)/,
|
|
254
|
+
/\b(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|authorization|password|secret|client[_ -]?secret|private[_ -]?key|session|cookie)\b/i,
|
|
255
|
+
/\bbearer\s+[a-z0-9._~+\/-]{8,}/i,
|
|
256
|
+
/\b(?:sk|pk|rk)_[a-z0-9_-]{8,}\b/i,
|
|
257
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{12,}\b/,
|
|
258
|
+
/\bgh[pousr]_[A-Za-z0-9]{16,}\b/,
|
|
259
|
+
/\beyJ[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
|
|
260
|
+
];
|
|
261
|
+
export function askTraceQuestionPreview(question) {
|
|
262
|
+
const normalized = question.replace(/\s+/g, ' ').trim();
|
|
263
|
+
if (!normalized || normalized.length > ASK_TRACE_PREVIEW_MAX_CHARS)
|
|
264
|
+
return undefined;
|
|
265
|
+
if (ASK_TRACE_PREVIEW_SENSITIVE_PATTERNS.some((pattern) => pattern.test(normalized)))
|
|
266
|
+
return undefined;
|
|
267
|
+
// Do not attempt to preserve literals, values, handles, or unclassified
|
|
268
|
+
// business nouns. The catalog can still identify its typed scenario without
|
|
269
|
+
// exposing those inputs.
|
|
270
|
+
if (!/^[A-Za-z ?-]+$/.test(normalized))
|
|
271
|
+
return undefined;
|
|
272
|
+
const words = normalized.toLowerCase().match(/[a-z]+/g);
|
|
273
|
+
if (!words?.length || words.some((word) => !ASK_TRACE_PREVIEW_ALLOWED_WORDS.has(word)))
|
|
274
|
+
return undefined;
|
|
275
|
+
return normalized;
|
|
276
|
+
}
|
|
277
|
+
function askTraceScenarioLabel(run) {
|
|
278
|
+
if (run.requestedMode === 'research' || run.route === 'research')
|
|
279
|
+
return 'Research';
|
|
280
|
+
const labels = {
|
|
281
|
+
certified_answer: 'Certified answer',
|
|
282
|
+
semantic_answer: 'Semantic answer',
|
|
283
|
+
generated_answer: 'Review-required SQL',
|
|
284
|
+
clarify: 'Clarification',
|
|
285
|
+
blocked: 'Blocked',
|
|
286
|
+
conversation: 'Conversation',
|
|
287
|
+
};
|
|
288
|
+
return labels[run.route] ?? 'Ask run';
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The notebook has exactly two trace client routes. Decode only the single
|
|
292
|
+
* detail segment for validation so a valid encoded run id (for example
|
|
293
|
+
* `run%3Aoffice-42`) reaches the SPA, while encoded slashes, malformed escapes,
|
|
294
|
+
* and nested paths stay ordinary 404s.
|
|
295
|
+
*/
|
|
296
|
+
export function isAskTraceClientDetailPath(pathname) {
|
|
297
|
+
const prefix = '/ask/traces/';
|
|
298
|
+
if (!pathname.startsWith(prefix))
|
|
299
|
+
return false;
|
|
300
|
+
const encodedRunId = pathname.slice(prefix.length);
|
|
301
|
+
if (!encodedRunId || encodedRunId.includes('/'))
|
|
302
|
+
return false;
|
|
303
|
+
try {
|
|
304
|
+
const runId = decodeURIComponent(encodedRunId);
|
|
305
|
+
return /^[0-9A-Za-z][0-9A-Za-z:_-]{0,255}$/.test(runId);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Ask owns only these client-side routes. Keep the static fallback explicit so
|
|
313
|
+
* a notebook reload works while an arbitrary missing path remains a real 404.
|
|
314
|
+
*/
|
|
315
|
+
export function isAskClientRoutePath(pathname) {
|
|
316
|
+
return pathname === '/ask'
|
|
317
|
+
|| pathname === '/ask/traces'
|
|
318
|
+
|| isAskTraceClientDetailPath(pathname);
|
|
319
|
+
}
|
|
175
320
|
/** UI catalog fallback labels are not declared project domains. */
|
|
176
321
|
/**
|
|
177
322
|
* Resolve a UI-pinned domain scope, tolerating one that no longer exists.
|
|
@@ -452,23 +597,20 @@ export async function scheduleCompoundAnalyticalTasks(input) {
|
|
|
452
597
|
/**
|
|
453
598
|
* Decide how a settled answer gets its business-facing prose.
|
|
454
599
|
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
* Certification, a DQL artifact, and the exploratory candidate are no longer
|
|
462
|
-
* vetoes. EXP-001's grain concern is real, but the answer to "the model might
|
|
463
|
-
* relabel an entity-level measure" is to VERIFY the claims against the fact set
|
|
464
|
-
* (`verified_facts`) and pass the grain statement in as a caveat — not to refuse
|
|
465
|
-
* to write a sentence.
|
|
600
|
+
* An ordinary Ask already spends its one permitted model call resolving meaning.
|
|
601
|
+
* Its settled answer is the deterministic, receipt-bound answer produced by the
|
|
602
|
+
* selected analytical tier; sending result rows to a second narrator would add
|
|
603
|
+
* a hidden provider phase, change the egress receipt, and make a governed
|
|
604
|
+
* semantic run look like Research. Only an explicit Research request may run
|
|
605
|
+
* this optional, fact-checked narration stage.
|
|
466
606
|
*/
|
|
467
607
|
export function planAgentRunNarration(governedAnswer, context) {
|
|
468
608
|
// A refusal is not narrated. It owes the user a reason or a clarifying
|
|
469
609
|
// question, which is the clarification lane's job, not the narrator's.
|
|
470
610
|
if (governedAnswer.kind === 'no_answer')
|
|
471
611
|
return { mode: 'skip', reason: 'no_answer' };
|
|
612
|
+
if (context.requestedMode !== 'research')
|
|
613
|
+
return { mode: 'skip', reason: 'ordinary_ask' };
|
|
472
614
|
if (!context.providerAvailable)
|
|
473
615
|
return { mode: 'skip', reason: 'no_provider' };
|
|
474
616
|
const maxRows = Math.max(0, context.rowEgress.maxNarrationRows);
|
|
@@ -518,6 +660,54 @@ export function trustStateForAgentAnswer(answer) {
|
|
|
518
660
|
return 'certified';
|
|
519
661
|
return semanticAnswerHasPassedAggregationProof(answer) ? 'governed' : 'review_required';
|
|
520
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* Persisted result payloads outlive the in-memory answer object, so they need
|
|
665
|
+
* the same trust gate as the visible Ask card. In particular, an exploratory
|
|
666
|
+
* result used to inherit the old `governed` fallback merely because it had
|
|
667
|
+
* rows. Completion is not governance: only an exact certified answer or a
|
|
668
|
+
* semantic answer carrying its aggregation proof may be persisted as governed.
|
|
669
|
+
*
|
|
670
|
+
* The `answer` overload is the normal Ask path and therefore has the proof.
|
|
671
|
+
* Generic notebook/result callers deliberately fail closed: route/status words
|
|
672
|
+
* such as `ai_generated`, `draft_ready`, and `analyst_review_required` are all
|
|
673
|
+
* review-required, and a bare legacy `governed` string never manufactures
|
|
674
|
+
* proof after the fact.
|
|
675
|
+
*/
|
|
676
|
+
function canonicalPersistedTrustState(input) {
|
|
677
|
+
if (input.answer) {
|
|
678
|
+
return input.answer.kind === 'no_answer'
|
|
679
|
+
? 'blocked'
|
|
680
|
+
: trustStateForAgentAnswer(input.answer);
|
|
681
|
+
}
|
|
682
|
+
const rawTrust = typeof input.trustState === 'string' ? input.trustState.trim().toLowerCase() : '';
|
|
683
|
+
const tier = typeof input.answerTier === 'string' ? input.answerTier.trim().toLowerCase() : '';
|
|
684
|
+
// These state names describe generated or analyst-review workflows, never a
|
|
685
|
+
// governed proof. Keep this list explicit so a newly added presentation
|
|
686
|
+
// label cannot silently gain governed trust through the old fallback.
|
|
687
|
+
if (rawTrust === 'ai_generated'
|
|
688
|
+
|| rawTrust === 'draft_ready'
|
|
689
|
+
|| rawTrust === 'analyst_review_required'
|
|
690
|
+
|| rawTrust === 'exploratory'
|
|
691
|
+
|| tier === 'exploratory_sql'
|
|
692
|
+
|| tier === 'generated_sql'
|
|
693
|
+
|| tier === 'ai_generated'
|
|
694
|
+
|| tier === 'draft_ready'
|
|
695
|
+
|| tier === 'analyst_review_required')
|
|
696
|
+
return 'review_required';
|
|
697
|
+
if (rawTrust === 'blocked')
|
|
698
|
+
return 'blocked';
|
|
699
|
+
if (rawTrust === 'not_applicable')
|
|
700
|
+
return 'not_applicable';
|
|
701
|
+
if (rawTrust === 'review_required' || rawTrust === 'grounded')
|
|
702
|
+
return rawTrust;
|
|
703
|
+
// A bare persisted `governed` label (or an unqualified `certified` label)
|
|
704
|
+
// lacks the in-memory certified block / semantic aggregation proof. It is
|
|
705
|
+
// safe to display as review-required until a current Ask answer supplies
|
|
706
|
+
// that proof above.
|
|
707
|
+
if (rawTrust === 'governed' || rawTrust === 'certified')
|
|
708
|
+
return 'review_required';
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
521
711
|
const TRUST_RANK = {
|
|
522
712
|
certified: 3,
|
|
523
713
|
governed: 2,
|
|
@@ -1387,12 +1577,59 @@ function apiErrorMessage(error) {
|
|
|
1387
1577
|
*/
|
|
1388
1578
|
const ASSUMED_PROVIDER_DISPATCH_MS = 12_000;
|
|
1389
1579
|
const DISPATCH_SETTLE_MARGIN_MS = 4_000;
|
|
1580
|
+
/**
|
|
1581
|
+
* The one runtime authority for provider-send caps.
|
|
1582
|
+
*
|
|
1583
|
+
* Research is limited to twelve physical sends total, one planner, and one
|
|
1584
|
+
* narrator. Ordinary generated lookup normally gets two sends across
|
|
1585
|
+
* candidate-ID meaning and planning/generation. A router-frozen bounded
|
|
1586
|
+
* exploratory plan may use one additional, same-plan provider correction only
|
|
1587
|
+
* after the generation response declines SQL. That third send is a `repair`,
|
|
1588
|
+
* not an LLM replan: it retains the frozen snapshot, target, closure, output
|
|
1589
|
+
* tuple, and route. Classification is a separate legacy/no-evidence phase,
|
|
1590
|
+
* but still consumes the total cap and cannot coexist with candidate-ID
|
|
1591
|
+
* meaning resolution in the ledger.
|
|
1592
|
+
*/
|
|
1593
|
+
export function agentRunProviderDispatchBudgetForMode(requestedMode) {
|
|
1594
|
+
if (requestedMode === 'research') {
|
|
1595
|
+
return {
|
|
1596
|
+
total: 12,
|
|
1597
|
+
classification: 1,
|
|
1598
|
+
// A bounded Research root may plan up to six independently routed
|
|
1599
|
+
// hypothesis children. Each child can make one candidate-ID meaning
|
|
1600
|
+
// resolution, all of which remain charged to this same twelve-send
|
|
1601
|
+
// ledger. Ordinary Ask deliberately remains one below.
|
|
1602
|
+
meaningResolution: 6,
|
|
1603
|
+
planning: 1,
|
|
1604
|
+
// One planner plus at most eight generation/tool sends. Together with
|
|
1605
|
+
// one meaning, one narrator, and one repair this cannot exceed twelve.
|
|
1606
|
+
generationGroup: 9,
|
|
1607
|
+
narration: 1,
|
|
1608
|
+
repair: 1,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
return {
|
|
1612
|
+
// One interpretation (candidate-ID meaning OR legacy classification), one
|
|
1613
|
+
// planning/generation transport, and exactly one eligible frozen-plan
|
|
1614
|
+
// correction. The phase-specific limits below keep the exceptional third
|
|
1615
|
+
// attempt from becoming a general Ask retry budget.
|
|
1616
|
+
total: 3,
|
|
1617
|
+
classification: 1,
|
|
1618
|
+
meaningResolution: 1,
|
|
1619
|
+
planning: 1,
|
|
1620
|
+
generationGroup: 1,
|
|
1621
|
+
narration: 0,
|
|
1622
|
+
repair: 1,
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1390
1625
|
export class RunScopedProviderDispatchEvidence {
|
|
1391
1626
|
policy;
|
|
1392
1627
|
runBudget;
|
|
1393
1628
|
rowEgress;
|
|
1394
1629
|
receipts = [];
|
|
1395
1630
|
phaseCounts = new Map();
|
|
1631
|
+
/** At most one physical same-provider transient retry may be admitted per run. */
|
|
1632
|
+
retryCount = 0;
|
|
1396
1633
|
currentRoute;
|
|
1397
1634
|
/** Wall-clock start of the previous dispatch, used to learn this provider's cost. */
|
|
1398
1635
|
lastDispatchStartedAtMs;
|
|
@@ -1443,7 +1680,9 @@ export class RunScopedProviderDispatchEvidence {
|
|
|
1443
1680
|
return !this.runBudget || this.runBudget.mayStartDiscovery(this.currentRoute);
|
|
1444
1681
|
}
|
|
1445
1682
|
observe(event, context) {
|
|
1446
|
-
const
|
|
1683
|
+
const isInterpretationPhase = context.dispatchPhase === 'classification'
|
|
1684
|
+
|| context.dispatchPhase === 'meaning_resolution';
|
|
1685
|
+
const softRoute = isInterpretationPhase ? 'clarify' : this.currentRoute;
|
|
1447
1686
|
// Admission control BEFORE the phase targets: starting a call that the hard
|
|
1448
1687
|
// deadline will kill mid-flight wastes the remaining budget and ends the run
|
|
1449
1688
|
// with nothing. Stopping here lets the caller answer from what it has.
|
|
@@ -1460,40 +1699,77 @@ export class RunScopedProviderDispatchEvidence {
|
|
|
1460
1699
|
else if (this.runBudget && !this.runBudget.mayStartDiscovery(softRoute)) {
|
|
1461
1700
|
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' });
|
|
1462
1701
|
}
|
|
1463
|
-
this.recordDispatchStart();
|
|
1464
1702
|
if (this.receipts.length >= this.policy.total) {
|
|
1465
1703
|
throw Object.assign(new Error(`Run-wide provider dispatch budget exhausted after ${this.policy.total} physical attempts.`), { code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED' });
|
|
1466
1704
|
}
|
|
1705
|
+
const retryOfAttemptIndex = context.retryOfAttemptIndex;
|
|
1706
|
+
const retryRequested = retryOfAttemptIndex !== undefined;
|
|
1707
|
+
const retryParent = retryRequested
|
|
1708
|
+
? this.receipts.find((receipt) => (receipt.provider === event.provider
|
|
1709
|
+
&& receipt.dispatchPhase === context.dispatchPhase
|
|
1710
|
+
&& receipt.purpose === context.purpose
|
|
1711
|
+
&& receipt.attemptIndex === retryOfAttemptIndex))
|
|
1712
|
+
: undefined;
|
|
1713
|
+
// A retry is transport recovery, not another generation, repair, or route
|
|
1714
|
+
// decision. It must be tied to the same admitted physical provider phase,
|
|
1715
|
+
// and the frozen exploratory repair lane is deliberately excluded so its
|
|
1716
|
+
// one reserved repair transport cannot be spent by a network retry.
|
|
1717
|
+
if (retryRequested && (!Number.isInteger(retryOfAttemptIndex)
|
|
1718
|
+
|| retryOfAttemptIndex < 1
|
|
1719
|
+
|| !retryParent
|
|
1720
|
+
|| this.retryCount >= 1
|
|
1721
|
+
|| context.dispatchPhase === 'repair')) {
|
|
1722
|
+
throw Object.assign(new Error('A provider retry must be the one permitted same-provider retry of an admitted non-repair physical attempt.'), { code: 'PROVIDER_DISPATCH_RETRY_NOT_ALLOWED' });
|
|
1723
|
+
}
|
|
1724
|
+
const admittedTransientRetry = retryRequested && Boolean(retryParent);
|
|
1467
1725
|
const phaseCount = this.phaseCounts.get(context.dispatchPhase) ?? 0;
|
|
1468
|
-
const phaseLimit = context.dispatchPhase === '
|
|
1469
|
-
? this.policy.
|
|
1470
|
-
: context.dispatchPhase === '
|
|
1471
|
-
? this.policy.
|
|
1472
|
-
: context.dispatchPhase === '
|
|
1473
|
-
? this.policy.
|
|
1474
|
-
:
|
|
1726
|
+
const phaseLimit = context.dispatchPhase === 'classification'
|
|
1727
|
+
? (this.policy.classification ?? 1)
|
|
1728
|
+
: context.dispatchPhase === 'meaning_resolution'
|
|
1729
|
+
? this.policy.meaningResolution
|
|
1730
|
+
: context.dispatchPhase === 'planning'
|
|
1731
|
+
? (this.policy.planning ?? this.policy.generationGroup)
|
|
1732
|
+
: context.dispatchPhase === 'repair'
|
|
1733
|
+
? this.policy.repair
|
|
1734
|
+
: context.dispatchPhase === 'narration'
|
|
1735
|
+
? this.policy.narration
|
|
1736
|
+
: this.policy.generationGroup;
|
|
1737
|
+
// Category-only classification is a legacy/no-evidence fallback, not an
|
|
1738
|
+
// alternate route into analytical binding. A run must record one or the
|
|
1739
|
+
// other: allowing both would turn an opaque category prompt into a false
|
|
1740
|
+
// claim that candidate IDs were resolved.
|
|
1741
|
+
const conflictingInterpretationPhase = isInterpretationPhase
|
|
1742
|
+
&& this.receipts.some((receipt) => (receipt.dispatchPhase === 'classification'
|
|
1743
|
+
|| receipt.dispatchPhase === 'meaning_resolution') && receipt.dispatchPhase !== context.dispatchPhase);
|
|
1744
|
+
if (conflictingInterpretationPhase) {
|
|
1745
|
+
throw Object.assign(new Error('The legacy category classifier and candidate-ID meaning resolution cannot both dispatch in one Ask run.'), { code: 'PROVIDER_INTERPRETATION_PHASE_CONFLICT' });
|
|
1746
|
+
}
|
|
1475
1747
|
// Planning and generation share one ledger because they compete for the
|
|
1476
1748
|
// same "work out the query" budget. Narration does not: it is the step that
|
|
1477
1749
|
// turns a settled result into an answer, and starving it produces a run
|
|
1478
1750
|
// that computed the right numbers and then could not say them.
|
|
1479
1751
|
const generationGroupCount = this.receipts.filter((receipt) => receipt.dispatchPhase === 'planning'
|
|
1480
1752
|
|| receipt.dispatchPhase === 'generation').length;
|
|
1481
|
-
if (phaseCount >= phaseLimit || ((context.dispatchPhase === 'planning' || context.dispatchPhase === 'generation')
|
|
1482
|
-
&& generationGroupCount >= this.policy.generationGroup)) {
|
|
1753
|
+
if (!admittedTransientRetry && (phaseCount >= phaseLimit || ((context.dispatchPhase === 'planning' || context.dispatchPhase === 'generation')
|
|
1754
|
+
&& generationGroupCount >= this.policy.generationGroup))) {
|
|
1483
1755
|
throw Object.assign(new Error(`Provider dispatch budget exhausted for ${context.dispatchPhase} after ${phaseLimit} physical attempts.`), { code: 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED' });
|
|
1484
1756
|
}
|
|
1485
1757
|
const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
|
|
1486
1758
|
const projectedRowCount = context.serializedResultShape?.resultRowCount ?? 0;
|
|
1487
1759
|
const projectedCumulativeRowCount = context.cumulativeResultRowCount ?? projectedRowCount;
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
|
|
1760
|
+
// The resolver is the single privacy authority. A legacy
|
|
1761
|
+
// `answer_narration` receipt can remain content-free for backwards
|
|
1762
|
+
// readability, but it can never disclose result rows. Research narration
|
|
1763
|
+
// requires both the explicit per-run consent and authority minted by that
|
|
1764
|
+
// resolver; a hand-built positive limit is not enough.
|
|
1765
|
+
const researchNarrationRowsAuthorized = context.purpose === 'research_narration'
|
|
1766
|
+
&& context.optIn
|
|
1767
|
+
&& this.rowEgress.resultRowAuthority === 'research_run_opt_in';
|
|
1768
|
+
const permittedRowLimit = researchNarrationRowsAuthorized
|
|
1491
1769
|
? this.rowEgress.maxNarrationRows
|
|
1492
|
-
: context.optIn && context.purpose === '
|
|
1493
|
-
?
|
|
1494
|
-
:
|
|
1495
|
-
? 200
|
|
1496
|
-
: 0;
|
|
1770
|
+
: context.optIn && context.purpose === 'research_tool'
|
|
1771
|
+
? this.rowEgress.maxToolRows
|
|
1772
|
+
: 0;
|
|
1497
1773
|
if (projectedRowCount > permittedRowLimit || projectedCumulativeRowCount > permittedRowLimit) {
|
|
1498
1774
|
throw Object.assign(new Error(permittedRowLimit === 0
|
|
1499
1775
|
? 'Provider egress blocked result rows without explicit Research run consent.'
|
|
@@ -1505,6 +1781,11 @@ export class RunScopedProviderDispatchEvidence {
|
|
|
1505
1781
|
purpose: context.purpose,
|
|
1506
1782
|
});
|
|
1507
1783
|
const rowCount = projectedRowCount;
|
|
1784
|
+
// A rejected admission is not a physical send. Do not let a rejected
|
|
1785
|
+
// egress/budget guard teach the run budget that the provider was slow, or
|
|
1786
|
+
// consume a receipt/phase count that a later trace would present as a
|
|
1787
|
+
// completed attempt.
|
|
1788
|
+
this.recordDispatchStart();
|
|
1508
1789
|
this.receipts.push(createProviderDispatchEgressReceipt({
|
|
1509
1790
|
purpose: context.purpose,
|
|
1510
1791
|
dispatchPhase: context.dispatchPhase,
|
|
@@ -1512,6 +1793,7 @@ export class RunScopedProviderDispatchEvidence {
|
|
|
1512
1793
|
...(event.model ? { model: event.model } : {}),
|
|
1513
1794
|
operation: event.operation,
|
|
1514
1795
|
attemptIndex: event.attemptIndex,
|
|
1796
|
+
...(retryOfAttemptIndex !== undefined ? { retryOfAttemptIndex } : {}),
|
|
1515
1797
|
options: event.options,
|
|
1516
1798
|
permittedCategories: rowCount > 0
|
|
1517
1799
|
? ['instructions', 'question', 'schema_metadata', 'governed_context', 'result_rows']
|
|
@@ -1523,7 +1805,13 @@ export class RunScopedProviderDispatchEvidence {
|
|
|
1523
1805
|
? { cumulativeResultRowCount: context.cumulativeResultRowCount }
|
|
1524
1806
|
: {}),
|
|
1525
1807
|
}));
|
|
1808
|
+
// Admission accounting belongs to this ledger only. The Ask provider
|
|
1809
|
+
// wrapper owns trace spans because it is the only boundary that observes
|
|
1810
|
+
// the matching HTTP completion/failure. Recording `ok` here would turn an
|
|
1811
|
+
// admitted-but-failed physical send into a false provider success.
|
|
1526
1812
|
this.phaseCounts.set(context.dispatchPhase, phaseCount + 1);
|
|
1813
|
+
if (admittedTransientRetry)
|
|
1814
|
+
this.retryCount += 1;
|
|
1527
1815
|
return envelope;
|
|
1528
1816
|
}
|
|
1529
1817
|
snapshot(fallbackReason = 'none') {
|
|
@@ -1538,6 +1826,327 @@ export class RunScopedProviderDispatchEvidence {
|
|
|
1538
1826
|
}
|
|
1539
1827
|
}
|
|
1540
1828
|
const agentRunProviderEvidenceContext = new AsyncLocalStorage();
|
|
1829
|
+
/**
|
|
1830
|
+
* Request-local trace context. It is intentionally independent of provider
|
|
1831
|
+
* accounting: tracing cannot admit a dispatch, consume a budget, or change a
|
|
1832
|
+
* connector call. Nested host callbacks can only append typed observations.
|
|
1833
|
+
*/
|
|
1834
|
+
const agentRunAskTraceContext = new AsyncLocalStorage();
|
|
1835
|
+
function activeAskTraceObserver() {
|
|
1836
|
+
const observer = agentRunAskTraceContext.getStore();
|
|
1837
|
+
return observer?.enabled ? observer : undefined;
|
|
1838
|
+
}
|
|
1839
|
+
function runtimeTraceFingerprint(value) {
|
|
1840
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
1841
|
+
}
|
|
1842
|
+
function providerTracePhase(phase) {
|
|
1843
|
+
switch (phase) {
|
|
1844
|
+
case 'classification': return 'classification';
|
|
1845
|
+
case 'meaning_resolution': return 'meaning_resolution';
|
|
1846
|
+
case 'planning': return 'planning';
|
|
1847
|
+
case 'generation': return 'generation';
|
|
1848
|
+
case 'repair': return 'repair';
|
|
1849
|
+
case 'narration': return 'narration';
|
|
1850
|
+
default: return 'unknown';
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Attach one physical provider transport to the current redacted Ask trace.
|
|
1855
|
+
* The caller remains the authority for admission and egress; this wrapper only
|
|
1856
|
+
* records the same physical send/settlement with its server-owned phase and
|
|
1857
|
+
* purpose. It is intentionally reusable for meaning and Research narration
|
|
1858
|
+
* so trace provider-attempt counts cannot drift from egress receipts.
|
|
1859
|
+
*/
|
|
1860
|
+
/**
|
|
1861
|
+
* @internal Exported solely for the local runtime boundary harness. It is not
|
|
1862
|
+
* an HTTP or durable API: production callers use it to pair the one provider
|
|
1863
|
+
* transport with its same-run trace span and egress receipt.
|
|
1864
|
+
*/
|
|
1865
|
+
export function createProviderDispatchTrace(input) {
|
|
1866
|
+
const observer = input.observer;
|
|
1867
|
+
const pending = new Map();
|
|
1868
|
+
// A transport failure is reported by `onProviderDispatchComplete` and removes
|
|
1869
|
+
// its pending entry before the outer promise rejects. Keep the number of
|
|
1870
|
+
// observed provider boundaries independent of `pending`: a completed
|
|
1871
|
+
// transport failure and a pre-send denial both already have a span, so
|
|
1872
|
+
// `settle(error)` must not manufacture a second synthetic attempt.
|
|
1873
|
+
let observedBoundaryCount = 0;
|
|
1874
|
+
const deniedKeys = new Set();
|
|
1875
|
+
// Some provider adapters emit a rejection notification after reporting the
|
|
1876
|
+
// same HTTP failure through completion. Once admission succeeded, that
|
|
1877
|
+
// notification is not a second denied send; it is the same physical
|
|
1878
|
+
// attempted transport and must retain its admitted/error span only.
|
|
1879
|
+
const admittedKeys = new Set();
|
|
1880
|
+
const key = (event) => `${event.provider}:${event.operation}:${event.attemptIndex}`;
|
|
1881
|
+
const diagnostic = (event, error) => classifyProviderFailure({
|
|
1882
|
+
message: error instanceof Error ? error.message : String(error ?? 'provider completion failed'),
|
|
1883
|
+
code: error && typeof error === 'object' ? String(error.code ?? '') : undefined,
|
|
1884
|
+
phase: providerTracePhase(input.phase),
|
|
1885
|
+
...(event?.provider ? { providerFingerprint: runtimeTraceFingerprint(event.provider) } : {}),
|
|
1886
|
+
...(event?.model ? { modelFingerprint: runtimeTraceFingerprint(event.model) } : {}),
|
|
1887
|
+
});
|
|
1888
|
+
const start = (event, admission, failure) => {
|
|
1889
|
+
observedBoundaryCount += 1;
|
|
1890
|
+
const failureDiagnostic = failure === undefined ? undefined : diagnostic(event, failure);
|
|
1891
|
+
const attempt = {
|
|
1892
|
+
version: 1,
|
|
1893
|
+
phase: providerTracePhase(input.phase),
|
|
1894
|
+
purpose: input.purpose,
|
|
1895
|
+
physicalAttemptIndex: event.attemptIndex,
|
|
1896
|
+
providerFingerprint: runtimeTraceFingerprint(event.provider),
|
|
1897
|
+
...(event.model ? { modelFingerprint: runtimeTraceFingerprint(event.model) } : {}),
|
|
1898
|
+
readiness: admission === 'admitted' ? 'ready' : 'unknown',
|
|
1899
|
+
admission,
|
|
1900
|
+
...(failureDiagnostic?.httpStatusClass ? { httpStatusClass: failureDiagnostic.httpStatusClass } : {}),
|
|
1901
|
+
...(failureDiagnostic?.retryable !== undefined ? { retryable: failureDiagnostic.retryable } : {}),
|
|
1902
|
+
...(failureDiagnostic?.safeAction ? { safeAction: failureDiagnostic.safeAction } : {}),
|
|
1903
|
+
...(failureDiagnostic?.cause ? { cause: failureDiagnostic.cause } : {}),
|
|
1904
|
+
provenance: 'live',
|
|
1905
|
+
};
|
|
1906
|
+
const spanId = observer?.startSpan({
|
|
1907
|
+
name: 'provider.attempt',
|
|
1908
|
+
stage: 'provider',
|
|
1909
|
+
reasonCode: failureDiagnostic ? 'provider_failure' : 'started',
|
|
1910
|
+
payload: { kind: 'provider', attempt },
|
|
1911
|
+
});
|
|
1912
|
+
return { spanId, provider: event.provider, ...(event.model ? { model: event.model } : {}), attempt: event.attemptIndex };
|
|
1913
|
+
};
|
|
1914
|
+
const finish = (entry, outcome, error, httpStatus) => {
|
|
1915
|
+
if (!entry.spanId)
|
|
1916
|
+
return;
|
|
1917
|
+
if (outcome === 'ok') {
|
|
1918
|
+
observer?.finishSpan(entry.spanId, { outcome: 'ok', reasonCode: 'completed' });
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const failureDiagnostic = diagnostic({ provider: entry.provider, ...(entry.model ? { model: entry.model } : {}) }, error ?? (typeof httpStatus === 'number'
|
|
1922
|
+
? Object.assign(new Error(`HTTP ${httpStatus}`), { code: `HTTP_${httpStatus}` })
|
|
1923
|
+
: undefined));
|
|
1924
|
+
observer?.finishSpan(entry.spanId, {
|
|
1925
|
+
outcome: outcome === 'cancelled' ? 'cancelled' : 'error',
|
|
1926
|
+
reasonCode: outcome === 'cancelled' ? 'cancelled' : 'provider_failure',
|
|
1927
|
+
payload: {
|
|
1928
|
+
kind: 'provider',
|
|
1929
|
+
attempt: {
|
|
1930
|
+
version: 1,
|
|
1931
|
+
phase: providerTracePhase(input.phase),
|
|
1932
|
+
purpose: input.purpose,
|
|
1933
|
+
physicalAttemptIndex: entry.attempt,
|
|
1934
|
+
providerFingerprint: runtimeTraceFingerprint(entry.provider),
|
|
1935
|
+
...(entry.model ? { modelFingerprint: runtimeTraceFingerprint(entry.model) } : {}),
|
|
1936
|
+
readiness: 'ready',
|
|
1937
|
+
admission: 'admitted',
|
|
1938
|
+
...(failureDiagnostic.httpStatusClass ? { httpStatusClass: failureDiagnostic.httpStatusClass } : {}),
|
|
1939
|
+
retryable: failureDiagnostic.retryable,
|
|
1940
|
+
safeAction: failureDiagnostic.safeAction,
|
|
1941
|
+
cause: outcome === 'cancelled' ? 'cancelled' : failureDiagnostic.cause,
|
|
1942
|
+
provenance: 'live',
|
|
1943
|
+
},
|
|
1944
|
+
},
|
|
1945
|
+
});
|
|
1946
|
+
};
|
|
1947
|
+
const recordDenied = (event, error) => {
|
|
1948
|
+
const eventKey = key(event);
|
|
1949
|
+
// A provider adapter can notify a rejection after our host admission
|
|
1950
|
+
// already threw, or after reporting a completed physical HTTP failure.
|
|
1951
|
+
// The first case retains one denied span and no receipt; the second is
|
|
1952
|
+
// already represented by its admitted/error span and must not be doubled.
|
|
1953
|
+
if (admittedKeys.has(eventKey))
|
|
1954
|
+
return;
|
|
1955
|
+
if (deniedKeys.has(eventKey))
|
|
1956
|
+
return;
|
|
1957
|
+
deniedKeys.add(eventKey);
|
|
1958
|
+
const entry = start(event, 'denied', error);
|
|
1959
|
+
const failureDiagnostic = diagnostic(event, error);
|
|
1960
|
+
observer?.finishSpan(entry.spanId, {
|
|
1961
|
+
outcome: 'denied',
|
|
1962
|
+
reasonCode: 'provider_failure',
|
|
1963
|
+
payload: {
|
|
1964
|
+
kind: 'provider',
|
|
1965
|
+
attempt: {
|
|
1966
|
+
version: 1,
|
|
1967
|
+
phase: providerTracePhase(input.phase),
|
|
1968
|
+
purpose: input.purpose,
|
|
1969
|
+
physicalAttemptIndex: event.attemptIndex,
|
|
1970
|
+
providerFingerprint: runtimeTraceFingerprint(event.provider),
|
|
1971
|
+
...(event.model ? { modelFingerprint: runtimeTraceFingerprint(event.model) } : {}),
|
|
1972
|
+
readiness: 'unknown',
|
|
1973
|
+
admission: 'denied',
|
|
1974
|
+
...(failureDiagnostic.httpStatusClass ? { httpStatusClass: failureDiagnostic.httpStatusClass } : {}),
|
|
1975
|
+
retryable: failureDiagnostic.retryable,
|
|
1976
|
+
safeAction: failureDiagnostic.safeAction,
|
|
1977
|
+
cause: failureDiagnostic.cause,
|
|
1978
|
+
provenance: 'live',
|
|
1979
|
+
},
|
|
1980
|
+
},
|
|
1981
|
+
});
|
|
1982
|
+
};
|
|
1983
|
+
const options = {
|
|
1984
|
+
onProviderDispatch: (event) => {
|
|
1985
|
+
// Admission is the physical-send boundary. First let the ledger accept
|
|
1986
|
+
// and receipt the exact envelope; only then start an admitted trace
|
|
1987
|
+
// span. If it rejects, record a denied boundary with no pending entry,
|
|
1988
|
+
// receipt, or claim that bytes left the process.
|
|
1989
|
+
try {
|
|
1990
|
+
const envelope = input.admit(event);
|
|
1991
|
+
const entry = start(event, 'admitted');
|
|
1992
|
+
const eventKey = key(event);
|
|
1993
|
+
admittedKeys.add(eventKey);
|
|
1994
|
+
pending.set(eventKey, [...(pending.get(eventKey) ?? []), entry]);
|
|
1995
|
+
return envelope;
|
|
1996
|
+
}
|
|
1997
|
+
catch (error) {
|
|
1998
|
+
recordDenied(event, error);
|
|
1999
|
+
throw error;
|
|
2000
|
+
}
|
|
2001
|
+
},
|
|
2002
|
+
onProviderDispatchComplete: (event) => {
|
|
2003
|
+
// A transport/process success is not yet an accepted meaning result. The
|
|
2004
|
+
// provider promise settles after parsing; close the matching physical
|
|
2005
|
+
// span from `settle` so malformed output remains a visible failure.
|
|
2006
|
+
if (event.outcome === 'ok')
|
|
2007
|
+
return;
|
|
2008
|
+
const eventKey = key(event);
|
|
2009
|
+
const entries = pending.get(eventKey) ?? [];
|
|
2010
|
+
const entry = entries.shift();
|
|
2011
|
+
if (entries.length > 0)
|
|
2012
|
+
pending.set(eventKey, entries);
|
|
2013
|
+
else
|
|
2014
|
+
pending.delete(eventKey);
|
|
2015
|
+
if (entry)
|
|
2016
|
+
finish(entry, event.outcome === 'cancelled' ? 'cancelled' : 'error', event.error, event.httpStatus);
|
|
2017
|
+
},
|
|
2018
|
+
onProviderDispatchRejected: (event) => {
|
|
2019
|
+
recordDenied(event, event.error);
|
|
2020
|
+
},
|
|
2021
|
+
};
|
|
2022
|
+
return {
|
|
2023
|
+
options,
|
|
2024
|
+
settle: (outcome, error) => {
|
|
2025
|
+
const entries = [...pending.values()].flat();
|
|
2026
|
+
pending.clear();
|
|
2027
|
+
for (const entry of entries)
|
|
2028
|
+
finish(entry, outcome, error);
|
|
2029
|
+
// A provider can fail before it reaches the dispatch observer (for
|
|
2030
|
+
// example subscription CLI readiness). Record that as one typed
|
|
2031
|
+
// provider boundary instead of allowing the root trace to fall back to
|
|
2032
|
+
// `unknown`.
|
|
2033
|
+
if (observedBoundaryCount === 0 && outcome !== 'ok' && observer?.enabled) {
|
|
2034
|
+
const failureDiagnostic = diagnostic(undefined, error);
|
|
2035
|
+
const span = observer.startSpan({
|
|
2036
|
+
name: 'provider.attempt',
|
|
2037
|
+
stage: 'provider',
|
|
2038
|
+
reasonCode: 'provider_failure',
|
|
2039
|
+
payload: {
|
|
2040
|
+
kind: 'provider',
|
|
2041
|
+
attempt: {
|
|
2042
|
+
version: 1,
|
|
2043
|
+
phase: providerTracePhase(input.phase),
|
|
2044
|
+
purpose: input.purpose,
|
|
2045
|
+
physicalAttemptIndex: 1,
|
|
2046
|
+
readiness: 'unknown',
|
|
2047
|
+
admission: 'unknown',
|
|
2048
|
+
retryable: failureDiagnostic.retryable,
|
|
2049
|
+
safeAction: failureDiagnostic.safeAction,
|
|
2050
|
+
cause: failureDiagnostic.cause,
|
|
2051
|
+
provenance: 'live',
|
|
2052
|
+
},
|
|
2053
|
+
},
|
|
2054
|
+
});
|
|
2055
|
+
observer.finishSpan(span, { outcome: outcome === 'cancelled' ? 'cancelled' : 'error', reasonCode: outcome === 'cancelled' ? 'cancelled' : 'provider_failure' });
|
|
2056
|
+
}
|
|
2057
|
+
},
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Router interpretation happens before the answer runner's AsyncLocal trace
|
|
2062
|
+
* scope exists. Its request already carries the server-owned observer, so
|
|
2063
|
+
* adapt the physical category-classification or candidate-ID meaning call to
|
|
2064
|
+
* the shared physical-send trace wrapper without changing router authority.
|
|
2065
|
+
*/
|
|
2066
|
+
function createRouterInterpretationProviderTrace(input) {
|
|
2067
|
+
const purpose = input.routerPhase === 'classification'
|
|
2068
|
+
? 'classification'
|
|
2069
|
+
: 'answer_generation';
|
|
2070
|
+
return createProviderDispatchTrace({
|
|
2071
|
+
observer: askTraceObserverForV1(input.request),
|
|
2072
|
+
phase: input.routerPhase,
|
|
2073
|
+
purpose,
|
|
2074
|
+
admit: (event) => {
|
|
2075
|
+
const ledger = agentRunProviderEvidenceContext.getStore();
|
|
2076
|
+
if (ledger) {
|
|
2077
|
+
return ledger.observe(event, {
|
|
2078
|
+
purpose,
|
|
2079
|
+
dispatchPhase: input.routerPhase,
|
|
2080
|
+
optIn: false,
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
|
|
2084
|
+
assertProviderPayloadAllowed(envelope, {
|
|
2085
|
+
allowResultRows: false,
|
|
2086
|
+
maxResultRows: 0,
|
|
2087
|
+
purpose,
|
|
2088
|
+
});
|
|
2089
|
+
return envelope;
|
|
2090
|
+
},
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* The hypothesis planner is a real Research provider dispatch, not a local
|
|
2095
|
+
* planning convenience. Keep its one bounded call on the same server-owned
|
|
2096
|
+
* ledger and trace as meaning, generation, and narration so it cannot evade
|
|
2097
|
+
* the Research-12 cap or disappear from the run receipt.
|
|
2098
|
+
*
|
|
2099
|
+
* `planResearchHypotheses` deliberately accepts a small `generate`-only
|
|
2100
|
+
* provider interface. This adapter preserves that seam while keeping the
|
|
2101
|
+
* physical transport authority at the local-runtime boundary.
|
|
2102
|
+
*
|
|
2103
|
+
* @internal Exported for the local runtime planner/egress regression only.
|
|
2104
|
+
*/
|
|
2105
|
+
export function createResearchHypothesisPlanningProvider(input) {
|
|
2106
|
+
const { provider, request, ledger } = input;
|
|
2107
|
+
return {
|
|
2108
|
+
name: provider.name,
|
|
2109
|
+
available: () => provider.available(),
|
|
2110
|
+
generate: async (messages, options) => {
|
|
2111
|
+
const planningTrace = createProviderDispatchTrace({
|
|
2112
|
+
observer: askTraceObserverForV1(request),
|
|
2113
|
+
phase: 'planning',
|
|
2114
|
+
purpose: 'answer_generation',
|
|
2115
|
+
admit: (event) => {
|
|
2116
|
+
if (ledger) {
|
|
2117
|
+
return ledger.observe(event, {
|
|
2118
|
+
purpose: 'answer_generation',
|
|
2119
|
+
dispatchPhase: 'planning',
|
|
2120
|
+
optIn: false,
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
|
|
2124
|
+
assertProviderPayloadAllowed(envelope, {
|
|
2125
|
+
allowResultRows: false,
|
|
2126
|
+
maxResultRows: 0,
|
|
2127
|
+
purpose: 'answer_generation',
|
|
2128
|
+
});
|
|
2129
|
+
return envelope;
|
|
2130
|
+
},
|
|
2131
|
+
});
|
|
2132
|
+
try {
|
|
2133
|
+
const response = await provider.generate(messages, {
|
|
2134
|
+
...options,
|
|
2135
|
+
// Hypothesis planning is exactly one preparation transport. The
|
|
2136
|
+
// shared run ledger enforces the remaining Research-12 ceiling.
|
|
2137
|
+
maxProviderDispatches: 1,
|
|
2138
|
+
...planningTrace.options,
|
|
2139
|
+
});
|
|
2140
|
+
planningTrace.settle('ok');
|
|
2141
|
+
return response;
|
|
2142
|
+
}
|
|
2143
|
+
catch (error) {
|
|
2144
|
+
planningTrace.settle(request.signal?.aborted || options?.signal?.aborted ? 'cancelled' : 'error', error);
|
|
2145
|
+
throw error;
|
|
2146
|
+
}
|
|
2147
|
+
},
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
1541
2150
|
function mergeRunScopedProviderDispatchEvidence(run, evidence) {
|
|
1542
2151
|
const providerEgressReceipts = [...evidence.providerEgressReceipts];
|
|
1543
2152
|
const elapsed = Math.max(0, Date.parse(run.completedAt) - Date.parse(run.startedAt));
|
|
@@ -1611,8 +2220,46 @@ function mergeRunScopedProviderDispatchEvidence(run, evidence) {
|
|
|
1611
2220
|
* @internal
|
|
1612
2221
|
*/
|
|
1613
2222
|
export async function executePreparedAgenticSqlBoundary(input) {
|
|
2223
|
+
const trace = input.traceObserver ?? activeAskTraceObserver();
|
|
2224
|
+
const sqlFingerprint = runtimeTraceFingerprint(input.preparedSql);
|
|
2225
|
+
const sqlPayload = {
|
|
2226
|
+
kind: 'sql',
|
|
2227
|
+
execution: {
|
|
2228
|
+
version: 1,
|
|
2229
|
+
sqlFingerprint,
|
|
2230
|
+
reviewRequired: true,
|
|
2231
|
+
},
|
|
2232
|
+
};
|
|
2233
|
+
// The statement reached this boundary from the frozen generated-plan path.
|
|
2234
|
+
// Do not infer a generation success later from execution counters: this span
|
|
2235
|
+
// records the actual prepared statement handoff (fingerprint only).
|
|
2236
|
+
const generationSpan = trace?.startSpan({
|
|
2237
|
+
name: 'sql.generate',
|
|
2238
|
+
stage: 'sql',
|
|
2239
|
+
reasonCode: 'started',
|
|
2240
|
+
payload: sqlPayload,
|
|
2241
|
+
});
|
|
2242
|
+
trace?.finishSpan(generationSpan, { outcome: 'ok', reasonCode: 'completed', payload: sqlPayload });
|
|
1614
2243
|
const capability = input.capability;
|
|
2244
|
+
const validationSpan = trace?.startSpan({
|
|
2245
|
+
name: 'sql.validate',
|
|
2246
|
+
stage: 'sql',
|
|
2247
|
+
reasonCode: 'started',
|
|
2248
|
+
payload: sqlPayload,
|
|
2249
|
+
});
|
|
2250
|
+
const validation = validateAuthorizedSqlReferences(input.preparedSql, undefined);
|
|
2251
|
+
if (!validation.ok) {
|
|
2252
|
+
trace?.finishSpan(validationSpan, { outcome: 'denied', reasonCode: 'sql_denied', payload: sqlPayload });
|
|
2253
|
+
throw analyticalError('The generated statement did not pass read-only SQL validation, so it was not executed.', { origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql' });
|
|
2254
|
+
}
|
|
2255
|
+
trace?.finishSpan(validationSpan, { outcome: 'ok', reasonCode: 'completed', payload: sqlPayload });
|
|
1615
2256
|
if (capability) {
|
|
2257
|
+
const authorizationSpan = trace?.startSpan({
|
|
2258
|
+
name: 'sql.authorize',
|
|
2259
|
+
stage: 'sql',
|
|
2260
|
+
reasonCode: 'started',
|
|
2261
|
+
payload: sqlPayload,
|
|
2262
|
+
});
|
|
1616
2263
|
const authorization = mintFinalSqlAuthorization({
|
|
1617
2264
|
sql: input.preparedSql,
|
|
1618
2265
|
proven: capability.provenIdentifiers.map((identifier) => ({
|
|
@@ -1626,7 +2273,6 @@ export async function executePreparedAgenticSqlBoundary(input) {
|
|
|
1626
2273
|
targetFingerprint: capability.targetFingerprint,
|
|
1627
2274
|
bindings: input.bindings,
|
|
1628
2275
|
});
|
|
1629
|
-
const validation = validateAuthorizedSqlReferences(input.preparedSql, undefined);
|
|
1630
2276
|
const verdict = verifyFinalSql(authorization, input.preparedSql, qualifyAuthorizationReferences(input.preparedSql, {
|
|
1631
2277
|
relations: validation.referencedRelations ?? [],
|
|
1632
2278
|
columns: validation.referencedColumns ?? [],
|
|
@@ -1638,16 +2284,71 @@ export async function executePreparedAgenticSqlBoundary(input) {
|
|
|
1638
2284
|
console.warn(`[dql] execution authorization: ${verdict.ok ? 'admitted' : 'REFUSED'} proven=${authorization.provenIdentifiers.length}${verdict.ok ? '' : ` reason=${verdict.reason}`}`);
|
|
1639
2285
|
}
|
|
1640
2286
|
if (!verdict.ok) {
|
|
2287
|
+
trace?.finishSpan(authorizationSpan, { outcome: 'denied', reasonCode: 'sql_denied', payload: sqlPayload });
|
|
1641
2288
|
throw analyticalError(verdict.reason ?? 'The statement was not authorized for execution.', { origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql' });
|
|
1642
2289
|
}
|
|
2290
|
+
trace?.finishSpan(authorizationSpan, { outcome: 'ok', reasonCode: 'completed', payload: sqlPayload });
|
|
2291
|
+
}
|
|
2292
|
+
const executionSpan = trace?.startSpan({
|
|
2293
|
+
name: 'sql.execute',
|
|
2294
|
+
stage: 'sql',
|
|
2295
|
+
reasonCode: 'started',
|
|
2296
|
+
payload: sqlPayload,
|
|
2297
|
+
});
|
|
2298
|
+
try {
|
|
2299
|
+
const result = await input.execute();
|
|
2300
|
+
trace?.finishSpan(executionSpan, { outcome: 'ok', reasonCode: 'completed', payload: sqlPayload });
|
|
2301
|
+
return result;
|
|
2302
|
+
}
|
|
2303
|
+
catch (error) {
|
|
2304
|
+
trace?.finishSpan(executionSpan, { outcome: 'error', reasonCode: 'sql_failure', payload: sqlPayload });
|
|
2305
|
+
throw error;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* The DQL-artifact executor reaches this callback only after the existing
|
|
2310
|
+
* compiler/read-only validation completed. Record that physical boundary when
|
|
2311
|
+
* an Ask trace is active, without teaching the tracer to choose a tier or
|
|
2312
|
+
* authorize a statement. Generated SQL keeps its stronger capability-bound
|
|
2313
|
+
* boundary above.
|
|
2314
|
+
*/
|
|
2315
|
+
async function executePreparedArtifactTraceBoundary(input) {
|
|
2316
|
+
const trace = activeAskTraceObserver();
|
|
2317
|
+
const payload = {
|
|
2318
|
+
kind: 'sql',
|
|
2319
|
+
execution: {
|
|
2320
|
+
version: 1,
|
|
2321
|
+
sqlFingerprint: runtimeTraceFingerprint(input.preparedSql),
|
|
2322
|
+
reviewRequired: input.reviewRequired,
|
|
2323
|
+
},
|
|
2324
|
+
};
|
|
2325
|
+
// Certified/semantic artifact execution arrives after its own authoritative
|
|
2326
|
+
// compiler checks. This wrapper has no independent validation or capability
|
|
2327
|
+
// verdict to observe, so it records only the physical execution instead of
|
|
2328
|
+
// manufacturing successful validate/authorize stages after the fact.
|
|
2329
|
+
const execution = trace?.startSpan({ name: 'sql.execute', stage: 'sql', reasonCode: 'started', payload });
|
|
2330
|
+
try {
|
|
2331
|
+
const result = await input.execute();
|
|
2332
|
+
trace?.finishSpan(execution, { outcome: 'ok', reasonCode: 'completed', payload });
|
|
2333
|
+
return result;
|
|
2334
|
+
}
|
|
2335
|
+
catch (error) {
|
|
2336
|
+
trace?.finishSpan(execution, { outcome: 'error', reasonCode: 'sql_failure', payload });
|
|
2337
|
+
throw error;
|
|
1643
2338
|
}
|
|
1644
|
-
return input.execute();
|
|
1645
2339
|
}
|
|
1646
2340
|
export async function startLocalServer(opts) {
|
|
1647
2341
|
const { rootDir, executor, connection: rawConnection, preferredPort, projectRoot = process.cwd() } = opts;
|
|
1648
2342
|
const bindHost = opts.host ?? process.env.DQL_HOST ?? '127.0.0.1';
|
|
1649
2343
|
const loopback = bindHost === '127.0.0.1' || bindHost === 'localhost' || bindHost === '::1';
|
|
1650
2344
|
const authToken = opts.authToken ?? process.env.DQL_SERVER_TOKEN;
|
|
2345
|
+
const trustedCliTraceToken = opts.trustedCliTraceToken;
|
|
2346
|
+
const cliAskTraceCapabilities = createLocalCliAskTraceCapabilityRegistryV1();
|
|
2347
|
+
// An ephemeral `dql agent ask` runtime receives its one-shot capability from
|
|
2348
|
+
// its parent process. Register it through the same short-lived, scoped
|
|
2349
|
+
// registry used by an already-running loopback runtime's challenge endpoint.
|
|
2350
|
+
if (trustedCliTraceToken)
|
|
2351
|
+
cliAskTraceCapabilities.issue({ capability: trustedCliTraceToken });
|
|
1651
2352
|
const runtimeVersion = readDqlRuntimeVersion();
|
|
1652
2353
|
// Warm the latest-version cache in the background (2s cap, 24h cache; offline → unknown).
|
|
1653
2354
|
void fetchLatestPublishedDqlVersion();
|
|
@@ -1664,6 +2365,9 @@ export async function startLocalServer(opts) {
|
|
|
1664
2365
|
if (gitRoot)
|
|
1665
2366
|
ensureLocalRuntimeGitignore(projectRoot);
|
|
1666
2367
|
let projectConfig = loadProjectConfig(projectRoot);
|
|
2368
|
+
// This opaque value lets the Ask browser cache distinguish a newly started
|
|
2369
|
+
// project/runtime on the same browser origin without exposing `projectRoot`.
|
|
2370
|
+
const conversationProjectIdentity = askConversationProjectIdentity(projectRoot);
|
|
1667
2371
|
const analyticalExecutionService = new ExecutionService({
|
|
1668
2372
|
executor,
|
|
1669
2373
|
projectRoot,
|
|
@@ -1959,7 +2663,10 @@ export async function startLocalServer(opts) {
|
|
|
1959
2663
|
};
|
|
1960
2664
|
const requireActiveConnection = (candidate = connection) => {
|
|
1961
2665
|
if (!candidate) {
|
|
1962
|
-
|
|
2666
|
+
// This happens before DQL compiles an artifact or hands SQL to a
|
|
2667
|
+
// connector. Preserve that physical boundary for the Ask trace: callers
|
|
2668
|
+
// can project the typed setup failure without claiming that SQL executed.
|
|
2669
|
+
throw analyticalError('No database connection is configured yet. Open Connections, add a warehouse or local DuckDB/file connection, then retry.', { origin: 'host', stage: 'execute', code: 'connection_not_configured' });
|
|
1963
2670
|
}
|
|
1964
2671
|
assertConnectionNodeCompatibility(candidate);
|
|
1965
2672
|
return candidate;
|
|
@@ -2288,17 +2995,20 @@ export async function startLocalServer(opts) {
|
|
|
2288
2995
|
return undefined;
|
|
2289
2996
|
return { columns: columns.length > 0 ? columns : Object.keys(rows[0]), rows };
|
|
2290
2997
|
};
|
|
2291
|
-
// Provider-backed narration for
|
|
2292
|
-
//
|
|
2293
|
-
|
|
2998
|
+
// Provider-backed narration for an explicitly consented Research result.
|
|
2999
|
+
// Ordinary Ask never enters this helper; deterministic narration remains the
|
|
3000
|
+
// fallback when Research has no row-egress authority.
|
|
3001
|
+
const narrateForAgentRun = async (input, researchResultRowsOptIn = false, traceObserver) => {
|
|
2294
3002
|
// Without caller consent, keep narration deterministic and make no physical
|
|
2295
3003
|
// provider dispatch. With consent, serialize only the bounded, redacted
|
|
2296
3004
|
// sample the transport receipt will account for — bounded by the project's
|
|
2297
3005
|
// own egress policy, so an admin kill-switch is not silently bypassed here.
|
|
2298
|
-
if (!
|
|
3006
|
+
if (!researchResultRowsOptIn || !input.result)
|
|
2299
3007
|
return narrateResult(input);
|
|
2300
3008
|
const narrationRowEgress = resolveProviderResultRowEgressPolicy({
|
|
2301
3009
|
projectSetting: projectConfig?.agent?.providerResultRowEgress,
|
|
3010
|
+
requestedMode: 'research',
|
|
3011
|
+
researchOptIn: researchResultRowsOptIn,
|
|
2302
3012
|
});
|
|
2303
3013
|
if (narrationRowEgress.maxNarrationRows === 0)
|
|
2304
3014
|
return narrateResult(input);
|
|
@@ -2310,32 +3020,57 @@ export async function startLocalServer(opts) {
|
|
|
2310
3020
|
...input,
|
|
2311
3021
|
result: safeResult,
|
|
2312
3022
|
};
|
|
3023
|
+
const narrationTrace = createProviderDispatchTrace({
|
|
3024
|
+
// This Research handler can execute after its async trace scope has
|
|
3025
|
+
// returned. Carry the server-owned observer rather than relying on
|
|
3026
|
+
// AsyncLocalStorage lifetime, so the physical narration receipt and
|
|
3027
|
+
// provider.attempt span remain one-to-one.
|
|
3028
|
+
observer: traceObserver ?? activeAskTraceObserver(),
|
|
3029
|
+
phase: 'narration',
|
|
3030
|
+
purpose: 'research_narration',
|
|
3031
|
+
admit: (event) => {
|
|
3032
|
+
const ledger = agentRunProviderEvidenceContext.getStore();
|
|
3033
|
+
if (ledger) {
|
|
3034
|
+
return ledger.observe(event, {
|
|
3035
|
+
purpose: 'research_narration',
|
|
3036
|
+
dispatchPhase: 'narration',
|
|
3037
|
+
optIn: true,
|
|
3038
|
+
serializedResultShape: {
|
|
3039
|
+
resultRowCount: safeResult.rows.length,
|
|
3040
|
+
columnCount: safeResult.columns.length,
|
|
3041
|
+
},
|
|
3042
|
+
cumulativeResultRowCount: safeResult.rows.length,
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
|
|
3046
|
+
assertProviderPayloadAllowed(envelope, {
|
|
3047
|
+
allowResultRows: false,
|
|
3048
|
+
maxResultRows: 0,
|
|
3049
|
+
purpose: 'research_narration',
|
|
3050
|
+
});
|
|
3051
|
+
return envelope;
|
|
3052
|
+
},
|
|
3053
|
+
});
|
|
2313
3054
|
return narrateResult(safeInput, {
|
|
2314
3055
|
complete: async ({ system, user, signal }) => {
|
|
2315
3056
|
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
2316
3057
|
if (!provider)
|
|
2317
3058
|
throw new Error('No AI provider configured for narration.');
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
columnCount: safeResult.columns.length,
|
|
2334
|
-
},
|
|
2335
|
-
cumulativeResultRowCount: safeResult.rows.length,
|
|
2336
|
-
}),
|
|
2337
|
-
} : {}),
|
|
2338
|
-
});
|
|
3059
|
+
try {
|
|
3060
|
+
const response = await provider.generate([{ role: 'system', content: system }, { role: 'user', content: user }], {
|
|
3061
|
+
maxTokens: 600,
|
|
3062
|
+
temperature: 0.2,
|
|
3063
|
+
signal,
|
|
3064
|
+
maxProviderDispatches: 2,
|
|
3065
|
+
...narrationTrace.options,
|
|
3066
|
+
});
|
|
3067
|
+
narrationTrace.settle('ok');
|
|
3068
|
+
return response;
|
|
3069
|
+
}
|
|
3070
|
+
catch (error) {
|
|
3071
|
+
narrationTrace.settle(signal?.aborted ? 'cancelled' : 'error', error);
|
|
3072
|
+
throw error;
|
|
3073
|
+
}
|
|
2339
3074
|
},
|
|
2340
3075
|
});
|
|
2341
3076
|
};
|
|
@@ -2343,18 +3078,32 @@ export async function startLocalServer(opts) {
|
|
|
2343
3078
|
return runGovernedAgentAnswerForRunInner(request, repair, route, onProgress, routeDecision);
|
|
2344
3079
|
}
|
|
2345
3080
|
async function runGovernedAgentAnswerForRunInner(request, repair, route = 'generated_answer', onProgress, routeDecision) {
|
|
2346
|
-
const
|
|
3081
|
+
const researchBranch = request.researchBranch;
|
|
3082
|
+
const isResearchChild = researchBranch?.childRunId === request.runId
|
|
3083
|
+
&& Boolean(researchBranch?.rootRunId)
|
|
3084
|
+
&& Boolean(researchBranch?.branchId);
|
|
3085
|
+
// The CLI may request one known provider, but the server remains the
|
|
3086
|
+
// authority for what that means. An unknown id is deliberately retained
|
|
3087
|
+
// long enough to become a typed model-selection/preflight diagnostic;
|
|
3088
|
+
// otherwise the old no-provider branch mislabeled every selection failure
|
|
3089
|
+
// as authentication.
|
|
3090
|
+
const requestedProvider = agentRunWorkspaceValue(request, 'provider');
|
|
3091
|
+
const governed = resolveGovernedAnswerRunner(projectRoot, requestedProvider);
|
|
2347
3092
|
let resolvedProvider = governed?.provider ?? null;
|
|
2348
3093
|
let runner = governed?.runner ?? null;
|
|
3094
|
+
const exactCertifiedProviderFreePlan = routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
3095
|
+
&& routeDecision.resolvedAnalyticalPlan.capability === 'certified_execution'
|
|
3096
|
+
&& routeDecision.analyticalCascadeDecision?.selectedTier === 'certified'
|
|
3097
|
+
&& routeDecision.analyticalCascadeDecision.planFrozen === true;
|
|
2349
3098
|
const exactProviderFreePlan = routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
2350
3099
|
&& (routeDecision.resolvedAnalyticalPlan.capability === 'certified_execution'
|
|
2351
3100
|
|| routeDecision.resolvedAnalyticalPlan.capability === 'semantic_execution');
|
|
2352
|
-
if ((!resolvedProvider || !runner) && exactProviderFreePlan) {
|
|
3101
|
+
if (exactCertifiedProviderFreePlan || ((!resolvedProvider || !runner) && exactProviderFreePlan)) {
|
|
2353
3102
|
const deterministicProvider = {
|
|
2354
3103
|
name: 'ollama',
|
|
2355
3104
|
available: async () => true,
|
|
2356
3105
|
generate: async () => {
|
|
2357
|
-
throw Object.assign(new Error('Exact certified
|
|
3106
|
+
throw Object.assign(new Error('Exact certified execution must not dispatch a provider.'), {
|
|
2358
3107
|
code: 'EXACT_ROUTE_PROVIDER_DISPATCH_FORBIDDEN',
|
|
2359
3108
|
});
|
|
2360
3109
|
},
|
|
@@ -2363,9 +3112,65 @@ export async function startLocalServer(opts) {
|
|
|
2363
3112
|
runner = createDqlAgentProviderRunner('ollama', deterministicProvider);
|
|
2364
3113
|
}
|
|
2365
3114
|
if (!resolvedProvider || !runner) {
|
|
2366
|
-
|
|
3115
|
+
const error = governedProviderPreflightError(requestedProvider);
|
|
3116
|
+
// This is the only no-provider path. It has no physical send, but it is
|
|
3117
|
+
// still a real readiness failure and must be visible as such rather than
|
|
3118
|
+
// manufactured later by the engine from an executor outcome.
|
|
3119
|
+
const trace = askTraceObserverForV1(request);
|
|
3120
|
+
const diagnostic = classifyProviderFailure({
|
|
3121
|
+
message: error.message,
|
|
3122
|
+
code: error.code,
|
|
3123
|
+
phase: 'preflight',
|
|
3124
|
+
});
|
|
3125
|
+
const span = trace.startSpan({
|
|
3126
|
+
name: 'provider.preflight',
|
|
3127
|
+
stage: 'provider',
|
|
3128
|
+
reasonCode: 'provider_preflight',
|
|
3129
|
+
payload: {
|
|
3130
|
+
kind: 'provider',
|
|
3131
|
+
attempt: {
|
|
3132
|
+
version: 1,
|
|
3133
|
+
phase: 'preflight',
|
|
3134
|
+
physicalAttemptIndex: 0,
|
|
3135
|
+
readiness: 'unavailable',
|
|
3136
|
+
admission: 'unknown',
|
|
3137
|
+
cause: diagnostic.cause,
|
|
3138
|
+
retryable: diagnostic.retryable,
|
|
3139
|
+
safeAction: diagnostic.safeAction,
|
|
3140
|
+
provenance: 'live',
|
|
3141
|
+
},
|
|
3142
|
+
},
|
|
3143
|
+
});
|
|
3144
|
+
trace.finishSpan(span, {
|
|
3145
|
+
outcome: 'unavailable',
|
|
3146
|
+
reasonCode: 'provider_preflight',
|
|
3147
|
+
});
|
|
3148
|
+
throw error;
|
|
2367
3149
|
}
|
|
2368
3150
|
let governedAnswer;
|
|
3151
|
+
// The answer loop intentionally owns its own user-facing execution
|
|
3152
|
+
// failure. Some tool adapters serialize that error before returning the
|
|
3153
|
+
// governed answer, which means the non-enumerable analytical error tag is
|
|
3154
|
+
// no longer available there. Keep the tiny, typed fact at the physical
|
|
3155
|
+
// frozen-plan callback boundary instead of parsing the returned text. This
|
|
3156
|
+
// covers every frozen execution tier; it is trace-only evidence and neither
|
|
3157
|
+
// changes the answer, its route, nor its trust state.
|
|
3158
|
+
let frozenExecutionSetupFailure;
|
|
3159
|
+
const captureFrozenConnectionSetupFailure = (error) => {
|
|
3160
|
+
const frozen = routeDecision?.analyticalCascadeDecision?.planFrozen === true;
|
|
3161
|
+
const detail = analyticalErrorDetail(error);
|
|
3162
|
+
if (frozen
|
|
3163
|
+
&& detail?.origin === 'host'
|
|
3164
|
+
&& detail.stage === 'execute'
|
|
3165
|
+
&& detail.code === 'connection_not_configured') {
|
|
3166
|
+
frozenExecutionSetupFailure = {
|
|
3167
|
+
version: 1,
|
|
3168
|
+
phase: 'execution',
|
|
3169
|
+
cause: 'connection_not_configured',
|
|
3170
|
+
safeAction: 'configure_connection',
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
};
|
|
2369
3174
|
let providerError;
|
|
2370
3175
|
let providerDispatchEvidence;
|
|
2371
3176
|
let providerBoundaryDiagnostic;
|
|
@@ -2480,34 +3285,75 @@ export async function startLocalServer(opts) {
|
|
|
2480
3285
|
// Local to this exact answer invocation. Compound children each enter this
|
|
2481
3286
|
// function separately, so no child can consume another child's capability.
|
|
2482
3287
|
const agenticExecutionCapabilityGate = new AgenticExecutionCapabilityGate();
|
|
2483
|
-
|
|
3288
|
+
// The first proposal and its one permitted same-plan repair are distinct
|
|
3289
|
+
// one-shot authorities. Never recycle the first capability after the
|
|
3290
|
+
// connector has consumed it, and never admit a second repair.
|
|
3291
|
+
let exploratoryAuthorization;
|
|
3292
|
+
const prepareExploratorySqlExecution = async (sql, _artifact, authorizationAttempt) => {
|
|
2484
3293
|
const cascade = routeDecision?.analyticalCascadeDecision;
|
|
2485
3294
|
const selectedAttempt = selectedExploratoryAttempt;
|
|
3295
|
+
const selectedPlan = routeDecision?.resolvedAnalyticalPlan;
|
|
3296
|
+
const authorizationStateMismatch = (message) => Object.assign(analyticalError(message, {
|
|
3297
|
+
// This is an internal lifecycle invariant, not a connector, model,
|
|
3298
|
+
// or retryable network failure. The engine turns this typed code
|
|
3299
|
+
// into the durable SQL-authorize incident below.
|
|
3300
|
+
origin: 'host', stage: 'validation', code: 'exploratory_authorization_state_mismatch',
|
|
3301
|
+
}), { code: 'INTERNAL_EXPLORATORY_AUTHORIZATION_STATE_MISMATCH' });
|
|
2486
3302
|
if (!cascade
|
|
2487
3303
|
|| cascade.selectedTier !== 'exploratory_sql'
|
|
2488
|
-
|| cascade.planFrozen
|
|
3304
|
+
|| !cascade.planFrozen
|
|
2489
3305
|
|| !selectedAttempt
|
|
2490
3306
|
|| selectedAttempt.outcome !== 'executable'
|
|
2491
|
-
|| selectedAttempt.
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
3307
|
+
|| !selectedAttempt.planFrozen
|
|
3308
|
+
|| selectedAttempt.candidateIds.length === 0
|
|
3309
|
+
|| !selectedPlan
|
|
3310
|
+
|| selectedPlan.capability !== 'bounded_exploration') {
|
|
3311
|
+
throw authorizationStateMismatch('The frozen exploratory plan was not available for SQL authorization, so execution was not attempted.');
|
|
3312
|
+
}
|
|
3313
|
+
const proposedSqlFingerprint = executionFingerprint(sql);
|
|
3314
|
+
const isRepair = authorizationAttempt?.index === 1;
|
|
3315
|
+
if (authorizationAttempt && !isRepair) {
|
|
3316
|
+
throw authorizationStateMismatch('The exploratory SQL repair authorization did not carry the required bounded repair index.');
|
|
3317
|
+
}
|
|
3318
|
+
if (isRepair) {
|
|
3319
|
+
const initial = exploratoryAuthorization?.initial;
|
|
3320
|
+
if (!initial || !authorizationAttempt.parentSqlFingerprint
|
|
3321
|
+
|| authorizationAttempt.parentSqlFingerprint !== initial.result.freeze.sqlFingerprint) {
|
|
3322
|
+
throw authorizationStateMismatch('The exploratory SQL repair did not bind to the initial frozen-plan SQL authorization, so execution was not attempted.');
|
|
3323
|
+
}
|
|
3324
|
+
const existingRepair = exploratoryAuthorization?.repair;
|
|
3325
|
+
if (existingRepair) {
|
|
3326
|
+
if (existingRepair.sqlFingerprint === proposedSqlFingerprint
|
|
3327
|
+
&& existingRepair.parentSqlFingerprint === authorizationAttempt.parentSqlFingerprint) {
|
|
3328
|
+
return existingRepair.result;
|
|
3329
|
+
}
|
|
3330
|
+
throw authorizationStateMismatch('A second or mismatched exploratory SQL repair was submitted after the one permitted same-plan repair authorization.');
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
else {
|
|
3334
|
+
const initial = exploratoryAuthorization?.initial;
|
|
3335
|
+
if (initial) {
|
|
3336
|
+
if (initial.sqlFingerprint === proposedSqlFingerprint)
|
|
3337
|
+
return initial.result;
|
|
3338
|
+
throw authorizationStateMismatch('A different SQL proposal was submitted after the exploratory plan had already been authorized.');
|
|
3339
|
+
}
|
|
3340
|
+
if (exploratoryAuthorization?.repair) {
|
|
3341
|
+
throw authorizationStateMismatch('The initial exploratory SQL authorization could not be replaced after a same-plan repair authorization.');
|
|
3342
|
+
}
|
|
2495
3343
|
}
|
|
2496
3344
|
if (!request.runId || !semanticConnection || !preparedContextPack || !preparedExploratoryContextPack) {
|
|
2497
|
-
throw
|
|
2498
|
-
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2499
|
-
});
|
|
3345
|
+
throw authorizationStateMismatch('The frozen exploratory plan could not be bound to this run, target, and metadata snapshot, so execution was not attempted.');
|
|
2500
3346
|
}
|
|
2501
3347
|
const retrievalSnapshotId = routeDecision?.retrievalEvidence?.snapshotId;
|
|
2502
3348
|
const retrievalSourceFingerprint = routeDecision?.retrievalEvidence?.sourceFingerprint;
|
|
2503
3349
|
const retrievalFreezeSnapshotId = retrievalSnapshotId ?? preparedContextPack.knowledgeLens.snapshotId;
|
|
2504
|
-
if (
|
|
3350
|
+
if (selectedPlan.snapshotId !== retrievalFreezeSnapshotId
|
|
3351
|
+
||
|
|
3352
|
+
(retrievalSnapshotId && retrievalSnapshotId !== preparedContextPack.knowledgeLens.snapshotId)
|
|
2505
3353
|
|| (retrievalSourceFingerprint
|
|
2506
3354
|
&& preparedContextPack.freshness.fingerprint
|
|
2507
3355
|
&& retrievalSourceFingerprint !== preparedContextPack.freshness.fingerprint)) {
|
|
2508
|
-
throw
|
|
2509
|
-
origin: 'governance_gate', stage: 'validation', code: 'snapshot_drift',
|
|
2510
|
-
});
|
|
3356
|
+
throw authorizationStateMismatch('The frozen exploratory plan no longer matches its retrieval snapshot or source fingerprint, so execution was not attempted.');
|
|
2511
3357
|
}
|
|
2512
3358
|
projectSnapshot();
|
|
2513
3359
|
projectSnapshots.assertCurrent(runProjectSnapshot.snapshotId);
|
|
@@ -2518,6 +3364,20 @@ export async function startLocalServer(opts) {
|
|
|
2518
3364
|
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2519
3365
|
});
|
|
2520
3366
|
}
|
|
3367
|
+
// The plan owns explicit outputs before SQL is generated. Do not allow a
|
|
3368
|
+
// generated query to execute a five-row "closest" table that omitted a
|
|
3369
|
+
// requested order/product identifier: review-required is not permission
|
|
3370
|
+
// to change the user-visible result tuple.
|
|
3371
|
+
const outputProjection = validateFrozenRequiredOutputProjection({
|
|
3372
|
+
plan: selectedPlan,
|
|
3373
|
+
sql,
|
|
3374
|
+
...(semanticDriver ? { dialect: semanticDriver } : {}),
|
|
3375
|
+
});
|
|
3376
|
+
if (!outputProjection.ok) {
|
|
3377
|
+
throw analyticalError('The selected exploratory query did not prove every explicitly requested frozen output against its exact source column, so it was not executed.', {
|
|
3378
|
+
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
3379
|
+
});
|
|
3380
|
+
}
|
|
2521
3381
|
const validation = validateAuthorizedSqlReferences(sql, preparedExploratoryContextPack, {
|
|
2522
3382
|
...(semanticDriver ? { dialect: semanticDriver } : {}),
|
|
2523
3383
|
runtimeSchema: preparedExploratoryQualifiedSchemaContext,
|
|
@@ -2570,23 +3430,19 @@ export async function startLocalServer(opts) {
|
|
|
2570
3430
|
proofs.set(`${runtimeTable.relation}.${column}`, 'schema_tool');
|
|
2571
3431
|
}
|
|
2572
3432
|
const candidateIds = [...selectedAttempt.candidateIds];
|
|
2573
|
-
const sqlFingerprint =
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
sourceFingerprint: preparedContextPack.freshness.fingerprint,
|
|
2580
|
-
targetFingerprint: target.identityFingerprint,
|
|
2581
|
-
candidateIds,
|
|
2582
|
-
sqlFingerprint,
|
|
2583
|
-
}));
|
|
2584
|
-
const planId = `exploratory-${planFingerprint.slice(0, 24)}`;
|
|
3433
|
+
const sqlFingerprint = proposedSqlFingerprint;
|
|
3434
|
+
// The immutable router plan is already frozen before SQL generation.
|
|
3435
|
+
// SQL authorization binds the generated bytes and live target *to* that
|
|
3436
|
+
// plan; it must not mint a replacement plan identity.
|
|
3437
|
+
const planFingerprint = selectedPlan.fingerprint;
|
|
3438
|
+
const planId = selectedPlan.planId;
|
|
2585
3439
|
const capability = createAgenticSqlExecutionCapability({
|
|
2586
3440
|
sql,
|
|
2587
3441
|
proven: [...proofs.entries()].map(([identifier, evidence]) => ({ identifier, evidence })),
|
|
2588
3442
|
runId: request.runId,
|
|
2589
|
-
executionId:
|
|
3443
|
+
executionId: isRepair
|
|
3444
|
+
? `${request.runId}:exploratory:repair:1:${sqlFingerprint.slice(0, 16)}`
|
|
3445
|
+
: `${request.runId}:exploratory:${sqlFingerprint.slice(0, 16)}`,
|
|
2590
3446
|
snapshotId: runProjectSnapshot.snapshotId,
|
|
2591
3447
|
planId,
|
|
2592
3448
|
targetFingerprint: target.identityFingerprint,
|
|
@@ -2596,13 +3452,20 @@ export async function startLocalServer(opts) {
|
|
|
2596
3452
|
// latter made a fully validated immutable proposal fail only after its
|
|
2597
3453
|
// exploratory plan had frozen.
|
|
2598
3454
|
bindings: { sqlParams: [], variables: {} },
|
|
3455
|
+
exploratoryAuthorizationAttempt: isRepair
|
|
3456
|
+
? {
|
|
3457
|
+
version: 1,
|
|
3458
|
+
index: 1,
|
|
3459
|
+
parentSqlFingerprint: authorizationAttempt.parentSqlFingerprint,
|
|
3460
|
+
}
|
|
3461
|
+
: { version: 1, index: 0 },
|
|
2599
3462
|
});
|
|
2600
3463
|
if (!capability) {
|
|
2601
3464
|
throw analyticalError('DQL could not mint a request-scoped exploratory execution capability, so it was not executed.', {
|
|
2602
3465
|
origin: 'governance_gate', stage: 'validation', code: 'unauthorized_sql',
|
|
2603
3466
|
});
|
|
2604
3467
|
}
|
|
2605
|
-
|
|
3468
|
+
const result = {
|
|
2606
3469
|
capability,
|
|
2607
3470
|
freeze: {
|
|
2608
3471
|
version: 1,
|
|
@@ -2614,11 +3477,33 @@ export async function startLocalServer(opts) {
|
|
|
2614
3477
|
sqlFingerprint: capability.candidateSqlFingerprint,
|
|
2615
3478
|
candidateIds,
|
|
2616
3479
|
authorization: 'capability_minted',
|
|
3480
|
+
requiredOutputBindings: outputProjection.bindingProofs,
|
|
3481
|
+
authorizationAttempt: isRepair
|
|
3482
|
+
? {
|
|
3483
|
+
version: 1,
|
|
3484
|
+
index: 1,
|
|
3485
|
+
parentSqlFingerprint: authorizationAttempt.parentSqlFingerprint,
|
|
3486
|
+
}
|
|
3487
|
+
: { version: 1, index: 0 },
|
|
2617
3488
|
},
|
|
2618
3489
|
};
|
|
3490
|
+
if (isRepair) {
|
|
3491
|
+
exploratoryAuthorization ??= {};
|
|
3492
|
+
exploratoryAuthorization.repair = {
|
|
3493
|
+
sqlFingerprint,
|
|
3494
|
+
parentSqlFingerprint: authorizationAttempt.parentSqlFingerprint,
|
|
3495
|
+
result,
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
else {
|
|
3499
|
+
exploratoryAuthorization ??= {};
|
|
3500
|
+
exploratoryAuthorization.initial = { sqlFingerprint, result };
|
|
3501
|
+
}
|
|
3502
|
+
return result;
|
|
2619
3503
|
};
|
|
2620
|
-
await runner.run({
|
|
3504
|
+
await agentRunAskTraceContext.run(askTraceObserverForV1(request), async () => runner.run(attachAskTraceObserverV1({
|
|
2621
3505
|
provider: resolvedProvider,
|
|
3506
|
+
...(exactCertifiedProviderFreePlan ? { providerPreflightRequired: false } : {}),
|
|
2622
3507
|
...(agentRunProviderEvidenceContext.getStore()
|
|
2623
3508
|
? { providerDispatchEvidenceSink: agentRunProviderEvidenceContext.getStore() }
|
|
2624
3509
|
: {}),
|
|
@@ -2641,9 +3526,14 @@ export async function startLocalServer(opts) {
|
|
|
2641
3526
|
},
|
|
2642
3527
|
reasoningEffort,
|
|
2643
3528
|
...(analysisDepth ? { analysisDepth } : {}),
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
3529
|
+
// A child remains a normal Ask cascade for routing and frozen-plan
|
|
3530
|
+
// authority, but every physical provider transport belongs to its
|
|
3531
|
+
// explicit Research root's ledger/trace budget. This prevents a
|
|
3532
|
+
// concurrent child from inheriting the ordinary Ask one-meaning cap.
|
|
3533
|
+
orchestrationMode: route === 'research' || isResearchChild ? 'research' : 'ask',
|
|
3534
|
+
allowProviderSemanticMemberSelection: route === 'research' || isResearchChild,
|
|
3535
|
+
researchResultRowsOptIn: (request.requestedMode === 'research' || isResearchChild)
|
|
3536
|
+
&& request.researchResultRowsOptIn === true,
|
|
2647
3537
|
projectRoot,
|
|
2648
3538
|
// Keys the execution authorization, so the proofs the analyst loop
|
|
2649
3539
|
// gathers can be checked against the statement this run executes.
|
|
@@ -2832,7 +3722,7 @@ export async function startLocalServer(opts) {
|
|
|
2832
3722
|
// deliberately narrower than exploratory preflight: it only qualifies
|
|
2833
3723
|
// an unambiguous FROM/JOIN leaf already present in the artifact and it
|
|
2834
3724
|
// never supplies a missing table, join, or column.
|
|
2835
|
-
executeCertifiedBlock: (node, invocation) => {
|
|
3725
|
+
executeCertifiedBlock: async (node, invocation) => {
|
|
2836
3726
|
const frozenCertifiedPlan = routeDecision?.analyticalCascadeDecision?.planFrozen === true
|
|
2837
3727
|
&& routeDecision.analyticalCascadeDecision.selectedTier === 'certified'
|
|
2838
3728
|
&& routeDecision.resolvedAnalyticalPlan?.capability === 'certified_execution';
|
|
@@ -2865,9 +3755,23 @@ export async function startLocalServer(opts) {
|
|
|
2865
3755
|
const certifiedSchemaContext = frozenCertifiedPlan
|
|
2866
3756
|
? buildFrozenCertifiedSchemaContext(preparedContextPack, runProjectSnapshot.manifest)
|
|
2867
3757
|
: preparedQualifiedSchemaContext;
|
|
2868
|
-
|
|
3758
|
+
try {
|
|
3759
|
+
return await executeCertifiedBlockForAgent(node, invocation, semanticConnection, semanticConnectionName, certifiedSchemaContext, frozenCertifiedPlan);
|
|
3760
|
+
}
|
|
3761
|
+
catch (error) {
|
|
3762
|
+
captureFrozenConnectionSetupFailure(error);
|
|
3763
|
+
throw error;
|
|
3764
|
+
}
|
|
3765
|
+
},
|
|
3766
|
+
executeGeneratedSql: async (sql, artifact) => {
|
|
3767
|
+
try {
|
|
3768
|
+
return await executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName);
|
|
3769
|
+
}
|
|
3770
|
+
catch (error) {
|
|
3771
|
+
captureFrozenConnectionSetupFailure(error);
|
|
3772
|
+
throw error;
|
|
3773
|
+
}
|
|
2869
3774
|
},
|
|
2870
|
-
executeGeneratedSql: (sql, artifact) => executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName),
|
|
2871
3775
|
prepareExploratorySqlExecution,
|
|
2872
3776
|
executeAgenticGeneratedSql: async (capability, sql, artifact) => {
|
|
2873
3777
|
if (!agenticExecutionCapabilityGate.consume(capability)) {
|
|
@@ -2875,20 +3779,34 @@ export async function startLocalServer(opts) {
|
|
|
2875
3779
|
origin: 'governance_gate', stage: 'execute', code: 'unauthorized_sql',
|
|
2876
3780
|
});
|
|
2877
3781
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
3782
|
+
try {
|
|
3783
|
+
return await executeGeneratedArtifactForAgent(request.question, sql, artifact, semanticConnection, semanticConnectionName, capability, {
|
|
3784
|
+
runId: request.runId,
|
|
3785
|
+
executionId: capability.executionId,
|
|
3786
|
+
snapshotId: runProjectSnapshot.snapshotId,
|
|
3787
|
+
planId: capability.planId,
|
|
3788
|
+
targetFingerprint: capability.targetFingerprint,
|
|
3789
|
+
});
|
|
3790
|
+
}
|
|
3791
|
+
catch (error) {
|
|
3792
|
+
captureFrozenConnectionSetupFailure(error);
|
|
3793
|
+
throw error;
|
|
3794
|
+
}
|
|
3795
|
+
},
|
|
3796
|
+
executeDqlArtifact: async (artifact) => {
|
|
3797
|
+
try {
|
|
3798
|
+
return await executeArtifactReferenceForAgent(artifact, request.question, semanticConnection, semanticConnectionName);
|
|
3799
|
+
}
|
|
3800
|
+
catch (error) {
|
|
3801
|
+
captureFrozenConnectionSetupFailure(error);
|
|
3802
|
+
throw error;
|
|
3803
|
+
}
|
|
2885
3804
|
},
|
|
2886
|
-
executeDqlArtifact: (artifact) => executeArtifactReferenceForAgent(artifact, request.question, semanticConnection, semanticConnectionName),
|
|
2887
3805
|
getSchemaContext: (question, preparedContextPack) => getSchemaContextForAgent(question, preparedContextPack, semanticConnection, request.executionTarget?.target === 'connection'
|
|
2888
3806
|
? request.executionTarget.connectionName
|
|
2889
3807
|
: undefined),
|
|
2890
3808
|
probeNamedRelations: (relations) => probeNamedRelationsForAgent(relations, semanticConnection),
|
|
2891
|
-
}, (turn) => {
|
|
3809
|
+
}, askTraceObserverForV1(request)), (turn) => {
|
|
2892
3810
|
if (turn.kind === 'thinking')
|
|
2893
3811
|
onProgress?.(turn.text);
|
|
2894
3812
|
if (turn.kind === 'tool_result' && turn.id === 'governed_answer') {
|
|
@@ -2899,13 +3817,22 @@ export async function startLocalServer(opts) {
|
|
|
2899
3817
|
providerDispatchEvidence = turn.dispatchEvidence;
|
|
2900
3818
|
providerBoundaryDiagnostic = turn.providerDiagnostic;
|
|
2901
3819
|
}
|
|
2902
|
-
}, runSignal);
|
|
3820
|
+
}, runSignal));
|
|
2903
3821
|
if (!governedAnswer) {
|
|
2904
3822
|
throw Object.assign(new Error(providerError ?? 'The AI provider did not return a governed answer.'), {
|
|
2905
3823
|
...(providerDispatchEvidence ? { providerDispatchEvidence } : {}),
|
|
2906
3824
|
...(providerBoundaryDiagnostic ? { providerDiagnostic: providerBoundaryDiagnostic } : {}),
|
|
2907
3825
|
});
|
|
2908
3826
|
}
|
|
3827
|
+
if (frozenExecutionSetupFailure && !governedAnswer.observabilityExecutionFailure) {
|
|
3828
|
+
// Preserve the fact captured at the actual pre-execution setup
|
|
3829
|
+
// boundary. The error itself remains redacted and the answer loop keeps
|
|
3830
|
+
// sole authority over the terminal answer/provenance.
|
|
3831
|
+
governedAnswer = {
|
|
3832
|
+
...governedAnswer,
|
|
3833
|
+
observabilityExecutionFailure: frozenExecutionSetupFailure,
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
2909
3836
|
return governedAnswer;
|
|
2910
3837
|
}
|
|
2911
3838
|
/**
|
|
@@ -3273,6 +4200,10 @@ export async function startLocalServer(opts) {
|
|
|
3273
4200
|
resultFingerprint: answer.result.resultFingerprint ?? answer.result.executionReceipt?.resultFingerprint,
|
|
3274
4201
|
executionReceipt: answer.result.executionReceipt,
|
|
3275
4202
|
answerTier: answer.route?.tier ?? answer.sourceTier,
|
|
4203
|
+
trustState: canonicalPersistedTrustState({
|
|
4204
|
+
answer,
|
|
4205
|
+
answerTier: answer.route?.tier ?? answer.sourceTier,
|
|
4206
|
+
}),
|
|
3276
4207
|
});
|
|
3277
4208
|
answer.result = {
|
|
3278
4209
|
...answer.result,
|
|
@@ -3281,6 +4212,7 @@ export async function startLocalServer(opts) {
|
|
|
3281
4212
|
rowCount: canonical.rowCount,
|
|
3282
4213
|
resultFingerprint: canonical.resultFingerprint,
|
|
3283
4214
|
...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
|
|
4215
|
+
...(canonical.trustState ? { trustState: canonical.trustState } : {}),
|
|
3284
4216
|
...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
|
|
3285
4217
|
};
|
|
3286
4218
|
}
|
|
@@ -3305,6 +4237,10 @@ export async function startLocalServer(opts) {
|
|
|
3305
4237
|
resultFingerprint: parent.value.result.resultFingerprint ?? parent.value.result.executionReceipt?.resultFingerprint,
|
|
3306
4238
|
executionReceipt: parent.value.result.executionReceipt,
|
|
3307
4239
|
answerTier: parent.value.route?.tier ?? parent.value.sourceTier,
|
|
4240
|
+
trustState: canonicalPersistedTrustState({
|
|
4241
|
+
answer: parent.value,
|
|
4242
|
+
answerTier: parent.value.route?.tier ?? parent.value.sourceTier,
|
|
4243
|
+
}),
|
|
3308
4244
|
})
|
|
3309
4245
|
: undefined, parent?.task);
|
|
3310
4246
|
},
|
|
@@ -3406,13 +4342,10 @@ export async function startLocalServer(opts) {
|
|
|
3406
4342
|
?? governedAnswer.result.executionReceipt?.resultFingerprint,
|
|
3407
4343
|
executionReceipt: governedAnswer.result.executionReceipt,
|
|
3408
4344
|
answerTier: governedAnswer.route?.tier ?? governedAnswer.sourceTier,
|
|
3409
|
-
trustState:
|
|
3410
|
-
|
|
3411
|
-
: governedAnswer.
|
|
3412
|
-
|
|
3413
|
-
: governedAnswer.reviewStatus === 'analyst_review_required'
|
|
3414
|
-
? 'review_required'
|
|
3415
|
-
: 'governed',
|
|
4345
|
+
trustState: canonicalPersistedTrustState({
|
|
4346
|
+
answer: governedAnswer,
|
|
4347
|
+
answerTier: governedAnswer.route?.tier ?? governedAnswer.sourceTier,
|
|
4348
|
+
}),
|
|
3416
4349
|
});
|
|
3417
4350
|
governedAnswer.result = {
|
|
3418
4351
|
...governedAnswer.result,
|
|
@@ -3426,6 +4359,7 @@ export async function startLocalServer(opts) {
|
|
|
3426
4359
|
...(canonical.executionTime !== undefined ? { executionTime: canonical.executionTime } : {}),
|
|
3427
4360
|
...(canonical.truncated ? { truncated: true } : {}),
|
|
3428
4361
|
...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
|
|
4362
|
+
...(canonical.trustState ? { trustState: canonical.trustState } : {}),
|
|
3429
4363
|
...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
|
|
3430
4364
|
};
|
|
3431
4365
|
}
|
|
@@ -3736,26 +4670,28 @@ export async function startLocalServer(opts) {
|
|
|
3736
4670
|
|| (isExploratory && !governedAnswer.result)
|
|
3737
4671
|
? undefined
|
|
3738
4672
|
: sql;
|
|
3739
|
-
//
|
|
3740
|
-
//
|
|
3741
|
-
//
|
|
3742
|
-
//
|
|
3743
|
-
//
|
|
3744
|
-
// it, so a bounded, redacted row sample goes with the request unless a
|
|
3745
|
-
// project admin has switched row egress off.
|
|
4673
|
+
// Ordinary Ask stops after its bounded meaning call and the selected
|
|
4674
|
+
// deterministic analytical execution. It must not quietly make a second
|
|
4675
|
+
// narration dispatch or disclose result rows: that would make a governed
|
|
4676
|
+
// semantic receipt look like an opted-in Research run. Explicit Research
|
|
4677
|
+
// alone may ask a provider to narrate the settled result.
|
|
3746
4678
|
let synthesizedAnswer;
|
|
3747
4679
|
const providerEgressReceipts = [
|
|
3748
4680
|
...(governedAnswer.providerEgressReceipts ?? []),
|
|
3749
4681
|
];
|
|
3750
4682
|
let narrationDurationMs = 0;
|
|
3751
|
-
const researchRowsOptIn =
|
|
4683
|
+
const researchRowsOptIn = request.requestedMode === 'research' && request.researchResultRowsOptIn === true;
|
|
3752
4684
|
const rowEgress = resolveProviderResultRowEgressPolicy({
|
|
3753
4685
|
projectSetting: projectConfig?.agent?.providerResultRowEgress,
|
|
4686
|
+
requestedMode: request.requestedMode,
|
|
3754
4687
|
researchOptIn: researchRowsOptIn,
|
|
3755
4688
|
});
|
|
3756
|
-
//
|
|
3757
|
-
//
|
|
3758
|
-
|
|
4689
|
+
// Do not even initialize a narration transport for ordinary Ask. This
|
|
4690
|
+
// preserves the one-meaning-call contract and ensures the provider egress
|
|
4691
|
+
// ledger contains only physical dispatches that actually occurred.
|
|
4692
|
+
const narrationProvider = request.requestedMode === 'research'
|
|
4693
|
+
? await createBlockStudioAssistProvider(projectRoot).catch(() => null)
|
|
4694
|
+
: null;
|
|
3759
4695
|
const narrationPlan = planAgentRunNarration(governedAnswer, {
|
|
3760
4696
|
requestedMode: request.requestedMode,
|
|
3761
4697
|
providerAvailable: Boolean(narrationProvider),
|
|
@@ -3798,13 +4734,33 @@ export async function startLocalServer(opts) {
|
|
|
3798
4734
|
if (narrationPlan.mode !== 'skip' && narrationProvider) {
|
|
3799
4735
|
const narrationStartedAtMs = Date.now();
|
|
3800
4736
|
const draft = governedAnswer.answer ?? governedAnswer.text;
|
|
3801
|
-
const
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
4737
|
+
const narrationTrace = createProviderDispatchTrace({
|
|
4738
|
+
// Answer-loop narration runs after the inner runner's AsyncLocal trace
|
|
4739
|
+
// scope. The request carries the canonical stored observer for this
|
|
4740
|
+
// run, so use it directly instead of losing the physical send here.
|
|
4741
|
+
observer: askTraceObserverForV1(request),
|
|
4742
|
+
phase: 'narration',
|
|
4743
|
+
purpose: 'research_narration',
|
|
4744
|
+
admit: (event) => {
|
|
4745
|
+
const ledger = agentRunProviderEvidenceContext.getStore();
|
|
4746
|
+
if (ledger) {
|
|
4747
|
+
return ledger.observe(event, {
|
|
4748
|
+
purpose: 'research_narration',
|
|
4749
|
+
dispatchPhase: 'narration',
|
|
4750
|
+
optIn: researchRowsOptIn && narrationMaxRows > 0,
|
|
4751
|
+
serializedResultShape: {
|
|
4752
|
+
resultRowCount: providerPreview?.rows.length ?? 0,
|
|
4753
|
+
columnCount: providerPreview?.columns.length ?? 0,
|
|
4754
|
+
},
|
|
4755
|
+
});
|
|
4756
|
+
}
|
|
4757
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
|
|
4758
|
+
assertProviderPayloadAllowed(envelope, {
|
|
4759
|
+
allowResultRows: false,
|
|
4760
|
+
maxResultRows: 0,
|
|
4761
|
+
purpose: 'research_narration',
|
|
4762
|
+
});
|
|
4763
|
+
return envelope;
|
|
3808
4764
|
},
|
|
3809
4765
|
});
|
|
3810
4766
|
const narrationDispatchOptions = (factCount = 0) => ({
|
|
@@ -3819,9 +4775,7 @@ export async function startLocalServer(opts) {
|
|
|
3819
4775
|
maxTokens: narrationMaxTokensForFacts(factCount),
|
|
3820
4776
|
temperature: 0.3,
|
|
3821
4777
|
maxProviderDispatches: 2,
|
|
3822
|
-
...
|
|
3823
|
-
? { onProviderDispatch: observeNarrationDispatch }
|
|
3824
|
-
: {}),
|
|
4778
|
+
...narrationTrace.options,
|
|
3825
4779
|
});
|
|
3826
4780
|
try {
|
|
3827
4781
|
const frame = governedAnswer.resolvedAnalyticalPlan?.analyticalFrame;
|
|
@@ -3881,8 +4835,10 @@ export async function startLocalServer(opts) {
|
|
|
3881
4835
|
validationFailures: [],
|
|
3882
4836
|
};
|
|
3883
4837
|
}
|
|
4838
|
+
narrationTrace.settle('ok');
|
|
3884
4839
|
}
|
|
3885
|
-
catch {
|
|
4840
|
+
catch (error) {
|
|
4841
|
+
narrationTrace.settle(request.signal?.aborted ? 'cancelled' : 'error', error);
|
|
3886
4842
|
// Keep the governed draft on any narration failure.
|
|
3887
4843
|
synthesizedAnswer = undefined;
|
|
3888
4844
|
narrationIntegrityReceipt = {
|
|
@@ -3896,27 +4852,10 @@ export async function startLocalServer(opts) {
|
|
|
3896
4852
|
narrationDurationMs = Date.now() - narrationStartedAtMs;
|
|
3897
4853
|
}
|
|
3898
4854
|
}
|
|
3899
|
-
//
|
|
3900
|
-
// not
|
|
3901
|
-
// the
|
|
3902
|
-
|
|
3903
|
-
const narrated = narrationSource !== undefined;
|
|
3904
|
-
const rowsSent = narrated ? providerPreview?.rows.length ?? 0 : 0;
|
|
3905
|
-
providerEgressReceipts.push(createProviderEgressReceipt({
|
|
3906
|
-
purpose: 'answer_narration',
|
|
3907
|
-
provider: narrated ? narrationProvider?.name ?? 'none' : 'none',
|
|
3908
|
-
permittedCategories: rowsSent > 0
|
|
3909
|
-
? ['instructions', 'question', 'governed_context', 'result_rows']
|
|
3910
|
-
: ['instructions', 'question', 'governed_context'],
|
|
3911
|
-
optIn: rowsSent > 0,
|
|
3912
|
-
redactionPolicyId: rowEgress.policyId,
|
|
3913
|
-
payload: narrated
|
|
3914
|
-
? { question: request.question, resultPreview: rowsSent > 0 ? providerPreview : undefined }
|
|
3915
|
-
: { resultRows: 0, providerDisabled: !narrationProvider },
|
|
3916
|
-
resultRowCount: rowsSent,
|
|
3917
|
-
columnCount: narrated ? providerPreview?.columns.length ?? 0 : 0,
|
|
3918
|
-
}));
|
|
3919
|
-
}
|
|
4855
|
+
// Provider egress receipts are evidence of a physical provider dispatch,
|
|
4856
|
+
// not a completeness checklist. `RunScopedProviderDispatchEvidence.observe`
|
|
4857
|
+
// writes the receipt immediately before a real call; deliberately skipped
|
|
4858
|
+
// narration therefore has no synthetic `answer_narration` receipt.
|
|
3920
4859
|
// A grounding/modeling gap is a REFUSAL. It was computed above but never
|
|
3921
4860
|
// reached this ternary, so it fell through to `needs_review` — the same
|
|
3922
4861
|
// status as a perfectly good uncertified answer. Every downstream trust
|
|
@@ -4149,6 +5088,9 @@ export async function startLocalServer(opts) {
|
|
|
4149
5088
|
...(governedAnswer.exploratoryExecutionFreeze
|
|
4150
5089
|
? { analyticalExecutionFreeze: governedAnswer.exploratoryExecutionFreeze }
|
|
4151
5090
|
: {}),
|
|
5091
|
+
...(governedAnswer.exploratoryRepairExecutionFreeze
|
|
5092
|
+
? { analyticalExecutionRepairFreeze: governedAnswer.exploratoryRepairExecutionFreeze }
|
|
5093
|
+
: {}),
|
|
4152
5094
|
};
|
|
4153
5095
|
};
|
|
4154
5096
|
/**
|
|
@@ -4209,18 +5151,44 @@ export async function startLocalServer(opts) {
|
|
|
4209
5151
|
...(request.history ?? []).slice(-6).map((message) => ({ role: message.role, content: message.text })),
|
|
4210
5152
|
{ role: 'user', content: request.question },
|
|
4211
5153
|
];
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
5154
|
+
// Conversation generation is a physical provider transport too.
|
|
5155
|
+
// It is outside the router's candidate-ID meaning call, so retain its
|
|
5156
|
+
// actual `generation` phase rather than relabelling it as meaning.
|
|
5157
|
+
const conversationTrace = createProviderDispatchTrace({
|
|
5158
|
+
observer: askTraceObserverForV1(request),
|
|
5159
|
+
phase: 'generation',
|
|
5160
|
+
purpose: 'answer_generation',
|
|
5161
|
+
admit: (event) => {
|
|
5162
|
+
const ledger = agentRunProviderEvidenceContext.getStore();
|
|
5163
|
+
if (ledger) {
|
|
5164
|
+
return ledger.observe(event, {
|
|
5165
|
+
purpose: 'answer_generation',
|
|
5166
|
+
dispatchPhase: 'generation',
|
|
5167
|
+
optIn: false,
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
5170
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
|
|
5171
|
+
assertProviderPayloadAllowed(envelope, {
|
|
5172
|
+
allowResultRows: false,
|
|
5173
|
+
maxResultRows: 0,
|
|
4218
5174
|
purpose: 'answer_generation',
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
5175
|
+
});
|
|
5176
|
+
return envelope;
|
|
5177
|
+
},
|
|
5178
|
+
});
|
|
5179
|
+
try {
|
|
5180
|
+
text = (await provider.generate(messages, {
|
|
5181
|
+
maxTokens: 320,
|
|
5182
|
+
temperature: 0.6,
|
|
5183
|
+
maxProviderDispatches: 2,
|
|
5184
|
+
...conversationTrace.options,
|
|
5185
|
+
})).trim();
|
|
5186
|
+
conversationTrace.settle('ok');
|
|
5187
|
+
}
|
|
5188
|
+
catch (error) {
|
|
5189
|
+
conversationTrace.settle(request.signal?.aborted ? 'cancelled' : 'error', error);
|
|
5190
|
+
throw error;
|
|
5191
|
+
}
|
|
4224
5192
|
// Perceived-latency: surface the reply as one delta for surfaces wired to stream.
|
|
4225
5193
|
if (text)
|
|
4226
5194
|
emitAnswerDelta?.(text);
|
|
@@ -4647,6 +5615,7 @@ export async function startLocalServer(opts) {
|
|
|
4647
5615
|
skill_draft: skillAuthoringRunExecutor,
|
|
4648
5616
|
research: async (researchContext) => {
|
|
4649
5617
|
const { runId, request, routeDecision, emit } = researchContext;
|
|
5618
|
+
const trace = askTraceObserverForV1(request);
|
|
4650
5619
|
const metrics = loadSemanticMetrics(projectRoot);
|
|
4651
5620
|
let blocks = collectPlanBlocks(projectRoot, { certifiedOnly: true });
|
|
4652
5621
|
const usedCertifiedOnly = blocks.length > 0;
|
|
@@ -4664,9 +5633,22 @@ export async function startLocalServer(opts) {
|
|
|
4664
5633
|
// unreachable, `planResearch` keeps its deterministic template, so
|
|
4665
5634
|
// research never depends on a model being available.
|
|
4666
5635
|
const researchPlanner = resolveGovernedAnswerRunner(projectRoot);
|
|
4667
|
-
const
|
|
5636
|
+
const rawResearchPlannerProvider = researchPlanner
|
|
4668
5637
|
? createGovernedTextProvider(researchPlanner.provider, projectRoot)
|
|
4669
5638
|
: undefined;
|
|
5639
|
+
const researchPlannerProvider = rawResearchPlannerProvider
|
|
5640
|
+
? createResearchHypothesisPlanningProvider({
|
|
5641
|
+
provider: rawResearchPlannerProvider,
|
|
5642
|
+
request,
|
|
5643
|
+
ledger: agentRunProviderEvidenceContext.getStore(),
|
|
5644
|
+
})
|
|
5645
|
+
: undefined;
|
|
5646
|
+
const researchPlanSpan = trace.startSpan({
|
|
5647
|
+
name: 'research.plan',
|
|
5648
|
+
stage: 'research',
|
|
5649
|
+
reasonCode: 'started',
|
|
5650
|
+
payload: { kind: 'research', branchCount: 0 },
|
|
5651
|
+
});
|
|
4670
5652
|
const plan = await planResearch({
|
|
4671
5653
|
question: request.question,
|
|
4672
5654
|
metrics,
|
|
@@ -4678,14 +5660,30 @@ export async function startLocalServer(opts) {
|
|
|
4678
5660
|
// When the user explicitly picked research, investigate — don't collapse to one step.
|
|
4679
5661
|
forceInvestigate: request.requestedMode === 'research',
|
|
4680
5662
|
rootPlan: routeDecision?.resolvedAnalyticalPlan,
|
|
5663
|
+
// Hypothesis planning is bounded by the same request deadline as
|
|
5664
|
+
// every child branch. The planner's internal 20s timer is only an
|
|
5665
|
+
// additional safety limit, never a replacement for cancellation.
|
|
5666
|
+
signal: request.signal,
|
|
4681
5667
|
});
|
|
4682
5668
|
// Direct governed answer ("the metric answers this directly"): don't wrap it in
|
|
4683
5669
|
// a review-required research dossier — run the query through the answer loop and
|
|
4684
5670
|
// return the executed result table + DQL artifact. A dossier is only for genuine
|
|
4685
5671
|
// multi-step investigation (plan.done === false) or explicit research mode.
|
|
4686
5672
|
if (plan.done && !plan.followUp && request.requestedMode !== 'research') {
|
|
5673
|
+
trace.finishSpan(researchPlanSpan, {
|
|
5674
|
+
outcome: 'ok',
|
|
5675
|
+
reasonCode: 'completed',
|
|
5676
|
+
payload: { kind: 'research', branchCount: plan.steps.length },
|
|
5677
|
+
});
|
|
4687
5678
|
return answerRunExecutor(researchContext);
|
|
4688
5679
|
}
|
|
5680
|
+
// Research may enter before ordinary Ask selected a root execution
|
|
5681
|
+
// route, but its child requirements must still begin from the reader's
|
|
5682
|
+
// own analytical tuple. Prefer the already-frozen/meaning seed when
|
|
5683
|
+
// one exists; otherwise build one deterministic host seed from the root
|
|
5684
|
+
// question. Planner prose and a prior root SQL are never inputs here.
|
|
5685
|
+
const researchRootRequirementSeed = routeDecision?.meaningResolution?.hostRequirementSeed
|
|
5686
|
+
?? buildAnalyticalRequirementSeedV1({ question: request.question });
|
|
4689
5687
|
// This is the executable, receipt-bound research plan. It carries the
|
|
4690
5688
|
// branch hypothesis/expectation/validator kind into every child rather
|
|
4691
5689
|
// than treating V2 as a presentation-only wrapper after the work ends.
|
|
@@ -4698,6 +5696,11 @@ export async function startLocalServer(opts) {
|
|
|
4698
5696
|
validatorKind: inferResearchValidatorKind(step.thought, step.expectation),
|
|
4699
5697
|
})),
|
|
4700
5698
|
});
|
|
5699
|
+
trace.finishSpan(researchPlanSpan, {
|
|
5700
|
+
outcome: 'ok',
|
|
5701
|
+
reasonCode: 'completed',
|
|
5702
|
+
payload: { kind: 'research', branchCount: typedResearchPlan.hypotheses.length },
|
|
5703
|
+
});
|
|
4701
5704
|
// The V2 contract is the executable branch authority: retain its stable
|
|
4702
5705
|
// hypothesis ID, wording, expectation, target, and validator kind while
|
|
4703
5706
|
// borrowing only the already-grounded action kind from the planner. This
|
|
@@ -4717,6 +5720,17 @@ export async function startLocalServer(opts) {
|
|
|
4717
5720
|
action: { ...planned.action, target: hypothesis.targetId },
|
|
4718
5721
|
}];
|
|
4719
5722
|
});
|
|
5723
|
+
// `typedResearchPlan` can retain a hypothesis that no longer has a
|
|
5724
|
+
// matching, catalog-grounded executable action (for example after a
|
|
5725
|
+
// duplicate target is folded during planning). Scope must describe the
|
|
5726
|
+
// branch set that can actually be investigated, not the presentation
|
|
5727
|
+
// plan that happened to be typed before that admission check.
|
|
5728
|
+
//
|
|
5729
|
+
// Keep this count independent of execution success: a provider or local
|
|
5730
|
+
// execution failure is a failed branch, not evidence that a supported
|
|
5731
|
+
// branch was never available. The ledger will still make failed verdicts
|
|
5732
|
+
// explicit below.
|
|
5733
|
+
let groundableResearchBranchCount = executableResearchBranches.length;
|
|
4720
5734
|
const typedHypothesesById = new Map(typedResearchPlan.hypotheses.map((hypothesis) => [hypothesis.id, hypothesis]));
|
|
4721
5735
|
const needsClarification = Boolean(plan.followUp);
|
|
4722
5736
|
const notebookPath = agentRunNotebookPath(request, runId);
|
|
@@ -4740,8 +5754,144 @@ export async function startLocalServer(opts) {
|
|
|
4740
5754
|
};
|
|
4741
5755
|
let researchRun;
|
|
4742
5756
|
const researchRuns = [];
|
|
5757
|
+
const researchBranchSpans = new Map();
|
|
5758
|
+
// These outcomes are independent of the notebook storage status. A
|
|
5759
|
+
// skipped branch is persisted as a reviewable child record (storage has
|
|
5760
|
+
// no `skipped` state), while the immutable Ask artifact/trace retains the
|
|
5761
|
+
// precise bounded Research verdict and stop reason.
|
|
5762
|
+
const researchBranchOutcomes = new Map();
|
|
5763
|
+
const researchBranchReceipts = [];
|
|
4743
5764
|
let researchWorkspaceError;
|
|
5765
|
+
// The notebook root is created before child execution. Keep its stable
|
|
5766
|
+
// identity outside the storage scope so a root deadline or user
|
|
5767
|
+
// cancellation can still persist a content-safe, restart-readable Ask
|
|
5768
|
+
// artifact after the child storage handle has closed.
|
|
5769
|
+
let researchRootRunId;
|
|
5770
|
+
let partialResearchArtifactPersisted = false;
|
|
5771
|
+
// The engine races every executor against the root cancellation signal.
|
|
5772
|
+
// Keep every admitted child in the current bounded wave available to a
|
|
5773
|
+
// synchronous root abort listener so the immutable partial artifact is
|
|
5774
|
+
// emitted before the outer engine resumes and persists the terminal run.
|
|
5775
|
+
const activeResearchBranches = new Map();
|
|
5776
|
+
let rootAbortReceiptPersisted = false;
|
|
5777
|
+
// Concurrent branches settle in wall-clock order. Receipts are product
|
|
5778
|
+
// evidence, so retain the planner order independently of timing and
|
|
5779
|
+
// never duplicate a child that the root abort listener already closed.
|
|
5780
|
+
const recordResearchBranchReceipt = (receipt) => {
|
|
5781
|
+
if (researchBranchReceipts.some((existing) => existing.childRunId === receipt.childRunId))
|
|
5782
|
+
return false;
|
|
5783
|
+
researchBranchReceipts.push(receipt);
|
|
5784
|
+
researchBranchReceipts.sort((left, right) => left.index - right.index || left.branchId.localeCompare(right.branchId));
|
|
5785
|
+
return true;
|
|
5786
|
+
};
|
|
5787
|
+
const partialResearchLedgerSnapshot = () => {
|
|
5788
|
+
const runsById = new Map(researchRuns.map((run) => [run.id, run]));
|
|
5789
|
+
const entries = researchBranchReceipts.slice(0, 6).map((receipt) => {
|
|
5790
|
+
const branch = runsById.get(receipt.childRunId);
|
|
5791
|
+
const branchContext = agentRunRecord(branch?.context)?.branch;
|
|
5792
|
+
const previewRecord = agentRunRecord(branch?.resultPreview);
|
|
5793
|
+
const executionReceipt = normalizeAnalyticalExecutionReceipt(previewRecord?.executionReceipt);
|
|
5794
|
+
const resultFingerprint = normalizeAnalyticalExecutionFingerprint(previewRecord?.resultFingerprint)
|
|
5795
|
+
?? executionReceipt?.resultFingerprint;
|
|
5796
|
+
const observed = receipt.state === 'completed'
|
|
5797
|
+
&& branch?.status === 'ready'
|
|
5798
|
+
&& Boolean(resultFingerprint);
|
|
5799
|
+
const status = observed
|
|
5800
|
+
? 'observed'
|
|
5801
|
+
: receipt.state === 'skipped'
|
|
5802
|
+
? 'skipped'
|
|
5803
|
+
: 'failed';
|
|
5804
|
+
return {
|
|
5805
|
+
id: receipt.childRunId,
|
|
5806
|
+
branchId: receipt.branchId,
|
|
5807
|
+
question: branch?.question
|
|
5808
|
+
?? typedHypothesesById.get(receipt.branchId)?.statement
|
|
5809
|
+
?? `Research branch ${receipt.index}`,
|
|
5810
|
+
status,
|
|
5811
|
+
...(previewRecord && Array.isArray(previewRecord.rows)
|
|
5812
|
+
? { rowCount: previewRecord.rows.length }
|
|
5813
|
+
: {}),
|
|
5814
|
+
...(resultFingerprint ? { resultFingerprint } : {}),
|
|
5815
|
+
...(executionReceipt ? { executionReceipt } : {}),
|
|
5816
|
+
facts: [agentRunString(branchContext?.expectation)
|
|
5817
|
+
?? typedHypothesesById.get(receipt.branchId)?.expectation
|
|
5818
|
+
?? `Research branch ${receipt.index} did not complete before the root run stopped.`],
|
|
5819
|
+
receipts: observed && resultFingerprint ? [resultFingerprint] : [],
|
|
5820
|
+
...(!observed ? {
|
|
5821
|
+
error: researchBranchOutcomes.get(receipt.branchId)?.error
|
|
5822
|
+
?? branch?.error
|
|
5823
|
+
?? (receipt.stopReason === 'run_deadline'
|
|
5824
|
+
? 'Research root reached its deadline while this branch was active.'
|
|
5825
|
+
: receipt.stopReason === 'cancelled'
|
|
5826
|
+
? 'Research was stopped by the user while this branch was active.'
|
|
5827
|
+
: 'Research branch did not produce an execution receipt or result fingerprint.'),
|
|
5828
|
+
} : {}),
|
|
5829
|
+
};
|
|
5830
|
+
});
|
|
5831
|
+
const researchLedger = buildResearchEvidenceLedger({
|
|
5832
|
+
rootQuestion: request.question,
|
|
5833
|
+
planId: plan.rootPlanId,
|
|
5834
|
+
snapshotId: routeDecision?.resolvedAnalyticalPlan?.snapshotId,
|
|
5835
|
+
entries,
|
|
5836
|
+
// The partial artifact is evidence for restart/review only; it is
|
|
5837
|
+
// never an accepted Research conclusion.
|
|
5838
|
+
stoppingReason: 'blocked',
|
|
5839
|
+
});
|
|
5840
|
+
return {
|
|
5841
|
+
researchLedger,
|
|
5842
|
+
researchLedgerV2: buildResearchEvidenceLedgerV2({
|
|
5843
|
+
rootQuestion: request.question,
|
|
5844
|
+
planId: plan.rootPlanId,
|
|
5845
|
+
snapshotId: routeDecision?.resolvedAnalyticalPlan?.snapshotId,
|
|
5846
|
+
groundableBranchCount: groundableResearchBranchCount,
|
|
5847
|
+
entries: researchLedger.entries.map((entry) => ({
|
|
5848
|
+
...entry,
|
|
5849
|
+
hypothesis: typedHypothesesById.get(entry.branchId)?.statement,
|
|
5850
|
+
verdict: entry.status === 'observed'
|
|
5851
|
+
? 'inconclusive'
|
|
5852
|
+
: entry.status === 'skipped'
|
|
5853
|
+
? 'skipped'
|
|
5854
|
+
: 'failed',
|
|
5855
|
+
counterEvidenceFactIds: [],
|
|
5856
|
+
})),
|
|
5857
|
+
stoppingReason: researchLedger.stoppingReason,
|
|
5858
|
+
}),
|
|
5859
|
+
};
|
|
5860
|
+
};
|
|
5861
|
+
const persistPartialResearchArtifact = (terminalReason) => {
|
|
5862
|
+
if (partialResearchArtifactPersisted || !researchRootRunId)
|
|
5863
|
+
return;
|
|
5864
|
+
const { researchLedger, researchLedgerV2 } = partialResearchLedgerSnapshot();
|
|
5865
|
+
const childRunIds = [...new Set(researchBranchReceipts.map((receipt) => receipt.childRunId))];
|
|
5866
|
+
const branchTrace = researchBranchReceipts.map((receipt) => ({
|
|
5867
|
+
branchId: receipt.branchId,
|
|
5868
|
+
childRunId: receipt.childRunId,
|
|
5869
|
+
spanId: researchBranchSpans.get(receipt.branchId)?.spanId,
|
|
5870
|
+
kind: 'research_branch',
|
|
5871
|
+
}));
|
|
5872
|
+
const artifact = agentRunArtifact('research_run', 'Partial Research evidence', {
|
|
5873
|
+
version: 2,
|
|
5874
|
+
partial: true,
|
|
5875
|
+
terminalReason,
|
|
5876
|
+
rootResearchRunId: researchRootRunId,
|
|
5877
|
+
childRunIds,
|
|
5878
|
+
researchBranchReceipts: [...researchBranchReceipts],
|
|
5879
|
+
researchLedger,
|
|
5880
|
+
researchLedgerV2,
|
|
5881
|
+
traceReference: trace.reference(),
|
|
5882
|
+
researchTrace: { branchTrace },
|
|
5883
|
+
}, researchRootRunId, 'blocked');
|
|
5884
|
+
partialResearchArtifactPersisted = true;
|
|
5885
|
+
emit({
|
|
5886
|
+
type: 'artifact.created',
|
|
5887
|
+
message: 'Saved partial Research receipts before the root run stopped.',
|
|
5888
|
+
route: 'research',
|
|
5889
|
+
trustState: 'blocked',
|
|
5890
|
+
payload: artifact,
|
|
5891
|
+
});
|
|
5892
|
+
};
|
|
4744
5893
|
if (!needsClarification) {
|
|
5894
|
+
let removeResearchRootAbortListener;
|
|
4745
5895
|
try {
|
|
4746
5896
|
const storage = openNotebookResearchStorage();
|
|
4747
5897
|
try {
|
|
@@ -4762,6 +5912,70 @@ export async function startLocalServer(opts) {
|
|
|
4762
5912
|
owner: agentRunWorkspaceValue(request, 'owner'),
|
|
4763
5913
|
context: researchContextEnvelope,
|
|
4764
5914
|
});
|
|
5915
|
+
researchRootRunId = created.id;
|
|
5916
|
+
const persistRootAbortBeforeEngineFinalizes = () => {
|
|
5917
|
+
if (rootAbortReceiptPersisted || !request.signal?.aborted)
|
|
5918
|
+
return;
|
|
5919
|
+
rootAbortReceiptPersisted = true;
|
|
5920
|
+
const userCancelled = isAgentRunUserCancellation(request.signal.reason);
|
|
5921
|
+
const terminalReason = userCancelled
|
|
5922
|
+
? 'cancelled'
|
|
5923
|
+
: 'run_deadline';
|
|
5924
|
+
for (const active of [...activeResearchBranches.values()].sort((left, right) => left.index - right.index)) {
|
|
5925
|
+
if (researchBranchReceipts.some((receipt) => receipt.childRunId === active.childRunId))
|
|
5926
|
+
continue;
|
|
5927
|
+
const message = userCancelled
|
|
5928
|
+
? 'Research was stopped by the user while this branch was active.'
|
|
5929
|
+
: 'Research root reached its deadline while this branch was active.';
|
|
5930
|
+
storage.updateRun(active.childRunId, {
|
|
5931
|
+
status: 'error',
|
|
5932
|
+
error: message,
|
|
5933
|
+
summary: 'Research branch stopped before producing a result.',
|
|
5934
|
+
reviewStatus: 'needs_review',
|
|
5935
|
+
});
|
|
5936
|
+
const stopped = storage.getRun(active.childRunId);
|
|
5937
|
+
if (stopped)
|
|
5938
|
+
researchRuns.push(withNotebookResearchChecklist(stopped));
|
|
5939
|
+
researchBranchOutcomes.set(active.branchId, {
|
|
5940
|
+
status: 'failed',
|
|
5941
|
+
stopReason: terminalReason,
|
|
5942
|
+
error: message,
|
|
5943
|
+
});
|
|
5944
|
+
recordResearchBranchReceipt({
|
|
5945
|
+
version: 1,
|
|
5946
|
+
branchId: active.branchId,
|
|
5947
|
+
childRunId: active.childRunId,
|
|
5948
|
+
index: active.index,
|
|
5949
|
+
state: 'failed',
|
|
5950
|
+
verdict: 'failed',
|
|
5951
|
+
stopReason: terminalReason,
|
|
5952
|
+
branchBudgetMs: active.branchBudgetMs,
|
|
5953
|
+
});
|
|
5954
|
+
trace.finishSpan(active.branchSpan, {
|
|
5955
|
+
outcome: userCancelled ? 'cancelled' : 'error',
|
|
5956
|
+
reasonCode: terminalReason,
|
|
5957
|
+
payload: {
|
|
5958
|
+
kind: 'research',
|
|
5959
|
+
branchId: active.branchId,
|
|
5960
|
+
hypothesisFingerprint: active.hypothesisFingerprint,
|
|
5961
|
+
verdict: 'failed',
|
|
5962
|
+
branchBudgetMs: active.branchBudgetMs,
|
|
5963
|
+
branchStopReason: terminalReason,
|
|
5964
|
+
},
|
|
5965
|
+
});
|
|
5966
|
+
}
|
|
5967
|
+
storage.updateRun(created.id, {
|
|
5968
|
+
status: 'error',
|
|
5969
|
+
error: userCancelled
|
|
5970
|
+
? 'Research was stopped by the user while a branch was active.'
|
|
5971
|
+
: 'Research root reached its deadline while a branch was active.',
|
|
5972
|
+
summary: 'Partial Research receipts were saved before the root run stopped.',
|
|
5973
|
+
reviewStatus: 'needs_review',
|
|
5974
|
+
});
|
|
5975
|
+
persistPartialResearchArtifact(terminalReason);
|
|
5976
|
+
};
|
|
5977
|
+
request.signal?.addEventListener('abort', persistRootAbortBeforeEngineFinalizes, { once: true });
|
|
5978
|
+
removeResearchRootAbortListener = () => request.signal?.removeEventListener('abort', persistRootAbortBeforeEngineFinalizes);
|
|
4765
5979
|
emit({
|
|
4766
5980
|
type: 'artifact.created',
|
|
4767
5981
|
message: 'Saved the immutable root research plan; executing bounded child branches.',
|
|
@@ -4788,6 +6002,15 @@ export async function startLocalServer(opts) {
|
|
|
4788
6002
|
expectation: 'Whether the frozen context contains enough evidence for a bounded answer.',
|
|
4789
6003
|
};
|
|
4790
6004
|
const branches = capResearchBranches(executableResearchBranches.length > 0 ? executableResearchBranches : [fallbackBranch], 6);
|
|
6005
|
+
// An explicit Research request with no catalog-grounded planner
|
|
6006
|
+
// step still has one bounded frozen-context branch. It is visible
|
|
6007
|
+
// as limited scope rather than being padded with invented
|
|
6008
|
+
// hypotheses or silently reported as three successful checks.
|
|
6009
|
+
// That fallback is an observable attempt, not catalog-grounded
|
|
6010
|
+
// evidence, so it must not inflate the claimed groundable count.
|
|
6011
|
+
groundableResearchBranchCount = executableResearchBranches.length > 0
|
|
6012
|
+
? branches.length
|
|
6013
|
+
: 0;
|
|
4791
6014
|
// The replan edge. Each branch tests one hypothesis; folding its
|
|
4792
6015
|
// outcome back into the state is what lets the investigation stop
|
|
4793
6016
|
// when the question is settled instead of grinding through a plan
|
|
@@ -4799,24 +6022,54 @@ export async function startLocalServer(opts) {
|
|
|
4799
6022
|
statement: branch.thought,
|
|
4800
6023
|
priorConfidence: 1 - position / (branches.length + 1),
|
|
4801
6024
|
})));
|
|
4802
|
-
|
|
6025
|
+
const executeResearchBranch = async (index, branchBudget) => {
|
|
4803
6026
|
const step = branches[index];
|
|
4804
|
-
if (request.signal?.aborted)
|
|
4805
|
-
rethrowIfCancelled(request.signal.reason, request.signal);
|
|
4806
|
-
// A hypothesis an earlier finding already closed is not
|
|
4807
|
-
// re-investigated, and an exhausted hop budget stops the run.
|
|
4808
|
-
const stillOpen = nextHypothesis(researchState);
|
|
4809
|
-
if (!stillOpen) {
|
|
4810
|
-
emit({
|
|
4811
|
-
type: 'executor.started',
|
|
4812
|
-
message: `Stopping early: ${researchState.hopsUsed} of ${branches.length} branches settled what could be settled.`,
|
|
4813
|
-
route: 'research',
|
|
4814
|
-
});
|
|
4815
|
-
break;
|
|
4816
|
-
}
|
|
4817
6027
|
const branchId = step.hypothesisId;
|
|
4818
|
-
|
|
6028
|
+
// The planner asset is an evidence hint, not a client-selected
|
|
6029
|
+
// identifier. Put it in the child question so the shared Ask
|
|
6030
|
+
// retriever can bind it against the child snapshot, rather than
|
|
6031
|
+
// treating a human-facing planner label as a forged structured
|
|
6032
|
+
// selection. The child router still owns the qualified IDs,
|
|
6033
|
+
// relationship closure, and frozen RAP.
|
|
6034
|
+
// Each child is an ordinary, bounded analytical Ask. Do not
|
|
6035
|
+
// carry the root's "Research …" wording into its source
|
|
6036
|
+
// question: the router correctly interprets that as a request
|
|
6037
|
+
// to open another Research run instead of resolving and
|
|
6038
|
+
// freezing this hypothesis's own tuple. The root/child link is
|
|
6039
|
+
// retained in the trace and workspace context below; this text
|
|
6040
|
+
// is only the child analytical requirement seed.
|
|
6041
|
+
// The planner target is an evidence hint, never an executable
|
|
6042
|
+
// query. Project it with the root host requirements and the
|
|
6043
|
+
// typed action before a child Ask sees it. This keeps a time
|
|
6044
|
+
// comparison or breakdown tied to the root metric instead of
|
|
6045
|
+
// collapsing every branch to the target label/baseline metric.
|
|
6046
|
+
const branchProjection = buildResearchBranchRequirementProjection({
|
|
6047
|
+
action: step.action,
|
|
6048
|
+
rootRequirementSeed: researchRootRequirementSeed,
|
|
6049
|
+
rootPlan: routeDecision?.resolvedAnalyticalPlan,
|
|
6050
|
+
});
|
|
6051
|
+
const branchQuestion = branchProjection.question;
|
|
6052
|
+
const branchRequirementSeed = branchProjection.requirementSeed;
|
|
4819
6053
|
const childId = `${created.id}:research:${index + 1}`;
|
|
6054
|
+
const hypothesisFingerprint = runtimeTraceFingerprint(`${step.thought}\u0000${step.expectation}`);
|
|
6055
|
+
const branchSpan = trace.startSpan({
|
|
6056
|
+
name: 'research.validate',
|
|
6057
|
+
stage: 'research',
|
|
6058
|
+
reasonCode: branchBudget.stopReason ?? 'started',
|
|
6059
|
+
payload: {
|
|
6060
|
+
kind: 'research',
|
|
6061
|
+
branchId,
|
|
6062
|
+
hypothesisFingerprint,
|
|
6063
|
+
...(branchBudget.branchBudgetMs ? { branchBudgetMs: branchBudget.branchBudgetMs } : {}),
|
|
6064
|
+
...(branchBudget.stopReason ? { branchStopReason: branchBudget.stopReason } : {}),
|
|
6065
|
+
},
|
|
6066
|
+
});
|
|
6067
|
+
researchBranchSpans.set(branchId, { spanId: branchSpan, hypothesisFingerprint });
|
|
6068
|
+
trace.recordLink({
|
|
6069
|
+
kind: 'research_branch',
|
|
6070
|
+
targetRunId: childId,
|
|
6071
|
+
hypothesisFingerprint,
|
|
6072
|
+
});
|
|
4820
6073
|
const child = storage.createRun({
|
|
4821
6074
|
id: childId,
|
|
4822
6075
|
notebookPath,
|
|
@@ -4844,6 +6097,43 @@ export async function startLocalServer(opts) {
|
|
|
4844
6097
|
},
|
|
4845
6098
|
},
|
|
4846
6099
|
});
|
|
6100
|
+
if (!branchBudget.branchBudgetMs) {
|
|
6101
|
+
const message = 'Research did not start this branch because the remaining run time is reserved for synthesis and persistence.';
|
|
6102
|
+
storage.updateRun(child.id, {
|
|
6103
|
+
status: 'error',
|
|
6104
|
+
error: message,
|
|
6105
|
+
summary: 'Research branch skipped because the bounded branch budget was exhausted.',
|
|
6106
|
+
reviewStatus: 'needs_review',
|
|
6107
|
+
});
|
|
6108
|
+
const skipped = storage.getRun(child.id);
|
|
6109
|
+
if (skipped)
|
|
6110
|
+
researchRuns.push(withNotebookResearchChecklist(skipped));
|
|
6111
|
+
researchBranchOutcomes.set(branchId, {
|
|
6112
|
+
status: 'skipped',
|
|
6113
|
+
stopReason: 'budget_exhausted',
|
|
6114
|
+
error: message,
|
|
6115
|
+
});
|
|
6116
|
+
recordResearchBranchReceipt({
|
|
6117
|
+
version: 1,
|
|
6118
|
+
branchId,
|
|
6119
|
+
childRunId: child.id,
|
|
6120
|
+
index: index + 1,
|
|
6121
|
+
state: 'skipped',
|
|
6122
|
+
verdict: 'skipped',
|
|
6123
|
+
stopReason: 'budget_exhausted',
|
|
6124
|
+
});
|
|
6125
|
+
trace.finishSpan(branchSpan, {
|
|
6126
|
+
outcome: 'skipped',
|
|
6127
|
+
reasonCode: 'budget_exhausted',
|
|
6128
|
+
payload: {
|
|
6129
|
+
kind: 'research',
|
|
6130
|
+
branchId,
|
|
6131
|
+
hypothesisFingerprint,
|
|
6132
|
+
branchStopReason: 'budget_exhausted',
|
|
6133
|
+
},
|
|
6134
|
+
});
|
|
6135
|
+
return { index, summary: message, strength: 0 };
|
|
6136
|
+
}
|
|
4847
6137
|
emit({
|
|
4848
6138
|
type: 'artifact.created',
|
|
4849
6139
|
message: `Started research branch ${index + 1} of ${branches.length}.`,
|
|
@@ -4851,36 +6141,168 @@ export async function startLocalServer(opts) {
|
|
|
4851
6141
|
trustState: 'review_required',
|
|
4852
6142
|
payload: { researchRunId: child.id, parentResearchRunId: created.id, branchId },
|
|
4853
6143
|
});
|
|
6144
|
+
const branchTimeout = AbortSignal.timeout(branchBudget.branchBudgetMs);
|
|
6145
|
+
const branchSignal = request.signal
|
|
6146
|
+
? AbortSignal.any([request.signal, branchTimeout])
|
|
6147
|
+
: branchTimeout;
|
|
6148
|
+
activeResearchBranches.set(child.id, {
|
|
6149
|
+
branchId,
|
|
6150
|
+
childRunId: child.id,
|
|
6151
|
+
index: index + 1,
|
|
6152
|
+
branchBudgetMs: branchBudget.branchBudgetMs,
|
|
6153
|
+
branchSpan,
|
|
6154
|
+
hypothesisFingerprint,
|
|
6155
|
+
});
|
|
4854
6156
|
try {
|
|
4855
|
-
const executed = await
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
6157
|
+
const executed = await awaitResearchBranchDeadline((async () => {
|
|
6158
|
+
/**
|
|
6159
|
+
* A Research branch is a child Ask run, not a notebook-SQL
|
|
6160
|
+
* shortcut. The root Research executor owns the one
|
|
6161
|
+
* run-scoped dispatch ledger, so preserving its AsyncLocal
|
|
6162
|
+
* context here makes planning, child meaning/generation,
|
|
6163
|
+
* bounded repair, and narration compete for the same
|
|
6164
|
+
* Research-12 physical-send budget. The child request has
|
|
6165
|
+
* no conversation/history or root SQL: each hypothesis must
|
|
6166
|
+
* retrieve, route, and freeze its own tuple/closure.
|
|
6167
|
+
*/
|
|
6168
|
+
const branchRequest = attachAskTraceObserverV1({
|
|
6169
|
+
question: branchQuestion,
|
|
6170
|
+
...(branchRequirementSeed ? { hostRequirementSeed: branchRequirementSeed } : {}),
|
|
6171
|
+
researchBranch: {
|
|
6172
|
+
rootRunId: created.id,
|
|
6173
|
+
childRunId: child.id,
|
|
6174
|
+
branchId,
|
|
4870
6175
|
index: index + 1,
|
|
4871
|
-
expectation: step.expectation,
|
|
4872
|
-
action: step.action,
|
|
4873
6176
|
},
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
6177
|
+
requestedMode: 'ask',
|
|
6178
|
+
runId: child.id,
|
|
6179
|
+
selectedObject: {
|
|
6180
|
+
kind: 'research',
|
|
6181
|
+
id: child.id,
|
|
6182
|
+
title: child.title,
|
|
6183
|
+
},
|
|
6184
|
+
executionTarget: request.executionTarget,
|
|
6185
|
+
audience: request.audience,
|
|
6186
|
+
reasoningEffort: request.reasoningEffort,
|
|
6187
|
+
analysisDepth: request.analysisDepth,
|
|
6188
|
+
thinkingMode: request.thinkingMode,
|
|
6189
|
+
signal: branchSignal,
|
|
6190
|
+
runBudget: request.runBudget,
|
|
6191
|
+
traceSurface: request.traceSurface,
|
|
6192
|
+
workspaceContext: {
|
|
6193
|
+
...(request.workspaceContext ?? {}),
|
|
6194
|
+
researchBranch: {
|
|
6195
|
+
rootRunId: created.id,
|
|
6196
|
+
childRunId: child.id,
|
|
6197
|
+
branchId,
|
|
6198
|
+
index: index + 1,
|
|
6199
|
+
targetId: step.action.target,
|
|
6200
|
+
validatorKind: step.validatorKind,
|
|
6201
|
+
},
|
|
6202
|
+
},
|
|
6203
|
+
}, trace);
|
|
6204
|
+
const branchDecision = await agentRunRouter.decide(branchRequest);
|
|
6205
|
+
const branchRoute = selectRoute(branchRequest, branchDecision);
|
|
6206
|
+
const frozenBranchRoute = frozenAnalyticalRoute(branchDecision);
|
|
6207
|
+
const branchPlanFrozen = branchDecision.analyticalCascadeDecision?.planFrozen === true
|
|
6208
|
+
&& branchDecision.resolvedAnalyticalPlan?.mode === 'authoritative';
|
|
6209
|
+
const executableBranchRoute = branchRoute === 'certified_answer'
|
|
6210
|
+
|| branchRoute === 'semantic_answer'
|
|
6211
|
+
|| branchRoute === 'generated_answer';
|
|
6212
|
+
const governedAnswer = branchPlanFrozen
|
|
6213
|
+
&& frozenBranchRoute === branchRoute
|
|
6214
|
+
&& executableBranchRoute
|
|
6215
|
+
? await runGovernedAgentAnswerForRun(branchRequest, undefined, branchRoute, undefined, branchDecision)
|
|
6216
|
+
: {
|
|
6217
|
+
kind: 'no_answer',
|
|
6218
|
+
text: 'This Research branch did not freeze a safe, hypothesis-specific analytical plan. DQL did not reuse the root query or execute a legacy notebook SQL fallback.',
|
|
6219
|
+
answer: 'This Research branch did not freeze a safe, hypothesis-specific analytical plan. DQL did not reuse the root query or execute a legacy notebook SQL fallback.',
|
|
6220
|
+
refusalCode: branchDecision.requiresClarification ? 'ambiguous' : 'grounding_gap',
|
|
6221
|
+
intentDecision: branchDecision,
|
|
6222
|
+
resolvedAnalyticalPlan: branchDecision.resolvedAnalyticalPlan,
|
|
6223
|
+
citations: [],
|
|
6224
|
+
considered: [],
|
|
6225
|
+
};
|
|
6226
|
+
return runNotebookResearch(storage, child, {
|
|
6227
|
+
domain: agentRunWorkspaceValue(request, 'domain'),
|
|
6228
|
+
owner: agentRunWorkspaceValue(request, 'owner'),
|
|
6229
|
+
sourceCellFingerprint,
|
|
6230
|
+
question: branchQuestion,
|
|
6231
|
+
intent: researchIntent,
|
|
6232
|
+
context: {
|
|
6233
|
+
...researchContextEnvelope,
|
|
6234
|
+
rootRunId: created.id,
|
|
6235
|
+
rootPlanId: plan.rootPlanId,
|
|
6236
|
+
branch: {
|
|
6237
|
+
id: branchId,
|
|
6238
|
+
hypothesisId: step.hypothesisId,
|
|
6239
|
+
hypothesis: step.thought,
|
|
6240
|
+
validatorKind: step.validatorKind,
|
|
6241
|
+
index: index + 1,
|
|
6242
|
+
expectation: step.expectation,
|
|
6243
|
+
action: step.action,
|
|
6244
|
+
},
|
|
6245
|
+
// Persist a compact, non-prompt branch authority
|
|
6246
|
+
// witness. It lets restart/trace review distinguish a
|
|
6247
|
+
// child that honestly could not freeze from one that
|
|
6248
|
+
// executed a frozen RAP, without granting Notebook
|
|
6249
|
+
// Research a second route authority.
|
|
6250
|
+
branchAuthority: {
|
|
6251
|
+
route: branchRoute,
|
|
6252
|
+
selectedTier: branchDecision.analyticalCascadeDecision?.selectedTier,
|
|
6253
|
+
planFrozen: branchPlanFrozen,
|
|
6254
|
+
planId: branchDecision.resolvedAnalyticalPlan?.planId,
|
|
6255
|
+
planFingerprint: branchDecision.resolvedAnalyticalPlan?.fingerprint,
|
|
6256
|
+
capability: branchDecision.resolvedAnalyticalPlan?.capability,
|
|
6257
|
+
executionId: branchDecision.resolvedAnalyticalPlan?.executionId,
|
|
6258
|
+
candidateIds: branchDecision.resolvedAnalyticalPlan?.selectedConceptIds,
|
|
6259
|
+
closureIds: branchDecision.resolvedAnalyticalPlan?.sourceRelationIds,
|
|
6260
|
+
},
|
|
6261
|
+
},
|
|
6262
|
+
executionConnection: researchExecutionConnection,
|
|
6263
|
+
executionConnectionName: researchExecutionConnectionName,
|
|
6264
|
+
signal: branchSignal,
|
|
6265
|
+
authoritativeBranch: {
|
|
6266
|
+
answer: governedAnswer,
|
|
6267
|
+
routeDecision: branchDecision,
|
|
6268
|
+
},
|
|
6269
|
+
});
|
|
6270
|
+
})(), branchSignal);
|
|
4882
6271
|
const branchRun = withNotebookResearchChecklist(executed);
|
|
4883
6272
|
researchRuns.push(branchRun);
|
|
6273
|
+
const branchFailed = branchRun.status === 'error';
|
|
6274
|
+
recordResearchBranchReceipt({
|
|
6275
|
+
version: 1,
|
|
6276
|
+
branchId,
|
|
6277
|
+
childRunId: child.id,
|
|
6278
|
+
index: index + 1,
|
|
6279
|
+
state: branchFailed ? 'failed' : 'completed',
|
|
6280
|
+
verdict: branchFailed ? 'failed' : 'inconclusive',
|
|
6281
|
+
stopReason: branchFailed ? 'execution_failed' : 'completed',
|
|
6282
|
+
branchBudgetMs: branchBudget.branchBudgetMs,
|
|
6283
|
+
});
|
|
6284
|
+
if (branchFailed) {
|
|
6285
|
+
const message = branchRun.error
|
|
6286
|
+
?? branchRun.summary
|
|
6287
|
+
?? 'Research branch stopped before producing a result.';
|
|
6288
|
+
researchBranchOutcomes.set(branchId, {
|
|
6289
|
+
status: 'failed',
|
|
6290
|
+
stopReason: 'execution_failed',
|
|
6291
|
+
error: message,
|
|
6292
|
+
});
|
|
6293
|
+
trace.finishSpan(branchSpan, {
|
|
6294
|
+
outcome: 'error',
|
|
6295
|
+
reasonCode: 'execution_failed',
|
|
6296
|
+
payload: {
|
|
6297
|
+
kind: 'research',
|
|
6298
|
+
branchId,
|
|
6299
|
+
hypothesisFingerprint,
|
|
6300
|
+
verdict: 'failed',
|
|
6301
|
+
branchBudgetMs: branchBudget.branchBudgetMs,
|
|
6302
|
+
branchStopReason: 'execution_failed',
|
|
6303
|
+
},
|
|
6304
|
+
});
|
|
6305
|
+
}
|
|
4884
6306
|
// Observe, then decide. A branch that produced rows is evidence
|
|
4885
6307
|
// for its hypothesis; one that did not is inconclusive, which is
|
|
4886
6308
|
// a real outcome and not a failure.
|
|
@@ -4891,47 +6313,173 @@ export async function startLocalServer(opts) {
|
|
|
4891
6313
|
// rows as `supports` would let the dossier report a driver the
|
|
4892
6314
|
// evidence never established, which is the failure mode the
|
|
4893
6315
|
// whole verified-fact chain exists to prevent.
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
hypothesisId: `h${index + 1}`,
|
|
4897
|
-
verdict: 'inconclusive',
|
|
6316
|
+
return {
|
|
6317
|
+
index,
|
|
4898
6318
|
summary: branchRun.summary ?? '',
|
|
4899
6319
|
strength: (branchRun.resultPreview?.rows?.length ?? 0) > 0
|
|
4900
6320
|
? 0.5
|
|
4901
6321
|
: 0.1,
|
|
4902
|
-
}
|
|
6322
|
+
};
|
|
4903
6323
|
}
|
|
4904
6324
|
catch (error) {
|
|
4905
|
-
// A child is a
|
|
4906
|
-
//
|
|
4907
|
-
//
|
|
4908
|
-
|
|
6325
|
+
// A child is a durable Research record even when its own
|
|
6326
|
+
// fair-share deadline expires. Only root/user cancellation
|
|
6327
|
+
// aborts the parent; a local child timeout becomes a typed
|
|
6328
|
+
// failed branch and the remaining branches receive their own
|
|
6329
|
+
// fair share (or explicit budget-exhausted receipts).
|
|
6330
|
+
const rootCancelled = Boolean(request.signal?.aborted);
|
|
6331
|
+
const userCancelled = rootCancelled && isAgentRunUserCancellation(request.signal?.reason);
|
|
6332
|
+
// The synchronous root abort listener has already recorded the
|
|
6333
|
+
// terminal child receipt and partial artifact. Do not race it
|
|
6334
|
+
// with a second child update after the engine has finalized.
|
|
6335
|
+
if (rootCancelled && rootAbortReceiptPersisted) {
|
|
6336
|
+
rethrowIfCancelled(error, request.signal);
|
|
6337
|
+
}
|
|
6338
|
+
const timedOut = branchTimeout.aborted && !rootCancelled;
|
|
6339
|
+
const stopReason = userCancelled
|
|
6340
|
+
? 'cancelled'
|
|
6341
|
+
: rootCancelled
|
|
6342
|
+
? 'run_deadline'
|
|
6343
|
+
: timedOut
|
|
6344
|
+
? 'research_branch_timeout'
|
|
6345
|
+
: 'execution_failed';
|
|
6346
|
+
const message = timedOut
|
|
6347
|
+
? 'Research branch reached its fair-share deadline before producing a result.'
|
|
6348
|
+
: error instanceof Error ? error.message : String(error);
|
|
4909
6349
|
storage.updateRun(child.id, {
|
|
4910
6350
|
status: 'error',
|
|
4911
6351
|
error: message,
|
|
4912
|
-
summary:
|
|
6352
|
+
summary: timedOut
|
|
6353
|
+
? 'Research branch timed out within its bounded validation window.'
|
|
6354
|
+
: 'Research branch stopped before producing a result.',
|
|
4913
6355
|
reviewStatus: 'needs_review',
|
|
4914
6356
|
});
|
|
4915
6357
|
const stopped = storage.getRun(child.id);
|
|
4916
6358
|
if (stopped)
|
|
4917
6359
|
researchRuns.push(withNotebookResearchChecklist(stopped));
|
|
6360
|
+
researchBranchOutcomes.set(branchId, {
|
|
6361
|
+
status: 'failed',
|
|
6362
|
+
stopReason,
|
|
6363
|
+
error: message,
|
|
6364
|
+
});
|
|
6365
|
+
recordResearchBranchReceipt({
|
|
6366
|
+
version: 1,
|
|
6367
|
+
branchId,
|
|
6368
|
+
childRunId: child.id,
|
|
6369
|
+
index: index + 1,
|
|
6370
|
+
state: timedOut ? 'timed_out' : 'failed',
|
|
6371
|
+
verdict: 'failed',
|
|
6372
|
+
stopReason,
|
|
6373
|
+
branchBudgetMs: branchBudget.branchBudgetMs,
|
|
6374
|
+
});
|
|
6375
|
+
trace.finishSpan(branchSpan, {
|
|
6376
|
+
outcome: userCancelled ? 'cancelled' : 'error',
|
|
6377
|
+
reasonCode: userCancelled
|
|
6378
|
+
? 'cancelled'
|
|
6379
|
+
: rootCancelled
|
|
6380
|
+
? 'run_deadline'
|
|
6381
|
+
: timedOut
|
|
6382
|
+
? 'research_branch_timeout'
|
|
6383
|
+
: 'execution_failed',
|
|
6384
|
+
payload: {
|
|
6385
|
+
kind: 'research',
|
|
6386
|
+
branchId,
|
|
6387
|
+
hypothesisFingerprint,
|
|
6388
|
+
verdict: 'failed',
|
|
6389
|
+
branchBudgetMs: branchBudget.branchBudgetMs,
|
|
6390
|
+
branchStopReason: stopReason,
|
|
6391
|
+
},
|
|
6392
|
+
});
|
|
6393
|
+
if (rootCancelled) {
|
|
6394
|
+
storage.updateRun(created.id, {
|
|
6395
|
+
status: 'error',
|
|
6396
|
+
error: userCancelled
|
|
6397
|
+
? 'Research was stopped by the user while a branch was active.'
|
|
6398
|
+
: 'Research root reached its deadline while a branch was active.',
|
|
6399
|
+
summary: 'Partial Research receipts were saved before the root run stopped.',
|
|
6400
|
+
reviewStatus: 'needs_review',
|
|
6401
|
+
});
|
|
6402
|
+
persistPartialResearchArtifact(userCancelled ? 'cancelled' : 'run_deadline');
|
|
6403
|
+
rethrowIfCancelled(error, request.signal);
|
|
6404
|
+
}
|
|
6405
|
+
return { index, summary: message, strength: 0 };
|
|
6406
|
+
}
|
|
6407
|
+
finally {
|
|
6408
|
+
activeResearchBranches.delete(child.id);
|
|
6409
|
+
}
|
|
6410
|
+
};
|
|
6411
|
+
let nextBranchIndex = 0;
|
|
6412
|
+
while (nextBranchIndex < branches.length) {
|
|
6413
|
+
if (request.signal?.aborted)
|
|
6414
|
+
rethrowIfCancelled(request.signal.reason, request.signal);
|
|
6415
|
+
// Fold an entire settled wave into the deterministic hypothesis
|
|
6416
|
+
// state before admitting the next one. No child completion order
|
|
6417
|
+
// is allowed to choose later work or alter the final dossier.
|
|
6418
|
+
const stillOpen = nextHypothesis(researchState);
|
|
6419
|
+
if (!stillOpen) {
|
|
6420
|
+
emit({
|
|
6421
|
+
type: 'executor.started',
|
|
6422
|
+
message: `Stopping early: ${researchState.hopsUsed} of ${branches.length} branches settled what could be settled.`,
|
|
6423
|
+
route: 'research',
|
|
6424
|
+
});
|
|
6425
|
+
break;
|
|
6426
|
+
}
|
|
6427
|
+
const remainingBranches = branches.length - nextBranchIndex;
|
|
6428
|
+
const branchBudget = allocateResearchBranchBudget({
|
|
6429
|
+
remainingMs: request.runBudget?.remainingMs() ?? 120_000,
|
|
6430
|
+
remainingBranches,
|
|
6431
|
+
maxConcurrentBranches: RESEARCH_MAX_CONCURRENT_BRANCHES,
|
|
6432
|
+
});
|
|
6433
|
+
const waveSize = Math.min(branchBudget.maxConcurrentBranches, remainingBranches);
|
|
6434
|
+
const waveIndexes = Array.from({ length: waveSize }, (_, offset) => nextBranchIndex + offset);
|
|
6435
|
+
emit({
|
|
6436
|
+
type: 'executor.started',
|
|
6437
|
+
message: branchBudget.branchBudgetMs
|
|
6438
|
+
? `Starting Research wave ${Math.floor(nextBranchIndex / RESEARCH_MAX_CONCURRENT_BRANCHES) + 1} with ${waveIndexes.length} bounded branches.`
|
|
6439
|
+
: `Skipping ${waveIndexes.length} Research branches because the remaining root time is reserved for synthesis and persistence.`,
|
|
6440
|
+
route: 'research',
|
|
6441
|
+
});
|
|
6442
|
+
// Run at most three independent branches together. The shared
|
|
6443
|
+
// branch budget is derived from remaining waves, not from the
|
|
6444
|
+
// number of individual branches, so an early slow branch cannot
|
|
6445
|
+
// starve the entire investigation.
|
|
6446
|
+
const settled = await Promise.allSettled(waveIndexes.map((index) => executeResearchBranch(index, branchBudget)));
|
|
6447
|
+
const rejected = settled.find((entry) => entry.status === 'rejected');
|
|
6448
|
+
if (rejected)
|
|
6449
|
+
throw rejected.reason;
|
|
6450
|
+
for (const finding of settled
|
|
6451
|
+
.filter((entry) => entry.status === 'fulfilled')
|
|
6452
|
+
.map((entry) => entry.value)
|
|
6453
|
+
.sort((left, right) => left.index - right.index)) {
|
|
4918
6454
|
researchState = applyFinding(researchState, {
|
|
4919
|
-
id: `f${index + 1}`,
|
|
4920
|
-
hypothesisId: `h${index + 1}`,
|
|
6455
|
+
id: `f${finding.index + 1}`,
|
|
6456
|
+
hypothesisId: `h${finding.index + 1}`,
|
|
4921
6457
|
verdict: 'inconclusive',
|
|
4922
|
-
summary:
|
|
4923
|
-
strength:
|
|
6458
|
+
summary: finding.summary,
|
|
6459
|
+
strength: finding.strength,
|
|
4924
6460
|
});
|
|
4925
|
-
rethrowIfCancelled(error, request.signal);
|
|
4926
6461
|
}
|
|
6462
|
+
nextBranchIndex += waveIndexes.length;
|
|
4927
6463
|
}
|
|
6464
|
+
const orderedResearchRuns = [...new Map(researchRuns.map((run) => [run.id, run])).values()].sort((left, right) => {
|
|
6465
|
+
const leftBranch = agentRunRecord(left.context)?.branch;
|
|
6466
|
+
const rightBranch = agentRunRecord(right.context)?.branch;
|
|
6467
|
+
const leftIndex = typeof leftBranch?.index === 'number' ? leftBranch.index : Number.MAX_SAFE_INTEGER;
|
|
6468
|
+
const rightIndex = typeof rightBranch?.index === 'number' ? rightBranch.index : Number.MAX_SAFE_INTEGER;
|
|
6469
|
+
return leftIndex - rightIndex || left.id.localeCompare(right.id);
|
|
6470
|
+
});
|
|
6471
|
+
researchRuns.splice(0, researchRuns.length, ...orderedResearchRuns);
|
|
4928
6472
|
researchRun = researchRuns[0];
|
|
4929
6473
|
}
|
|
4930
6474
|
finally {
|
|
6475
|
+
removeResearchRootAbortListener?.();
|
|
4931
6476
|
storage.close();
|
|
4932
6477
|
}
|
|
4933
6478
|
}
|
|
4934
6479
|
catch (error) {
|
|
6480
|
+
if (request.signal?.aborted) {
|
|
6481
|
+
persistPartialResearchArtifact(isAgentRunUserCancellation(request.signal.reason) ? 'cancelled' : 'run_deadline');
|
|
6482
|
+
}
|
|
4935
6483
|
rethrowIfCancelled(error, request.signal);
|
|
4936
6484
|
researchWorkspaceError = formatNotebookResearchStorageError(error);
|
|
4937
6485
|
}
|
|
@@ -4959,6 +6507,8 @@ export async function startLocalServer(opts) {
|
|
|
4959
6507
|
snapshotId: routeDecision?.resolvedAnalyticalPlan?.snapshotId,
|
|
4960
6508
|
entries: researchRuns.slice(0, 6).map((branch, index) => {
|
|
4961
6509
|
const branchContext = agentRunRecord(branch.context)?.branch;
|
|
6510
|
+
const branchId = agentRunString(branchContext?.id) ?? `branch:${index + 1}`;
|
|
6511
|
+
const branchOutcome = researchBranchOutcomes.get(branchId);
|
|
4962
6512
|
const branchPreviewRecord = agentRunRecord(branch.resultPreview);
|
|
4963
6513
|
const branchPreview = coerceNarrateResultData(branchPreviewRecord);
|
|
4964
6514
|
const previewRecord = branchPreviewRecord;
|
|
@@ -4970,12 +6520,14 @@ export async function startLocalServer(opts) {
|
|
|
4970
6520
|
// (on the result or its execution receipt) can make a branch
|
|
4971
6521
|
// observed (AGT-016/033).
|
|
4972
6522
|
const executionProof = resultFingerprint;
|
|
4973
|
-
const observed = branch.status === 'ready' && Boolean(executionProof);
|
|
6523
|
+
const observed = !branchOutcome && branch.status === 'ready' && Boolean(executionProof);
|
|
4974
6524
|
return {
|
|
4975
6525
|
id: branch.id,
|
|
4976
|
-
branchId
|
|
6526
|
+
branchId,
|
|
4977
6527
|
question: branch.question,
|
|
4978
|
-
status: observed
|
|
6528
|
+
status: observed
|
|
6529
|
+
? 'observed'
|
|
6530
|
+
: branchOutcome?.status ?? (branch.status === 'error' ? 'failed' : 'skipped'),
|
|
4979
6531
|
...(branchPreviewRecord && Array.isArray(branchPreviewRecord.rows)
|
|
4980
6532
|
? { rowCount: branchPreviewRecord.rows.length }
|
|
4981
6533
|
: {}),
|
|
@@ -4987,7 +6539,8 @@ export async function startLocalServer(opts) {
|
|
|
4987
6539
|
// receipt/fingerprint (AGT-016/033).
|
|
4988
6540
|
receipts: executionProof ? [executionProof] : [],
|
|
4989
6541
|
...(!observed ? {
|
|
4990
|
-
error:
|
|
6542
|
+
error: branchOutcome?.error
|
|
6543
|
+
?? branch.error
|
|
4991
6544
|
?? (branch.status === 'error'
|
|
4992
6545
|
? branch.summary
|
|
4993
6546
|
: 'Research branch did not produce an execution receipt or result fingerprint.'),
|
|
@@ -4996,11 +6549,13 @@ export async function startLocalServer(opts) {
|
|
|
4996
6549
|
}),
|
|
4997
6550
|
stoppingReason: needsClarification
|
|
4998
6551
|
? 'not_started'
|
|
4999
|
-
:
|
|
5000
|
-
? '
|
|
5001
|
-
:
|
|
5002
|
-
? '
|
|
5003
|
-
:
|
|
6552
|
+
: researchBranchReceipts.some((receipt) => receipt.stopReason === 'budget_exhausted')
|
|
6553
|
+
? 'budget'
|
|
6554
|
+
: researchRuns.some((run) => run.status === 'error')
|
|
6555
|
+
? 'insufficient_evidence'
|
|
6556
|
+
: executableResearchBranches.length > 6
|
|
6557
|
+
? 'budget'
|
|
6558
|
+
: 'completed',
|
|
5004
6559
|
});
|
|
5005
6560
|
// V2 carries a verdict per bounded hypothesis and makes a deliberately
|
|
5006
6561
|
// small investigation visible to the caller. A returned row is still
|
|
@@ -5010,7 +6565,7 @@ export async function startLocalServer(opts) {
|
|
|
5010
6565
|
rootQuestion: request.question,
|
|
5011
6566
|
planId: plan.rootPlanId,
|
|
5012
6567
|
snapshotId: routeDecision?.resolvedAnalyticalPlan?.snapshotId,
|
|
5013
|
-
groundableBranchCount:
|
|
6568
|
+
groundableBranchCount: groundableResearchBranchCount,
|
|
5014
6569
|
entries: researchLedger.entries.map((entry, index) => ({
|
|
5015
6570
|
...entry,
|
|
5016
6571
|
hypothesis: typedHypothesesById.get(entry.branchId)?.statement ?? plan.steps[index]?.thought,
|
|
@@ -5042,6 +6597,26 @@ export async function startLocalServer(opts) {
|
|
|
5042
6597
|
})),
|
|
5043
6598
|
stoppingReason: researchLedger.stoppingReason,
|
|
5044
6599
|
});
|
|
6600
|
+
for (const entry of researchLedgerV2.entries) {
|
|
6601
|
+
const branch = researchBranchSpans.get(entry.branchId);
|
|
6602
|
+
if (!branch)
|
|
6603
|
+
continue;
|
|
6604
|
+
// Timeout/skipped spans were finalized at the physical branch boundary
|
|
6605
|
+
// with a typed reason. Do not overwrite that story with a generic
|
|
6606
|
+
// ledger projection during synthesis.
|
|
6607
|
+
if (researchBranchOutcomes.has(entry.branchId))
|
|
6608
|
+
continue;
|
|
6609
|
+
trace.finishSpan(branch.spanId, {
|
|
6610
|
+
outcome: entry.verdict === 'failed' ? 'error' : entry.verdict === 'skipped' ? 'skipped' : 'ok',
|
|
6611
|
+
reasonCode: entry.verdict === 'failed' ? 'unknown' : 'completed',
|
|
6612
|
+
payload: {
|
|
6613
|
+
kind: 'research',
|
|
6614
|
+
branchId: entry.branchId,
|
|
6615
|
+
hypothesisFingerprint: branch.hypothesisFingerprint,
|
|
6616
|
+
verdict: entry.verdict,
|
|
6617
|
+
},
|
|
6618
|
+
});
|
|
6619
|
+
}
|
|
5045
6620
|
// A query that ran and matched 0 rows STILL executed — treat it as a clean,
|
|
5046
6621
|
// grounded execution (not "no result"), so an empty answer is surfaced as
|
|
5047
6622
|
// "0 rows matched" rather than silently downgraded to review-required.
|
|
@@ -5051,19 +6626,28 @@ export async function startLocalServer(opts) {
|
|
|
5051
6626
|
&& !researchWorkspaceError
|
|
5052
6627
|
&& researchRuns.length > 0
|
|
5053
6628
|
&& researchRuns.every((run) => run.status === 'ready');
|
|
5054
|
-
|
|
6629
|
+
// Finalization has a reserved slice. It can always construct the
|
|
6630
|
+
// deterministic receipt-bound story, but a fresh provider narration may
|
|
6631
|
+
// not begin after its soft cutoff.
|
|
6632
|
+
const narration = !needsClarification && researchResultData && (!request.runBudget || request.runBudget.mayStartNarration())
|
|
5055
6633
|
? await narrateForAgentRun({
|
|
5056
6634
|
question: request.question,
|
|
5057
6635
|
intent: request.intent,
|
|
5058
6636
|
result: researchResultData,
|
|
5059
6637
|
evidence: plan.sources,
|
|
5060
6638
|
reviewRequired: true,
|
|
5061
|
-
}, request.researchResultRowsOptIn === true)
|
|
6639
|
+
}, request.researchResultRowsOptIn === true, trace)
|
|
5062
6640
|
: undefined;
|
|
5063
6641
|
// The cross-branch story. Every branch tested a hypothesis and produced a
|
|
5064
6642
|
// finding; narrating only the one result the executor happened to carry
|
|
5065
6643
|
// reported a single fact and discarded the rest, which is the visible
|
|
5066
6644
|
// half of "research answers one question instead of telling a story".
|
|
6645
|
+
const researchSynthesisSpan = trace.startSpan({
|
|
6646
|
+
name: 'research.synthesize',
|
|
6647
|
+
stage: 'research',
|
|
6648
|
+
reasonCode: 'started',
|
|
6649
|
+
payload: { kind: 'research', branchCount: researchLedgerV2.entries.length },
|
|
6650
|
+
});
|
|
5067
6651
|
const researchStory = !needsClarification && researchLedgerV2.entries.length > 0
|
|
5068
6652
|
? synthesizeResearchNarrative({
|
|
5069
6653
|
question: request.question,
|
|
@@ -5077,6 +6661,11 @@ export async function startLocalServer(opts) {
|
|
|
5077
6661
|
})),
|
|
5078
6662
|
})
|
|
5079
6663
|
: undefined;
|
|
6664
|
+
trace.finishSpan(researchSynthesisSpan, {
|
|
6665
|
+
outcome: 'ok',
|
|
6666
|
+
reasonCode: 'completed',
|
|
6667
|
+
payload: { kind: 'research', branchCount: researchLedgerV2.entries.length },
|
|
6668
|
+
});
|
|
5080
6669
|
const summary = needsClarification
|
|
5081
6670
|
? 'Needs clarification before running deeper research.'
|
|
5082
6671
|
// The story leads; the verified-fact narration follows it, so the
|
|
@@ -5095,14 +6684,31 @@ export async function startLocalServer(opts) {
|
|
|
5095
6684
|
: plan.done
|
|
5096
6685
|
? 'Prepared a direct grounded-answer plan.'
|
|
5097
6686
|
: 'Prepared a grounded research plan over real DQL assets.');
|
|
5098
|
-
const
|
|
5099
|
-
|
|
5100
|
-
|
|
6687
|
+
const branchBudgetExhausted = researchBranchReceipts.some((receipt) => receipt.stopReason === 'budget_exhausted');
|
|
6688
|
+
const branchTimedOut = researchBranchReceipts.some((receipt) => receipt.stopReason === 'research_branch_timeout');
|
|
6689
|
+
const allResearchBranchesBoundedOut = researchBranchReceipts.length > 0
|
|
6690
|
+
&& researchBranchReceipts.every((receipt) => receipt.stopReason === 'research_branch_timeout' || receipt.stopReason === 'budget_exhausted')
|
|
6691
|
+
&& branchTimedOut;
|
|
6692
|
+
const scopePrefix = [
|
|
6693
|
+
!needsClarification && researchLedgerV2.limitedScope
|
|
6694
|
+
? 'Limited research scope: fewer than three groundable branches were available.'
|
|
6695
|
+
: undefined,
|
|
6696
|
+
allResearchBranchesBoundedOut
|
|
6697
|
+
? 'Limited Research: every admitted branch reached its bounded window before producing a receipt-backed finding. Inspect the branch failures and retry a narrower Research question.'
|
|
6698
|
+
: branchTimedOut
|
|
6699
|
+
? 'One or more Research branches reached their fair-share deadline; the story uses only completed receipts and marks the remaining evidence as inconclusive or skipped.'
|
|
6700
|
+
: undefined,
|
|
6701
|
+
branchBudgetExhausted
|
|
6702
|
+
? 'Remaining branches were skipped because time was reserved for synthesis and persistence.'
|
|
6703
|
+
: undefined,
|
|
6704
|
+
].filter((value) => Boolean(value)).join(' ');
|
|
6705
|
+
const scopedSummary = scopePrefix ? `${scopePrefix} ${summary}` : summary;
|
|
5101
6706
|
return {
|
|
5102
6707
|
summary: scopedSummary,
|
|
5103
|
-
answer
|
|
5104
|
-
|
|
5105
|
-
|
|
6708
|
+
// Ask renders `answer` ahead of `summary`. Preserve the exact scoped
|
|
6709
|
+
// story there so a no-provider/no-target Research run cannot hide the
|
|
6710
|
+
// limited-scope warning behind a generic "no executed result" line.
|
|
6711
|
+
answer: plan.followUp?.question ?? scopedSummary,
|
|
5106
6712
|
status: needsClarification ? 'needs_clarification' : 'needs_review',
|
|
5107
6713
|
trustState: needsClarification ? 'not_applicable' : (researchExecutedCleanly ? 'grounded' : 'review_required'),
|
|
5108
6714
|
stopReason: needsClarification ? 'needs_clarification' : 'human_review_required',
|
|
@@ -5113,6 +6719,14 @@ export async function startLocalServer(opts) {
|
|
|
5113
6719
|
typedResearchPlan,
|
|
5114
6720
|
researchLedger,
|
|
5115
6721
|
researchLedgerV2,
|
|
6722
|
+
researchBranchReceipts,
|
|
6723
|
+
researchBudget: {
|
|
6724
|
+
version: 1,
|
|
6725
|
+
finalizationReserveMs: RESEARCH_BRANCH_FINALIZATION_RESERVE_MS,
|
|
6726
|
+
branchTimedOut,
|
|
6727
|
+
branchBudgetExhausted,
|
|
6728
|
+
allResearchBranchesBoundedOut,
|
|
6729
|
+
},
|
|
5116
6730
|
researchRun,
|
|
5117
6731
|
researchRuns,
|
|
5118
6732
|
researchRunId: researchRun?.id,
|
|
@@ -5142,13 +6756,19 @@ export async function startLocalServer(opts) {
|
|
|
5142
6756
|
: 'The query executed cleanly against real data and returned rows.')
|
|
5143
6757
|
: 'No executed result was available; the output stays exploratory pending review.', { rowCount: Array.isArray(researchResultRecord?.rows) ? researchResultRecord.rows.length : 0 }),
|
|
5144
6758
|
agentRunEvaluation('research-scope', 'Research scope', !researchLedgerV2.limitedScope, researchLedgerV2.limitedScope ? 'warning' : 'info', researchLedgerV2.limitedScope
|
|
5145
|
-
? `Limited research scope: ${researchLedgerV2.groundableBranchCount} of at least 3 branches
|
|
6759
|
+
? `Limited research scope: ${researchLedgerV2.groundableBranchCount} of at least 3 evidence-supported branches were available for this investigation.`
|
|
5146
6760
|
: `${researchLedgerV2.groundableBranchCount} groundable branches were retained with verdicts and counter-evidence slots.`, { groundableBranchCount: researchLedgerV2.groundableBranchCount, limitedScope: researchLedgerV2.limitedScope }),
|
|
5147
6761
|
],
|
|
5148
6762
|
nextActions: needsClarification
|
|
5149
6763
|
? [{ id: 'answer-follow-up', label: 'Answer follow-up', route: 'research' }]
|
|
5150
6764
|
: [
|
|
5151
6765
|
...(researchRun?.id ? [{ id: 'open-research', label: 'Open research dossier', artifactKind: 'research_run' }] : []),
|
|
6766
|
+
...(allResearchBranchesBoundedOut
|
|
6767
|
+
? [
|
|
6768
|
+
{ id: 'inspect-research-failures', label: 'Inspect branch failures', route: 'research' },
|
|
6769
|
+
{ id: 'retry-narrower-research', label: 'Retry narrower Research', route: 'research' },
|
|
6770
|
+
]
|
|
6771
|
+
: []),
|
|
5152
6772
|
{ id: 'create-block', label: 'Review DQL draft', route: 'dql_block_draft', artifactKind: 'dql_block_draft' },
|
|
5153
6773
|
...(researchRuns.some((run) => run.generatedSql || run.reviewedSql) ? [{ id: 'insert-sql', label: 'Insert SQL preview', route: 'sql_cell', artifactKind: 'sql_cell' }] : []),
|
|
5154
6774
|
],
|
|
@@ -5450,22 +7070,15 @@ export async function startLocalServer(opts) {
|
|
|
5450
7070
|
// lookup, and governed execution for the lifetime of a request. This removes
|
|
5451
7071
|
// both positional catalog truncation and the previous duplicate retrieval pass.
|
|
5452
7072
|
const preparedAgentContextPacks = new WeakMap();
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
: undefined;
|
|
5463
|
-
if (!provider)
|
|
5464
|
-
return undefined;
|
|
5465
|
-
return (question, candidates) => rerankCandidates(provider, question, candidates, {
|
|
5466
|
-
timeoutMs: Math.round(2_500 * deadlineScale()),
|
|
5467
|
-
});
|
|
5468
|
-
})();
|
|
7073
|
+
// The candidate-ID meaning call is the sole model-owned relevance decision
|
|
7074
|
+
// for an ordinary Ask. Do not attach the optional raw-provider cross
|
|
7075
|
+
// encoder here: it bypasses the request-scoped dispatch ledger and can turn
|
|
7076
|
+
// a meaning + generation + frozen-plan repair into four physical provider
|
|
7077
|
+
// sends. Deterministic retrieval already fuses exact, lexical, vector and
|
|
7078
|
+
// graph evidence before role-balanced admission; the canonical meaning
|
|
7079
|
+
// resolver may reorder only those admitted IDs under the normal Ask budget.
|
|
7080
|
+
// Research has its own explicitly traced planner/evidence paths and does
|
|
7081
|
+
// not borrow an unobserved rerank transport from this shared builder.
|
|
5469
7082
|
const pendingAgentContextPacks = new WeakMap();
|
|
5470
7083
|
const buildAgentRunContextPack = async (request) => {
|
|
5471
7084
|
const prepared = preparedAgentContextPacks.get(request);
|
|
@@ -5481,6 +7094,10 @@ export async function startLocalServer(opts) {
|
|
|
5481
7094
|
// this only inside the provider adapter allowed stale catalog matches to win
|
|
5482
7095
|
// before "they" / "this amount" became customer-scoped context.
|
|
5483
7096
|
const followUp = resolveAgentFollowUpContext(request.conversationContext, request.question);
|
|
7097
|
+
// Persist only the server-produced binding classification on the ephemeral
|
|
7098
|
+
// run request. The engine/trace can explain why prior state was (or was
|
|
7099
|
+
// not) admitted without receiving any prior rows or prompt content.
|
|
7100
|
+
request.conversationBinding = followUp?.binding ?? 'none';
|
|
5484
7101
|
const serverSnapshot = agentRunRecord(request.conversationContext?.conversationEnvelope
|
|
5485
7102
|
?? request.conversationContext?.serverSnapshot);
|
|
5486
7103
|
const topicRelation = agentRunString(serverSnapshot?.topicRelation);
|
|
@@ -5533,10 +7150,6 @@ export async function startLocalServer(opts) {
|
|
|
5533
7150
|
},
|
|
5534
7151
|
strictness: request.analysisDepth === 'deep' ? 'exploratory' : 'balanced',
|
|
5535
7152
|
limit: request.analysisDepth === 'deep' ? 120 : 80,
|
|
5536
|
-
// The runtime PRE-BUILDS this pack, so wiring the reranker only at the
|
|
5537
|
-
// provider's own `buildLocalContextPack` left it unreachable on the
|
|
5538
|
-
// common path — the prepared pack is used and that call never happens.
|
|
5539
|
-
...(agentRerankCandidates ? { rerankCandidates: agentRerankCandidates } : {}),
|
|
5540
7153
|
domainContext: requestedDomain
|
|
5541
7154
|
? resolveUiDomainContext({
|
|
5542
7155
|
manifest: snapshot.manifest,
|
|
@@ -5578,6 +7191,7 @@ export async function startLocalServer(opts) {
|
|
|
5578
7191
|
knowledgeLens: pack.knowledgeLens,
|
|
5579
7192
|
analyticalPolicies: pack.skills.flatMap((skill) => skill.analyticalPolicy ? [skill.analyticalPolicy] : []),
|
|
5580
7193
|
contextObjects: pack.objects,
|
|
7194
|
+
retrievalLanes: pack.retrievalDiagnostics.lanes,
|
|
5581
7195
|
durationMs: Date.now() - startedAt,
|
|
5582
7196
|
truncated: pack.retrievalDiagnostics.topRejected.length > 0,
|
|
5583
7197
|
});
|
|
@@ -5735,59 +7349,91 @@ export async function startLocalServer(opts) {
|
|
|
5735
7349
|
// Provider-agnostic completion the planner injects. Reuses the configured provider
|
|
5736
7350
|
// adapter; throwing here makes the planner fall back to its deterministic path.
|
|
5737
7351
|
const agentRunPlanner = createLlmAgentRunPlanner({
|
|
5738
|
-
complete: async ({ system, user, signal }) => {
|
|
7352
|
+
complete: async ({ system, user, request, signal }) => {
|
|
5739
7353
|
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
5740
7354
|
if (!provider)
|
|
5741
7355
|
throw new Error('No AI provider configured for planning.');
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
7356
|
+
const planningTrace = createProviderDispatchTrace({
|
|
7357
|
+
observer: askTraceObserverForV1(request),
|
|
7358
|
+
phase: 'planning',
|
|
7359
|
+
purpose: 'answer_generation',
|
|
7360
|
+
admit: (event) => {
|
|
7361
|
+
const ledger = agentRunProviderEvidenceContext.getStore();
|
|
7362
|
+
if (ledger) {
|
|
7363
|
+
return ledger.observe(event, {
|
|
7364
|
+
purpose: 'answer_generation',
|
|
7365
|
+
dispatchPhase: 'planning',
|
|
7366
|
+
optIn: false,
|
|
7367
|
+
});
|
|
7368
|
+
}
|
|
7369
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(event.provider, event.envelope);
|
|
7370
|
+
assertProviderPayloadAllowed(envelope, {
|
|
7371
|
+
allowResultRows: false,
|
|
7372
|
+
maxResultRows: 0,
|
|
5752
7373
|
purpose: 'answer_generation',
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
} : {}),
|
|
7374
|
+
});
|
|
7375
|
+
return envelope;
|
|
7376
|
+
},
|
|
5757
7377
|
});
|
|
7378
|
+
try {
|
|
7379
|
+
const response = await provider.generate([
|
|
7380
|
+
{ role: 'system', content: system },
|
|
7381
|
+
{ role: 'user', content: user },
|
|
7382
|
+
], {
|
|
7383
|
+
maxTokens: 700,
|
|
7384
|
+
temperature: 0.1,
|
|
7385
|
+
signal,
|
|
7386
|
+
// A planner is one bounded preparation transport, never a retry
|
|
7387
|
+
// lane. The run-scoped ledger also enforces the cross-phase cap.
|
|
7388
|
+
maxProviderDispatches: 1,
|
|
7389
|
+
...planningTrace.options,
|
|
7390
|
+
});
|
|
7391
|
+
planningTrace.settle('ok');
|
|
7392
|
+
return response;
|
|
7393
|
+
}
|
|
7394
|
+
catch (error) {
|
|
7395
|
+
planningTrace.settle(signal?.aborted ? 'cancelled' : 'error', error);
|
|
7396
|
+
throw error;
|
|
7397
|
+
}
|
|
5758
7398
|
},
|
|
5759
7399
|
getCatalogContext: buildRankedAgentRunCatalogContext,
|
|
5760
7400
|
});
|
|
5761
7401
|
// Explicit selections and conversation-only turns remain deterministic;
|
|
5762
7402
|
// each fresh natural-language analytical turn gets one bounded candidate-ID
|
|
5763
|
-
//
|
|
5764
|
-
//
|
|
7403
|
+
// meaning call by default. The legacy/no-evidence category classifier is
|
|
7404
|
+
// separately labelled and cannot coexist with that analytical call. The
|
|
7405
|
+
// rollback is host-owned and cannot be supplied by an HTTP/MCP client.
|
|
5765
7406
|
const agentRunRouter = createHybridRouter({
|
|
5766
7407
|
requireMeaningCallForNaturalLanguage: opts.requireMeaningCallForNaturalLanguage ?? true,
|
|
5767
|
-
complete: async ({ system, user, signal }) => {
|
|
7408
|
+
complete: async ({ system, user, signal, request, phase }) => {
|
|
5768
7409
|
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
5769
7410
|
if (!provider)
|
|
5770
7411
|
throw new Error('No AI provider configured for meaning resolution.');
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
], {
|
|
5775
|
-
maxTokens: 600,
|
|
5776
|
-
temperature: 0,
|
|
5777
|
-
// AGT-009/PERF-002: ambiguity gets one bounded resolver call. If the
|
|
5778
|
-
// provider stalls, the router falls back to its evidence-only decision
|
|
5779
|
-
// instead of leaving the UI in "Generating and validating SQL" for a
|
|
5780
|
-
// minute or more.
|
|
5781
|
-
signal: boundedAgentMeaningSignal(signal),
|
|
5782
|
-
maxProviderDispatches: 1,
|
|
5783
|
-
...(agentRunProviderEvidenceContext.getStore() ? {
|
|
5784
|
-
onProviderDispatch: (event) => agentRunProviderEvidenceContext.getStore().observe(event, {
|
|
5785
|
-
purpose: 'answer_generation',
|
|
5786
|
-
dispatchPhase: 'meaning_resolution',
|
|
5787
|
-
optIn: false,
|
|
5788
|
-
}),
|
|
5789
|
-
} : {}),
|
|
7412
|
+
const dispatchTrace = createRouterInterpretationProviderTrace({
|
|
7413
|
+
request,
|
|
7414
|
+
routerPhase: phase,
|
|
5790
7415
|
});
|
|
7416
|
+
try {
|
|
7417
|
+
const response = await provider.generate([
|
|
7418
|
+
{ role: 'system', content: system },
|
|
7419
|
+
{ role: 'user', content: user },
|
|
7420
|
+
], {
|
|
7421
|
+
maxTokens: 600,
|
|
7422
|
+
temperature: 0,
|
|
7423
|
+
// AGT-009/PERF-002: a fresh natural-language Ask gets one bounded
|
|
7424
|
+
// candidate-ID-only interpretation call. A certified exact route
|
|
7425
|
+
// never reaches this callback, preserving its zero-provider path.
|
|
7426
|
+
signal: boundedAgentMeaningSignal(signal),
|
|
7427
|
+
maxProviderDispatches: 1,
|
|
7428
|
+
...(dispatchTrace?.options ?? {}),
|
|
7429
|
+
});
|
|
7430
|
+
dispatchTrace?.settle('ok');
|
|
7431
|
+
return response;
|
|
7432
|
+
}
|
|
7433
|
+
catch (error) {
|
|
7434
|
+
dispatchTrace?.settle(signal?.aborted ? 'cancelled' : 'error', error);
|
|
7435
|
+
throw error;
|
|
7436
|
+
}
|
|
5791
7437
|
},
|
|
5792
7438
|
getEvidence: buildAgentRunEvidence,
|
|
5793
7439
|
getCatalogContext: buildRankedAgentRunCatalogContext,
|
|
@@ -5799,6 +7445,11 @@ export async function startLocalServer(opts) {
|
|
|
5799
7445
|
path: defaultAgentRunSqlitePath(projectRoot),
|
|
5800
7446
|
legacyJsonPath: defaultAgentRunStorePath(projectRoot),
|
|
5801
7447
|
});
|
|
7448
|
+
// OBS-002: traces intentionally live outside agent-runs.sqlite. A corrupt,
|
|
7449
|
+
// oversized, or schema-newer trace store must not make Ask unavailable.
|
|
7450
|
+
const askTraceStore = new AskTraceSqliteStoreV1({
|
|
7451
|
+
path: defaultAskTraceSqlitePath(projectRoot),
|
|
7452
|
+
});
|
|
5802
7453
|
const conversationStorePath = await prepareConversationPath(projectRoot);
|
|
5803
7454
|
// A run may outlive its streaming browser connection, so cancellation is
|
|
5804
7455
|
// server-owned and keyed by run id rather than relying on fetch abort alone.
|
|
@@ -5848,6 +7499,18 @@ export async function startLocalServer(opts) {
|
|
|
5848
7499
|
gates: defaultAgentRunGates,
|
|
5849
7500
|
planner: agentRunPlanner,
|
|
5850
7501
|
router: agentRunRouter,
|
|
7502
|
+
traceObserverFactory: ({ runId, request, requestedMode }) => createAskTraceObserverV1({
|
|
7503
|
+
store: askTraceStore,
|
|
7504
|
+
runId,
|
|
7505
|
+
// This host-only value is attached after HTTP capability validation.
|
|
7506
|
+
// The public JSON parser never accepts a caller-supplied surface.
|
|
7507
|
+
surface: request.traceSurface ?? 'browser',
|
|
7508
|
+
mode: requestedMode === 'research' ? 'research' : 'ask',
|
|
7509
|
+
// The durable trace never receives the question text. This server-side
|
|
7510
|
+
// fingerprint is stable enough to correlate a reviewed reproduction.
|
|
7511
|
+
questionFingerprint: `sha256:${createHash('sha256').update(request.question).digest('hex')}`,
|
|
7512
|
+
...(request.threadId ? { threadId: request.threadId } : {}),
|
|
7513
|
+
}),
|
|
5851
7514
|
});
|
|
5852
7515
|
const runNotebookForApp = async (appId, notebookPath) => {
|
|
5853
7516
|
const absPath = safeJoin(projectRoot, notebookPath);
|
|
@@ -6046,25 +7709,31 @@ export async function startLocalServer(opts) {
|
|
|
6046
7709
|
sqlParams: plan?.sqlParams,
|
|
6047
7710
|
variables: { ...(plan?.variables ?? {}), ...invocation.values },
|
|
6048
7711
|
executePrepared: async (preparation) => {
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
7712
|
+
return executePreparedArtifactTraceBoundary({
|
|
7713
|
+
preparedSql: preparation.executedSql,
|
|
7714
|
+
reviewRequired: metadata.reviewRequired ?? true,
|
|
7715
|
+
execute: async () => {
|
|
7716
|
+
if (pinnedSemanticCompile) {
|
|
7717
|
+
const semanticExecution = await executeTargetBoundSemanticQuery({
|
|
7718
|
+
executor,
|
|
7719
|
+
connection: activeConnection,
|
|
7720
|
+
projectRoot,
|
|
7721
|
+
plannedAdapter: pinnedSemanticCompile.engine,
|
|
7722
|
+
metricFlow: pinnedSemanticCompile.engine === 'metricflow-cli'
|
|
7723
|
+
? resolveMetricFlowTargetMetadata(projectRoot, projectConfig)
|
|
7724
|
+
: undefined,
|
|
7725
|
+
compile: async () => pinnedSemanticCompile,
|
|
7726
|
+
prepareSql: () => ({ sql: preparation.executedSql, connection: preparation.connection }),
|
|
7727
|
+
rowBound: invocationInput?.rowLimit ?? pinnedSemanticCompile.effectiveRequest.limit,
|
|
7728
|
+
});
|
|
7729
|
+
if (semanticExecution) {
|
|
7730
|
+
semanticExecutionHolder.value = semanticExecution;
|
|
7731
|
+
return semanticExecution.result;
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
return executor.executeQuery(preparation.executedSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), preparation.connection);
|
|
6065
7735
|
}
|
|
6066
|
-
}
|
|
6067
|
-
return executor.executeQuery(preparation.executedSql, plan?.sqlParams ?? [], runtimeVariables({ ...(plan?.variables ?? {}), ...invocation.values }), preparation.connection);
|
|
7736
|
+
});
|
|
6068
7737
|
},
|
|
6069
7738
|
});
|
|
6070
7739
|
const semanticExecution = semanticExecutionHolder.value;
|
|
@@ -6137,6 +7806,7 @@ export async function startLocalServer(opts) {
|
|
|
6137
7806
|
path: block.filePath,
|
|
6138
7807
|
domain: block.domain,
|
|
6139
7808
|
chartType: block.chartType,
|
|
7809
|
+
reviewRequired: false,
|
|
6140
7810
|
...(qualifiedSchemaContext?.length ? { qualifiedSchemaContext } : {}),
|
|
6141
7811
|
}, invocationInput, executionConnection, executionConnectionName);
|
|
6142
7812
|
return {
|
|
@@ -6181,6 +7851,11 @@ export async function startLocalServer(opts) {
|
|
|
6181
7851
|
name: artifact.name,
|
|
6182
7852
|
path: artifact.sourcePath,
|
|
6183
7853
|
domain: artifact.source.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1],
|
|
7854
|
+
// Trust is owned by the route that froze this artifact. A governed
|
|
7855
|
+
// semantic artifact is not exploratory merely because it is not a
|
|
7856
|
+
// certified block; only the explicit review-required artifact lane
|
|
7857
|
+
// carries reviewRequired into the physical SQL span.
|
|
7858
|
+
reviewRequired: artifact.trustState === 'review_required',
|
|
6184
7859
|
}, {
|
|
6185
7860
|
question,
|
|
6186
7861
|
parameters: invocation.values,
|
|
@@ -6502,6 +8177,32 @@ export async function startLocalServer(opts) {
|
|
|
6502
8177
|
const repairs = boundedSql === preflight.sql
|
|
6503
8178
|
? [...preflight.repairs]
|
|
6504
8179
|
: [...preflight.repairs, `Applied the requested overall top-${requestedTopN} bound before exploratory execution.`];
|
|
8180
|
+
// This is the only deterministic SQL rewrite point on the exploratory
|
|
8181
|
+
// execution path. Record it here, rather than inferring a successful SQL
|
|
8182
|
+
// repair later from a generic engine retry event.
|
|
8183
|
+
if (repairs.length > 0) {
|
|
8184
|
+
const trace = activeAskTraceObserver();
|
|
8185
|
+
const repairPayload = {
|
|
8186
|
+
kind: 'sql',
|
|
8187
|
+
execution: {
|
|
8188
|
+
version: 1,
|
|
8189
|
+
sqlFingerprint: runtimeTraceFingerprint(boundedSql),
|
|
8190
|
+
planFingerprint: runtimeTraceFingerprint(normalizedCandidateSql),
|
|
8191
|
+
reviewRequired: true,
|
|
8192
|
+
},
|
|
8193
|
+
};
|
|
8194
|
+
const repairSpan = trace?.startSpan({
|
|
8195
|
+
name: 'sql.repair',
|
|
8196
|
+
stage: 'sql',
|
|
8197
|
+
reasonCode: 'repair_attempted',
|
|
8198
|
+
payload: repairPayload,
|
|
8199
|
+
});
|
|
8200
|
+
trace?.finishSpan(repairSpan, {
|
|
8201
|
+
outcome: 'ok',
|
|
8202
|
+
reasonCode: 'repair_attempted',
|
|
8203
|
+
payload: repairPayload,
|
|
8204
|
+
});
|
|
8205
|
+
}
|
|
6505
8206
|
if (preflight.blockedReason) {
|
|
6506
8207
|
return failed(preflight.blockedReason, boundedSql, repairs, [], 'exploratory_preflight_blocked');
|
|
6507
8208
|
}
|
|
@@ -6852,33 +8553,43 @@ export async function startLocalServer(opts) {
|
|
|
6852
8553
|
maxResultRows: 0,
|
|
6853
8554
|
purpose: 'repair_sql',
|
|
6854
8555
|
});
|
|
8556
|
+
const repairDispatchTrace = createProviderDispatchTrace({
|
|
8557
|
+
observer: activeAskTraceObserver(),
|
|
8558
|
+
phase: 'repair',
|
|
8559
|
+
purpose: 'repair_sql',
|
|
8560
|
+
admit: (event) => {
|
|
8561
|
+
const envelope = prepareProviderWireEnvelopeForDispatch(provider.name, event.envelope);
|
|
8562
|
+
assertProviderPayloadAllowed(envelope, {
|
|
8563
|
+
allowResultRows: false,
|
|
8564
|
+
maxResultRows: 0,
|
|
8565
|
+
purpose: 'repair_sql',
|
|
8566
|
+
});
|
|
8567
|
+
providerRoundTrips += 1;
|
|
8568
|
+
providerEgressReceipts.push(createProviderDispatchEgressReceipt({
|
|
8569
|
+
purpose: 'repair_sql',
|
|
8570
|
+
dispatchPhase: 'repair',
|
|
8571
|
+
provider: provider.name,
|
|
8572
|
+
permittedCategories: ['instructions', 'question', 'schema_metadata', 'governed_context'],
|
|
8573
|
+
optIn: false,
|
|
8574
|
+
envelope,
|
|
8575
|
+
}));
|
|
8576
|
+
return envelope;
|
|
8577
|
+
},
|
|
8578
|
+
});
|
|
6855
8579
|
let raw;
|
|
6856
8580
|
try {
|
|
6857
8581
|
raw = await provider.generate(repairEnvelope.messages, {
|
|
6858
8582
|
maxTokens: 1200,
|
|
6859
8583
|
temperature: 0,
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
allowResultRows: false,
|
|
6865
|
-
maxResultRows: 0,
|
|
6866
|
-
purpose: 'repair_sql',
|
|
6867
|
-
});
|
|
6868
|
-
providerRoundTrips += 1;
|
|
6869
|
-
providerEgressReceipts.push(createProviderDispatchEgressReceipt({
|
|
6870
|
-
purpose: 'repair_sql',
|
|
6871
|
-
dispatchPhase: 'repair',
|
|
6872
|
-
provider: provider.name,
|
|
6873
|
-
permittedCategories: ['instructions', 'question', 'schema_metadata', 'governed_context'],
|
|
6874
|
-
optIn: false,
|
|
6875
|
-
envelope,
|
|
6876
|
-
}));
|
|
6877
|
-
return envelope;
|
|
6878
|
-
},
|
|
8584
|
+
// This is the one parent-bound repair transport. A provider retry
|
|
8585
|
+
// would be a second repair attempt, so the host never authorizes it.
|
|
8586
|
+
maxProviderDispatches: 1,
|
|
8587
|
+
...repairDispatchTrace.options,
|
|
6879
8588
|
});
|
|
8589
|
+
repairDispatchTrace.settle('ok');
|
|
6880
8590
|
}
|
|
6881
|
-
catch {
|
|
8591
|
+
catch (error) {
|
|
8592
|
+
repairDispatchTrace.settle('error', error);
|
|
6882
8593
|
throw Object.assign(boundedRepairError('The AI provider could not complete this repair. Check Provider Settings and retry.', 502), {
|
|
6883
8594
|
providerDispatchEvidence: {
|
|
6884
8595
|
providerEgressReceipts: [...providerEgressReceipts],
|
|
@@ -7443,6 +9154,15 @@ export async function startLocalServer(opts) {
|
|
|
7443
9154
|
warnings: [notebookResearchStorageUnavailableMessage],
|
|
7444
9155
|
});
|
|
7445
9156
|
const runNotebookResearch = async (storage, run, input = {}) => {
|
|
9157
|
+
// A fair-share branch signal is separate from the root run signal. Check it
|
|
9158
|
+
// at each durable boundary so an overrun cannot later overwrite the
|
|
9159
|
+
// timeout/skipped receipt the parent has already recorded.
|
|
9160
|
+
const throwIfResearchSignalAborted = () => {
|
|
9161
|
+
if (!input.signal?.aborted)
|
|
9162
|
+
return;
|
|
9163
|
+
throw input.signal.reason ?? new DOMException('The Research branch deadline elapsed.', 'TimeoutError');
|
|
9164
|
+
};
|
|
9165
|
+
throwIfResearchSignalAborted();
|
|
7446
9166
|
const question = notebookResearchString(input.question) || run.question;
|
|
7447
9167
|
const domain = notebookResearchString(input.domain) ?? run.domain;
|
|
7448
9168
|
const owner = notebookResearchString(input.owner) ?? run.owner;
|
|
@@ -7471,33 +9191,50 @@ export async function startLocalServer(opts) {
|
|
|
7471
9191
|
error: '',
|
|
7472
9192
|
});
|
|
7473
9193
|
try {
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
},
|
|
7485
|
-
strictness: 'exploratory',
|
|
7486
|
-
limit: 160,
|
|
7487
|
-
}).catch((error) => {
|
|
7488
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7489
|
-
return {
|
|
9194
|
+
throwIfResearchSignalAborted();
|
|
9195
|
+
// An explicit Research child has already retrieved, interpreted, and
|
|
9196
|
+
// frozen its own authoritative Ask plan before it reaches this durable
|
|
9197
|
+
// Notebook record. Rebuilding a broad Notebook context pack here is
|
|
9198
|
+
// both duplicate work and a fairness bug: it can consume the child's
|
|
9199
|
+
// entire branch window *after* a safe SQL result already exists. Keep a
|
|
9200
|
+
// deliberately small receipt projection instead. It is reporting-only;
|
|
9201
|
+
// execution remains bound to the child RAP supplied above.
|
|
9202
|
+
const contextPack = input.authoritativeBranch
|
|
9203
|
+
? {
|
|
7490
9204
|
id: '',
|
|
7491
|
-
routeDecision:
|
|
9205
|
+
routeDecision: input.authoritativeBranch.routeDecision,
|
|
7492
9206
|
evidenceRoles: [],
|
|
7493
|
-
warnings: [
|
|
9207
|
+
warnings: [],
|
|
7494
9208
|
retrievalDiagnostics: { selectedEvidence: [] },
|
|
7495
|
-
}
|
|
7496
|
-
|
|
7497
|
-
|
|
9209
|
+
}
|
|
9210
|
+
: await buildLocalContextPack(projectRoot, {
|
|
9211
|
+
question,
|
|
9212
|
+
mode: 'question',
|
|
9213
|
+
surface: 'notebook',
|
|
9214
|
+
selectedContext: {
|
|
9215
|
+
...notebookResearchSelectedContext(run, context),
|
|
9216
|
+
domain,
|
|
9217
|
+
owner,
|
|
9218
|
+
intent,
|
|
9219
|
+
researchPattern: notebookResearchIntentPattern(intent),
|
|
9220
|
+
},
|
|
9221
|
+
strictness: 'exploratory',
|
|
9222
|
+
limit: 160,
|
|
9223
|
+
}).catch((error) => {
|
|
9224
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9225
|
+
return {
|
|
9226
|
+
id: '',
|
|
9227
|
+
routeDecision: undefined,
|
|
9228
|
+
evidenceRoles: [],
|
|
9229
|
+
warnings: [`Context pack failed: ${message}`],
|
|
9230
|
+
retrievalDiagnostics: { selectedEvidence: [] },
|
|
9231
|
+
};
|
|
9232
|
+
});
|
|
9233
|
+
throwIfResearchSignalAborted();
|
|
9234
|
+
let governedAnswer = input.authoritativeBranch?.answer;
|
|
7498
9235
|
let providerError;
|
|
7499
9236
|
const generationWarnings = [];
|
|
7500
|
-
if (!generatedSql && !reviewedSql) {
|
|
9237
|
+
if (!generatedSql && !reviewedSql && !input.authoritativeBranch) {
|
|
7501
9238
|
const governedResearch = resolveGovernedAnswerRunner(projectRoot);
|
|
7502
9239
|
const resolvedProvider = governedResearch?.provider ?? null;
|
|
7503
9240
|
const runner = governedResearch?.runner ?? null;
|
|
@@ -7507,6 +9244,7 @@ export async function startLocalServer(opts) {
|
|
|
7507
9244
|
else {
|
|
7508
9245
|
const researchSignal = input.signal ?? new AbortController().signal;
|
|
7509
9246
|
try {
|
|
9247
|
+
throwIfResearchSignalAborted();
|
|
7510
9248
|
await runner.run({
|
|
7511
9249
|
provider: resolvedProvider,
|
|
7512
9250
|
messages: [{ role: 'user', content: notebookResearchAgentPrompt(question, intent) }],
|
|
@@ -7537,6 +9275,7 @@ export async function startLocalServer(opts) {
|
|
|
7537
9275
|
if (turn.kind === 'error')
|
|
7538
9276
|
providerError = turn.message;
|
|
7539
9277
|
}, researchSignal);
|
|
9278
|
+
throwIfResearchSignalAborted();
|
|
7540
9279
|
}
|
|
7541
9280
|
catch (error) {
|
|
7542
9281
|
rethrowIfCancelled(error, input.signal);
|
|
@@ -7559,7 +9298,11 @@ export async function startLocalServer(opts) {
|
|
|
7559
9298
|
|| agentAnswerHasExecutionFailure(governedAnswer)
|
|
7560
9299
|
|| Boolean(governedAnswer.analyticalFailure)
|
|
7561
9300
|
|| (!governedAnswer.result && !generatedSql && !dqlArtifact);
|
|
7562
|
-
|
|
9301
|
+
// A root baseline is an execution receipt for its own analytical tuple,
|
|
9302
|
+
// not a reusable SQL authority for every Research hypothesis. Explicit
|
|
9303
|
+
// Research children always carry their own router-frozen RAP, so they
|
|
9304
|
+
// must never borrow this generic Notebook fallback.
|
|
9305
|
+
if (!input.authoritativeBranch && !reviewedSql && baselineSql && deeperCompositionFailed) {
|
|
7563
9306
|
const failure = governedAnswer?.executionError
|
|
7564
9307
|
?? governedAnswer?.analyticalFailure?.message
|
|
7565
9308
|
?? governedAnswer?.answer
|
|
@@ -7582,13 +9325,16 @@ export async function startLocalServer(opts) {
|
|
|
7582
9325
|
let resultPreview;
|
|
7583
9326
|
let previewError;
|
|
7584
9327
|
if (!usedBaselineFallback && governedAnswer?.result?.rows && !reviewedSql) {
|
|
9328
|
+
throwIfResearchSignalAborted();
|
|
7585
9329
|
resultPreview = normalizeNotebookAgentResult(governedAnswer.result);
|
|
7586
9330
|
}
|
|
7587
|
-
else if (sqlForPreview) {
|
|
9331
|
+
else if (sqlForPreview && !input.authoritativeBranch) {
|
|
7588
9332
|
try {
|
|
9333
|
+
throwIfResearchSignalAborted();
|
|
7589
9334
|
const previewSql = buildAgentPreviewSql(sqlForPreview);
|
|
7590
9335
|
const previewStart = Date.now();
|
|
7591
9336
|
resultPreview = await executeLocalSqlForStoredResult(previewSql, input.executionConnection);
|
|
9337
|
+
throwIfResearchSignalAborted();
|
|
7592
9338
|
recordNotebookQueryRun(projectRoot, {
|
|
7593
9339
|
notebookPath: run.notebookPath,
|
|
7594
9340
|
cellId: run.sourceCellId,
|
|
@@ -7617,7 +9363,8 @@ export async function startLocalServer(opts) {
|
|
|
7617
9363
|
});
|
|
7618
9364
|
}
|
|
7619
9365
|
}
|
|
7620
|
-
const routeDecision =
|
|
9366
|
+
const routeDecision = input.authoritativeBranch?.routeDecision
|
|
9367
|
+
?? notebookResearchRouteDecisionForRun(run, contextPack.routeDecision, sqlForPreview);
|
|
7621
9368
|
const display = resultPreview
|
|
7622
9369
|
? recommendVisualization(projectRoot, {
|
|
7623
9370
|
prompt: question,
|
|
@@ -7665,11 +9412,6 @@ export async function startLocalServer(opts) {
|
|
|
7665
9412
|
...(governedAnswer?.citations ?? []),
|
|
7666
9413
|
].slice(0, 40),
|
|
7667
9414
|
};
|
|
7668
|
-
const summary = usedBaselineFallback && resultPreview && !previewError
|
|
7669
|
-
? 'Research retained the successful Ask baseline and revalidated it on the same data target. No unverified replacement query was accepted; use the dossier to add the next breakdown or comparison.'
|
|
7670
|
-
: notebookResearchString(governedAnswer?.answer)
|
|
7671
|
-
?? notebookResearchString(governedAnswer?.text)
|
|
7672
|
-
?? notebookResearchSummary(question, resultPreview, previewError);
|
|
7673
9415
|
const previewRecord = agentRunRecord(resultPreview);
|
|
7674
9416
|
const executionReceipt = normalizeAnalyticalExecutionReceipt(previewRecord?.executionReceipt);
|
|
7675
9417
|
// Do not treat a child run ID as execution evidence. The canonical
|
|
@@ -7683,6 +9425,29 @@ export async function startLocalServer(opts) {
|
|
|
7683
9425
|
?? (executionUnavailable
|
|
7684
9426
|
? 'Research did not produce an executed result or execution receipt; the branch remains review-required.'
|
|
7685
9427
|
: undefined);
|
|
9428
|
+
// A Research child reaches this point only after its own Ask cascade
|
|
9429
|
+
// selected and executed a frozen plan. Once that execution has a
|
|
9430
|
+
// canonical receipt, preserve it immediately with a deterministic,
|
|
9431
|
+
// fact-bound branch narrative. In particular, do not route the result
|
|
9432
|
+
// back through ordinary Ask narration (or another provider transport)
|
|
9433
|
+
// while the branch's fair-share deadline is running. The root Research
|
|
9434
|
+
// synthesis consumes this persisted observation later.
|
|
9435
|
+
const authoritativeExecutedBranch = Boolean(input.authoritativeBranch
|
|
9436
|
+
&& resultPreview
|
|
9437
|
+
&& !previewError
|
|
9438
|
+
&& executionProof);
|
|
9439
|
+
const summary = authoritativeExecutedBranch
|
|
9440
|
+
? deterministicResearchBranchSummary({
|
|
9441
|
+
question,
|
|
9442
|
+
result: resultPreview,
|
|
9443
|
+
executionReceipt,
|
|
9444
|
+
analyticalNarrative: governedAnswer?.analyticalNarrative,
|
|
9445
|
+
})
|
|
9446
|
+
: usedBaselineFallback && resultPreview && !previewError
|
|
9447
|
+
? 'Research retained the successful Ask baseline and revalidated it on the same data target. No unverified replacement query was accepted; use the dossier to add the next breakdown or comparison.'
|
|
9448
|
+
: notebookResearchString(governedAnswer?.answer)
|
|
9449
|
+
?? notebookResearchString(governedAnswer?.text)
|
|
9450
|
+
?? notebookResearchSummary(question, resultPreview, previewError);
|
|
7686
9451
|
const recommendation = previewError
|
|
7687
9452
|
? 'Review the SQL, selected metadata, and connection context before rerunning.'
|
|
7688
9453
|
: dqlArtifact && !reviewedSql
|
|
@@ -7700,6 +9465,7 @@ export async function startLocalServer(opts) {
|
|
|
7700
9465
|
dqlArtifact,
|
|
7701
9466
|
routeDecision,
|
|
7702
9467
|
});
|
|
9468
|
+
throwIfResearchSignalAborted();
|
|
7703
9469
|
return storage.updateRun(run.id, {
|
|
7704
9470
|
domain,
|
|
7705
9471
|
owner,
|
|
@@ -8497,7 +10263,7 @@ export async function startLocalServer(opts) {
|
|
|
8497
10263
|
res.setHeader('Access-Control-Allow-Origin', requestOrigin);
|
|
8498
10264
|
res.setHeader('Vary', 'Origin');
|
|
8499
10265
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
8500
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Idempotency-Key');
|
|
10266
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Idempotency-Key, X-DQL-Ask-Trace-Capability');
|
|
8501
10267
|
if (!originAllowed && path.startsWith('/api/')) {
|
|
8502
10268
|
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
8503
10269
|
res.end(serializeJSON({ error: 'Origin is not allowed.' }));
|
|
@@ -8516,6 +10282,21 @@ export async function startLocalServer(opts) {
|
|
|
8516
10282
|
return;
|
|
8517
10283
|
}
|
|
8518
10284
|
}
|
|
10285
|
+
// Existing `dql notebook` runtimes cannot hand their in-memory capability
|
|
10286
|
+
// to a later CLI process. A short-lived challenge is therefore issued only
|
|
10287
|
+
// when BOTH the server binding and requesting socket are loopback. Remote
|
|
10288
|
+
// and browser-origin requests retain their normal `browser` attribution.
|
|
10289
|
+
if (req.method === 'GET' && path === '/api/ask-traces/cli-capability') {
|
|
10290
|
+
if (!loopback || !isLoopbackRemoteAddress(req.socket.remoteAddress)) {
|
|
10291
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10292
|
+
res.end(serializeJSON({ code: 'TRACE_CLI_CAPABILITY_FORBIDDEN', error: 'CLI trace attribution is available only to a loopback local runtime.' }));
|
|
10293
|
+
return;
|
|
10294
|
+
}
|
|
10295
|
+
const capability = cliAskTraceCapabilities.issue();
|
|
10296
|
+
res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
10297
|
+
res.end(serializeJSON(capability));
|
|
10298
|
+
return;
|
|
10299
|
+
}
|
|
8519
10300
|
if (req.method === 'GET' && path === '/api/health') {
|
|
8520
10301
|
// REL-002: expose runtime/build identity so a server started before a
|
|
8521
10302
|
// rebuild or running a different version than the project pin is visibly
|
|
@@ -9692,6 +11473,169 @@ export async function startLocalServer(opts) {
|
|
|
9692
11473
|
res.end(serializeJSON({ runs, total: stored.length, limit }));
|
|
9693
11474
|
return;
|
|
9694
11475
|
}
|
|
11476
|
+
// OBS-005: local-only Ask trace APIs. These endpoints return the compact
|
|
11477
|
+
// trace envelope or its typed, redacted detail; they never read a prompt,
|
|
11478
|
+
// SQL string, result row, provider response, or live network exporter.
|
|
11479
|
+
const writeTraceStoreUnavailable = (traceStatus) => {
|
|
11480
|
+
const schemaUnsupported = traceStatus.reason === 'unsupported_schema';
|
|
11481
|
+
res.writeHead(schemaUnsupported ? 409 : 503, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11482
|
+
res.end(serializeJSON({
|
|
11483
|
+
code: schemaUnsupported ? 'TRACE_SCHEMA_UNSUPPORTED' : 'TRACE_STORE_UNAVAILABLE',
|
|
11484
|
+
error: schemaUnsupported
|
|
11485
|
+
? 'The local Ask trace schema is newer than this DQL runtime can read.'
|
|
11486
|
+
: 'Local Ask trace storage is unavailable.',
|
|
11487
|
+
status: traceStatus,
|
|
11488
|
+
}));
|
|
11489
|
+
};
|
|
11490
|
+
if (req.method === 'GET' && path === '/api/ask-traces/status') {
|
|
11491
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11492
|
+
res.end(serializeJSON({ status: askTraceStore.status() }));
|
|
11493
|
+
return;
|
|
11494
|
+
}
|
|
11495
|
+
if (req.method === 'GET' && path === '/api/ask-traces') {
|
|
11496
|
+
const traceStatus = askTraceStore.status();
|
|
11497
|
+
if (!traceStatus.available) {
|
|
11498
|
+
writeTraceStoreUnavailable(traceStatus);
|
|
11499
|
+
return;
|
|
11500
|
+
}
|
|
11501
|
+
const rawLimit = Number(url.searchParams.get('limit'));
|
|
11502
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(100, Math.floor(rawLimit)) : 50;
|
|
11503
|
+
// Trace-store filters are all allowlisted scalar receipt fields. The
|
|
11504
|
+
// question preview is deliberately NOT a trace-store filter: prompts do
|
|
11505
|
+
// not live in ask-observability.sqlite and list pagination remains
|
|
11506
|
+
// snapshot/receipt based rather than text-search based.
|
|
11507
|
+
const traceListInput = {
|
|
11508
|
+
limit,
|
|
11509
|
+
...(url.searchParams.get('cursor') ? { cursor: url.searchParams.get('cursor') } : {}),
|
|
11510
|
+
...(url.searchParams.get('status') ? { status: url.searchParams.get('status') } : {}),
|
|
11511
|
+
...(url.searchParams.get('mode') ? { mode: url.searchParams.get('mode') } : {}),
|
|
11512
|
+
...(url.searchParams.get('trustState') ? { trustState: url.searchParams.get('trustState') } : {}),
|
|
11513
|
+
...(url.searchParams.get('selectedTier') ? { selectedTier: url.searchParams.get('selectedTier') } : {}),
|
|
11514
|
+
...(url.searchParams.get('surface') ? { surface: url.searchParams.get('surface') } : {}),
|
|
11515
|
+
...(url.searchParams.get('recordingStatus') ? { recordingStatus: url.searchParams.get('recordingStatus') } : {}),
|
|
11516
|
+
};
|
|
11517
|
+
const page = askTraceStore.list(traceListInput);
|
|
11518
|
+
const traces = await Promise.all(page.traces.map(async (trace) => {
|
|
11519
|
+
const run = await agentRunStore.get(trace.runId);
|
|
11520
|
+
if (!run)
|
|
11521
|
+
return trace;
|
|
11522
|
+
const questionPreview = askTraceQuestionPreview(run.question);
|
|
11523
|
+
return {
|
|
11524
|
+
...trace,
|
|
11525
|
+
...(questionPreview ? { questionPreview } : {}),
|
|
11526
|
+
scenarioLabel: askTraceScenarioLabel(run),
|
|
11527
|
+
};
|
|
11528
|
+
}));
|
|
11529
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11530
|
+
res.end(serializeJSON({ ...page, traces }));
|
|
11531
|
+
return;
|
|
11532
|
+
}
|
|
11533
|
+
if (req.method === 'GET' && /^\/api\/ask-traces\/by-run\/[^/]+$/.test(path)) {
|
|
11534
|
+
const traceStatus = askTraceStore.status();
|
|
11535
|
+
if (!traceStatus.available) {
|
|
11536
|
+
writeTraceStoreUnavailable(traceStatus);
|
|
11537
|
+
return;
|
|
11538
|
+
}
|
|
11539
|
+
const runId = decodeURIComponent(path.slice('/api/ask-traces/by-run/'.length));
|
|
11540
|
+
const trace = askTraceStore.getByRun(runId);
|
|
11541
|
+
if (!trace) {
|
|
11542
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11543
|
+
res.end(serializeJSON({ code: 'TRACE_NOT_FOUND', error: 'No local trace was found for this Ask run.' }));
|
|
11544
|
+
return;
|
|
11545
|
+
}
|
|
11546
|
+
if (trace.envelope.recordingStatus === 'detail_expired') {
|
|
11547
|
+
res.writeHead(410, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11548
|
+
res.end(serializeJSON({ code: 'TRACE_DETAIL_EXPIRED', error: 'Detailed trace evidence has expired; the run summary remains available.', envelope: trace.envelope }));
|
|
11549
|
+
return;
|
|
11550
|
+
}
|
|
11551
|
+
const run = await agentRunStore.get(runId);
|
|
11552
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11553
|
+
// The canonical Ask story is persisted on the AgentRun, not rebuilt from
|
|
11554
|
+
// spans. Join it only at this local API boundary so trace storage never
|
|
11555
|
+
// becomes prompt/result retention and old runs explicitly omit it.
|
|
11556
|
+
res.end(serializeJSON({
|
|
11557
|
+
...trace,
|
|
11558
|
+
...(run?.diagnosticReceiptV4?.summary ? { decisionSummary: run.diagnosticReceiptV4.summary } : {}),
|
|
11559
|
+
}));
|
|
11560
|
+
return;
|
|
11561
|
+
}
|
|
11562
|
+
if (req.method === 'GET' && /^\/api\/ask-traces\/[0-9a-f]{32}\/export$/.test(path)) {
|
|
11563
|
+
const traceStatus = askTraceStore.status();
|
|
11564
|
+
if (!traceStatus.available) {
|
|
11565
|
+
writeTraceStoreUnavailable(traceStatus);
|
|
11566
|
+
return;
|
|
11567
|
+
}
|
|
11568
|
+
const traceId = path.split('/')[3] ?? '';
|
|
11569
|
+
const trace = askTraceStore.get(traceId);
|
|
11570
|
+
if (!trace) {
|
|
11571
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11572
|
+
res.end(serializeJSON({ code: 'TRACE_NOT_FOUND', error: 'No local trace was found.' }));
|
|
11573
|
+
return;
|
|
11574
|
+
}
|
|
11575
|
+
if (trace.envelope.recordingStatus === 'detail_expired') {
|
|
11576
|
+
res.writeHead(410, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11577
|
+
res.end(serializeJSON({ code: 'TRACE_DETAIL_EXPIRED', error: 'Detailed trace evidence has expired; it cannot be exported.' }));
|
|
11578
|
+
return;
|
|
11579
|
+
}
|
|
11580
|
+
if ((url.searchParams.get('profile') ?? 'strict') !== 'strict') {
|
|
11581
|
+
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11582
|
+
res.end(serializeJSON({ code: 'TRACE_EXPORT_REDACTION_FAILED', error: 'The runtime export endpoint only serves strict redacted bundles.' }));
|
|
11583
|
+
return;
|
|
11584
|
+
}
|
|
11585
|
+
try {
|
|
11586
|
+
const run = await agentRunStore.get(trace.envelope.runId);
|
|
11587
|
+
const bundle = createAskTracePortableBundleV1(trace, {
|
|
11588
|
+
profile: 'strict',
|
|
11589
|
+
runReceipt: run,
|
|
11590
|
+
provenance: 'recorded',
|
|
11591
|
+
});
|
|
11592
|
+
askTraceStore.recordExportReceipt(trace.envelope.traceId, {
|
|
11593
|
+
version: 1,
|
|
11594
|
+
profile: 'strict',
|
|
11595
|
+
bundleFingerprint: bundle.manifest.bundleFingerprint,
|
|
11596
|
+
exportedAt: bundle.manifest.createdAt,
|
|
11597
|
+
checksums: {
|
|
11598
|
+
'manifest.json': `sha256:${createHash('sha256').update(serializeJSON(bundle.manifest)).digest('hex')}`,
|
|
11599
|
+
...bundle.manifest.checksums,
|
|
11600
|
+
},
|
|
11601
|
+
canaryPassed: true,
|
|
11602
|
+
...(bundle.trace.envelope.traceFingerprint ? { traceFingerprint: bundle.trace.envelope.traceFingerprint } : {}),
|
|
11603
|
+
});
|
|
11604
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Disposition': `attachment; filename="ask-trace-${traceId}.json"` });
|
|
11605
|
+
res.end(serializeJSON(bundle));
|
|
11606
|
+
}
|
|
11607
|
+
catch {
|
|
11608
|
+
res.writeHead(422, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11609
|
+
res.end(serializeJSON({ code: 'TRACE_EXPORT_REDACTION_FAILED', error: 'The local trace could not be exported safely.' }));
|
|
11610
|
+
}
|
|
11611
|
+
return;
|
|
11612
|
+
}
|
|
11613
|
+
if (req.method === 'GET' && /^\/api\/ask-traces\/[0-9a-f]{32}$/.test(path)) {
|
|
11614
|
+
const traceStatus = askTraceStore.status();
|
|
11615
|
+
if (!traceStatus.available) {
|
|
11616
|
+
writeTraceStoreUnavailable(traceStatus);
|
|
11617
|
+
return;
|
|
11618
|
+
}
|
|
11619
|
+
const traceId = path.split('/').at(-1) ?? '';
|
|
11620
|
+
const trace = askTraceStore.get(traceId);
|
|
11621
|
+
if (!trace) {
|
|
11622
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11623
|
+
res.end(serializeJSON({ code: 'TRACE_NOT_FOUND', error: 'No local trace was found.' }));
|
|
11624
|
+
return;
|
|
11625
|
+
}
|
|
11626
|
+
if (trace.envelope.recordingStatus === 'detail_expired') {
|
|
11627
|
+
res.writeHead(410, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11628
|
+
res.end(serializeJSON({ code: 'TRACE_DETAIL_EXPIRED', error: 'Detailed trace evidence has expired; the run summary remains available.', envelope: trace.envelope }));
|
|
11629
|
+
return;
|
|
11630
|
+
}
|
|
11631
|
+
const run = await agentRunStore.get(trace.envelope.runId);
|
|
11632
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11633
|
+
res.end(serializeJSON({
|
|
11634
|
+
...trace,
|
|
11635
|
+
...(run?.diagnosticReceiptV4?.summary ? { decisionSummary: run.diagnosticReceiptV4.summary } : {}),
|
|
11636
|
+
}));
|
|
11637
|
+
return;
|
|
11638
|
+
}
|
|
9695
11639
|
/**
|
|
9696
11640
|
* One-click bounded execution repair.
|
|
9697
11641
|
*
|
|
@@ -9837,6 +11781,41 @@ export async function startLocalServer(opts) {
|
|
|
9837
11781
|
const repairStartedAt = new Date().toISOString();
|
|
9838
11782
|
const repairStartedAtMs = Date.now();
|
|
9839
11783
|
const sourceFailureForReservation = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
|
|
11784
|
+
// A one-click repair is a derived Ask run, not an invisible side effect
|
|
11785
|
+
// of the original failure. Give it its own addressable local trace and
|
|
11786
|
+
// keep the source run/trace plus immutable plan fingerprint as typed
|
|
11787
|
+
// linkage. No prompt, SQL, result row, or raw failure text enters it.
|
|
11788
|
+
const repairTrace = createAskTraceObserverV1({
|
|
11789
|
+
store: askTraceStore,
|
|
11790
|
+
runId: derivedRunId,
|
|
11791
|
+
surface: 'browser',
|
|
11792
|
+
mode: sourceRun.requestedMode === 'research' ? 'research' : 'ask',
|
|
11793
|
+
questionFingerprint: runtimeTraceFingerprint(sourceRun.question),
|
|
11794
|
+
...(sourceRun.traceReference?.traceId ? { parentTraceId: sourceRun.traceReference.traceId } : {}),
|
|
11795
|
+
parentRunId: sourceRun.id,
|
|
11796
|
+
});
|
|
11797
|
+
repairTrace.recordLink({
|
|
11798
|
+
kind: 'derived_repair',
|
|
11799
|
+
targetRunId: sourceRun.id,
|
|
11800
|
+
...(sourceRun.traceReference?.traceId ? { targetTraceId: sourceRun.traceReference.traceId } : {}),
|
|
11801
|
+
});
|
|
11802
|
+
const repairTracePayload = {
|
|
11803
|
+
kind: 'sql',
|
|
11804
|
+
execution: {
|
|
11805
|
+
version: 1,
|
|
11806
|
+
tier: 'exploratory_sql',
|
|
11807
|
+
planFingerprint: capability.planFingerprint,
|
|
11808
|
+
sqlFingerprint: capability.sqlFingerprint,
|
|
11809
|
+
targetFingerprint: capability.targetFingerprint,
|
|
11810
|
+
reviewRequired: true,
|
|
11811
|
+
},
|
|
11812
|
+
};
|
|
11813
|
+
const repairTraceSpan = repairTrace.startSpan({
|
|
11814
|
+
name: 'sql.repair',
|
|
11815
|
+
stage: 'sql',
|
|
11816
|
+
reasonCode: 'repair_attempted',
|
|
11817
|
+
payload: repairTracePayload,
|
|
11818
|
+
});
|
|
9840
11819
|
const reservationEvent = {
|
|
9841
11820
|
id: `${derivedRunId}:repair`,
|
|
9842
11821
|
runId: derivedRunId,
|
|
@@ -9893,119 +11872,137 @@ export async function startLocalServer(opts) {
|
|
|
9893
11872
|
}
|
|
9894
11873
|
let repaired;
|
|
9895
11874
|
try {
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
normalizedSqlFingerprint: executionFingerprint(sqlRepair.repairedSql),
|
|
9968
|
-
parameterFingerprint: repairedExecutionReceipt.parameterFingerprint,
|
|
9969
|
-
provenanceFingerprint: executionFingerprint(stableExecutionValue(repairedInvocation.resolvedParameters.map((parameter) => ({
|
|
9970
|
-
name: parameter.name,
|
|
9971
|
-
source: parameter.source,
|
|
9972
|
-
})))),
|
|
9973
|
-
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
11875
|
+
repaired = await agentRunAskTraceContext.run(repairTrace, async () => {
|
|
11876
|
+
// Compile the immutable original wrapper first. A provider receives only
|
|
11877
|
+
// this embedded SQL, never the DQL wrapper or its surrounding fields.
|
|
11878
|
+
const invocation = prepareBlockInvocation({
|
|
11879
|
+
source: sourceDqlArtifact.source,
|
|
11880
|
+
parameters: sourceDqlArtifact.parameterValues,
|
|
11881
|
+
question: sourceRun.question,
|
|
11882
|
+
surface: 'ask_ai',
|
|
11883
|
+
});
|
|
11884
|
+
if (invocation.errors.length || invocation.unresolvedParameters.length) {
|
|
11885
|
+
throw boundedRepairError(invocation.errors.join(' ') || `Provide required parameters before retrying: ${invocation.unresolvedParameters.join(', ')}.`, 409);
|
|
11886
|
+
}
|
|
11887
|
+
const originalPlan = buildExecutionPlan({
|
|
11888
|
+
id: 'ask-execution-repair-source',
|
|
11889
|
+
type: 'dql',
|
|
11890
|
+
source: sourceDqlArtifact.source,
|
|
11891
|
+
title: sourceDqlArtifact.name,
|
|
11892
|
+
}, {
|
|
11893
|
+
semanticLayer,
|
|
11894
|
+
driver: targetConnection.driver,
|
|
11895
|
+
parameters: invocation.values,
|
|
11896
|
+
});
|
|
11897
|
+
if (!originalPlan?.sql?.trim())
|
|
11898
|
+
throw boundedRepairError('The original DQL wrapper did not compile to SQL.', 409);
|
|
11899
|
+
if (capability.sqlFingerprint !== capabilityHash(originalPlan.sql.trim())) {
|
|
11900
|
+
throw boundedRepairError('The original compiled SQL no longer matches its immutable repair capability.', 409);
|
|
11901
|
+
}
|
|
11902
|
+
const sqlRepair = await executeBoundedSqlRepair({
|
|
11903
|
+
question: sourceRun.question,
|
|
11904
|
+
sourceSql: originalPlan.sql,
|
|
11905
|
+
targetConnection,
|
|
11906
|
+
targetConnectionName,
|
|
11907
|
+
sqlParams: originalPlan.sqlParams,
|
|
11908
|
+
variables: { ...originalPlan.variables, ...invocation.values },
|
|
11909
|
+
});
|
|
11910
|
+
const embeddedSql = restoreNotebookDqlParameterInterpolations(sqlRepair.repairedSql, originalPlan.sqlParams);
|
|
11911
|
+
const repairedSource = replaceNotebookDqlQueryForRepair(sourceDqlArtifact.source, embeddedSql);
|
|
11912
|
+
if (!repairedSource)
|
|
11913
|
+
throw boundedRepairError('DQL could not reinsert the repaired SQL into the immutable wrapper.', 422);
|
|
11914
|
+
const repairedInvocation = prepareBlockInvocation({
|
|
11915
|
+
source: repairedSource,
|
|
11916
|
+
parameters: sourceDqlArtifact.parameterValues,
|
|
11917
|
+
question: sourceRun.question,
|
|
11918
|
+
surface: 'ask_ai',
|
|
11919
|
+
});
|
|
11920
|
+
if (repairedInvocation.errors.length || repairedInvocation.unresolvedParameters.length) {
|
|
11921
|
+
throw boundedRepairError('The SQL-only repair did not preserve a compilable DQL wrapper.', 422);
|
|
11922
|
+
}
|
|
11923
|
+
const repairedPlan = buildExecutionPlan({
|
|
11924
|
+
id: 'ask-execution-repair-derived',
|
|
11925
|
+
type: 'dql',
|
|
11926
|
+
source: repairedSource,
|
|
11927
|
+
title: sourceDqlArtifact.name,
|
|
11928
|
+
}, {
|
|
11929
|
+
semanticLayer,
|
|
11930
|
+
driver: targetConnection.driver,
|
|
11931
|
+
parameters: repairedInvocation.values,
|
|
11932
|
+
});
|
|
11933
|
+
if (!repairedPlan?.sql?.trim() || normalizeSqlForComparison(repairedPlan.sql) !== normalizeSqlForComparison(sqlRepair.repairedSql)) {
|
|
11934
|
+
throw boundedRepairError('The repaired wrapper recompiled to different SQL, so DQL stopped before publishing it.', 422);
|
|
11935
|
+
}
|
|
11936
|
+
const repairedExecutionReceipt = createDqlArtifactExecutionReceipt(repairedSource, sqlRepair.result.sql ?? repairedPlan.sql, repairedInvocation.values, sqlRepair.result);
|
|
11937
|
+
const repairedExecutableArtifact = sqlRepair.result.executableArtifact
|
|
11938
|
+
? {
|
|
11939
|
+
...sqlRepair.result.executableArtifact,
|
|
11940
|
+
kind: 'sql_block',
|
|
11941
|
+
dqlFingerprint: executionFingerprint(repairedSource),
|
|
11942
|
+
sourceFingerprint: executionFingerprint(stableExecutionValue({
|
|
11943
|
+
name: sourceDqlArtifact.name ?? null,
|
|
11944
|
+
path: sourceDqlArtifact.sourcePath ?? null,
|
|
11945
|
+
})),
|
|
9974
11946
|
compiledSqlFingerprint: executionFingerprint(repairedPlan.sql),
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
11947
|
+
normalizedSqlFingerprint: executionFingerprint(sqlRepair.repairedSql),
|
|
11948
|
+
parameterFingerprint: repairedExecutionReceipt.parameterFingerprint,
|
|
11949
|
+
provenanceFingerprint: executionFingerprint(stableExecutionValue(repairedInvocation.resolvedParameters.map((parameter) => ({
|
|
11950
|
+
name: parameter.name,
|
|
11951
|
+
source: parameter.source,
|
|
11952
|
+
})))),
|
|
11953
|
+
planFingerprint: executionFingerprint(stableExecutionValue({
|
|
11954
|
+
compiledSqlFingerprint: executionFingerprint(repairedPlan.sql),
|
|
11955
|
+
parameterCount: repairedPlan.sqlParams?.length ?? 0,
|
|
11956
|
+
variableNames: Object.keys(repairedPlan.variables ?? {}).sort(),
|
|
11957
|
+
})),
|
|
11958
|
+
trustState: 'review_required',
|
|
11959
|
+
receipt: repairedExecutionReceipt,
|
|
11960
|
+
}
|
|
11961
|
+
: undefined;
|
|
11962
|
+
const repairedArtifact = {
|
|
11963
|
+
...sourceDqlArtifact,
|
|
11964
|
+
source: repairedSource,
|
|
11965
|
+
compiledSql: repairedPlan.sql,
|
|
9978
11966
|
trustState: 'review_required',
|
|
9979
|
-
|
|
9980
|
-
}
|
|
9981
|
-
: undefined;
|
|
9982
|
-
const repairedArtifact = {
|
|
9983
|
-
...sourceDqlArtifact,
|
|
9984
|
-
source: repairedSource,
|
|
9985
|
-
compiledSql: repairedPlan.sql,
|
|
9986
|
-
trustState: 'review_required',
|
|
9987
|
-
persistence: 'transient',
|
|
9988
|
-
executionReceipt: repairedExecutionReceipt,
|
|
9989
|
-
...(repairedExecutableArtifact ? { executableArtifact: repairedExecutableArtifact } : {}),
|
|
9990
|
-
...(repairedInvocation.parameters.length ? { parameters: repairedInvocation.parameters } : {}),
|
|
9991
|
-
...(Object.keys(repairedInvocation.values).length ? { parameterValues: repairedInvocation.values } : {}),
|
|
9992
|
-
};
|
|
9993
|
-
repaired = {
|
|
9994
|
-
...sqlRepair,
|
|
9995
|
-
result: {
|
|
9996
|
-
...sqlRepair.result,
|
|
11967
|
+
persistence: 'transient',
|
|
9997
11968
|
executionReceipt: repairedExecutionReceipt,
|
|
9998
11969
|
...(repairedExecutableArtifact ? { executableArtifact: repairedExecutableArtifact } : {}),
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
11970
|
+
...(repairedInvocation.parameters.length ? { parameters: repairedInvocation.parameters } : {}),
|
|
11971
|
+
...(Object.keys(repairedInvocation.values).length ? { parameterValues: repairedInvocation.values } : {}),
|
|
11972
|
+
};
|
|
11973
|
+
return {
|
|
11974
|
+
...sqlRepair,
|
|
11975
|
+
result: {
|
|
11976
|
+
...sqlRepair.result,
|
|
11977
|
+
executionReceipt: repairedExecutionReceipt,
|
|
11978
|
+
...(repairedExecutableArtifact ? { executableArtifact: repairedExecutableArtifact } : {}),
|
|
11979
|
+
dqlArtifact: repairedArtifact,
|
|
11980
|
+
},
|
|
11981
|
+
repairedSql: repairedPlan.sql,
|
|
11982
|
+
repairedSource,
|
|
11983
|
+
sqlParams: repairedPlan.sqlParams,
|
|
11984
|
+
variables: { ...repairedPlan.variables, ...repairedInvocation.values },
|
|
11985
|
+
};
|
|
11986
|
+
});
|
|
11987
|
+
repairTrace.finishSpan(repairTraceSpan, {
|
|
11988
|
+
outcome: 'ok',
|
|
11989
|
+
reasonCode: 'repair_attempted',
|
|
11990
|
+
payload: repairTracePayload,
|
|
11991
|
+
});
|
|
10006
11992
|
}
|
|
10007
11993
|
catch (error) {
|
|
10008
11994
|
const repairError = error;
|
|
11995
|
+
repairTrace.finishSpan(repairTraceSpan, {
|
|
11996
|
+
outcome: 'error',
|
|
11997
|
+
reasonCode: 'sql_failure',
|
|
11998
|
+
payload: repairTracePayload,
|
|
11999
|
+
});
|
|
12000
|
+
const repairTraceReference = repairTrace.finalize({
|
|
12001
|
+
status: 'blocked',
|
|
12002
|
+
terminalOutcome: 'blocked',
|
|
12003
|
+
trustState: 'blocked',
|
|
12004
|
+
selectedTier: 'exploratory_sql',
|
|
12005
|
+
});
|
|
10009
12006
|
const dispatchEvidence = providerDispatchTerminalEvidence(error);
|
|
10010
12007
|
const failedAt = new Date().toISOString();
|
|
10011
12008
|
const failedTelemetry = {
|
|
@@ -10053,6 +12050,7 @@ export async function startLocalServer(opts) {
|
|
|
10053
12050
|
providerEgressReceiptFingerprints: failedReceipts.map((receipt) => executionFingerprint(stableExecutionValue(receipt))),
|
|
10054
12051
|
repairCapabilityFingerprint: executionFingerprint(stableExecutionValue(capability)),
|
|
10055
12052
|
},
|
|
12053
|
+
...(repairTraceReference ? { traceReference: repairTraceReference } : {}),
|
|
10056
12054
|
});
|
|
10057
12055
|
res.writeHead(repairError.status ?? 500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
10058
12056
|
res.end(serializeJSON({
|
|
@@ -10069,6 +12067,21 @@ export async function startLocalServer(opts) {
|
|
|
10069
12067
|
const sourceFailure = analyticalFailedRunFromAgentRun(sourceRun)?.failure;
|
|
10070
12068
|
const presentationContext = repairPresentationContextFromAgentRun(sourceRun);
|
|
10071
12069
|
const executionResultFingerprint = result.executionReceipt?.resultFingerprint;
|
|
12070
|
+
const resultTraceSpan = repairTrace.startSpan({
|
|
12071
|
+
name: 'result.normalize',
|
|
12072
|
+
stage: 'result',
|
|
12073
|
+
reasonCode: 'result_accepted',
|
|
12074
|
+
payload: {
|
|
12075
|
+
kind: 'result',
|
|
12076
|
+
...(executionResultFingerprint ? { resultFingerprint: executionResultFingerprint } : {}),
|
|
12077
|
+
rowCount: result.rowCount,
|
|
12078
|
+
trustState: 'review_required',
|
|
12079
|
+
},
|
|
12080
|
+
});
|
|
12081
|
+
repairTrace.finishSpan(resultTraceSpan, {
|
|
12082
|
+
outcome: 'ok',
|
|
12083
|
+
reasonCode: 'result_accepted',
|
|
12084
|
+
});
|
|
10072
12085
|
const repairTotalDurationMs = Date.now() - repairStartedAtMs;
|
|
10073
12086
|
const repairTelemetry = {
|
|
10074
12087
|
version: 1,
|
|
@@ -10212,6 +12225,14 @@ export async function startLocalServer(opts) {
|
|
|
10212
12225
|
attempt: 1,
|
|
10213
12226
|
},
|
|
10214
12227
|
};
|
|
12228
|
+
const repairTraceReference = repairTrace.finalize({
|
|
12229
|
+
status: 'completed',
|
|
12230
|
+
terminalOutcome: 'needs_review',
|
|
12231
|
+
trustState: 'review_required',
|
|
12232
|
+
selectedTier: 'exploratory_sql',
|
|
12233
|
+
});
|
|
12234
|
+
if (repairTraceReference)
|
|
12235
|
+
derivedRun.traceReference = repairTraceReference;
|
|
10215
12236
|
await agentRunStore.save(derivedRun);
|
|
10216
12237
|
recordConversationTurn(threadId ? getConversationStore() : null, threadId, derivedRun);
|
|
10217
12238
|
res.writeHead(201, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
@@ -10291,6 +12312,17 @@ export async function startLocalServer(opts) {
|
|
|
10291
12312
|
res.end(serializeJSON({ error: parsed.error ?? 'Invalid agent run request.' }));
|
|
10292
12313
|
return;
|
|
10293
12314
|
}
|
|
12315
|
+
// A trace surface is host-controlled metadata. Do not parse it from
|
|
12316
|
+
// the JSON body: only the unguessable per-runtime capability passed
|
|
12317
|
+
// from `dql agent ask` may mark this local request as CLI.
|
|
12318
|
+
const traceSurface = cliAskTraceCapabilities.consume({
|
|
12319
|
+
capability: req.headers['x-dql-ask-trace-capability'],
|
|
12320
|
+
scope: 'agent-runs',
|
|
12321
|
+
loopbackServer: loopback,
|
|
12322
|
+
remoteAddress: req.socket.remoteAddress,
|
|
12323
|
+
});
|
|
12324
|
+
if (traceSurface)
|
|
12325
|
+
parsed.request.traceSurface = traceSurface;
|
|
10294
12326
|
// The answer-loop cascade now proves certified/semantic/generated tiers
|
|
10295
12327
|
// after retrieval and execution. Avoid pre-routing ordinary Ask runs from
|
|
10296
12328
|
// token-overlap signals; explicit callers may still provide signals.
|
|
@@ -10355,14 +12387,9 @@ export async function startLocalServer(opts) {
|
|
|
10355
12387
|
inheritedSignal: ingressSignal,
|
|
10356
12388
|
});
|
|
10357
12389
|
parsed.request.signal = parsed.request.runBudget.hardSignal;
|
|
10358
|
-
|
|
10359
|
-
// + planning/generation (3) + narration (2) + one repair. The old
|
|
10360
|
-
// `total: 2` left nothing for narration once generation had run,
|
|
10361
|
-
// which is why the answer arrived as a deterministic fact-join.
|
|
10362
|
-
const runProviderEvidence = new RunScopedProviderDispatchEvidence(parsed.request.requestedMode === 'research'
|
|
10363
|
-
? { total: 14, meaningResolution: 1, generationGroup: 11, narration: 2, repair: 1 }
|
|
10364
|
-
: { total: 6, meaningResolution: 1, generationGroup: 3, narration: 2, repair: 1 }, parsed.request.runBudget, resolveProviderResultRowEgressPolicy({
|
|
12390
|
+
const runProviderEvidence = new RunScopedProviderDispatchEvidence(agentRunProviderDispatchBudgetForMode(parsed.request.requestedMode), parsed.request.runBudget, resolveProviderResultRowEgressPolicy({
|
|
10365
12391
|
projectSetting: projectConfig?.agent?.providerResultRowEgress,
|
|
12392
|
+
requestedMode: parsed.request.requestedMode,
|
|
10366
12393
|
researchOptIn: parsed.request.requestedMode === 'research'
|
|
10367
12394
|
&& parsed.request.researchResultRowsOptIn === true,
|
|
10368
12395
|
}));
|
|
@@ -11568,7 +13595,12 @@ export async function startLocalServer(opts) {
|
|
|
11568
13595
|
const limit = Number(url.searchParams.get('limit') ?? '50');
|
|
11569
13596
|
const includeArchived = url.searchParams.get('archived') === '1';
|
|
11570
13597
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
11571
|
-
res.end(serializeJSON({
|
|
13598
|
+
res.end(serializeJSON({
|
|
13599
|
+
threads: store.listThreads({ limit: Number.isFinite(limit) ? limit : 50, includeArchived }),
|
|
13600
|
+
// Browser cache identity only. It is an opaque one-way fingerprint
|
|
13601
|
+
// and intentionally does not become conversation/trace evidence.
|
|
13602
|
+
projectIdentity: conversationProjectIdentity,
|
|
13603
|
+
}));
|
|
11572
13604
|
return;
|
|
11573
13605
|
}
|
|
11574
13606
|
if (req.method === 'POST' && path === '/api/agent/threads') {
|
|
@@ -18725,7 +20757,12 @@ table: ${table}${tagList}
|
|
|
18725
20757
|
res.end('Method not allowed');
|
|
18726
20758
|
return;
|
|
18727
20759
|
}
|
|
18728
|
-
|
|
20760
|
+
// The notebook owns these explicit client routes. Keep the fallback
|
|
20761
|
+
// deliberately narrow: arbitrary missing paths remain real 404s.
|
|
20762
|
+
const requestedPath = path === '/'
|
|
20763
|
+
|| isAskClientRoutePath(path)
|
|
20764
|
+
? '/index.html'
|
|
20765
|
+
: path;
|
|
18729
20766
|
const filePath = safeJoin(rootDir, requestedPath);
|
|
18730
20767
|
if (!filePath || !existsSync(filePath) || statSync(filePath).isDirectory()) {
|
|
18731
20768
|
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -18773,6 +20810,7 @@ table: ${table}${tagList}
|
|
|
18773
20810
|
projectWatchers.length = 0;
|
|
18774
20811
|
unsubscribeOperationEvents();
|
|
18775
20812
|
projectRefreshCoordinator.close();
|
|
20813
|
+
askTraceStore.close();
|
|
18776
20814
|
for (const client of operationSseClients) {
|
|
18777
20815
|
try {
|
|
18778
20816
|
client.end();
|
|
@@ -18815,7 +20853,10 @@ function providerDispatchTerminalEvidence(value) {
|
|
|
18815
20853
|
|| (evidence.providerRoundTrips ?? -1) < 0)
|
|
18816
20854
|
return undefined;
|
|
18817
20855
|
return {
|
|
18818
|
-
providerEgressReceipts: evidence.providerEgressReceipts
|
|
20856
|
+
providerEgressReceipts: evidence.providerEgressReceipts.flatMap((receipt) => {
|
|
20857
|
+
const normalized = normalizeProviderEgressReceiptV1(receipt);
|
|
20858
|
+
return normalized ? [normalized] : [];
|
|
20859
|
+
}),
|
|
18819
20860
|
providerRoundTrips: evidence.providerRoundTrips,
|
|
18820
20861
|
toolCalls: Number.isInteger(evidence.toolCalls) && (evidence.toolCalls ?? -1) >= 0 ? evidence.toolCalls : 0,
|
|
18821
20862
|
sqlExecutions: Number.isInteger(evidence.sqlExecutions) && (evidence.sqlExecutions ?? -1) >= 0 ? evidence.sqlExecutions : 0,
|
|
@@ -19121,7 +21162,10 @@ function normalizeQueryResult(result, semanticRefs) {
|
|
|
19121
21162
|
truncated: result?.truncated,
|
|
19122
21163
|
resultFingerprint: result?.resultFingerprint,
|
|
19123
21164
|
executionReceipt: result?.executionReceipt,
|
|
19124
|
-
trustState:
|
|
21165
|
+
trustState: canonicalPersistedTrustState({
|
|
21166
|
+
trustState: result?.trustState,
|
|
21167
|
+
answerTier: result?.answerTier,
|
|
21168
|
+
}),
|
|
19125
21169
|
answerTier: result?.answerTier,
|
|
19126
21170
|
});
|
|
19127
21171
|
const rawRows = Array.isArray(result?.rows) ? result.rows : [];
|
|
@@ -19174,7 +21218,20 @@ export function resolveDefaultLLMProvider(projectRoot) {
|
|
|
19174
21218
|
* answer-loop runner — never the MCP `claudeCodeRunner`, which doesn't emit a governed
|
|
19175
21219
|
* answer envelope. Everything else uses the Settings-resolved default runner.
|
|
19176
21220
|
*/
|
|
19177
|
-
export function resolveGovernedAnswerRunner(projectRoot) {
|
|
21221
|
+
export function resolveGovernedAnswerRunner(projectRoot, requestedProvider) {
|
|
21222
|
+
// This is intentionally before configuration discovery. It lets the
|
|
21223
|
+
// canonical AgentRun preflight distinguish an explicit unknown provider from
|
|
21224
|
+
// an absent configuration, while a known-but-unavailable provider still
|
|
21225
|
+
// reaches its real adapter readiness check (for example Ollama network
|
|
21226
|
+
// failure rather than a fabricated authentication error).
|
|
21227
|
+
if (requestedProvider !== undefined) {
|
|
21228
|
+
if (!isGovernedAnswerProviderId(requestedProvider))
|
|
21229
|
+
return null;
|
|
21230
|
+
return {
|
|
21231
|
+
provider: requestedProvider,
|
|
21232
|
+
runner: createDqlAgentProviderRunner(requestedProvider),
|
|
21233
|
+
};
|
|
21234
|
+
}
|
|
19178
21235
|
// Runtime eval cassettes are an explicit, offline provider source. Resolve
|
|
19179
21236
|
// them before Settings because the CI fixture intentionally has no user
|
|
19180
21237
|
// provider configuration; otherwise Ask exits at this earlier gate and never
|
|
@@ -19214,6 +21271,21 @@ function isGovernedAnswerProviderId(value) {
|
|
|
19214
21271
|
|| value === 'claude-code'
|
|
19215
21272
|
|| value === 'codex';
|
|
19216
21273
|
}
|
|
21274
|
+
/**
|
|
21275
|
+
* The no-runner outcome is still an authoritative provider preflight result.
|
|
21276
|
+
* Preserve an explicit invalid selection as `MODEL_NOT_FOUND` instead of
|
|
21277
|
+
* collapsing it into the historical generic authentication error.
|
|
21278
|
+
*/
|
|
21279
|
+
export function governedProviderPreflightError(requestedProvider) {
|
|
21280
|
+
const invalidRequestedProvider = Boolean(requestedProvider)
|
|
21281
|
+
&& !isGovernedAnswerProviderId(requestedProvider);
|
|
21282
|
+
return Object.assign(new Error(invalidRequestedProvider
|
|
21283
|
+
? 'The selected AI provider is not available in this local runtime. Choose a configured provider in Settings and retry.'
|
|
21284
|
+
: 'No AI provider is configured. Configure a subscription (Claude Code / Codex), OpenAI, Gemini, Ollama, or a custom OpenAI-compatible endpoint in Settings.'), {
|
|
21285
|
+
code: invalidRequestedProvider ? 'MODEL_NOT_FOUND' : 'AUTHENTICATION_FAILED',
|
|
21286
|
+
providerPhase: 'preflight',
|
|
21287
|
+
});
|
|
21288
|
+
}
|
|
19217
21289
|
/** Map a runner provider id to the settings id whose reasoning ceiling applies. */
|
|
19218
21290
|
function reasoningSettingsIdFor(provider) {
|
|
19219
21291
|
return provider === 'claude-agent-sdk' ? 'anthropic' : provider;
|
|
@@ -21567,6 +23639,24 @@ function agentRunTelemetryForAnswer(answer, egressReceipts, totalDurationMs, nar
|
|
|
21567
23639
|
function executionFingerprint(value) {
|
|
21568
23640
|
return createHash('sha256').update(value).digest('hex');
|
|
21569
23641
|
}
|
|
23642
|
+
/**
|
|
23643
|
+
* Opaque browser-cache identity for local Ask conversations. A browser origin
|
|
23644
|
+
* is not a project boundary: a user can stop one `dql notebook` process and
|
|
23645
|
+
* start a different project on the same port. The client therefore receives
|
|
23646
|
+
* only this one-way, server-owned value and never a local path. It is not a
|
|
23647
|
+
* secret: it is intentionally linkable only within the local browser/runtime
|
|
23648
|
+
* across restarts so stale cache can be rejected; never log or export it.
|
|
23649
|
+
*/
|
|
23650
|
+
export function askConversationProjectIdentity(projectRoot) {
|
|
23651
|
+
let canonicalRoot;
|
|
23652
|
+
try {
|
|
23653
|
+
canonicalRoot = realpathSync(projectRoot);
|
|
23654
|
+
}
|
|
23655
|
+
catch {
|
|
23656
|
+
canonicalRoot = resolve(projectRoot);
|
|
23657
|
+
}
|
|
23658
|
+
return `sha256:${executionFingerprint(`dql.ask.conversation-cache.v1\0${canonicalRoot}`)}`;
|
|
23659
|
+
}
|
|
21570
23660
|
function buildSemanticAggregationCompilerReceipt(input) {
|
|
21571
23661
|
const plan = input.plan;
|
|
21572
23662
|
const metricId = plan?.analyticalFrame?.metricConceptIds[0];
|
|
@@ -29909,6 +31999,252 @@ export function predictDispatchMs(observed, assumedMs = ASSUMED_PROVIDER_DISPATC
|
|
|
29909
31999
|
}
|
|
29910
32000
|
const AGENT_MEANING_TIMEOUT_BASE_MS = 10_000;
|
|
29911
32001
|
const AGENT_MEANING_TIMEOUT_MS = AGENT_MEANING_TIMEOUT_BASE_MS * deadlineScale();
|
|
32002
|
+
/**
|
|
32003
|
+
* A Research root owns a fixed 120-second deadline. Reserve the last slice
|
|
32004
|
+
* for durable branch receipts, deterministic synthesis, and run persistence;
|
|
32005
|
+
* otherwise the first slow child can consume the entire investigation and
|
|
32006
|
+
* leave the user with neither an answer nor an explanation of the failure.
|
|
32007
|
+
*
|
|
32008
|
+
* This is deliberately a local runtime scheduling policy, not a second
|
|
32009
|
+
* product deadline. The root AgentRunBudget remains the one hard authority.
|
|
32010
|
+
*/
|
|
32011
|
+
export const RESEARCH_BRANCH_FINALIZATION_RESERVE_MS = 15_000;
|
|
32012
|
+
export const RESEARCH_MIN_BRANCH_EXECUTION_MS = 1_000;
|
|
32013
|
+
/**
|
|
32014
|
+
* Research branches are independent bounded investigations. Run a small
|
|
32015
|
+
* deterministic wave so a five-hypothesis plan does not divide the root
|
|
32016
|
+
* budget into five unusably short serial windows. This is deliberately below
|
|
32017
|
+
* the provider cap and keeps cancellation/finalization responsive.
|
|
32018
|
+
*/
|
|
32019
|
+
export const RESEARCH_MAX_CONCURRENT_BRANCHES = 3;
|
|
32020
|
+
/**
|
|
32021
|
+
* Fair-share one child deadline from the remaining root time. The formula is
|
|
32022
|
+
* intentionally deterministic so a trace can explain why a branch stopped:
|
|
32023
|
+
* after finalization is reserved, divide the usable window by the number of
|
|
32024
|
+
* remaining bounded waves. Every branch in the current wave receives the
|
|
32025
|
+
* same window. A too-small share is a skipped branch, not a late
|
|
32026
|
+
* provider/warehouse admission.
|
|
32027
|
+
*/
|
|
32028
|
+
export function allocateResearchBranchBudget(input) {
|
|
32029
|
+
const remainingMs = Math.max(0, Math.trunc(input.remainingMs));
|
|
32030
|
+
const remainingBranches = Math.max(1, Math.trunc(input.remainingBranches));
|
|
32031
|
+
const finalizationReserveMs = Math.max(0, Math.trunc(input.finalizationReserveMs ?? RESEARCH_BRANCH_FINALIZATION_RESERVE_MS));
|
|
32032
|
+
const minExecutionMs = Math.max(1, Math.trunc(input.minExecutionMs ?? RESEARCH_MIN_BRANCH_EXECUTION_MS));
|
|
32033
|
+
const maxConcurrentBranches = Math.max(1, Math.min(remainingBranches, Math.trunc(input.maxConcurrentBranches ?? RESEARCH_MAX_CONCURRENT_BRANCHES)));
|
|
32034
|
+
const remainingWaves = Math.ceil(remainingBranches / maxConcurrentBranches);
|
|
32035
|
+
const usableMs = Math.max(0, remainingMs - finalizationReserveMs);
|
|
32036
|
+
const branchBudgetMs = Math.floor(usableMs / remainingWaves);
|
|
32037
|
+
if (branchBudgetMs < minExecutionMs) {
|
|
32038
|
+
return {
|
|
32039
|
+
version: 1,
|
|
32040
|
+
remainingMs,
|
|
32041
|
+
finalizationReserveMs,
|
|
32042
|
+
maxConcurrentBranches,
|
|
32043
|
+
remainingWaves,
|
|
32044
|
+
stopReason: 'budget_exhausted',
|
|
32045
|
+
};
|
|
32046
|
+
}
|
|
32047
|
+
return {
|
|
32048
|
+
version: 1,
|
|
32049
|
+
remainingMs,
|
|
32050
|
+
finalizationReserveMs,
|
|
32051
|
+
maxConcurrentBranches,
|
|
32052
|
+
remainingWaves,
|
|
32053
|
+
branchBudgetMs,
|
|
32054
|
+
};
|
|
32055
|
+
}
|
|
32056
|
+
/**
|
|
32057
|
+
* Convert one typed Research action into an ordinary Ask requirement seed.
|
|
32058
|
+
*
|
|
32059
|
+
* The older branch runner reduced every action to the final token of its
|
|
32060
|
+
* target. That meant `compare_time` and `breakdown` silently discarded the
|
|
32061
|
+
* root metric (and, for time, its required role/grain), so distinct research
|
|
32062
|
+
* branches could all execute the same baseline. This projection is host-only:
|
|
32063
|
+
* it preserves the root tuple and adds the action's one requested operation,
|
|
32064
|
+
* but never turns a planner label into a trusted candidate or SQL authority.
|
|
32065
|
+
*/
|
|
32066
|
+
export function buildResearchBranchRequirementProjection(input) {
|
|
32067
|
+
const targetHint = researchBranchTargetHint(input.action.target);
|
|
32068
|
+
const rootRequirements = input.rootRequirementSeed.requirements;
|
|
32069
|
+
const rootPlan = input.rootPlan;
|
|
32070
|
+
const resolvedPlanTerms = (bindings) => bindings
|
|
32071
|
+
.filter((binding) => binding.status === 'resolved')
|
|
32072
|
+
.map((binding) => binding.requested);
|
|
32073
|
+
const rootMeasures = uniqueResearchBranchTerms(rootRequirements.measures, rootPlan ? resolvedPlanTerms(rootPlan.query.measures) : []);
|
|
32074
|
+
const rootDimensions = uniqueResearchBranchTerms(rootRequirements.dimensions, rootPlan ? resolvedPlanTerms(rootPlan.query.dimensions) : []);
|
|
32075
|
+
const rootEntityTerms = uniqueResearchBranchTerms(rootRequirements.entityTerms);
|
|
32076
|
+
const rootEntityDisplayTerms = uniqueResearchBranchTerms(rootRequirements.entityDisplayTerms);
|
|
32077
|
+
const rootMemberTerms = uniqueResearchBranchTerms(rootRequirements.memberTerms);
|
|
32078
|
+
const rootOutputTerms = uniqueResearchBranchTerms(rootRequirements.outputTerms ?? []);
|
|
32079
|
+
const rootTimeGrain = researchBranchTimeGrain(rootRequirements.time?.grain
|
|
32080
|
+
?? rootPlan?.query.timeGrain
|
|
32081
|
+
?? rootPlan?.analyticalFrame?.timeContext?.grain);
|
|
32082
|
+
const rootTime = rootRequirements.time
|
|
32083
|
+
? { ...rootRequirements.time }
|
|
32084
|
+
: rootTimeGrain
|
|
32085
|
+
? {
|
|
32086
|
+
role: 'time_axis',
|
|
32087
|
+
grain: rootTimeGrain,
|
|
32088
|
+
requiresDeclaredFiscalCalendar: false,
|
|
32089
|
+
}
|
|
32090
|
+
: undefined;
|
|
32091
|
+
const baseRequirements = (inputOverrides) => ({
|
|
32092
|
+
version: 1,
|
|
32093
|
+
measures: rootMeasures,
|
|
32094
|
+
dimensions: rootDimensions,
|
|
32095
|
+
entityTerms: rootEntityTerms,
|
|
32096
|
+
entityDisplayTerms: rootEntityDisplayTerms,
|
|
32097
|
+
memberTerms: rootMemberTerms,
|
|
32098
|
+
...(rootOutputTerms.length > 0 ? { outputTerms: rootOutputTerms } : {}),
|
|
32099
|
+
...(rootRequirements.grain ? { grain: rootRequirements.grain } : {}),
|
|
32100
|
+
...(rootRequirements.ranking ? { ranking: { ...rootRequirements.ranking } } : {}),
|
|
32101
|
+
...(rootTime ? { time: rootTime } : {}),
|
|
32102
|
+
...inputOverrides,
|
|
32103
|
+
});
|
|
32104
|
+
const rootTimeRange = input.rootRequirementSeed.queryIntent.timeRange
|
|
32105
|
+
?? rootPlan?.query.timeRange;
|
|
32106
|
+
const questionWithRootTimeRange = (question) => rootTimeRange
|
|
32107
|
+
? `${question} for ${rootTimeRange}`
|
|
32108
|
+
: question;
|
|
32109
|
+
/**
|
|
32110
|
+
* A child request has one host-owned tuple. The planner's target can
|
|
32111
|
+
* improve retrieval, but it must not erase a root filter, output, rank,
|
|
32112
|
+
* fiscal binding, or the root measure just because this particular branch
|
|
32113
|
+
* happens to look up a metric. Rebuild the seed for the child wording, then
|
|
32114
|
+
* restore the immutable root query intent and add only an action-owned
|
|
32115
|
+
* dimension where the action actually requires one.
|
|
32116
|
+
*/
|
|
32117
|
+
const projectedSeed = (question, requirements) => {
|
|
32118
|
+
const seed = buildAnalyticalRequirementSeedV1({ question, requirements });
|
|
32119
|
+
const rootIntent = input.rootRequirementSeed.queryIntent;
|
|
32120
|
+
const queryIntentDimensions = uniqueResearchBranchTerms(rootIntent.dimensions, requirements.dimensions, requirements.entityDisplayTerms);
|
|
32121
|
+
const queryIntentMeasures = uniqueResearchBranchTerms(rootIntent.measures, requirements.measures);
|
|
32122
|
+
return {
|
|
32123
|
+
...seed,
|
|
32124
|
+
queryIntent: {
|
|
32125
|
+
measures: queryIntentMeasures,
|
|
32126
|
+
dimensions: queryIntentDimensions,
|
|
32127
|
+
// Filter/member interpretation is host-owned on the root request. A
|
|
32128
|
+
// branch may bind it to a child snapshot, but may not discard it.
|
|
32129
|
+
filters: rootIntent.filters.map((filter) => ({ ...filter })),
|
|
32130
|
+
...(rootIntent.timeRange ? { timeRange: rootIntent.timeRange } : {}),
|
|
32131
|
+
...(rootIntent.timeGrain
|
|
32132
|
+
? { timeGrain: rootIntent.timeGrain }
|
|
32133
|
+
: requirements.time?.grain ? { timeGrain: requirements.time.grain } : {}),
|
|
32134
|
+
...(rootIntent.order ? { order: rootIntent.order } : {}),
|
|
32135
|
+
...(rootIntent.limit !== undefined ? { limit: rootIntent.limit } : {}),
|
|
32136
|
+
...(rootIntent.fiscalCalendarId ? { fiscalCalendarId: rootIntent.fiscalCalendarId } : {}),
|
|
32137
|
+
...(rootIntent.fiscalDateRoleId ? { fiscalDateRoleId: rootIntent.fiscalDateRoleId } : {}),
|
|
32138
|
+
},
|
|
32139
|
+
};
|
|
32140
|
+
};
|
|
32141
|
+
if (input.action.kind === 'lookup_metric') {
|
|
32142
|
+
// The target remains a retrieval phrase only. Keeping the root tuple here
|
|
32143
|
+
// matters for multi-metric, ranked, fiscal, filtered research: a lookup
|
|
32144
|
+
// branch cannot silently become a query for just the planner's metric.
|
|
32145
|
+
const question = `Focus on ${targetHint} while answering: ${researchBranchRootQuestion(input.rootRequirementSeed.sourceQuestion)}`;
|
|
32146
|
+
return {
|
|
32147
|
+
version: 1,
|
|
32148
|
+
action: input.action.kind,
|
|
32149
|
+
question,
|
|
32150
|
+
requirementSeed: projectedSeed(question, baseRequirements({})),
|
|
32151
|
+
};
|
|
32152
|
+
}
|
|
32153
|
+
if (input.action.kind === 'breakdown' && rootMeasures.length > 0) {
|
|
32154
|
+
const question = questionWithRootTimeRange(`Show ${rootMeasures.join(' and ')} by ${targetHint}`);
|
|
32155
|
+
return {
|
|
32156
|
+
version: 1,
|
|
32157
|
+
action: input.action.kind,
|
|
32158
|
+
question,
|
|
32159
|
+
requirementSeed: projectedSeed(question, baseRequirements({
|
|
32160
|
+
dimensions: uniqueResearchBranchTerms(rootDimensions, [targetHint]),
|
|
32161
|
+
})),
|
|
32162
|
+
};
|
|
32163
|
+
}
|
|
32164
|
+
if (input.action.kind === 'compare_time' && rootMeasures.length > 0) {
|
|
32165
|
+
const question = questionWithRootTimeRange(`Compare ${rootMeasures.join(' and ')} over time by ${targetHint}`);
|
|
32166
|
+
return {
|
|
32167
|
+
version: 1,
|
|
32168
|
+
action: input.action.kind,
|
|
32169
|
+
question,
|
|
32170
|
+
requirementSeed: projectedSeed(question, baseRequirements({
|
|
32171
|
+
// Keep the time target in the role-balanced requirement package as
|
|
32172
|
+
// a retrieval term. The child router still verifies its exact
|
|
32173
|
+
// time-axis role against its own candidate capability before plan
|
|
32174
|
+
// freeze; a planner label alone cannot authorize a time field.
|
|
32175
|
+
dimensions: uniqueResearchBranchTerms(rootDimensions, [targetHint]),
|
|
32176
|
+
time: {
|
|
32177
|
+
role: 'time_axis',
|
|
32178
|
+
...(rootTimeGrain ? { grain: rootTimeGrain } : {}),
|
|
32179
|
+
...(rootRequirements.time?.fiscalPeriod
|
|
32180
|
+
? { fiscalPeriod: rootRequirements.time.fiscalPeriod }
|
|
32181
|
+
: {}),
|
|
32182
|
+
requiresDeclaredFiscalCalendar: rootRequirements.time?.requiresDeclaredFiscalCalendar ?? false,
|
|
32183
|
+
},
|
|
32184
|
+
})),
|
|
32185
|
+
};
|
|
32186
|
+
}
|
|
32187
|
+
// Blocks, lineage, and app composition do not invent an analytical tuple.
|
|
32188
|
+
// Their target remains a bounded retrieval phrase, and the child cascade
|
|
32189
|
+
// decides whether it can freeze a compatible route.
|
|
32190
|
+
return {
|
|
32191
|
+
version: 1,
|
|
32192
|
+
action: input.action.kind,
|
|
32193
|
+
question: targetHint,
|
|
32194
|
+
};
|
|
32195
|
+
}
|
|
32196
|
+
function researchBranchTargetHint(target) {
|
|
32197
|
+
return target
|
|
32198
|
+
.trim()
|
|
32199
|
+
.split(/[/:]/)
|
|
32200
|
+
.at(-1)
|
|
32201
|
+
?.split('.')
|
|
32202
|
+
.at(-1)
|
|
32203
|
+
?.replace(/[_-]+/g, ' ')
|
|
32204
|
+
.trim() || 'the planned analytical target';
|
|
32205
|
+
}
|
|
32206
|
+
function researchBranchRootQuestion(sourceQuestion) {
|
|
32207
|
+
const withoutResearchPrefix = sourceQuestion
|
|
32208
|
+
.replace(/^\s*(?:deep\s+)?research\b\s*(?:about|on|into|for)?\s*/i, '')
|
|
32209
|
+
.trim();
|
|
32210
|
+
return withoutResearchPrefix || 'the root analytical question';
|
|
32211
|
+
}
|
|
32212
|
+
function uniqueResearchBranchTerms(...groups) {
|
|
32213
|
+
return [...new Set(groups
|
|
32214
|
+
.flat()
|
|
32215
|
+
.map((term) => term.trim())
|
|
32216
|
+
.filter(Boolean))];
|
|
32217
|
+
}
|
|
32218
|
+
function researchBranchTimeGrain(value) {
|
|
32219
|
+
return value === 'day' || value === 'week' || value === 'month' || value === 'quarter' || value === 'year'
|
|
32220
|
+
? value
|
|
32221
|
+
: undefined;
|
|
32222
|
+
}
|
|
32223
|
+
/**
|
|
32224
|
+
* Race a child against its own signal while consuming an eventual late
|
|
32225
|
+
* rejection. `runNotebookResearch` also receives that signal and checks it at
|
|
32226
|
+
* persistence boundaries, so a slow provider/query cannot overwrite the
|
|
32227
|
+
* already-recorded timeout receipt after this promise rejects.
|
|
32228
|
+
*/
|
|
32229
|
+
export function awaitResearchBranchDeadline(work, signal) {
|
|
32230
|
+
if (signal.aborted) {
|
|
32231
|
+
void work.catch(() => undefined);
|
|
32232
|
+
return Promise.reject(signal.reason ?? new DOMException('The Research branch deadline elapsed.', 'TimeoutError'));
|
|
32233
|
+
}
|
|
32234
|
+
return new Promise((resolve, reject) => {
|
|
32235
|
+
let settled = false;
|
|
32236
|
+
const finish = (callback) => {
|
|
32237
|
+
if (settled)
|
|
32238
|
+
return;
|
|
32239
|
+
settled = true;
|
|
32240
|
+
signal.removeEventListener('abort', onAbort);
|
|
32241
|
+
callback();
|
|
32242
|
+
};
|
|
32243
|
+
const onAbort = () => finish(() => reject(signal.reason ?? new DOMException('The Research branch deadline elapsed.', 'TimeoutError')));
|
|
32244
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
32245
|
+
work.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
32246
|
+
});
|
|
32247
|
+
}
|
|
29912
32248
|
export function boundedAgentMeaningSignal(signal, timeoutMs = AGENT_MEANING_TIMEOUT_MS) {
|
|
29913
32249
|
const timeout = AbortSignal.timeout(Math.max(1, timeoutMs));
|
|
29914
32250
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
@@ -30566,7 +32902,10 @@ function normalizeNotebookAgentResult(result) {
|
|
|
30566
32902
|
executionTime: result.executionTime,
|
|
30567
32903
|
resultFingerprint: result.resultFingerprint,
|
|
30568
32904
|
executionReceipt: result.executionReceipt,
|
|
30569
|
-
trustState:
|
|
32905
|
+
trustState: canonicalPersistedTrustState({
|
|
32906
|
+
trustState: result.executableArtifact?.trustState,
|
|
32907
|
+
answerTier: result.answerTier,
|
|
32908
|
+
}),
|
|
30570
32909
|
answerTier: result.answerTier,
|
|
30571
32910
|
});
|
|
30572
32911
|
return {
|
|
@@ -30577,6 +32916,7 @@ function normalizeNotebookAgentResult(result) {
|
|
|
30577
32916
|
executionTime: canonical.executionTime ?? 0,
|
|
30578
32917
|
...(canonical.truncated ? { truncated: true } : {}),
|
|
30579
32918
|
...(canonical.executionReceipt ? { executionReceipt: canonical.executionReceipt } : {}),
|
|
32919
|
+
...(canonical.trustState ? { trustState: canonical.trustState } : {}),
|
|
30580
32920
|
...(canonical.answerTier ? { answerTier: canonical.answerTier } : {}),
|
|
30581
32921
|
};
|
|
30582
32922
|
}
|
|
@@ -30589,6 +32929,20 @@ function notebookResearchSummary(question, result, error) {
|
|
|
30589
32929
|
}
|
|
30590
32930
|
return `Research plan created for "${question}". Add or generate SQL, then run a bounded preview.`;
|
|
30591
32931
|
}
|
|
32932
|
+
/**
|
|
32933
|
+
* Persisted Research children are deliberately narrated without another model
|
|
32934
|
+
* call after a frozen Ask plan has returned rows. This keeps the branch inside
|
|
32935
|
+
* its fair-share deadline and makes the stored finding a direct consequence of
|
|
32936
|
+
* its execution receipt rather than an unbounded second interpretation pass.
|
|
32937
|
+
*/
|
|
32938
|
+
function deterministicResearchBranchSummary(input) {
|
|
32939
|
+
const factNarrative = notebookResearchString(input.analyticalNarrative?.text);
|
|
32940
|
+
const fallback = notebookResearchSummary(input.question, input.result, undefined);
|
|
32941
|
+
const receiptKind = input.executionReceipt?.resultFingerprint
|
|
32942
|
+
? 'receipt-bound'
|
|
32943
|
+
: 'execution-fingerprint-bound';
|
|
32944
|
+
return `${factNarrative ?? fallback} This ${receiptKind} Research branch was retained for synthesis without a follow-up provider narration.`;
|
|
32945
|
+
}
|
|
30592
32946
|
function recordNotebookQueryRun(projectRoot, input) {
|
|
30593
32947
|
try {
|
|
30594
32948
|
recordQueryRun(projectRoot, {
|