@duckcodeailabs/dql-agent 1.14.2 → 1.14.4
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/agent-run-engine.d.ts +239 -4
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +2924 -85
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +51 -3
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-planner.d.ts +6 -0
- package/dist/agent-run-planner.d.ts.map +1 -1
- package/dist/agent-run-planner.js +2 -0
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +591 -11
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +9 -1
- package/dist/agentic/analyst-loop.d.ts.map +1 -1
- package/dist/agentic/analyst-loop.js +204 -4
- package/dist/agentic/analyst-loop.js.map +1 -1
- package/dist/agentic/assumptions.d.ts +1 -1
- package/dist/agentic/assumptions.d.ts.map +1 -1
- package/dist/agentic/assumptions.js.map +1 -1
- package/dist/agentic/ledger-tools.d.ts.map +1 -1
- package/dist/agentic/ledger-tools.js +8 -1
- package/dist/agentic/ledger-tools.js.map +1 -1
- package/dist/agentic/orchestrator-policy.d.ts +3 -1
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -1
- package/dist/agentic/orchestrator-policy.js +1 -1
- package/dist/agentic/orchestrator-policy.js.map +1 -1
- package/dist/agentic/research-agent.d.ts.map +1 -1
- package/dist/agentic/research-agent.js +10 -1
- package/dist/agentic/research-agent.js.map +1 -1
- package/dist/agentic/sql-authorization.d.ts +8 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -1
- package/dist/agentic/sql-authorization.js +3 -0
- package/dist/agentic/sql-authorization.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts +13 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +441 -32
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/analytical-compatibility.d.ts.map +1 -1
- package/dist/analytical-compatibility.js +8 -1
- package/dist/analytical-compatibility.js.map +1 -1
- package/dist/analytical-failure-repair.d.ts.map +1 -1
- package/dist/analytical-failure-repair.js +6 -2
- package/dist/analytical-failure-repair.js.map +1 -1
- package/dist/analytical-frame.d.ts +42 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +490 -26
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-orchestration.d.ts +1259 -9
- package/dist/analytical-orchestration.d.ts.map +1 -1
- package/dist/analytical-orchestration.js +970 -84
- package/dist/analytical-orchestration.js.map +1 -1
- package/dist/answer-loop.d.ts +230 -10
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1182 -85
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +27 -0
- package/dist/answer-shape.d.ts.map +1 -1
- package/dist/answer-shape.js +55 -0
- package/dist/answer-shape.js.map +1 -1
- package/dist/ask-observability/index.d.ts +11 -0
- package/dist/ask-observability/index.d.ts.map +1 -0
- package/dist/ask-observability/index.js +7 -0
- package/dist/ask-observability/index.js.map +1 -0
- package/dist/ask-observability/instrumentation.d.ts +35 -0
- package/dist/ask-observability/instrumentation.d.ts.map +1 -0
- package/dist/ask-observability/instrumentation.js +523 -0
- package/dist/ask-observability/instrumentation.js.map +1 -0
- package/dist/ask-observability/observer.d.ts +62 -0
- package/dist/ask-observability/observer.d.ts.map +1 -0
- package/dist/ask-observability/observer.js +537 -0
- package/dist/ask-observability/observer.js.map +1 -0
- package/dist/ask-observability/portable.d.ts +98 -0
- package/dist/ask-observability/portable.d.ts.map +1 -0
- package/dist/ask-observability/portable.js +1089 -0
- package/dist/ask-observability/portable.js.map +1 -0
- package/dist/ask-observability/store.d.ts +130 -0
- package/dist/ask-observability/store.d.ts.map +1 -0
- package/dist/ask-observability/store.js +1114 -0
- package/dist/ask-observability/store.js.map +1 -0
- package/dist/ask-observability/types.d.ts +380 -0
- package/dist/ask-observability/types.d.ts.map +1 -0
- package/dist/ask-observability/types.js +2 -0
- package/dist/ask-observability/types.js.map +1 -0
- package/dist/ask-observability/utils.d.ts +21 -0
- package/dist/ask-observability/utils.d.ts.map +1 -0
- package/dist/ask-observability/utils.js +102 -0
- package/dist/ask-observability/utils.js.map +1 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts +850 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.d.ts.map +1 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js +1716 -0
- package/dist/ask-runtime/ask-agent-runtime-v2.js.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts +95 -0
- package/dist/ask-runtime/ask-analyst-runtime.d.ts.map +1 -0
- package/dist/ask-runtime/ask-analyst-runtime.js +8280 -0
- package/dist/ask-runtime/ask-analyst-runtime.js.map +1 -0
- package/dist/conversation/session-store.d.ts +31 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +38 -0
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts +4 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +85 -3
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +2 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -0
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +18 -0
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/generated-analytical-proposal.d.ts +37 -1
- package/dist/generated-analytical-proposal.d.ts.map +1 -1
- package/dist/generated-analytical-proposal.js +246 -3
- package/dist/generated-analytical-proposal.js.map +1 -1
- package/dist/governed-relational-compiler.d.ts +15 -0
- package/dist/governed-relational-compiler.d.ts.map +1 -1
- package/dist/governed-relational-compiler.js +99 -1
- package/dist/governed-relational-compiler.js.map +1 -1
- package/dist/index.d.ts +21 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -7
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +87 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +2 -2
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +32 -2
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +207 -6
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +439 -53
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +78 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +14 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +29 -2
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +28 -2
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +373 -16
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +9 -1
- package/dist/plan-execution-adapter.d.ts.map +1 -1
- package/dist/plan-execution-adapter.js +93 -13
- package/dist/plan-execution-adapter.js.map +1 -1
- package/dist/provider-egress.d.ts +28 -11
- package/dist/provider-egress.d.ts.map +1 -1
- package/dist/provider-egress.js +72 -22
- package/dist/provider-egress.js.map +1 -1
- package/dist/providers/claude.d.ts +2 -2
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +201 -37
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/dispatch.d.ts +27 -1
- package/dist/providers/dispatch.d.ts.map +1 -1
- package/dist/providers/dispatch.js +63 -1
- package/dist/providers/dispatch.js.map +1 -1
- package/dist/providers/gemini.js +10 -10
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.js +15 -15
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +213 -43
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +131 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/relationship-proof.d.ts +81 -0
- package/dist/relationship-proof.d.ts.map +1 -1
- package/dist/relationship-proof.js +228 -4
- package/dist/relationship-proof.js.map +1 -1
- package/dist/requirement-clauses.d.ts +23 -0
- package/dist/requirement-clauses.d.ts.map +1 -0
- package/dist/requirement-clauses.js +70 -0
- package/dist/requirement-clauses.js.map +1 -0
- package/dist/resolved-analytical-plan.d.ts +18 -4
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +953 -50
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +118 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +2481 -232
- package/dist/router.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +23 -4
- package/dist/semantic-bridge/compose.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,1716 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask Agent Runtime V2
|
|
3
|
+
*
|
|
4
|
+
* V1 made a deterministic interpretation/coverage check the authority for a
|
|
5
|
+
* natural-language Ask. That is safe, but it is the wrong authority for an
|
|
6
|
+
* open-ended business question: an incomplete certified or semantic candidate
|
|
7
|
+
* is an observation for the agent, not proof that a safe generated route does
|
|
8
|
+
* not exist. V2 keeps the existing execution guards and moves only business
|
|
9
|
+
* interpretation and route progression behind one bounded tool kernel.
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally host-neutral. The CLI adapts its existing
|
|
12
|
+
* answer-loop, MetricFlow and analyst-loop tools to this contract; no provider
|
|
13
|
+
* payload, SQL, result row, credential, file path, or hidden reasoning is
|
|
14
|
+
* persisted here.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import { classifyConversationalTurn } from '../intent-controller.js';
|
|
18
|
+
import { buildAnalyticalRequirementSet, evidenceCandidateRoles, selectRoleBalancedMeaningCandidates } from '../analytical-orchestration.js';
|
|
19
|
+
export const ASK_V2_CANONICAL_TOOLS = [
|
|
20
|
+
'inspect_ask_context',
|
|
21
|
+
'inspect_conversation_result',
|
|
22
|
+
'inspect_business_context',
|
|
23
|
+
'inspect_certified_candidates',
|
|
24
|
+
'run_certified',
|
|
25
|
+
'inspect_semantic_candidates',
|
|
26
|
+
'compile_and_run_semantic',
|
|
27
|
+
'inspect_relational_context',
|
|
28
|
+
'compile_and_run_dql',
|
|
29
|
+
'validate_and_run_sql',
|
|
30
|
+
'search_values',
|
|
31
|
+
'request_clarification',
|
|
32
|
+
'finish_answer',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* This registry is deliberately process-local and keyed by object identity.
|
|
36
|
+
* It is the final server-owned boundary between a runner return value and the
|
|
37
|
+
* engine's generic gate bypass. Persisted receipts remain readable, but they
|
|
38
|
+
* cannot be replayed as live execution authority in a later run.
|
|
39
|
+
*/
|
|
40
|
+
const askV2ExecutionReceiptAttestations = new WeakMap();
|
|
41
|
+
function stableAskV2Json(value) {
|
|
42
|
+
if (value === null || typeof value !== 'object')
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
if (Array.isArray(value))
|
|
45
|
+
return `[${value.map(stableAskV2Json).join(',')}]`;
|
|
46
|
+
const record = value;
|
|
47
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableAskV2Json(record[key])}`).join(',')}}`;
|
|
48
|
+
}
|
|
49
|
+
function askV2Fingerprint(value) {
|
|
50
|
+
return `sha256:${createHash('sha256').update(stableAskV2Json(value)).digest('hex')}`;
|
|
51
|
+
}
|
|
52
|
+
function sortedAskV2Strings(values) {
|
|
53
|
+
return [...new Set((values ?? [])
|
|
54
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
55
|
+
.map((value) => value.trim()))].sort();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Fingerprint only the snapshot-local facts the semantic compiler can consume.
|
|
59
|
+
* Retrieval rank, descriptions, lane membership, and display prose are
|
|
60
|
+
* deliberately absent: they do not change a compiler authority. Conversely,
|
|
61
|
+
* the full analytical capability stays present, including its model-qualified
|
|
62
|
+
* time dimensions, joins, outputs, and parameter contract. This is used both
|
|
63
|
+
* when the host captures capabilities and when the provider resolves an
|
|
64
|
+
* opaque ID, so exact duplicate cards are safe while divergent duplicates are
|
|
65
|
+
* withheld before compiler execution.
|
|
66
|
+
*/
|
|
67
|
+
export function askV2SemanticCandidateAuthorityFingerprint(candidate) {
|
|
68
|
+
return askV2Fingerprint({
|
|
69
|
+
candidateId: candidate.qualifiedId ?? candidate.id,
|
|
70
|
+
kind: candidate.kind,
|
|
71
|
+
semanticObjectType: candidate.semanticObjectType ?? null,
|
|
72
|
+
trustTier: candidate.trustTier,
|
|
73
|
+
semanticRuntimeName: candidate.semanticRuntimeName ?? candidate.name,
|
|
74
|
+
semanticModel: candidate.semanticModel ?? null,
|
|
75
|
+
domain: candidate.domain ?? null,
|
|
76
|
+
dataType: candidate.dataType ?? null,
|
|
77
|
+
aggregation: candidate.aggregation ?? null,
|
|
78
|
+
primaryEntity: candidate.primaryEntity ?? null,
|
|
79
|
+
aliases: sortedAskV2Strings(candidate.aliases),
|
|
80
|
+
dimensions: sortedAskV2Strings(candidate.dimensions),
|
|
81
|
+
timeGrains: sortedAskV2Strings(candidate.timeGrains),
|
|
82
|
+
requiredParameters: sortedAskV2Strings(candidate.requiredParameters),
|
|
83
|
+
sourceObjects: sortedAskV2Strings(candidate.sourceObjects),
|
|
84
|
+
analyticalCapability: candidate.analyticalCapability ?? null,
|
|
85
|
+
sameSnapshotRoleExtension: candidate.sameSnapshotRoleExtension ?? null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Return only the semantic object roles that can be bound directly by a
|
|
90
|
+
* compiler. A semantic model, saved query, and other context/container card
|
|
91
|
+
* may be useful retrieval evidence, but it is never an executable field just
|
|
92
|
+
* because its display/runtime name resembles one. Keep this classification
|
|
93
|
+
* deliberately structural: the V2 host captures a capability only from an
|
|
94
|
+
* exact object type (or the legacy qualified-ID segment where old snapshots
|
|
95
|
+
* omitted that type), never from a trust tier or a fuzzy name.
|
|
96
|
+
*/
|
|
97
|
+
export function askV2ExecutableSemanticRoles(candidate) {
|
|
98
|
+
const objectType = candidate.semanticObjectType;
|
|
99
|
+
if (objectType === 'model' || objectType === 'saved_query')
|
|
100
|
+
return undefined;
|
|
101
|
+
const semanticMember = candidate.kind === 'semantic_member';
|
|
102
|
+
const canonicalSegments = (candidate.qualifiedId ?? candidate.id)
|
|
103
|
+
.split(/[:/]/)
|
|
104
|
+
.map((segment) => segment.trim().toLowerCase());
|
|
105
|
+
const legacyObjectType = canonicalSegments.find((segment) => (segment === 'metric' || segment === 'measure' || segment === 'dimension' || segment === 'entity'));
|
|
106
|
+
const metric = candidate.kind === 'semantic_metric'
|
|
107
|
+
|| (semanticMember && (objectType === 'metric' || objectType === 'measure'
|
|
108
|
+
|| legacyObjectType === 'metric' || legacyObjectType === 'measure'));
|
|
109
|
+
if (metric)
|
|
110
|
+
return ['metric'];
|
|
111
|
+
const entity = semanticMember && (objectType === 'entity' || legacyObjectType === 'entity');
|
|
112
|
+
const dimension = semanticMember && (objectType === 'dimension' || legacyObjectType === 'dimension');
|
|
113
|
+
if (!entity && !dimension)
|
|
114
|
+
return undefined;
|
|
115
|
+
const roles = ['dimension', 'filter_dimension'];
|
|
116
|
+
// An entity key is a bindable grouping/filter field, but it is not a time
|
|
117
|
+
// axis merely because a container card accidentally advertises grains.
|
|
118
|
+
if (!entity && (candidate.timeGrains?.length ?? 0) > 0)
|
|
119
|
+
roles.splice(1, 0, 'time_dimension');
|
|
120
|
+
return roles;
|
|
121
|
+
}
|
|
122
|
+
/** Stable digest of the admitted immutable candidate closure. */
|
|
123
|
+
export function askV2RetainedCandidateFingerprint(candidateIds) {
|
|
124
|
+
return askV2Fingerprint([...new Set(candidateIds.filter((id) => typeof id === 'string' && id.trim()))].sort());
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Return the canonical execution-result identity without retaining the result
|
|
128
|
+
* in a receipt. Existing connector fingerprints win; the fallback covers
|
|
129
|
+
* host-faithful test/local result envelopes that predate that field.
|
|
130
|
+
*/
|
|
131
|
+
export function askV2ExecutionResultFingerprint(result) {
|
|
132
|
+
if (!result || typeof result !== 'object' || Array.isArray(result))
|
|
133
|
+
return undefined;
|
|
134
|
+
const record = result;
|
|
135
|
+
const explicit = typeof record.resultFingerprint === 'string' && record.resultFingerprint.trim()
|
|
136
|
+
? record.resultFingerprint.trim()
|
|
137
|
+
: undefined;
|
|
138
|
+
if (explicit)
|
|
139
|
+
return explicit;
|
|
140
|
+
const executionReceipt = record.executionReceipt;
|
|
141
|
+
if (executionReceipt && typeof executionReceipt === 'object' && !Array.isArray(executionReceipt)) {
|
|
142
|
+
const receiptFingerprint = executionReceipt.resultFingerprint;
|
|
143
|
+
if (typeof receiptFingerprint === 'string' && receiptFingerprint.trim())
|
|
144
|
+
return receiptFingerprint.trim();
|
|
145
|
+
}
|
|
146
|
+
const hasRows = Array.isArray(record.rows);
|
|
147
|
+
const hasColumns = Array.isArray(record.columns);
|
|
148
|
+
const hasRowCount = typeof record.rowCount === 'number' && Number.isFinite(record.rowCount);
|
|
149
|
+
if (!hasRows && !hasColumns && !hasRowCount)
|
|
150
|
+
return undefined;
|
|
151
|
+
return askV2Fingerprint({
|
|
152
|
+
columns: hasColumns ? record.columns : [],
|
|
153
|
+
rows: hasRows ? record.rows : [],
|
|
154
|
+
rowCount: hasRowCount ? Math.max(0, Math.trunc(record.rowCount)) : record.rows?.length ?? 0,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Mint one server-owned capability for an authoritative V2 run. */
|
|
158
|
+
export function createAskV2ExecutionCapabilityV1(input) {
|
|
159
|
+
const { state } = input;
|
|
160
|
+
if (state.mode !== 'authoritative_v2' || !state.snapshotId || !input.id.trim() || !input.runId.trim())
|
|
161
|
+
return undefined;
|
|
162
|
+
const capability = {
|
|
163
|
+
version: 1,
|
|
164
|
+
id: input.id,
|
|
165
|
+
runId: input.runId,
|
|
166
|
+
snapshotId: state.snapshotId,
|
|
167
|
+
...(state.sourceFingerprint ? { sourceFingerprint: state.sourceFingerprint } : {}),
|
|
168
|
+
retainedCandidateFingerprint: askV2RetainedCandidateFingerprint(state.retainedCandidateIds),
|
|
169
|
+
...(state.exactCertifiedCandidateId ? { exactCertifiedCandidateId: state.exactCertifiedCandidateId } : {}),
|
|
170
|
+
};
|
|
171
|
+
return Object.freeze(capability);
|
|
172
|
+
}
|
|
173
|
+
function hasExecutedFrozenAskV2Plan(state) {
|
|
174
|
+
const plan = state.resolvedPlan;
|
|
175
|
+
return state.mode === 'authoritative_v2'
|
|
176
|
+
&& state.terminalOutcome?.kind === 'finish_answer'
|
|
177
|
+
&& state.terminalOutcome.origin === 'execution'
|
|
178
|
+
&& Boolean(plan?.frozen)
|
|
179
|
+
&& Boolean(plan?.id)
|
|
180
|
+
&& Boolean(plan?.fingerprint)
|
|
181
|
+
&& Boolean(plan?.candidateIds.length)
|
|
182
|
+
&& plan.candidateIds.every((id) => state.retainedCandidateIds.includes(id))
|
|
183
|
+
&& state.observations.some((observation) => (observation.outcome === 'executed'
|
|
184
|
+
&& observation.origin === 'execution'
|
|
185
|
+
&& observation.planId === plan.id
|
|
186
|
+
&& (observation.tool === 'run_certified'
|
|
187
|
+
|| observation.tool === 'compile_and_run_semantic'
|
|
188
|
+
|| observation.tool === 'compile_and_run_dql'
|
|
189
|
+
|| observation.tool === 'validate_and_run_sql')));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Mint the only receipt accepted by the engine's V2 terminal boundary. This
|
|
193
|
+
* is called after a real tool execution advances the cloned provider state.
|
|
194
|
+
* It rejects pre-freeze, non-executed, stale-snapshot, or mismatched-result
|
|
195
|
+
* states before an attestation is ever registered.
|
|
196
|
+
*/
|
|
197
|
+
export function mintAskV2ExecutionReceiptV1(input) {
|
|
198
|
+
const { state, capability } = input;
|
|
199
|
+
if (!state || !capability || !hasExecutedFrozenAskV2Plan(state))
|
|
200
|
+
return undefined;
|
|
201
|
+
const plan = state.resolvedPlan;
|
|
202
|
+
const resultFingerprint = askV2ExecutionResultFingerprint(input.result);
|
|
203
|
+
if (!resultFingerprint
|
|
204
|
+
|| state.snapshotId !== capability.snapshotId
|
|
205
|
+
|| state.sourceFingerprint !== capability.sourceFingerprint
|
|
206
|
+
|| askV2RetainedCandidateFingerprint(state.retainedCandidateIds) !== capability.retainedCandidateFingerprint
|
|
207
|
+
|| plan.snapshotId !== undefined && plan.snapshotId !== capability.snapshotId
|
|
208
|
+
|| (capability.exactCertifiedCandidateId !== undefined
|
|
209
|
+
&& plan.tier === 'certified'
|
|
210
|
+
&& (plan.candidateIds.length !== 1 || plan.candidateIds[0] !== capability.exactCertifiedCandidateId)))
|
|
211
|
+
return undefined;
|
|
212
|
+
const receipt = {
|
|
213
|
+
version: 1,
|
|
214
|
+
mode: 'authoritative_v2',
|
|
215
|
+
capabilityId: capability.id,
|
|
216
|
+
runId: capability.runId,
|
|
217
|
+
snapshotId: capability.snapshotId,
|
|
218
|
+
...(capability.sourceFingerprint ? { sourceFingerprint: capability.sourceFingerprint } : {}),
|
|
219
|
+
retainedCandidateFingerprint: capability.retainedCandidateFingerprint,
|
|
220
|
+
planId: plan.id,
|
|
221
|
+
planFingerprint: plan.fingerprint,
|
|
222
|
+
tier: plan.tier,
|
|
223
|
+
candidateIds: [...plan.candidateIds],
|
|
224
|
+
resultFingerprint,
|
|
225
|
+
frozen: true,
|
|
226
|
+
executed: true,
|
|
227
|
+
};
|
|
228
|
+
askV2ExecutionReceiptAttestations.set(receipt, {
|
|
229
|
+
capability,
|
|
230
|
+
snapshotId: capability.snapshotId,
|
|
231
|
+
...(capability.sourceFingerprint ? { sourceFingerprint: capability.sourceFingerprint } : {}),
|
|
232
|
+
retainedCandidateIds: [...state.retainedCandidateIds],
|
|
233
|
+
planId: plan.id,
|
|
234
|
+
planFingerprint: plan.fingerprint,
|
|
235
|
+
tier: plan.tier,
|
|
236
|
+
candidateIds: [...plan.candidateIds],
|
|
237
|
+
resultFingerprint,
|
|
238
|
+
});
|
|
239
|
+
return Object.freeze(receipt);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Verify a live receipt at the engine boundary. A persisted, copied, forged,
|
|
243
|
+
* stale, legacy, or pre-freeze receipt has no WeakMap attestation and cannot
|
|
244
|
+
* skip the generic evaluator.
|
|
245
|
+
*/
|
|
246
|
+
export function isAskV2ExecutionReceiptAuthorizedV1(input) {
|
|
247
|
+
const { receipt, capability, state } = input;
|
|
248
|
+
if (!receipt || !capability || !state
|
|
249
|
+
|| state.mode !== 'authoritative_v2'
|
|
250
|
+
|| receipt.version !== 1
|
|
251
|
+
|| receipt.mode !== 'authoritative_v2'
|
|
252
|
+
|| receipt.frozen !== true
|
|
253
|
+
|| receipt.executed !== true
|
|
254
|
+
|| receipt.capabilityId !== capability.id
|
|
255
|
+
|| receipt.runId !== input.runId
|
|
256
|
+
|| capability.runId !== input.runId
|
|
257
|
+
|| receipt.snapshotId !== capability.snapshotId
|
|
258
|
+
|| receipt.sourceFingerprint !== capability.sourceFingerprint
|
|
259
|
+
|| receipt.retainedCandidateFingerprint !== capability.retainedCandidateFingerprint
|
|
260
|
+
|| state.snapshotId !== capability.snapshotId
|
|
261
|
+
|| state.sourceFingerprint !== capability.sourceFingerprint
|
|
262
|
+
|| askV2RetainedCandidateFingerprint(state.retainedCandidateIds) !== capability.retainedCandidateFingerprint
|
|
263
|
+
|| !receipt.planId.trim()
|
|
264
|
+
|| !receipt.planFingerprint?.trim()
|
|
265
|
+
|| !receipt.resultFingerprint?.trim()
|
|
266
|
+
|| receipt.candidateIds.length === 0
|
|
267
|
+
|| new Set(receipt.candidateIds).size !== receipt.candidateIds.length
|
|
268
|
+
|| !receipt.candidateIds.every((id) => state.retainedCandidateIds.includes(id))
|
|
269
|
+
|| (capability.exactCertifiedCandidateId !== undefined
|
|
270
|
+
&& receipt.tier === 'certified'
|
|
271
|
+
&& (receipt.candidateIds.length !== 1 || receipt.candidateIds[0] !== capability.exactCertifiedCandidateId)))
|
|
272
|
+
return false;
|
|
273
|
+
const attestation = askV2ExecutionReceiptAttestations.get(receipt);
|
|
274
|
+
const resultFingerprint = askV2ExecutionResultFingerprint(input.result);
|
|
275
|
+
return Boolean(attestation
|
|
276
|
+
&& attestation.capability === capability
|
|
277
|
+
&& attestation.snapshotId === receipt.snapshotId
|
|
278
|
+
&& attestation.sourceFingerprint === receipt.sourceFingerprint
|
|
279
|
+
&& askV2RetainedCandidateFingerprint(attestation.retainedCandidateIds) === receipt.retainedCandidateFingerprint
|
|
280
|
+
&& attestation.planId === receipt.planId
|
|
281
|
+
&& attestation.planFingerprint === receipt.planFingerprint
|
|
282
|
+
&& attestation.tier === receipt.tier
|
|
283
|
+
&& JSON.stringify(attestation.candidateIds) === JSON.stringify(receipt.candidateIds)
|
|
284
|
+
&& attestation.resultFingerprint === receipt.resultFingerprint
|
|
285
|
+
&& resultFingerprint === receipt.resultFingerprint);
|
|
286
|
+
}
|
|
287
|
+
/** Provider egress is a host policy, not something the LLM may request. */
|
|
288
|
+
export function defaultProviderResultEgressPolicyV2(input) {
|
|
289
|
+
const allowRows = input.transport === 'local' || input.allowRemoteRows === true;
|
|
290
|
+
return {
|
|
291
|
+
version: 2,
|
|
292
|
+
transport: input.transport,
|
|
293
|
+
maximumRows: 20,
|
|
294
|
+
maximumColumns: 20,
|
|
295
|
+
maximumCells: 400,
|
|
296
|
+
allowRows,
|
|
297
|
+
allowedKinds: allowRows
|
|
298
|
+
? ['facts', 'aggregates', 'schema', 'fingerprints', 'bounded_rows']
|
|
299
|
+
: ['facts', 'aggregates', 'schema', 'fingerprints'],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Build a V4 ledger from the V2 tool boundary itself. In particular lineage
|
|
304
|
+
* is represented by its own atomic relationship handles; it is never inferred
|
|
305
|
+
* from (or reused as) an analytical result branch.
|
|
306
|
+
*/
|
|
307
|
+
export function recordAskV2ResearchLedger(state, branchReceipts) {
|
|
308
|
+
if (state.turnClass !== 'research')
|
|
309
|
+
return undefined;
|
|
310
|
+
// Keep old persisted ledgers readable. New V2 ledgers are written only from
|
|
311
|
+
// actual branch receipts, never inferred from an unrelated root tool trace.
|
|
312
|
+
if (!branchReceipts)
|
|
313
|
+
return state.researchLedgerV4;
|
|
314
|
+
const branches = branchReceipts.slice(0, ASK_V2_BUDGETS.research.branches).map((branch) => ({
|
|
315
|
+
id: branch.id,
|
|
316
|
+
verdict: branch.verdict,
|
|
317
|
+
evidenceHandleIds: [...new Set(branch.evidenceHandleIds)].slice(0, 24),
|
|
318
|
+
...(branch.validatorEvidenceHandleIds?.length
|
|
319
|
+
? { validatorEvidenceHandleIds: [...new Set(branch.validatorEvidenceHandleIds)].slice(0, 24) }
|
|
320
|
+
: {}),
|
|
321
|
+
...(branch.counterEvidenceHandleIds?.length
|
|
322
|
+
? { counterEvidenceHandleIds: [...new Set(branch.counterEvidenceHandleIds)].slice(0, 24) }
|
|
323
|
+
: {}),
|
|
324
|
+
...(branch.childReceiptFingerprint ? { childReceiptFingerprint: branch.childReceiptFingerprint } : {}),
|
|
325
|
+
lineageProgram: branch.lineageProgram ?? 'not_run',
|
|
326
|
+
}));
|
|
327
|
+
const ledger = {
|
|
328
|
+
version: 4,
|
|
329
|
+
rootQuestionFingerprint: `sha256:${createHash('sha256').update(`${state.snapshotId ?? ''}|${state.sourceFingerprint ?? ''}|research`).digest('hex')}`,
|
|
330
|
+
...(state.snapshotId ? { snapshotId: state.snapshotId } : {}),
|
|
331
|
+
branches,
|
|
332
|
+
limitedScope: branches.length < 3,
|
|
333
|
+
};
|
|
334
|
+
state.researchLedgerV4 = ledger;
|
|
335
|
+
return ledger;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Project the existing mixed V3 Research ledger into the V2 tool-runtime
|
|
339
|
+
* reader contract. This is deliberately a projection, not a second research
|
|
340
|
+
* planner: it retains opaque receipt/fact identities and the dedicated local
|
|
341
|
+
* lineage marker while excluding question text, SQL, rows, prompts, and
|
|
342
|
+
* provider material.
|
|
343
|
+
*/
|
|
344
|
+
export function projectResearchEvidenceLedgerV4(ledger) {
|
|
345
|
+
return {
|
|
346
|
+
version: 4,
|
|
347
|
+
rootQuestionFingerprint: ledger.rootQuestionFingerprint,
|
|
348
|
+
...(ledger.snapshotId ? { snapshotId: ledger.snapshotId } : {}),
|
|
349
|
+
branches: ledger.entries.slice(0, ASK_V2_BUDGETS.research.branches).map((entry) => ({
|
|
350
|
+
id: entry.id,
|
|
351
|
+
verdict: entry.verdict,
|
|
352
|
+
evidenceHandleIds: [...new Set([
|
|
353
|
+
...entry.factIds,
|
|
354
|
+
...entry.counterEvidenceFactIds,
|
|
355
|
+
...entry.receiptFingerprints,
|
|
356
|
+
])].slice(0, 24),
|
|
357
|
+
...(entry.receiptFingerprints.length ? { validatorEvidenceHandleIds: [...new Set(entry.receiptFingerprints)].slice(0, 24) } : {}),
|
|
358
|
+
...(entry.counterEvidenceFactIds.length ? { counterEvidenceHandleIds: [...new Set(entry.counterEvidenceFactIds)].slice(0, 24) } : {}),
|
|
359
|
+
lineageProgram: entry.evidenceKind === 'lineage_graph' ? 'dedicated' : 'not_run',
|
|
360
|
+
})),
|
|
361
|
+
limitedScope: ledger.limitedScope,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
export const ASK_V2_BUDGETS = {
|
|
365
|
+
ask: {
|
|
366
|
+
durationMs: 45_000,
|
|
367
|
+
providerDispatches: 6,
|
|
368
|
+
toolCalls: 8,
|
|
369
|
+
expansions: 2,
|
|
370
|
+
executions: 2,
|
|
371
|
+
repairs: 1,
|
|
372
|
+
valueSearches: 1,
|
|
373
|
+
clarifications: 1,
|
|
374
|
+
},
|
|
375
|
+
contextual: { durationMs: 15_000, providerDispatches: 2, toolCalls: 4 },
|
|
376
|
+
research: { durationMs: 120_000, providerDispatches: 12, toolCalls: 24, branches: 6, repairs: 2 },
|
|
377
|
+
};
|
|
378
|
+
/**
|
|
379
|
+
* A state may cross the package/CLI boundary several times during one Ask.
|
|
380
|
+
* Keep the active kernel process-local, while also deriving counters from the
|
|
381
|
+
* persisted observations so a reloaded receipt remains truthful.
|
|
382
|
+
*/
|
|
383
|
+
const activeToolKernels = new WeakMap();
|
|
384
|
+
const ASK_V2_INSPECTION_TOOLS = new Set([
|
|
385
|
+
'inspect_ask_context',
|
|
386
|
+
'inspect_conversation_result',
|
|
387
|
+
'inspect_business_context',
|
|
388
|
+
'inspect_certified_candidates',
|
|
389
|
+
'inspect_semantic_candidates',
|
|
390
|
+
'inspect_relational_context',
|
|
391
|
+
]);
|
|
392
|
+
/**
|
|
393
|
+
* A rejected immutable inspection tells the controller which already-issued
|
|
394
|
+
* action to take. It cannot discover new snapshot evidence, so it must not
|
|
395
|
+
* consume the logical V2 tool budget (including after receipt reload).
|
|
396
|
+
*/
|
|
397
|
+
function askV2ObservationConsumesToolBudget(observation) {
|
|
398
|
+
return !observation.executionAuthorized
|
|
399
|
+
&& observation.reasonCode !== 'ASK_V2_REDUNDANT_INSPECTION'
|
|
400
|
+
&& observation.reasonCode !== 'SEMANTIC_TIME_BINDING_COMPLETED'
|
|
401
|
+
&& !(ASK_V2_INSPECTION_TOOLS.has(observation.tool)
|
|
402
|
+
&& observation.reasonCode === 'ASK_V2_TOOL_PROGRESSION_REQUIRED');
|
|
403
|
+
}
|
|
404
|
+
function initialKernelCounters(state) {
|
|
405
|
+
const executionTools = new Set([
|
|
406
|
+
'run_certified', 'compile_and_run_semantic', 'compile_and_run_dql', 'validate_and_run_sql',
|
|
407
|
+
]);
|
|
408
|
+
const initial = {
|
|
409
|
+
toolCalls: 0,
|
|
410
|
+
executionAttempts: 0,
|
|
411
|
+
repairs: 0,
|
|
412
|
+
expansions: 0,
|
|
413
|
+
valueSearches: 0,
|
|
414
|
+
clarifications: 0,
|
|
415
|
+
};
|
|
416
|
+
const authorizedAttempts = new Set();
|
|
417
|
+
for (const observation of state.observations) {
|
|
418
|
+
// The host authorization observation is internal to the one tool call;
|
|
419
|
+
// it must not consume a second LLM tool budget after a process reload.
|
|
420
|
+
// A rejected immutable re-inspection carries no new retrieval or
|
|
421
|
+
// execution evidence. It must not consume the logical tool budget on a
|
|
422
|
+
// reload; live transport policy already prevents it from becoming a
|
|
423
|
+
// second discovery round.
|
|
424
|
+
if (askV2ObservationConsumesToolBudget(observation))
|
|
425
|
+
initial.toolCalls += 1;
|
|
426
|
+
if (executionTools.has(observation.tool)) {
|
|
427
|
+
const attemptKey = `${observation.tool}:${observation.planId ?? observation.inputFingerprint ?? observation.reasonCode}:${observation.samePlanRepair === true ? 'repair' : 'initial'}`;
|
|
428
|
+
if (observation.executionAuthorized) {
|
|
429
|
+
if (!authorizedAttempts.has(attemptKey)) {
|
|
430
|
+
authorizedAttempts.add(attemptKey);
|
|
431
|
+
initial.executionAttempts += 1;
|
|
432
|
+
if (observation.samePlanRepair)
|
|
433
|
+
initial.repairs += 1;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else if (!authorizedAttempts.size && (observation.outcome === 'executed' || observation.outcome === 'error')) {
|
|
437
|
+
// Old V4 persisted observations predate authorization evidence.
|
|
438
|
+
initial.executionAttempts += 1;
|
|
439
|
+
if (observation.retryable)
|
|
440
|
+
initial.repairs += 1;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (observation.tool === 'inspect_ask_context' && observation.reasonCode === 'same_snapshot_extension')
|
|
444
|
+
initial.expansions += 1;
|
|
445
|
+
if (observation.tool === 'search_values')
|
|
446
|
+
initial.valueSearches += 1;
|
|
447
|
+
if (observation.tool === 'request_clarification' && observation.outcome === 'needs_input')
|
|
448
|
+
initial.clarifications += 1;
|
|
449
|
+
}
|
|
450
|
+
return initial;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Tool-kernel safety is deliberately deterministic: it does not decide business
|
|
454
|
+
* meaning, but it prevents a model from skipping a complete earlier tier,
|
|
455
|
+
* widening a snapshot, recursively asking itself, or retrying indefinitely.
|
|
456
|
+
*/
|
|
457
|
+
export function createAskToolKernelV2(state) {
|
|
458
|
+
const active = activeToolKernels.get(state);
|
|
459
|
+
if (active)
|
|
460
|
+
return active;
|
|
461
|
+
const initial = initialKernelCounters(state);
|
|
462
|
+
let toolCalls = initial.toolCalls;
|
|
463
|
+
let executionAttempts = initial.executionAttempts;
|
|
464
|
+
let repairs = initial.repairs;
|
|
465
|
+
let expansions = initial.expansions;
|
|
466
|
+
let valueSearches = initial.valueSearches;
|
|
467
|
+
let clarifications = initial.clarifications;
|
|
468
|
+
const priority = ['certified', 'semantic', 'governed_relational', 'exploratory_sql'];
|
|
469
|
+
const executionToolTier = {
|
|
470
|
+
run_certified: 'certified',
|
|
471
|
+
compile_and_run_semantic: 'semantic',
|
|
472
|
+
compile_and_run_dql: 'governed_relational',
|
|
473
|
+
validate_and_run_sql: 'exploratory_sql',
|
|
474
|
+
};
|
|
475
|
+
const inspectionToolTier = {
|
|
476
|
+
inspect_certified_candidates: 'certified',
|
|
477
|
+
inspect_semantic_candidates: 'semantic',
|
|
478
|
+
inspect_relational_context: 'governed_relational',
|
|
479
|
+
};
|
|
480
|
+
const issuedMaterialClarificationChoices = () => {
|
|
481
|
+
const ambiguous = state.tierStates?.semantic;
|
|
482
|
+
if (ambiguous?.status !== 'ambiguous')
|
|
483
|
+
return [];
|
|
484
|
+
const tierCandidateIds = new Set(ambiguous.candidateIds);
|
|
485
|
+
const retained = new Set(state.retainedCandidateIds);
|
|
486
|
+
const choices = (ambiguous.clarificationChoices ?? []).filter((choice) => (choice.version === 1
|
|
487
|
+
&& Boolean(choice.id.trim())
|
|
488
|
+
&& Boolean(choice.label.trim())
|
|
489
|
+
&& Boolean(choice.resultFingerprint.trim())
|
|
490
|
+
&& choice.candidateIds.length > 0
|
|
491
|
+
&& choice.candidateIds.every((id) => retained.has(id) && tierCandidateIds.has(id))));
|
|
492
|
+
const uniqueIds = new Set(choices.map((choice) => choice.id));
|
|
493
|
+
const uniqueResults = new Set(choices.map((choice) => choice.resultFingerprint));
|
|
494
|
+
return choices.length >= 2 && uniqueIds.size === choices.length && uniqueResults.size === choices.length
|
|
495
|
+
? choices
|
|
496
|
+
: [];
|
|
497
|
+
};
|
|
498
|
+
const maxTools = state.turnClass === 'research' ? ASK_V2_BUDGETS.research.toolCalls : ASK_V2_BUDGETS.ask.toolCalls;
|
|
499
|
+
const maxExecutions = state.turnClass === 'research' ? 1 : ASK_V2_BUDGETS.ask.executions;
|
|
500
|
+
const explicitlyBypassesTierPriority = (tier, candidateIds) => {
|
|
501
|
+
const reference = state.explicitQualifiedArtifactReference;
|
|
502
|
+
return Boolean(reference
|
|
503
|
+
&& reference.tier === tier
|
|
504
|
+
// The first tool-policy check happens before the provider tool's opaque
|
|
505
|
+
// arguments are available. Authorization repeats this with selected IDs
|
|
506
|
+
// and rejects a lower-tier tool that tries to use any other candidate.
|
|
507
|
+
&& (!candidateIds || candidateIds.includes(reference.candidateId)));
|
|
508
|
+
};
|
|
509
|
+
const priorTierComplete = (tier, candidateIds) => {
|
|
510
|
+
if (explicitlyBypassesTierPriority(tier, candidateIds))
|
|
511
|
+
return undefined;
|
|
512
|
+
const index = priority.indexOf(tier);
|
|
513
|
+
const hostComplete = priority.slice(0, index).find((earlier) => state.tierStates?.[earlier]?.status === 'complete');
|
|
514
|
+
if (hostComplete)
|
|
515
|
+
return hostComplete;
|
|
516
|
+
// An execution that has frozen a real result remains complete even if a
|
|
517
|
+
// host did not materialize the additive tier-state field (old reader or
|
|
518
|
+
// persisted V4 state). An inspection alone never has this authority.
|
|
519
|
+
return state.observations.find((observation) => {
|
|
520
|
+
// A physical tool invocation is not a completed tier merely because it
|
|
521
|
+
// passed local argument validation. The host owns `complete` before an
|
|
522
|
+
// execution, and a fallback observation becomes complete only after the
|
|
523
|
+
// executor returned a validated result. Treating `eligible` as complete
|
|
524
|
+
// here previously let a presentation-only observation block a genuine
|
|
525
|
+
// lower-tier fallback.
|
|
526
|
+
if (observation.outcome !== 'executed')
|
|
527
|
+
return false;
|
|
528
|
+
return executionToolTier[observation.tool] !== undefined
|
|
529
|
+
&& observation.tier !== undefined
|
|
530
|
+
&& priority.indexOf(observation.tier) < index;
|
|
531
|
+
})?.tier;
|
|
532
|
+
};
|
|
533
|
+
const earlierTierInspected = (tier) => {
|
|
534
|
+
const index = priority.indexOf(tier);
|
|
535
|
+
return priority.slice(0, index).every((earlier) => {
|
|
536
|
+
if (earlier === 'governed_relational' && hasInitialRelationshipClosure())
|
|
537
|
+
return true;
|
|
538
|
+
return state.observations.some((observation) => inspectionToolTier[observation.tool] === earlier
|
|
539
|
+
|| executionToolTier[observation.tool] === earlier);
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
const hasToolObservation = (tool) => state.observations.some((observation) => observation.tool === tool);
|
|
543
|
+
/**
|
|
544
|
+
* The initial provider package already contains the immutable role-balanced
|
|
545
|
+
* cards and atomic relationship-path handles. `inspect_ask_context` may
|
|
546
|
+
* render that package for a controller, but it is not an admission gate:
|
|
547
|
+
* requiring a second inspection before every action both hid retrieval from
|
|
548
|
+
* direct canonical-tool callers and spent a physical dispatch that must be
|
|
549
|
+
* reserved for execution/narration. The DQL tool still validates every
|
|
550
|
+
* selected path/candidate against this snapshot closure before it can
|
|
551
|
+
* authorize or execute anything.
|
|
552
|
+
*/
|
|
553
|
+
const hasInitialRelationshipClosure = () => ((state.relationshipPathHandles?.length ?? 0) > 0);
|
|
554
|
+
const relationalContextInspected = () => (hasToolObservation('inspect_relational_context') || hasInitialRelationshipClosure());
|
|
555
|
+
const hasExecutedTool = () => state.observations.some((observation) => (observation.outcome === 'executed' && executionToolTier[observation.tool] !== undefined));
|
|
556
|
+
const analyticalTurn = () => state.turnClass === 'analytics' || state.turnClass === 'prior_result' || state.turnClass === 'research';
|
|
557
|
+
const toolPolicy = () => {
|
|
558
|
+
const all = [...ASK_V2_CANONICAL_TOOLS];
|
|
559
|
+
// A frozen execution-target mismatch is a host terminal, not a request
|
|
560
|
+
// for the controller to choose another compiler, route, or repair. Keep
|
|
561
|
+
// the live policy empty as well as denying calls below so text and native
|
|
562
|
+
// transports cannot turn it into a cross-engine retry.
|
|
563
|
+
if (state.terminalOutcome?.kind === 'execution_failure') {
|
|
564
|
+
return {
|
|
565
|
+
allowedToolNames: [],
|
|
566
|
+
instruction: 'The frozen execution target did not match the compiler result. Do not call another tool or choose another route.',
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (hasExecutedTool()) {
|
|
570
|
+
return {
|
|
571
|
+
allowedToolNames: ['finish_answer'],
|
|
572
|
+
instruction: 'A validated execution result is available. Call finish_answer now; do not inspect more context or choose another tier.',
|
|
573
|
+
// Executing a validated plan is not itself the conversational
|
|
574
|
+
// terminal. The provider must pass through this host control so the
|
|
575
|
+
// result/narration boundary is explicit and durable. Native and text
|
|
576
|
+
// transports use this to discard post-execution prose and reserve one
|
|
577
|
+
// bounded controller action for `finish_answer`.
|
|
578
|
+
terminalActionToolNames: ['finish_answer'],
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (!analyticalTurn()) {
|
|
582
|
+
const noClarification = all.filter((tool) => tool !== 'request_clarification');
|
|
583
|
+
const contextual = state.turnClass === 'definition' || state.turnClass === 'business_context';
|
|
584
|
+
const businessContextInspected = state.observations.some((observation) => (observation.tool === 'inspect_business_context' && observation.outcome === 'eligible'));
|
|
585
|
+
if (contextual && businessContextInspected) {
|
|
586
|
+
return {
|
|
587
|
+
allowedToolNames: ['finish_answer'],
|
|
588
|
+
instruction: 'Retrieved business context is available. Call finish_answer with the host-issued evidence IDs; do not answer in prose outside the tool.',
|
|
589
|
+
// A contextual answer has the same explicit host completion
|
|
590
|
+
// boundary as a validated execution result. Without this marker,
|
|
591
|
+
// bounded native/text tool loops treat the second provider send as
|
|
592
|
+
// an ordinary prose composition turn instead of exposing the only
|
|
593
|
+
// evidence-bound finish control.
|
|
594
|
+
terminalActionToolNames: ['finish_answer'],
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return { allowedToolNames: noClarification };
|
|
598
|
+
}
|
|
599
|
+
const certifiedInspected = hasToolObservation('inspect_certified_candidates');
|
|
600
|
+
const semanticInspected = hasToolObservation('inspect_semantic_candidates');
|
|
601
|
+
const relationalInspected = relationalContextInspected();
|
|
602
|
+
const certifiedState = state.tierStates?.certified;
|
|
603
|
+
const semanticState = state.tierStates?.semantic;
|
|
604
|
+
const relationalState = state.tierStates?.governed_relational;
|
|
605
|
+
const relationalAvailable = relationalState?.status === 'available'
|
|
606
|
+
|| relationalState?.status === 'complete'
|
|
607
|
+
|| (!relationalState && hasInitialRelationshipClosure());
|
|
608
|
+
const certifiedPriorityBypassed = explicitlyBypassesTierPriority('semantic')
|
|
609
|
+
|| explicitlyBypassesTierPriority('governed_relational');
|
|
610
|
+
const committedTier = state.resolvedPlan?.frozen ? undefined : state.controllerTier;
|
|
611
|
+
const committedExecutionTool = committedTier === 'certified'
|
|
612
|
+
? 'run_certified'
|
|
613
|
+
: committedTier === 'semantic'
|
|
614
|
+
? 'compile_and_run_semantic'
|
|
615
|
+
: committedTier === 'governed_relational'
|
|
616
|
+
? 'compile_and_run_dql'
|
|
617
|
+
: committedTier === 'exploratory_sql'
|
|
618
|
+
? 'validate_and_run_sql'
|
|
619
|
+
: undefined;
|
|
620
|
+
// A host semantic validator can return a precise, repairable binding
|
|
621
|
+
// observation (for example an omitted time axis with no unique admitted
|
|
622
|
+
// completion). Keep the controller on that same capability-bound tool
|
|
623
|
+
// rather than silently advancing to relational/SQL or re-inspecting an
|
|
624
|
+
// immutable snapshot. Only host-issued actions may narrow this policy.
|
|
625
|
+
const semanticRecoveryTools = (semanticState?.safeNextTools ?? [])
|
|
626
|
+
.filter((tool) => tool === 'compile_and_run_semantic' || tool === 'request_clarification');
|
|
627
|
+
if (certifiedInspected && semanticInspected && semanticRecoveryTools.length > 0
|
|
628
|
+
&& (certifiedState?.status !== 'complete' || certifiedPriorityBypassed)) {
|
|
629
|
+
return {
|
|
630
|
+
allowedToolNames: semanticRecoveryTools,
|
|
631
|
+
instruction: semanticRecoveryTools.includes('request_clarification')
|
|
632
|
+
? 'The host issued a material semantic ambiguity. Request the stable clarification now; do not infer a time axis or change tiers.'
|
|
633
|
+
: 'The semantic binding was incomplete or unavailable before freeze. Correct the same admitted semantic tool arguments, or return a typed gap; do not change tiers or repeat inspection.',
|
|
634
|
+
terminalActionToolNames: semanticRecoveryTools,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
if (certifiedState?.status === 'complete' && !certifiedPriorityBypassed) {
|
|
638
|
+
if (!certifiedInspected) {
|
|
639
|
+
return {
|
|
640
|
+
allowedToolNames: ['inspect_certified_candidates'],
|
|
641
|
+
instruction: 'The immutable snapshot proves one or more complete certified artifacts. Inspect those artifacts now so the controller can choose the bound certified candidate; do not inspect or commit a lower tier.',
|
|
642
|
+
terminalActionToolNames: ['inspect_certified_candidates'],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
allowedToolNames: ['run_certified'],
|
|
647
|
+
instruction: 'The immutable snapshot proves one or more complete certified artifacts. Run one inspected certified candidate before any lower tier.',
|
|
648
|
+
terminalActionToolNames: ['run_certified'],
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
// An eligible inspection with an executable host capability is the
|
|
652
|
+
// controller's route choice, not an invitation to re-inspect immutable
|
|
653
|
+
// evidence from another tier. Keep the LLM in control of bindings and
|
|
654
|
+
// execution, but require the one chosen tier's tool now. A host-proven
|
|
655
|
+
// complete earlier tier still wins above, and canCall repeats that guard
|
|
656
|
+
// before any capability is minted.
|
|
657
|
+
if (committedExecutionTool) {
|
|
658
|
+
const committedState = state.tierStates?.[committedTier];
|
|
659
|
+
const recovery = (committedState?.safeNextTools ?? [])
|
|
660
|
+
.filter((tool) => tool === committedExecutionTool || tool === 'request_clarification');
|
|
661
|
+
if (committedState?.status === 'ambiguous' && issuedMaterialClarificationChoices().length >= 2) {
|
|
662
|
+
return {
|
|
663
|
+
allowedToolNames: ['request_clarification'],
|
|
664
|
+
instruction: 'The selected tier has a host-issued material ambiguity. Request the stable clarification now; do not inspect or change tiers.',
|
|
665
|
+
terminalActionToolNames: ['request_clarification'],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
if (recovery.length > 0) {
|
|
669
|
+
return {
|
|
670
|
+
allowedToolNames: recovery,
|
|
671
|
+
instruction: recovery.includes('request_clarification')
|
|
672
|
+
? 'The selected tier requires the host-issued material clarification. Do not inspect another tier.'
|
|
673
|
+
: `Correct the same selected ${committedTier} tool arguments now. Do not inspect another tier or answer in prose.`,
|
|
674
|
+
terminalActionToolNames: recovery,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
if (committedState?.status !== 'unavailable' && committedState?.status !== 'ineligible' && committedState?.status !== 'ambiguous') {
|
|
678
|
+
return {
|
|
679
|
+
allowedToolNames: [committedExecutionTool],
|
|
680
|
+
instruction: `The controller selected the eligible ${committedTier} tier from the immutable snapshot. Call ${committedExecutionTool} now with admitted IDs; do not inspect another tier or answer in prose.`,
|
|
681
|
+
terminalActionToolNames: [committedExecutionTool],
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
// A semantic inspection gives the model all metric/time compatibility
|
|
686
|
+
// cards it needs. It must now establish whether a certified block is
|
|
687
|
+
// complete instead of spending provider turns on unrelated context.
|
|
688
|
+
if (semanticInspected && !certifiedInspected && !certifiedPriorityBypassed) {
|
|
689
|
+
return {
|
|
690
|
+
allowedToolNames: ['inspect_certified_candidates'],
|
|
691
|
+
instruction: 'Semantic candidates are inspected. Inspect certified candidates next to enforce tier priority before compiling semantic SQL.',
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
// Once the certified tier is inspected and not complete, compatible
|
|
695
|
+
// semantic evidence is sufficient for the next action. This is a control
|
|
696
|
+
// boundary, not deterministic execution: only the LLM may supply the
|
|
697
|
+
// selected metric/dimension/time IDs to compile_and_run_semantic.
|
|
698
|
+
if (certifiedInspected && semanticInspected && (semanticState?.status === 'available' || semanticState?.status === 'complete')) {
|
|
699
|
+
// The model may determine from the inspected cards that the requested
|
|
700
|
+
// tuple spans a certified relationship closure rather than the semantic
|
|
701
|
+
// layer. The initial snapshot already exposed that closure, so retain
|
|
702
|
+
// DQL/SQL as model-controlled alternatives when it is present. They
|
|
703
|
+
// remain fully capability-bound by their own tools; this never upgrades
|
|
704
|
+
// semantic availability into a route decision or skips tier safety.
|
|
705
|
+
const relationalAlternatives = hasInitialRelationshipClosure()
|
|
706
|
+
? ['compile_and_run_dql', 'validate_and_run_sql']
|
|
707
|
+
: [];
|
|
708
|
+
return {
|
|
709
|
+
allowedToolNames: ['compile_and_run_semantic', ...relationalAlternatives],
|
|
710
|
+
instruction: relationalAlternatives.length
|
|
711
|
+
? 'Certified is not complete. Execute the highest compatible tier now: use compile_and_run_semantic when the selected metric/dimensions are compatible; otherwise use admitted path-bound compile_and_run_dql, or review-required SQL only when DQL cannot prove the tuple. Do not repeat inspection.'
|
|
712
|
+
: 'Certified is not complete and compatible semantic evidence is inspected. Call compile_and_run_semantic now with admitted IDs; do not repeat inspection.',
|
|
713
|
+
terminalActionToolNames: ['compile_and_run_semantic', ...relationalAlternatives],
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (certifiedInspected && semanticInspected && semanticState?.status === 'ambiguous'
|
|
717
|
+
&& issuedMaterialClarificationChoices().length >= 2) {
|
|
718
|
+
return {
|
|
719
|
+
allowedToolNames: ['request_clarification'],
|
|
720
|
+
instruction: 'The compatible semantic meanings remain materially ambiguous. Request one stable clarification instead of inspecting more context.',
|
|
721
|
+
terminalActionToolNames: ['request_clarification'],
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (certifiedInspected && semanticInspected && semanticState?.status === 'ambiguous' && !relationalInspected) {
|
|
725
|
+
return {
|
|
726
|
+
allowedToolNames: ['inspect_relational_context'],
|
|
727
|
+
instruction: 'Semantic candidates are not backed by a host-issued material choice set. Inspect governed relational context rather than asking an unsupported clarification.',
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (certifiedInspected && semanticInspected && semanticState?.status === 'ambiguous'
|
|
731
|
+
&& relationalInspected && relationalAvailable) {
|
|
732
|
+
return {
|
|
733
|
+
allowedToolNames: ['compile_and_run_dql', 'validate_and_run_sql'],
|
|
734
|
+
instruction: 'Semantic meanings remain unresolved without a host-issued material clarification. Execute the admitted governed relational plan when it proves the tuple, otherwise validate one review-required SQL proposal; do not repeat inspection.',
|
|
735
|
+
terminalActionToolNames: ['compile_and_run_dql', 'validate_and_run_sql'],
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
if (certifiedInspected && !semanticInspected) {
|
|
739
|
+
return {
|
|
740
|
+
// The inspection has already established that no certified candidate
|
|
741
|
+
// is complete. Prefer semantic inspection next, but leave the
|
|
742
|
+
// snapshot-bound certified validation tool visible so an LLM that
|
|
743
|
+
// asks why the context-only block cannot run receives the precise
|
|
744
|
+
// tuple-not-proven observation rather than a generic controller
|
|
745
|
+
// denial. That tool cannot execute because its own immutable
|
|
746
|
+
// completeness check remains authoritative.
|
|
747
|
+
allowedToolNames: ['inspect_semantic_candidates', 'run_certified'],
|
|
748
|
+
instruction: 'No complete certified tier was proven. Inspect semantic candidates next before relational or SQL exploration. A certified run is allowed only to record its snapshot tuple validation.',
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (certifiedInspected && semanticInspected && (semanticState?.status === 'unavailable' || semanticState?.status === 'ineligible') && !relationalInspected) {
|
|
752
|
+
return {
|
|
753
|
+
allowedToolNames: ['inspect_relational_context'],
|
|
754
|
+
instruction: 'Certified and semantic tiers cannot execute from this snapshot. Inspect admitted governed relational context next.',
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
if (certifiedInspected && semanticInspected && (semanticState?.status === 'unavailable' || semanticState?.status === 'ineligible') && relationalInspected) {
|
|
758
|
+
if (relationalState?.status === 'complete') {
|
|
759
|
+
return {
|
|
760
|
+
allowedToolNames: ['compile_and_run_dql'],
|
|
761
|
+
instruction: 'Certified and semantic tiers cannot execute and the host proves a complete governed relational tuple. Compile admitted DQL next.',
|
|
762
|
+
terminalActionToolNames: ['compile_and_run_dql'],
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
if (relationalState?.status === 'available') {
|
|
766
|
+
// Availability means the snapshot has relationship evidence, not
|
|
767
|
+
// that one governed DQL program proves the requested tuple. The
|
|
768
|
+
// controller may choose a path-bound DQL program, or safely validate
|
|
769
|
+
// exploratory SQL against the same admitted closure. Only a complete
|
|
770
|
+
// earlier tier can forbid that lower route.
|
|
771
|
+
return {
|
|
772
|
+
allowedToolNames: ['compile_and_run_dql', 'validate_and_run_sql'],
|
|
773
|
+
instruction: 'Certified and semantic tiers cannot execute. Governed relational evidence is available but not complete for this request: choose admitted DQL when it proves the tuple, otherwise validate one review-required SQL proposal against the admitted closure.',
|
|
774
|
+
terminalActionToolNames: ['compile_and_run_dql', 'validate_and_run_sql'],
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (relationalAvailable) {
|
|
778
|
+
return {
|
|
779
|
+
allowedToolNames: ['compile_and_run_dql', 'validate_and_run_sql'],
|
|
780
|
+
instruction: 'Certified and semantic tiers cannot execute. The immutable initial snapshot already supplied an admitted relationship closure: execute path-bound DQL when it proves the tuple, otherwise validate one review-required SQL proposal. Do not repeat inspection.',
|
|
781
|
+
terminalActionToolNames: ['compile_and_run_dql', 'validate_and_run_sql'],
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
if (relationalState?.status === 'unavailable' || relationalState?.status === 'ineligible') {
|
|
785
|
+
return {
|
|
786
|
+
allowedToolNames: ['validate_and_run_sql'],
|
|
787
|
+
instruction: 'Earlier governed tiers are unavailable or ineligible. Validate one read-only SQL proposal against the admitted exploratory closure.',
|
|
788
|
+
terminalActionToolNames: ['validate_and_run_sql'],
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// Snapshot retrieval is immutable. Do not let an analytical controller
|
|
793
|
+
// spend its executable reserve re-reading context/business/conversation
|
|
794
|
+
// cards that cannot have changed. The remaining tier inspectors are the
|
|
795
|
+
// only meaningful discovery actions until a compiler/validator tool is
|
|
796
|
+
// required by the branches above.
|
|
797
|
+
const remainingInspectors = [
|
|
798
|
+
// The initial role-balanced package is already in the first provider
|
|
799
|
+
// prompt. This optional one-time tool is retained for controllers that
|
|
800
|
+
// need to render its safe cards/handles explicitly (and for a precise
|
|
801
|
+
// snapshot-mismatch diagnostic), but it is never a prerequisite for
|
|
802
|
+
// acting on those admitted cards.
|
|
803
|
+
!hasToolObservation('inspect_ask_context') ? 'inspect_ask_context' : undefined,
|
|
804
|
+
!certifiedInspected ? 'inspect_certified_candidates' : undefined,
|
|
805
|
+
!semanticInspected ? 'inspect_semantic_candidates' : undefined,
|
|
806
|
+
!relationalInspected ? 'inspect_relational_context' : undefined,
|
|
807
|
+
].filter((tool) => Boolean(tool));
|
|
808
|
+
if (remainingInspectors.length) {
|
|
809
|
+
return {
|
|
810
|
+
allowedToolNames: remainingInspectors,
|
|
811
|
+
instruction: 'Inspect only the remaining analytical tier evidence from this immutable snapshot. Do not repeat context or business inspection; after the required tier evidence is available, execute or return a typed clarification/denial.',
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
// An analytical turn cannot finish as ungrounded prose before a tool
|
|
816
|
+
// produces a validated result or a typed clarification/gap.
|
|
817
|
+
allowedToolNames: all.filter((tool) => tool !== 'finish_answer' && tool !== 'request_clarification'
|
|
818
|
+
&& tool !== 'inspect_ask_context' && tool !== 'inspect_business_context'
|
|
819
|
+
&& tool !== 'inspect_conversation_result'),
|
|
820
|
+
};
|
|
821
|
+
};
|
|
822
|
+
const controllerTier = () => {
|
|
823
|
+
if (state.resolvedPlan?.frozen)
|
|
824
|
+
return state.resolvedPlan.tier;
|
|
825
|
+
if (state.controllerTier)
|
|
826
|
+
return state.controllerTier;
|
|
827
|
+
// The compact trace must describe the controller's last real execution
|
|
828
|
+
// decision, including a pre-freeze compiler/validation failure. Falling
|
|
829
|
+
// through to the *next* policy action made a failed semantic invocation
|
|
830
|
+
// look like governed relational work even though that tool never ran.
|
|
831
|
+
const attempted = state.observations.slice().reverse().find((observation) => (executionToolTier[observation.tool] !== undefined));
|
|
832
|
+
if (attempted?.tier)
|
|
833
|
+
return attempted.tier;
|
|
834
|
+
const policy = toolPolicy();
|
|
835
|
+
const names = policy.terminalActionToolNames?.length
|
|
836
|
+
? policy.terminalActionToolNames
|
|
837
|
+
: policy.allowedToolNames;
|
|
838
|
+
return names.map((name) => executionToolTier[name] ?? inspectionToolTier[name]).find(Boolean);
|
|
839
|
+
};
|
|
840
|
+
const kernel = {
|
|
841
|
+
state,
|
|
842
|
+
canCall(tool, input = {}) {
|
|
843
|
+
if (state.terminalOutcome?.kind === 'execution_failure') {
|
|
844
|
+
return { ok: false, reasonCode: state.terminalOutcome.reasonCode };
|
|
845
|
+
}
|
|
846
|
+
const inspectionTools = ASK_V2_INSPECTION_TOOLS;
|
|
847
|
+
// Immutable retrieval means a repeated inspector cannot reveal a new
|
|
848
|
+
// candidate, coverage state, or relationship path. Reject it before
|
|
849
|
+
// normal policy/budget handling so the next controller prompt receives
|
|
850
|
+
// the current executable safe action instead of treating repetition as
|
|
851
|
+
// evidence that the data is missing. A bounded same-snapshot extension
|
|
852
|
+
// is the sole exception.
|
|
853
|
+
const repeatedInitialContext = tool === 'inspect_ask_context'
|
|
854
|
+
&& input.expansion !== true
|
|
855
|
+
&& hasToolObservation('inspect_ask_context');
|
|
856
|
+
const emptyOrExhaustedExpansion = tool === 'inspect_ask_context'
|
|
857
|
+
&& input.expansion === true
|
|
858
|
+
&& (expansions >= ASK_V2_BUDGETS.ask.expansions
|
|
859
|
+
|| state.expansionCandidateIds.slice(expansions * 12, (expansions + 1) * 12).length === 0);
|
|
860
|
+
const repeatedImmutableInspection = inspectionTools.has(tool)
|
|
861
|
+
&& tool !== 'inspect_ask_context'
|
|
862
|
+
&& hasToolObservation(tool);
|
|
863
|
+
if (repeatedInitialContext || emptyOrExhaustedExpansion || repeatedImmutableInspection) {
|
|
864
|
+
const progress = toolPolicy();
|
|
865
|
+
return {
|
|
866
|
+
ok: false,
|
|
867
|
+
reasonCode: 'ASK_V2_REDUNDANT_INSPECTION',
|
|
868
|
+
safeNextTools: progress.terminalActionToolNames?.length
|
|
869
|
+
? progress.terminalActionToolNames
|
|
870
|
+
: progress.allowedToolNames,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
if (toolCalls >= maxTools)
|
|
874
|
+
return { ok: false, reasonCode: 'ASK_TOOL_BUDGET_EXHAUSTED' };
|
|
875
|
+
const progress = toolPolicy();
|
|
876
|
+
const tier = executionToolTier[tool];
|
|
877
|
+
// Tier-truth materialization records snapshot provenance, not a model
|
|
878
|
+
// tool call. A uniquely exact Tier 1 artifact may therefore enter its
|
|
879
|
+
// one real host invocation without manufacturing an
|
|
880
|
+
// `inspect_certified_candidates` or `run_certified` observation first.
|
|
881
|
+
// Keep this exception capability-like: it is available only to the
|
|
882
|
+
// server-owned fast path, only for the exact state-owned candidate, and
|
|
883
|
+
// only while the immutable workspace still proves exactly that one
|
|
884
|
+
// complete certified tuple. Ordinary provider calls continue through
|
|
885
|
+
// the inspector-first policy below.
|
|
886
|
+
const directExactCertifiedExecution = tool === 'run_certified'
|
|
887
|
+
&& input.directExactCertifiedExecution === true
|
|
888
|
+
&& state.tierStates?.certified?.status === 'complete'
|
|
889
|
+
&& state.exactCertifiedCandidateId !== undefined
|
|
890
|
+
&& state.tierStates.certified.candidateIds.length === 1
|
|
891
|
+
&& state.tierStates.certified.candidateIds[0] === state.exactCertifiedCandidateId
|
|
892
|
+
&& input.candidateIds?.length === 1
|
|
893
|
+
&& input.candidateIds[0] === state.exactCertifiedCandidateId;
|
|
894
|
+
if (tier) {
|
|
895
|
+
if (state.turnClass === 'definition' || state.turnClass === 'business_context' || state.turnClass === 'general') {
|
|
896
|
+
return { ok: false, reasonCode: 'CONTEXTUAL_TURN_EXECUTION_NOT_ALLOWED' };
|
|
897
|
+
}
|
|
898
|
+
const earlier = priorTierComplete(tier, input.candidateIds);
|
|
899
|
+
if (earlier) {
|
|
900
|
+
const safeNextTools = earlier === 'certified'
|
|
901
|
+
? ['run_certified']
|
|
902
|
+
: earlier === 'semantic'
|
|
903
|
+
? ['compile_and_run_semantic']
|
|
904
|
+
: ['compile_and_run_dql'];
|
|
905
|
+
return { ok: false, reasonCode: 'EARLIER_COMPLETE_TIER_REQUIRED', safeNextTools };
|
|
906
|
+
}
|
|
907
|
+
// A frozen plan is stronger than the pre-freeze inspection protocol:
|
|
908
|
+
// no later route may replace it, even if that later route was never
|
|
909
|
+
// inspected. Report an already-complete earlier tier first because it
|
|
910
|
+
// is the actionable reason for a lower-tier request; a higher-tier
|
|
911
|
+
// replacement remains a post-freeze route-change denial.
|
|
912
|
+
if (state.resolvedPlan?.frozen && state.resolvedPlan.tier !== tier) {
|
|
913
|
+
return { ok: false, reasonCode: 'POST_FREEZE_ROUTE_CHANGE_DENIED' };
|
|
914
|
+
}
|
|
915
|
+
// Once the host has authorized a plan, the only permissible second
|
|
916
|
+
// attempt is the explicitly-marked same-plan repair below. Do not
|
|
917
|
+
// let an identical ordinary tool call look like a fresh pre-freeze
|
|
918
|
+
// execution attempt: that would silently bypass the repair budget.
|
|
919
|
+
if (state.resolvedPlan?.frozen && state.resolvedPlan.tier === tier && !input.repair) {
|
|
920
|
+
return { ok: false, reasonCode: 'POST_FREEZE_REPAIR_REQUIRED' };
|
|
921
|
+
}
|
|
922
|
+
// A same-plan repair is bound to the frozen tool/plan. It must not
|
|
923
|
+
// re-run pre-freeze inspection just because the original evidence was
|
|
924
|
+
// compacted after execution; the repair and execution ceilings below
|
|
925
|
+
// remain the authority.
|
|
926
|
+
if (state.resolvedPlan?.frozen && state.resolvedPlan.tier === tier && input.repair) {
|
|
927
|
+
const frozen = state.resolvedPlan;
|
|
928
|
+
const requestedCandidateIds = input.candidateIds
|
|
929
|
+
? [...new Set(input.candidateIds)].filter((id) => state.retainedCandidateIds.includes(id)).sort()
|
|
930
|
+
: undefined;
|
|
931
|
+
const frozenCandidateIds = [...frozen.candidateIds].sort();
|
|
932
|
+
const requestedPathIds = input.relationshipPathIds ? [...new Set(input.relationshipPathIds)].sort() : undefined;
|
|
933
|
+
const frozenPathIds = [...(frozen.relationshipPathIds ?? [])].sort();
|
|
934
|
+
const sameCandidates = !requestedCandidateIds
|
|
935
|
+
|| (requestedCandidateIds.length === frozenCandidateIds.length && requestedCandidateIds.every((id, index) => id === frozenCandidateIds[index]));
|
|
936
|
+
const samePaths = !requestedPathIds
|
|
937
|
+
|| (requestedPathIds.length === frozenPathIds.length && requestedPathIds.every((id, index) => id === frozenPathIds[index]));
|
|
938
|
+
const sameBindings = !input.bindingFingerprint
|
|
939
|
+
|| !frozen.bindingFingerprint
|
|
940
|
+
|| input.bindingFingerprint === frozen.bindingFingerprint;
|
|
941
|
+
if (!sameCandidates || !samePaths || !sameBindings) {
|
|
942
|
+
return { ok: false, reasonCode: 'POST_FREEZE_PLAN_MUTATION_DENIED' };
|
|
943
|
+
}
|
|
944
|
+
if (repairs >= (state.turnClass === 'research' ? ASK_V2_BUDGETS.research.repairs : ASK_V2_BUDGETS.ask.repairs)) {
|
|
945
|
+
return { ok: false, reasonCode: 'ASK_REPAIR_BUDGET_EXHAUSTED' };
|
|
946
|
+
}
|
|
947
|
+
if (executionAttempts >= maxExecutions)
|
|
948
|
+
return { ok: false, reasonCode: 'ASK_EXECUTION_BUDGET_EXHAUSTED' };
|
|
949
|
+
return { ok: true };
|
|
950
|
+
}
|
|
951
|
+
// A live controller commitment is created only by the matching
|
|
952
|
+
// host-backed inspection. It may let that tier execute without
|
|
953
|
+
// burning later-tier discovery turns, while the earlier-complete
|
|
954
|
+
// guard above remains authoritative.
|
|
955
|
+
if (!earlierTierInspected(tier) && state.controllerTier !== tier) {
|
|
956
|
+
return { ok: false, reasonCode: 'EARLIER_TIER_INSPECTION_REQUIRED' };
|
|
957
|
+
}
|
|
958
|
+
if (executionAttempts >= maxExecutions)
|
|
959
|
+
return { ok: false, reasonCode: 'ASK_EXECUTION_BUDGET_EXHAUSTED' };
|
|
960
|
+
if (input.repair && repairs >= (state.turnClass === 'research' ? ASK_V2_BUDGETS.research.repairs : ASK_V2_BUDGETS.ask.repairs)) {
|
|
961
|
+
return { ok: false, reasonCode: 'ASK_REPAIR_BUDGET_EXHAUSTED' };
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// Keep tier/freeze denials above as the primary reason. Once ordinary
|
|
965
|
+
// safety permits a call, the live policy may still narrow discovery to
|
|
966
|
+
// the next LLM-controlled action (for example semantic compilation).
|
|
967
|
+
if (!progress.allowedToolNames.includes(tool) && !directExactCertifiedExecution) {
|
|
968
|
+
// A raw/text controller can still submit an admitted semantic handle
|
|
969
|
+
// after the inspector has reported a host-unavailable or ineligible
|
|
970
|
+
// semantic tier. Let the canonical semantic tool return its precise
|
|
971
|
+
// pre-freeze capability/argument diagnostic, but never promote that
|
|
972
|
+
// call into a committed route or an execution authorization. Native
|
|
973
|
+
// tool surfaces remain narrowed to the actual next safe action.
|
|
974
|
+
const semanticValidationProbe = tool === 'compile_and_run_semantic'
|
|
975
|
+
&& input.candidateIds?.length
|
|
976
|
+
&& (state.tierStates?.semantic?.status === 'unavailable' || state.tierStates?.semantic?.status === 'ineligible');
|
|
977
|
+
if (semanticValidationProbe)
|
|
978
|
+
return { ok: true };
|
|
979
|
+
// An out-of-snapshot relationship handle is a local validation
|
|
980
|
+
// incident, not an alternate lower-tier execution route. Let the DQL
|
|
981
|
+
// tool consume that malformed request so it can return the precise
|
|
982
|
+
// `...PATH_NOT_ADMITTED` observation; it cannot reach a compiler or
|
|
983
|
+
// executor. This keeps a hostile or stale text-tool request
|
|
984
|
+
// diagnosable while the native tool surface remains narrowed to the
|
|
985
|
+
// safe next action.
|
|
986
|
+
const malformedRelationshipPathRequest = tool === 'compile_and_run_dql'
|
|
987
|
+
&& input.relationshipPathIds?.some((id) => !(state.relationshipPathHandles ?? []).some((path) => path.id === id));
|
|
988
|
+
if (malformedRelationshipPathRequest)
|
|
989
|
+
return { ok: true };
|
|
990
|
+
return {
|
|
991
|
+
ok: false,
|
|
992
|
+
reasonCode: tool === 'request_clarification'
|
|
993
|
+
? 'ASK_V2_CLARIFICATION_NOT_MATERIALLY_AMBIGUOUS'
|
|
994
|
+
: hasExecutedTool() ? 'ASK_V2_TERMINAL_NARRATION_REQUIRED' : 'ASK_V2_TOOL_PROGRESSION_REQUIRED',
|
|
995
|
+
safeNextTools: progress.allowedToolNames,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (input.expansion && expansions >= ASK_V2_BUDGETS.ask.expansions)
|
|
999
|
+
return { ok: false, reasonCode: 'ASK_EXPANSION_BUDGET_EXHAUSTED' };
|
|
1000
|
+
if (tool === 'search_values' && valueSearches >= ASK_V2_BUDGETS.ask.valueSearches)
|
|
1001
|
+
return { ok: false, reasonCode: 'ASK_VALUE_SEARCH_BUDGET_EXHAUSTED' };
|
|
1002
|
+
if (tool === 'request_clarification' && clarifications >= ASK_V2_BUDGETS.ask.clarifications)
|
|
1003
|
+
return { ok: false, reasonCode: 'ASK_CLARIFICATION_ALREADY_REQUESTED' };
|
|
1004
|
+
return { ok: true };
|
|
1005
|
+
},
|
|
1006
|
+
toolPolicy,
|
|
1007
|
+
observe(observation) {
|
|
1008
|
+
// A host authorization/freeze is evidence within one canonical tool
|
|
1009
|
+
// call. Keep it out of the provider-tool budget while retaining it in
|
|
1010
|
+
// the durable receipt before compiler/warehouse work begins.
|
|
1011
|
+
// A host-only semantic argument completion is emitted inside the same
|
|
1012
|
+
// physical tool call before authorization. Preserve it in V8 without
|
|
1013
|
+
// double-charging the provider tool budget.
|
|
1014
|
+
if (askV2ObservationConsumesToolBudget(observation))
|
|
1015
|
+
toolCalls += 1;
|
|
1016
|
+
const executionTier = executionToolTier[observation.tool];
|
|
1017
|
+
const tier = executionTier ?? inspectionToolTier[observation.tool] ?? observation.tier;
|
|
1018
|
+
if (tier) {
|
|
1019
|
+
// A compiler/compatibility miss is not a warehouse execution. Counting
|
|
1020
|
+
// it here would consume the exploratory attempt before the agent had
|
|
1021
|
+
// actually reached a runnable later tier.
|
|
1022
|
+
if (executionTier && observation.executionAuthorized) {
|
|
1023
|
+
executionAttempts += 1;
|
|
1024
|
+
if (observation.samePlanRepair)
|
|
1025
|
+
repairs += 1;
|
|
1026
|
+
}
|
|
1027
|
+
else if (executionTier && !state.observations.some((item) => item.executionAuthorized)
|
|
1028
|
+
&& (observation.outcome === 'executed' || observation.outcome === 'error')) {
|
|
1029
|
+
// Backward-compatible reader behavior for a pre-authorization V4
|
|
1030
|
+
// receipt. New runs count the minted capability instead.
|
|
1031
|
+
executionAttempts += 1;
|
|
1032
|
+
if (observation.retryable)
|
|
1033
|
+
repairs += 1;
|
|
1034
|
+
}
|
|
1035
|
+
// Freeze at host authorization/capability minting, before a compiler
|
|
1036
|
+
// or executor is called. A failure after this point is terminal
|
|
1037
|
+
// unless the same frozen plan receives its one permitted repair.
|
|
1038
|
+
if (executionTier && observation.executionAuthorized && observation.outcome === 'eligible' && !state.resolvedPlan?.frozen) {
|
|
1039
|
+
const ids = [...new Set(observation.candidateIds)].filter((id) => state.retainedCandidateIds.includes(id));
|
|
1040
|
+
state.resolvedPlan = {
|
|
1041
|
+
version: 3,
|
|
1042
|
+
id: observation.planId ?? `ask-v2:${tier}:${state.snapshotId ?? 'snapshot'}`,
|
|
1043
|
+
snapshotId: state.snapshotId,
|
|
1044
|
+
tier,
|
|
1045
|
+
candidateIds: ids,
|
|
1046
|
+
frozen: true,
|
|
1047
|
+
reviewRequired: tier === 'exploratory_sql',
|
|
1048
|
+
...(observation.inputFingerprint ? { bindingFingerprint: observation.inputFingerprint } : {}),
|
|
1049
|
+
...(observation.relationshipPathIds?.length
|
|
1050
|
+
? { relationshipPathIds: [...new Set(observation.relationshipPathIds)].sort() }
|
|
1051
|
+
: {}),
|
|
1052
|
+
...(observation.outputFingerprint ? { targetFingerprint: observation.outputFingerprint } : {}),
|
|
1053
|
+
fingerprint: observation.inputFingerprint
|
|
1054
|
+
?? `sha256:${createHash('sha256').update([tier, state.snapshotId ?? '', ...ids].join('|')).digest('hex')}`,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
// Execution is a host-backed proof that the selected tuple was
|
|
1058
|
+
// complete. Compiler/availability misses remain observations and
|
|
1059
|
+
// intentionally allow the same agent to continue down the cascade.
|
|
1060
|
+
if (executionTier && observation.outcome === 'executed') {
|
|
1061
|
+
state.tierStates = {
|
|
1062
|
+
...state.tierStates,
|
|
1063
|
+
[tier]: {
|
|
1064
|
+
version: 1,
|
|
1065
|
+
status: 'complete',
|
|
1066
|
+
candidateIds: [...new Set(observation.candidateIds)].filter((id) => state.retainedCandidateIds.includes(id)),
|
|
1067
|
+
reasonCode: observation.reasonCode,
|
|
1068
|
+
},
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
const tierAttempts = state.tierAttempts ?? (state.tierAttempts = []);
|
|
1072
|
+
tierAttempts.push({
|
|
1073
|
+
version: 2,
|
|
1074
|
+
tier,
|
|
1075
|
+
outcome: observation.outcome,
|
|
1076
|
+
reasonCode: observation.reasonCode,
|
|
1077
|
+
candidateIds: [...new Set(observation.candidateIds)].filter((id) => state.retainedCandidateIds.includes(id)),
|
|
1078
|
+
frozen: state.resolvedPlan?.frozen === true && state.resolvedPlan.tier === tier,
|
|
1079
|
+
...(typeof observation.durationMs === 'number' ? { durationMs: observation.durationMs } : {}),
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (observation.tool === 'search_values')
|
|
1083
|
+
valueSearches += 1;
|
|
1084
|
+
if (observation.tool === 'request_clarification' && observation.outcome === 'needs_input')
|
|
1085
|
+
clarifications += 1;
|
|
1086
|
+
if (observation.tool === 'inspect_ask_context' && observation.reasonCode === 'same_snapshot_extension')
|
|
1087
|
+
expansions += 1;
|
|
1088
|
+
state.observations.push({ ...observation, ...(tier ? { tier } : {}) });
|
|
1089
|
+
if (observation.outcome === 'denied')
|
|
1090
|
+
state.terminal = 'denied';
|
|
1091
|
+
if (observation.outcome === 'needs_input')
|
|
1092
|
+
state.terminal = 'clarification';
|
|
1093
|
+
return state.observations.at(-1);
|
|
1094
|
+
},
|
|
1095
|
+
diagnosticReceipt(finalStopReason = state.terminal ?? 'in_progress', outcome = {
|
|
1096
|
+
connectionAttempted: false,
|
|
1097
|
+
executionAttempts: 0,
|
|
1098
|
+
factCount: 0,
|
|
1099
|
+
narration: 'not_retained',
|
|
1100
|
+
}, activity) {
|
|
1101
|
+
const count = (value, fallback) => (typeof value === 'number' && Number.isFinite(value)
|
|
1102
|
+
? Math.max(0, Math.floor(value))
|
|
1103
|
+
: fallback);
|
|
1104
|
+
return {
|
|
1105
|
+
version: 8,
|
|
1106
|
+
mode: state.mode,
|
|
1107
|
+
turnClass: state.turnClass,
|
|
1108
|
+
snapshotId: state.snapshotId,
|
|
1109
|
+
retainedCandidateCount: state.retainedCandidateIds.length,
|
|
1110
|
+
initialCandidateCount: state.initialCandidateIds.length,
|
|
1111
|
+
expansionCount: expansions,
|
|
1112
|
+
objective: state.turnClass,
|
|
1113
|
+
contextCoverage: state.contextCoverage ?? [],
|
|
1114
|
+
excludedCandidateCount: state.excludedCandidateCount ?? 0,
|
|
1115
|
+
exclusionReasonCodes: state.exclusionReasonCodes ?? [],
|
|
1116
|
+
observations: state.observations,
|
|
1117
|
+
tierAttempts: state.tierAttempts ?? [],
|
|
1118
|
+
...(controllerTier() ? { controllerTier: controllerTier() } : {}),
|
|
1119
|
+
...(state.semanticRuntime ? { semanticRuntime: state.semanticRuntime } : {}),
|
|
1120
|
+
planFrozen: state.resolvedPlan?.frozen === true,
|
|
1121
|
+
...(state.terminalOutcome ? { terminalOutcome: state.terminalOutcome } : {}),
|
|
1122
|
+
outcome,
|
|
1123
|
+
activity: {
|
|
1124
|
+
// A kernel receipt has no authority to claim that a provider send
|
|
1125
|
+
// occurred. The local server supplies this from physical egress
|
|
1126
|
+
// receipts when it persists a live run.
|
|
1127
|
+
providerDispatches: count(activity?.providerDispatches, 0),
|
|
1128
|
+
toolCalls: count(activity?.toolCalls, toolCalls),
|
|
1129
|
+
executionAttempts: count(activity?.executionAttempts, outcome.executionAttempts),
|
|
1130
|
+
repairs: count(activity?.repairs, repairs),
|
|
1131
|
+
},
|
|
1132
|
+
toolDurationMs: state.observations.reduce((total, observation) => total + Math.max(0, observation.durationMs ?? 0), 0),
|
|
1133
|
+
finalStopReason,
|
|
1134
|
+
};
|
|
1135
|
+
},
|
|
1136
|
+
};
|
|
1137
|
+
activeToolKernels.set(state, kernel);
|
|
1138
|
+
return kernel;
|
|
1139
|
+
}
|
|
1140
|
+
/** Record a live host/tool observation against the shared V2 kernel. */
|
|
1141
|
+
export function observeAskAgentV2Tool(state, observation) {
|
|
1142
|
+
if (!state)
|
|
1143
|
+
return undefined;
|
|
1144
|
+
return createAskToolKernelV2(state).observe(observation);
|
|
1145
|
+
}
|
|
1146
|
+
/** Record the host's tuple-completeness verdict without treating cards as SQL authority. */
|
|
1147
|
+
export function setAskV2TierState(state, tier, next) {
|
|
1148
|
+
if (!state)
|
|
1149
|
+
return undefined;
|
|
1150
|
+
const allowed = new Set(state.retainedCandidateIds);
|
|
1151
|
+
const value = {
|
|
1152
|
+
version: 1,
|
|
1153
|
+
...next,
|
|
1154
|
+
candidateIds: [...new Set(next.candidateIds)].filter((id) => allowed.has(id)),
|
|
1155
|
+
...(next.safeNextTools?.length ? { safeNextTools: [...new Set(next.safeNextTools)] } : {}),
|
|
1156
|
+
...(next.clarificationChoices?.length
|
|
1157
|
+
? {
|
|
1158
|
+
clarificationChoices: next.clarificationChoices.map((choice) => ({
|
|
1159
|
+
version: 1,
|
|
1160
|
+
id: choice.id,
|
|
1161
|
+
label: choice.label,
|
|
1162
|
+
candidateIds: [...new Set(choice.candidateIds)].filter((id) => allowed.has(id)),
|
|
1163
|
+
resultFingerprint: choice.resultFingerprint,
|
|
1164
|
+
})),
|
|
1165
|
+
}
|
|
1166
|
+
: {}),
|
|
1167
|
+
};
|
|
1168
|
+
state.tierStates = { ...state.tierStates, [tier]: value };
|
|
1169
|
+
return value;
|
|
1170
|
+
}
|
|
1171
|
+
/** Seal a V2 terminal without converting it into a legacy business verdict. */
|
|
1172
|
+
export function finishAskAgentV2Turn(state, outcome) {
|
|
1173
|
+
if (!state)
|
|
1174
|
+
return;
|
|
1175
|
+
state.terminalOutcome = outcome;
|
|
1176
|
+
state.terminal = outcome.kind === 'finish_answer'
|
|
1177
|
+
? 'completed'
|
|
1178
|
+
: outcome.kind === 'clarification'
|
|
1179
|
+
? 'clarification'
|
|
1180
|
+
: outcome.kind === 'denied'
|
|
1181
|
+
? 'denied'
|
|
1182
|
+
: outcome.kind === 'budget_exhausted'
|
|
1183
|
+
? 'budget_exhausted'
|
|
1184
|
+
: 'error';
|
|
1185
|
+
}
|
|
1186
|
+
/** A stale bridge must never become a second retrieval or a prompt source. */
|
|
1187
|
+
export function askAgentV2WorkspaceMatches(state, bridge) {
|
|
1188
|
+
// Identity is optional only for older V2 state that did not persist it. If
|
|
1189
|
+
// the state has an identity, a bridge which omits it is *not* a match: using
|
|
1190
|
+
// that bridge would silently replace the immutable retrieval snapshot.
|
|
1191
|
+
const snapshotMatches = !state?.snapshotId
|
|
1192
|
+
|| (typeof bridge?.snapshotId === 'string' && bridge.snapshotId === state.snapshotId);
|
|
1193
|
+
const sourceMatches = !state?.sourceFingerprint
|
|
1194
|
+
|| (typeof bridge?.sourceFingerprint === 'string' && bridge.sourceFingerprint === state.sourceFingerprint);
|
|
1195
|
+
return Boolean(state
|
|
1196
|
+
&& bridge?.version === 2
|
|
1197
|
+
&& typeof bridge.getContextPack === 'function'
|
|
1198
|
+
&& snapshotMatches
|
|
1199
|
+
&& sourceMatches);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Materialize Tier 1 tuple completeness from the immutable server workspace
|
|
1203
|
+
* before the first V2 tool policy is calculated. The old fast-path-only
|
|
1204
|
+
* behavior exposed this truth only when there was one certified candidate,
|
|
1205
|
+
* allowing a semantic inspector to commit a lower tier while two complete
|
|
1206
|
+
* certified artifacts were present. The workspace is the authority here;
|
|
1207
|
+
* provider cards and persisted controller state never create completeness.
|
|
1208
|
+
*
|
|
1209
|
+
* `certifiedExecutionAvailable` is deliberately host-provided rather than
|
|
1210
|
+
* inferred from an artifact card. A complete fit with no authorized local
|
|
1211
|
+
* executor is a pre-freeze unavailable observation, not a Tier 1 priority
|
|
1212
|
+
* trap. The provider adapter passes its actual execution callback; the engine
|
|
1213
|
+
* uses the bridge's host readiness hook for the zero-provider shortcut.
|
|
1214
|
+
*/
|
|
1215
|
+
export function materializeAskV2WorkspaceTierTruth(state, bridge, reference) {
|
|
1216
|
+
if (!state || !askAgentV2WorkspaceMatches(state, bridge))
|
|
1217
|
+
return undefined;
|
|
1218
|
+
const workspace = bridge?.getToolWorkspace?.();
|
|
1219
|
+
if (!workspace || workspace.version !== 1
|
|
1220
|
+
|| (state.snapshotId !== undefined && workspace.snapshotId !== state.snapshotId)
|
|
1221
|
+
|| (state.sourceFingerprint !== undefined && workspace.sourceFingerprint !== state.sourceFingerprint))
|
|
1222
|
+
return undefined;
|
|
1223
|
+
const retained = new Set(state.retainedCandidateIds);
|
|
1224
|
+
const candidatesById = new Map(workspace.candidates
|
|
1225
|
+
.map((candidate) => [candidate.qualifiedId ?? candidate.id, candidate])
|
|
1226
|
+
.filter(([candidateId]) => retained.has(candidateId)));
|
|
1227
|
+
const certifiedCandidateIds = [...candidatesById.entries()]
|
|
1228
|
+
.filter(([, candidate]) => candidate.kind === 'certified_block' && candidate.trustTier === 'certified')
|
|
1229
|
+
.map(([candidateId]) => candidateId);
|
|
1230
|
+
const completeFitCandidateIds = [...new Set(workspace.certifiedCompleteCandidateIds ?? [])]
|
|
1231
|
+
.filter((candidateId) => {
|
|
1232
|
+
const candidate = candidatesById.get(candidateId);
|
|
1233
|
+
return candidate?.kind === 'certified_block'
|
|
1234
|
+
&& candidate.trustTier === 'certified';
|
|
1235
|
+
});
|
|
1236
|
+
const currentCompleteCandidateIds = [];
|
|
1237
|
+
let sawStaleArtifact = false;
|
|
1238
|
+
let sawUnboundArtifact = false;
|
|
1239
|
+
for (const candidateId of completeFitCandidateIds) {
|
|
1240
|
+
const value = workspace.certifiedArtifacts?.get(candidateId);
|
|
1241
|
+
if (!value || typeof value !== 'object') {
|
|
1242
|
+
sawUnboundArtifact = true;
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
const handle = value;
|
|
1246
|
+
if (handle.version !== 1
|
|
1247
|
+
|| typeof handle.revisionFingerprint !== 'string'
|
|
1248
|
+
|| typeof handle.isCurrent !== 'function'
|
|
1249
|
+
|| handle.artifact === undefined) {
|
|
1250
|
+
sawUnboundArtifact = true;
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
try {
|
|
1254
|
+
if (!handle.isCurrent()) {
|
|
1255
|
+
sawStaleArtifact = true;
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
catch {
|
|
1260
|
+
sawStaleArtifact = true;
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
currentCompleteCandidateIds.push(candidateId);
|
|
1264
|
+
}
|
|
1265
|
+
let bridgeCertifiedExecutionAvailable = false;
|
|
1266
|
+
if (reference?.certifiedExecutionAvailable !== undefined) {
|
|
1267
|
+
bridgeCertifiedExecutionAvailable = reference.certifiedExecutionAvailable === true;
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
try {
|
|
1271
|
+
bridgeCertifiedExecutionAvailable = bridge?.isCertifiedExecutionAvailable?.() === true;
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
bridgeCertifiedExecutionAvailable = false;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (!state.resolvedPlan?.frozen) {
|
|
1278
|
+
if (bridgeCertifiedExecutionAvailable && currentCompleteCandidateIds.length > 0) {
|
|
1279
|
+
setAskV2TierState(state, 'certified', {
|
|
1280
|
+
status: 'complete',
|
|
1281
|
+
candidateIds: currentCompleteCandidateIds,
|
|
1282
|
+
reasonCode: workspace.tierStates?.certified?.reasonCode ?? 'CERTIFIED_COMPLETE_FOR_REQUEST',
|
|
1283
|
+
safeNextTools: ['run_certified'],
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
const unavailable = completeFitCandidateIds.length > 0;
|
|
1288
|
+
const reasonCode = !bridgeCertifiedExecutionAvailable
|
|
1289
|
+
? 'CERTIFIED_EXECUTOR_UNAVAILABLE'
|
|
1290
|
+
: sawStaleArtifact
|
|
1291
|
+
? 'CERTIFIED_ARTIFACT_STALE'
|
|
1292
|
+
: sawUnboundArtifact
|
|
1293
|
+
? 'CERTIFIED_ARTIFACT_NOT_BOUND_TO_SNAPSHOT'
|
|
1294
|
+
: certifiedCandidateIds.length > 0
|
|
1295
|
+
? 'CERTIFIED_TUPLE_NOT_PROVEN_BY_SNAPSHOT'
|
|
1296
|
+
: 'CERTIFIED_CANDIDATES_EMPTY';
|
|
1297
|
+
setAskV2TierState(state, 'certified', {
|
|
1298
|
+
status: unavailable || sawStaleArtifact || sawUnboundArtifact || certifiedCandidateIds.length === 0
|
|
1299
|
+
? 'unavailable'
|
|
1300
|
+
: 'ineligible',
|
|
1301
|
+
candidateIds: completeFitCandidateIds.length > 0 ? completeFitCandidateIds : certifiedCandidateIds,
|
|
1302
|
+
reasonCode,
|
|
1303
|
+
});
|
|
1304
|
+
// A persisted fast-path/plan/controller commitment is only an
|
|
1305
|
+
// optimization before freeze. Do not carry it into a workspace that no
|
|
1306
|
+
// longer has a current, executable certified artifact; doing so traps
|
|
1307
|
+
// semantic fallback behind stale Tier 1 evidence.
|
|
1308
|
+
delete state.exactCertifiedCandidateId;
|
|
1309
|
+
if (state.candidatePlan?.intendedTool === 'run_certified')
|
|
1310
|
+
delete state.candidatePlan;
|
|
1311
|
+
if (state.resolvedPlan?.tier === 'certified' && !state.resolvedPlan.frozen)
|
|
1312
|
+
delete state.resolvedPlan;
|
|
1313
|
+
if (state.controllerTier === 'certified')
|
|
1314
|
+
delete state.controllerTier;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const exactReferences = new Set();
|
|
1318
|
+
const addReference = (value) => {
|
|
1319
|
+
if (typeof value !== 'string' || !value.trim())
|
|
1320
|
+
return;
|
|
1321
|
+
const normalized = value.trim();
|
|
1322
|
+
if (candidatesById.has(normalized))
|
|
1323
|
+
exactReferences.add(normalized);
|
|
1324
|
+
};
|
|
1325
|
+
addReference(reference?.selectedEvidenceId);
|
|
1326
|
+
const question = reference?.question ?? '';
|
|
1327
|
+
for (const candidate of candidatesById.values()) {
|
|
1328
|
+
// A textual bypass requires the exact canonical qualified ID, never a
|
|
1329
|
+
// display name, runtime name, alias, or an unqualified legacy ID.
|
|
1330
|
+
if (candidate.qualifiedId && question.includes(candidate.qualifiedId)) {
|
|
1331
|
+
exactReferences.add(candidate.qualifiedId);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const explicitReferences = [];
|
|
1335
|
+
for (const referenceId of exactReferences) {
|
|
1336
|
+
const candidate = candidatesById.get(referenceId);
|
|
1337
|
+
if (!candidate)
|
|
1338
|
+
continue;
|
|
1339
|
+
const candidateId = candidate.qualifiedId ?? candidate.id;
|
|
1340
|
+
if (candidate.qualifiedId !== candidateId)
|
|
1341
|
+
continue;
|
|
1342
|
+
if (askV2ExecutableSemanticRoles(candidate)) {
|
|
1343
|
+
explicitReferences.push({ version: 1, tier: 'semantic', candidateId });
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
// Only a real DQL modeling artifact receives the Tier 1 exception.
|
|
1347
|
+
// dbt/schema/relationship cards remain retrieval context, not an
|
|
1348
|
+
// explicit executable DQL program.
|
|
1349
|
+
if (candidate.kind === 'dql_modeling') {
|
|
1350
|
+
explicitReferences.push({ version: 1, tier: 'governed_relational', candidateId });
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const explicit = explicitReferences.length === 1 ? explicitReferences[0] : undefined;
|
|
1354
|
+
if (!state.resolvedPlan?.frozen) {
|
|
1355
|
+
if (explicit) {
|
|
1356
|
+
state.explicitQualifiedArtifactReference = explicit;
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
delete state.explicitQualifiedArtifactReference;
|
|
1360
|
+
// Reloaded V4 state can retain a lower controller tier from a time when
|
|
1361
|
+
// Tier 1 completeness was not materialized. Clear that pre-freeze
|
|
1362
|
+
// commitment so the next policy forces the certified inspection.
|
|
1363
|
+
if (currentCompleteCandidateIds.length > 0
|
|
1364
|
+
&& bridgeCertifiedExecutionAvailable
|
|
1365
|
+
&& state.controllerTier
|
|
1366
|
+
&& state.controllerTier !== 'certified') {
|
|
1367
|
+
delete state.controllerTier;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return workspace;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Retrieval-first V2 ingress. It deliberately does not prove a business
|
|
1375
|
+
* tuple or choose a compiler. That choice is made by the bounded provider
|
|
1376
|
+
* tool runtime after it sees the immutable candidate workspace.
|
|
1377
|
+
*/
|
|
1378
|
+
export function createAskAgentRuntimeV2(options) {
|
|
1379
|
+
// Serving defaults to shadow until an operator explicitly enables the
|
|
1380
|
+
// canary. V2 can observe an Ask in shadow, but it must never replace the
|
|
1381
|
+
// established answer path merely because a runtime was constructed.
|
|
1382
|
+
const mode = options.mode ?? 'shadow_v2';
|
|
1383
|
+
return {
|
|
1384
|
+
mode,
|
|
1385
|
+
async decide(request) {
|
|
1386
|
+
if (!isAskRequestV2(request) || mode === 'legacy_v1')
|
|
1387
|
+
return options.legacyRouter.decide(request);
|
|
1388
|
+
const conversationalKind = classifyConversationalTurn(request.question, Boolean(request.history?.length || request.conversationContext));
|
|
1389
|
+
if (conversationalKind) {
|
|
1390
|
+
return {
|
|
1391
|
+
action: 'converse',
|
|
1392
|
+
category: 'conversational',
|
|
1393
|
+
conversationalKind,
|
|
1394
|
+
confidence: 1,
|
|
1395
|
+
followsUp: Boolean(request.history?.length),
|
|
1396
|
+
reason: 'This turn does not require analytical execution.',
|
|
1397
|
+
askAgentV2Decision: { version: 2, mode, state: emptyState(mode, 'general', request) },
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
const turnClass = classifyTurnV2(request);
|
|
1401
|
+
const evidence = await options.getEvidence?.(request);
|
|
1402
|
+
const state = createState(mode, turnClass, request, evidence);
|
|
1403
|
+
applyHostCertifiedFastPath(state, request);
|
|
1404
|
+
const v2Decision = { version: 2, mode, state };
|
|
1405
|
+
if (mode === 'shadow_v2') {
|
|
1406
|
+
const legacy = await options.legacyRouter.decide(request);
|
|
1407
|
+
return { ...legacy, askAgentV2Decision: v2Decision };
|
|
1408
|
+
}
|
|
1409
|
+
if (request.selectedResultBindingGap) {
|
|
1410
|
+
state.terminal = 'clarification';
|
|
1411
|
+
return {
|
|
1412
|
+
action: 'clarify',
|
|
1413
|
+
confidence: 1,
|
|
1414
|
+
followsUp: true,
|
|
1415
|
+
source: 'heuristic',
|
|
1416
|
+
reason: 'The selected prior result could not be rebound, so one specific member selection is required.',
|
|
1417
|
+
clarifyingQuestion: request.selectedResultBindingGap.message,
|
|
1418
|
+
requiresClarification: true,
|
|
1419
|
+
// The members the reference could have meant, when the host resolved
|
|
1420
|
+
// candidates but could not choose between them.
|
|
1421
|
+
...(request.selectedResultBindingGap.options?.length
|
|
1422
|
+
? { clarificationOptions: request.selectedResultBindingGap.options }
|
|
1423
|
+
: {}),
|
|
1424
|
+
askAgentV2Decision: v2Decision,
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
// Explicit Research is the only ingress that changes branch budgets.
|
|
1428
|
+
if (turnClass === 'research') {
|
|
1429
|
+
return {
|
|
1430
|
+
action: 'investigate',
|
|
1431
|
+
confidence: 0.9,
|
|
1432
|
+
followsUp: Boolean(request.history?.length),
|
|
1433
|
+
source: 'heuristic',
|
|
1434
|
+
reason: 'Research was explicitly requested; the same immutable snapshot enters the bounded research tool runtime.',
|
|
1435
|
+
askAgentV2Decision: v2Decision,
|
|
1436
|
+
retrievalEvidence: retrievalProjection(evidence),
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
// The key V2 cutover: zero candidate/coverage verdicts become context
|
|
1440
|
+
// for the agent, never a router-owned analytical coverage terminal. The
|
|
1441
|
+
// bounded V2 tool runtime receives the same snapshot and performs the
|
|
1442
|
+
// certified -> semantic -> relational -> exploratory progression.
|
|
1443
|
+
return {
|
|
1444
|
+
action: 'answer',
|
|
1445
|
+
confidence: evidence ? 0.82 : 0.5,
|
|
1446
|
+
followsUp: Boolean(request.history?.length),
|
|
1447
|
+
source: 'llm',
|
|
1448
|
+
reason: evidence
|
|
1449
|
+
? 'Retrieved an immutable role-balanced context workspace; the bounded Ask agent will choose and validate the next tool.'
|
|
1450
|
+
: 'No retrieval snapshot is currently available; the Ask agent may provide a general answer or report a typed availability observation.',
|
|
1451
|
+
askAgentV2Decision: v2Decision,
|
|
1452
|
+
retrievalEvidence: retrievalProjection(evidence),
|
|
1453
|
+
};
|
|
1454
|
+
},
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function createState(mode, turnClass, request, evidence) {
|
|
1458
|
+
const retained = (evidence?.candidates ?? []).slice(0, 128);
|
|
1459
|
+
const requirements = buildAnalyticalRequirementSet({ question: request.question, parsedIntent: evidence?.parsedIntent });
|
|
1460
|
+
const initial = selectRoleBalancedMeaningCandidates({ candidates: retained, requirements, maxCandidates: 24 });
|
|
1461
|
+
const initialIds = initial.map(stableCandidateId);
|
|
1462
|
+
const retainedIds = retained.map(stableCandidateId);
|
|
1463
|
+
const contextCoverage = contextCoverageForWorkspace(evidence, retained);
|
|
1464
|
+
return {
|
|
1465
|
+
version: 4,
|
|
1466
|
+
mode,
|
|
1467
|
+
turnClass,
|
|
1468
|
+
snapshotId: evidence?.snapshotId,
|
|
1469
|
+
sourceFingerprint: evidence?.sourceFingerprint,
|
|
1470
|
+
retainedCandidateIds: retainedIds,
|
|
1471
|
+
initialCandidateIds: initialIds,
|
|
1472
|
+
expansionCandidateIds: retainedIds.filter((id) => !initialIds.includes(id)).slice(0, 24),
|
|
1473
|
+
contextCoverage,
|
|
1474
|
+
excludedCandidateCount: Math.max(0, (evidence?.candidates.length ?? 0) - retained.length),
|
|
1475
|
+
exclusionReasonCodes: (evidence?.candidates.length ?? 0) > retained.length
|
|
1476
|
+
? ['WORKSPACE_CANDIDATE_CAP']
|
|
1477
|
+
: [],
|
|
1478
|
+
relationshipPathHandles: (evidence?.relationshipPathHandles ?? [])
|
|
1479
|
+
.slice(0, 8)
|
|
1480
|
+
.map((path) => ({
|
|
1481
|
+
version: 1,
|
|
1482
|
+
id: path.id,
|
|
1483
|
+
edgeIds: path.edgeIds.slice(0, 12),
|
|
1484
|
+
...(path.candidateIds?.length ? { candidateIds: path.candidateIds.slice(0, 24) } : {}),
|
|
1485
|
+
...(evidence?.snapshotId ? { snapshotId: evidence.snapshotId } : {}),
|
|
1486
|
+
})),
|
|
1487
|
+
conversation: {
|
|
1488
|
+
version: 2,
|
|
1489
|
+
sourceTurnId: request.trustedTaskAnchor?.sourceTurnId,
|
|
1490
|
+
selectedMemberId: request.selectedResultBinding?.canonicalColumn,
|
|
1491
|
+
selectedMemberBinding: request.selectedResultBinding?.value,
|
|
1492
|
+
clarificationId: request.selectedEvidenceId,
|
|
1493
|
+
availableResultHandleIds: request.priorResultMemberBinding?.values.map((value, index) => `member:${index}:${fingerprint(value)}`) ?? [],
|
|
1494
|
+
...(request.selectedResultBindingGap?.options?.length
|
|
1495
|
+
? { ambiguousMemberLabels: request.selectedResultBindingGap.options.map((option) => option.label).slice(0, 8) }
|
|
1496
|
+
: {}),
|
|
1497
|
+
},
|
|
1498
|
+
observations: evidence ? [] : [{
|
|
1499
|
+
version: 1,
|
|
1500
|
+
tool: 'inspect_ask_context',
|
|
1501
|
+
outcome: 'unavailable',
|
|
1502
|
+
reasonCode: 'RETRIEVAL_SNAPSHOT_UNAVAILABLE',
|
|
1503
|
+
candidateIds: [],
|
|
1504
|
+
safeAction: 'refresh_metadata_or_answer_general_question',
|
|
1505
|
+
}],
|
|
1506
|
+
tierAttempts: [],
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Promote exactly one host-proven certified artifact into the V2 Tier 1 fast
|
|
1511
|
+
* path. This is not a second meaning resolver: completeness, admission, and
|
|
1512
|
+
* artifact capture are all supplied by the same immutable retrieval workspace
|
|
1513
|
+
* that the normal V2 tools consume. Multiple candidates remain a provider
|
|
1514
|
+
* tool-runtime choice, and a missing/stale workspace never becomes a mutable
|
|
1515
|
+
* catalog lookup.
|
|
1516
|
+
*/
|
|
1517
|
+
function applyHostCertifiedFastPath(state, request) {
|
|
1518
|
+
const bridge = request.askAgentV2Workspace;
|
|
1519
|
+
const workspace = materializeAskV2WorkspaceTierTruth(state, bridge, {
|
|
1520
|
+
question: request.question,
|
|
1521
|
+
selectedEvidenceId: request.selectedEvidenceId,
|
|
1522
|
+
certifiedExecutionAvailable: (() => {
|
|
1523
|
+
try {
|
|
1524
|
+
return bridge?.isCertifiedExecutionAvailable?.() === true;
|
|
1525
|
+
}
|
|
1526
|
+
catch {
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
})(),
|
|
1530
|
+
});
|
|
1531
|
+
if (state.mode !== 'authoritative_v2' || state.turnClass === 'research' || !workspace)
|
|
1532
|
+
return;
|
|
1533
|
+
const retained = new Set(state.retainedCandidateIds);
|
|
1534
|
+
const certifiedTier = state.tierStates?.certified;
|
|
1535
|
+
const complete = certifiedTier?.status === 'complete'
|
|
1536
|
+
? [...new Set(certifiedTier.candidateIds)]
|
|
1537
|
+
.filter((candidateId) => retained.has(candidateId))
|
|
1538
|
+
: [];
|
|
1539
|
+
// A provider-free route is safe only for a unique immutable candidate. Do
|
|
1540
|
+
// not choose between two valid blocks on the model's behalf or by ranking.
|
|
1541
|
+
if (complete.length !== 1)
|
|
1542
|
+
return;
|
|
1543
|
+
const candidateId = complete[0];
|
|
1544
|
+
state.exactCertifiedCandidateId = candidateId;
|
|
1545
|
+
state.candidatePlan = {
|
|
1546
|
+
version: 1,
|
|
1547
|
+
turnClass: state.turnClass,
|
|
1548
|
+
candidateIds: [candidateId],
|
|
1549
|
+
intendedTool: 'run_certified',
|
|
1550
|
+
requirementFingerprint: fingerprint([
|
|
1551
|
+
state.snapshotId ?? '',
|
|
1552
|
+
state.sourceFingerprint ?? '',
|
|
1553
|
+
'certified',
|
|
1554
|
+
candidateId,
|
|
1555
|
+
].join('|')),
|
|
1556
|
+
};
|
|
1557
|
+
// Exact Tier 1 evidence must be visible to the compact workspace too. This
|
|
1558
|
+
// avoids a role-balanced card cap hiding the only provider-free execution
|
|
1559
|
+
// candidate while retaining all server-side candidates for normal fallback.
|
|
1560
|
+
state.initialCandidateIds = [candidateId, ...state.initialCandidateIds.filter((id) => id !== candidateId)].slice(0, 24);
|
|
1561
|
+
state.expansionCandidateIds = state.retainedCandidateIds
|
|
1562
|
+
.filter((id) => !state.initialCandidateIds.includes(id))
|
|
1563
|
+
.slice(0, 24);
|
|
1564
|
+
setAskV2TierState(state, 'certified', {
|
|
1565
|
+
status: 'complete',
|
|
1566
|
+
candidateIds: [candidateId],
|
|
1567
|
+
reasonCode: certifiedTier?.reasonCode ?? 'CERTIFIED_COMPLETE_FOR_REQUEST',
|
|
1568
|
+
safeNextTools: ['run_certified'],
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Release a certified tier claim that turned out to be unprovable in flight.
|
|
1573
|
+
*
|
|
1574
|
+
* The fast path narrows the tool policy to `run_certified` and nothing else.
|
|
1575
|
+
* When the downstream admission/proof then refuses that one tool, the policy
|
|
1576
|
+
* used to have no exit: the turn looped run_certified → denied → finish
|
|
1577
|
+
* denied until the deadline, and the user saw a fabricated "validation"
|
|
1578
|
+
* message. The bridge now avoids taking an unprovable claim at all; this
|
|
1579
|
+
* release is the in-flight defense for anything the bridge could not know.
|
|
1580
|
+
*/
|
|
1581
|
+
export function releaseAskV2CertifiedTierLock(state, reasonCode) {
|
|
1582
|
+
state.exactCertifiedCandidateId = undefined;
|
|
1583
|
+
if (state.candidatePlan?.intendedTool === 'run_certified')
|
|
1584
|
+
state.candidatePlan = undefined;
|
|
1585
|
+
if (state.controllerTier === 'certified')
|
|
1586
|
+
state.controllerTier = undefined;
|
|
1587
|
+
setAskV2TierState(state, 'certified', {
|
|
1588
|
+
status: 'available',
|
|
1589
|
+
candidateIds: state.tierStates?.certified?.candidateIds ?? [],
|
|
1590
|
+
reasonCode,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
/** Build a safe source-level explanation without persisting source contents. */
|
|
1594
|
+
function contextCoverageForWorkspace(evidence, retained) {
|
|
1595
|
+
if (!evidence) {
|
|
1596
|
+
return [{
|
|
1597
|
+
version: 2,
|
|
1598
|
+
source: 'business',
|
|
1599
|
+
status: 'unavailable',
|
|
1600
|
+
admittedCandidateCount: 0,
|
|
1601
|
+
excludedCandidateCount: 0,
|
|
1602
|
+
reasonCodes: ['RETRIEVAL_SNAPSHOT_UNAVAILABLE'],
|
|
1603
|
+
}];
|
|
1604
|
+
}
|
|
1605
|
+
const fullBySource = new Map();
|
|
1606
|
+
const retainedBySource = new Map();
|
|
1607
|
+
for (const candidate of evidence.candidates) {
|
|
1608
|
+
const source = sourceForCandidate(candidate);
|
|
1609
|
+
fullBySource.set(source, (fullBySource.get(source) ?? 0) + 1);
|
|
1610
|
+
}
|
|
1611
|
+
for (const candidate of retained) {
|
|
1612
|
+
const source = sourceForCandidate(candidate);
|
|
1613
|
+
retainedBySource.set(source, (retainedBySource.get(source) ?? 0) + 1);
|
|
1614
|
+
}
|
|
1615
|
+
const reported = new Map();
|
|
1616
|
+
for (const coverage of evidence.diagnostics?.sourceCoverage ?? []) {
|
|
1617
|
+
const source = coverage.source;
|
|
1618
|
+
reported.set(source, coverage);
|
|
1619
|
+
}
|
|
1620
|
+
const sources = new Set([
|
|
1621
|
+
...fullBySource.keys(),
|
|
1622
|
+
...reported.keys(),
|
|
1623
|
+
]);
|
|
1624
|
+
return [...sources].sort().map((source) => {
|
|
1625
|
+
const declared = reported.get(source);
|
|
1626
|
+
const fullCount = fullBySource.get(source) ?? 0;
|
|
1627
|
+
const admittedCandidateCount = retainedBySource.get(source) ?? 0;
|
|
1628
|
+
return {
|
|
1629
|
+
version: 2,
|
|
1630
|
+
source,
|
|
1631
|
+
status: declared?.status ?? (fullCount > 0 ? 'available' : 'empty'),
|
|
1632
|
+
admittedCandidateCount,
|
|
1633
|
+
excludedCandidateCount: Math.max(0, fullCount - admittedCandidateCount),
|
|
1634
|
+
// A human-readable source reason may contain customer-specific wording.
|
|
1635
|
+
// Persist only an allowlisted state code in the durable V2 receipt.
|
|
1636
|
+
reasonCodes: [declared ? `SOURCE_${declared.status.toUpperCase()}` : 'CANDIDATE_WORKSPACE'],
|
|
1637
|
+
};
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
function emptyState(mode, turnClass, request) {
|
|
1641
|
+
return createState(mode, turnClass, request, undefined);
|
|
1642
|
+
}
|
|
1643
|
+
function retrievalProjection(evidence) {
|
|
1644
|
+
if (!evidence)
|
|
1645
|
+
return undefined;
|
|
1646
|
+
return {
|
|
1647
|
+
snapshotId: evidence.snapshotId,
|
|
1648
|
+
sourceFingerprint: evidence.sourceFingerprint,
|
|
1649
|
+
continuityFingerprint: evidence.continuityFingerprint,
|
|
1650
|
+
candidateCount: Math.min(128, evidence.candidates.length),
|
|
1651
|
+
candidateIds: evidence.candidates.slice(0, 128).map(stableCandidateId),
|
|
1652
|
+
candidateTraceMetadata: evidence.candidates.slice(0, 128).flatMap((candidate) => {
|
|
1653
|
+
const roles = evidenceCandidateRoles(candidate);
|
|
1654
|
+
const role = roles[0];
|
|
1655
|
+
return role ? [{ candidateId: stableCandidateId(candidate), role, source: traceSourceForCandidate(candidate) }] : [];
|
|
1656
|
+
}),
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function sourceForCandidate(candidate) {
|
|
1660
|
+
if (candidate.kind === 'certified_block')
|
|
1661
|
+
return 'certified';
|
|
1662
|
+
if (candidate.kind === 'semantic_metric' || candidate.kind === 'semantic_member')
|
|
1663
|
+
return 'semantic';
|
|
1664
|
+
if (candidate.kind === 'dql_modeling')
|
|
1665
|
+
return 'governed_relational';
|
|
1666
|
+
if (candidate.kind === 'dbt_model' || candidate.kind === 'dbt_source')
|
|
1667
|
+
return 'dbt_manifest';
|
|
1668
|
+
if (candidate.kind === 'sql_column' || candidate.kind === 'sql_table')
|
|
1669
|
+
return 'runtime_schema';
|
|
1670
|
+
return 'business';
|
|
1671
|
+
}
|
|
1672
|
+
/** Existing trace readers have a narrower source vocabulary than V2 handles. */
|
|
1673
|
+
function traceSourceForCandidate(candidate) {
|
|
1674
|
+
const source = sourceForCandidate(candidate);
|
|
1675
|
+
return source === 'business' ? 'exploratory' : source;
|
|
1676
|
+
}
|
|
1677
|
+
function stableCandidateId(candidate) {
|
|
1678
|
+
return candidate.qualifiedId ?? candidate.id;
|
|
1679
|
+
}
|
|
1680
|
+
function fingerprint(value) {
|
|
1681
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
1682
|
+
}
|
|
1683
|
+
function classifyTurnV2(request) {
|
|
1684
|
+
if (request.requestedMode === 'research')
|
|
1685
|
+
return 'research';
|
|
1686
|
+
if (request.selectedEvidenceId || request.clarificationSourceQuestion)
|
|
1687
|
+
return 'clarification_response';
|
|
1688
|
+
// An AMBIGUOUS prior-result reference is still a prior-result turn. The host
|
|
1689
|
+
// resolves candidates it cannot choose between into this gap; classifying
|
|
1690
|
+
// the turn as fresh analytics would frame the clarification as a brand-new
|
|
1691
|
+
// question and lose the very result it is asking about.
|
|
1692
|
+
if (request.selectedResultBinding || request.priorResultMemberBinding || request.selectedResultBindingGap) {
|
|
1693
|
+
return 'prior_result';
|
|
1694
|
+
}
|
|
1695
|
+
const question = request.question.toLowerCase();
|
|
1696
|
+
// "What is revenue for each customer?" is a grouped analytical request,
|
|
1697
|
+
// not a metadata definition. Definition routing stays warehouse-free only
|
|
1698
|
+
// when the wording has no aggregate, grouping, ranking, or entity shape.
|
|
1699
|
+
const analyticalShape = /\b(top|bottom|highest|lowest|total|count|sum|average|avg|revenue|sales|orders?|customers?|products?|regions?|by|per|each|every|for each|group(?:ed)?|trend|compare|rank)\b/.test(question);
|
|
1700
|
+
if (/\b(what is|define|definition|meaning of|explain)\b/.test(question)
|
|
1701
|
+
&& /\b(metric|measure|dimension|model|block|revenue|customer)\b/.test(question)
|
|
1702
|
+
&& !analyticalShape)
|
|
1703
|
+
return 'definition';
|
|
1704
|
+
if (/\b(why|business context|background|how does|tell me about)\b/.test(question) && !/\b(top|by |revenue|count|sum|average|sales|customer|product|region)\b/.test(question))
|
|
1705
|
+
return 'business_context';
|
|
1706
|
+
if (/^(hi|hello|thanks|thank you|what can you do)\b/.test(question.trim()))
|
|
1707
|
+
return 'general';
|
|
1708
|
+
return 'analytics';
|
|
1709
|
+
}
|
|
1710
|
+
function isAskRequestV2(request) {
|
|
1711
|
+
return request.requestedMode === undefined
|
|
1712
|
+
|| request.requestedMode === 'auto'
|
|
1713
|
+
|| request.requestedMode === 'ask'
|
|
1714
|
+
|| request.requestedMode === 'research';
|
|
1715
|
+
}
|
|
1716
|
+
//# sourceMappingURL=ask-agent-runtime-v2.js.map
|