@duckcodeailabs/dql-agent 1.13.4 → 1.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-run-engine.d.ts +66 -3
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +441 -28
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +100 -32
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/aggregation-safety-proof.d.ts +41 -0
- package/dist/aggregation-safety-proof.d.ts.map +1 -0
- package/dist/aggregation-safety-proof.js +511 -0
- package/dist/aggregation-safety-proof.js.map +1 -0
- package/dist/analytical-compatibility.d.ts.map +1 -1
- package/dist/analytical-compatibility.js +42 -25
- package/dist/analytical-compatibility.js.map +1 -1
- package/dist/analytical-execution-graph.d.ts +4 -21
- package/dist/analytical-execution-graph.d.ts.map +1 -1
- package/dist/analytical-execution-graph.js +27 -0
- package/dist/analytical-execution-graph.js.map +1 -1
- package/dist/analytical-frame.d.ts +12 -1
- package/dist/analytical-frame.d.ts.map +1 -1
- package/dist/analytical-frame.js +102 -2
- package/dist/analytical-frame.js.map +1 -1
- package/dist/analytical-result-facts.d.ts +53 -0
- package/dist/analytical-result-facts.d.ts.map +1 -1
- package/dist/analytical-result-facts.js +144 -0
- package/dist/analytical-result-facts.js.map +1 -1
- package/dist/answer-loop.d.ts +40 -3
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +681 -37
- package/dist/answer-loop.js.map +1 -1
- package/dist/cascade/budgets.js +1 -1
- package/dist/cascade/route-policy.d.ts +1 -1
- package/dist/cascade/route-policy.d.ts.map +1 -1
- package/dist/cascade/route-policy.js +2 -0
- package/dist/cascade/route-policy.js.map +1 -1
- package/dist/generated-analytical-proposal.d.ts +37 -0
- package/dist/generated-analytical-proposal.d.ts.map +1 -0
- package/dist/generated-analytical-proposal.js +219 -0
- package/dist/generated-analytical-proposal.js.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +8 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.js +102 -6
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +23 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +61 -8
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +46 -2
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +55 -2
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +323 -36
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/metadata/sql-context-validation.d.ts +3 -0
- package/dist/metadata/sql-context-validation.d.ts.map +1 -1
- package/dist/metadata/sql-context-validation.js +16 -5
- package/dist/metadata/sql-context-validation.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 +81 -32
- package/dist/plan-execution-adapter.js.map +1 -1
- package/dist/provider-egress.d.ts +122 -0
- package/dist/provider-egress.d.ts.map +1 -0
- package/dist/provider-egress.js +504 -0
- package/dist/provider-egress.js.map +1 -0
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +76 -33
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/dispatch.d.ts +15 -0
- package/dist/providers/dispatch.d.ts.map +1 -0
- package/dist/providers/dispatch.js +25 -0
- package/dist/providers/dispatch.js.map +1 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +16 -8
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +29 -10
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +92 -36
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +43 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/relationship-proof.d.ts +19 -0
- package/dist/relationship-proof.d.ts.map +1 -0
- package/dist/relationship-proof.js +75 -0
- package/dist/relationship-proof.js.map +1 -0
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +17 -11
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +25 -3
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +369 -27
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +30 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +705 -44
- package/dist/router.js.map +1 -1
- package/dist/semantic-identities.d.ts +18 -0
- package/dist/semantic-identities.d.ts.map +1 -0
- package/dist/semantic-identities.js +60 -0
- package/dist/semantic-identities.js.map +1 -0
- package/dist/semantic-proof-authority.d.ts +29 -0
- package/dist/semantic-proof-authority.d.ts.map +1 -0
- package/dist/semantic-proof-authority.js +34 -0
- package/dist/semantic-proof-authority.js.map +1 -0
- package/package.json +4 -4
package/dist/agent-run-engine.js
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { normalizeProviderEgressReceiptV1, } from '@duckcodeailabs/dql-core';
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { classifyConversationalTurn, decideAgentAction, } from "./intent-controller.js";
|
|
5
|
+
import { classifyConversationalTurn, decideAgentAction, looksLikeComposeApp, } from "./intent-controller.js";
|
|
5
6
|
import { selectCascadeRunRoute } from "./cascade/route-policy.js";
|
|
6
7
|
import { canUseEngineEscalation, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, recordEngineEscalation, recordLaneRepair, } from "./cascade/budgets.js";
|
|
8
|
+
import { buildAnalysisQuestionPlan } from "./metadata/analysis-planner.js";
|
|
7
9
|
import { conversationHistoryFromContext, isLikelyClarificationReply, } from "./conversation/snapshot.js";
|
|
8
10
|
/** Routes a stakeholder may never land on (analyst authoring lives in the Notebook). */
|
|
9
11
|
const ANALYST_ONLY_ROUTES = new Set(["sql_cell", "dql_block_draft", "modeling_draft", "skill_draft"]);
|
|
10
12
|
/**
|
|
11
|
-
* Compare the pre-execution plan with the
|
|
12
|
-
*
|
|
13
|
+
* Compare the pre-execution plan with the selected route without changing it.
|
|
14
|
+
* Authoritative plans never emit or persist this migration-only diagnostic.
|
|
13
15
|
* Acceptance: AGT-013, API-006.
|
|
14
16
|
*/
|
|
15
17
|
export function compareResolvedPlanShadow(decision, actualRoute) {
|
|
16
18
|
const plan = decision.resolvedAnalyticalPlan;
|
|
17
|
-
if (!plan)
|
|
19
|
+
if (!plan || plan.mode !== 'shadow')
|
|
18
20
|
return undefined;
|
|
19
21
|
const plannedRoute = decision.action === 'investigate'
|
|
20
22
|
? 'research'
|
|
@@ -36,6 +38,58 @@ export function compareResolvedPlanShadow(decision, actualRoute) {
|
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
const DEFAULT_MAX_STEPS = 4;
|
|
41
|
+
export function agentRouteDeadlineMs(route) {
|
|
42
|
+
if (route === 'certified_answer' || route === 'semantic_answer')
|
|
43
|
+
return 5_000;
|
|
44
|
+
if (route === 'clarify')
|
|
45
|
+
return 10_000;
|
|
46
|
+
// Generation may now take a genuine tool round (look something up, then use
|
|
47
|
+
// it) instead of a single blind shot, so the discovery window has to cover it.
|
|
48
|
+
// Subscription-CLI providers cost roughly 10-15s per dispatch, so a window
|
|
49
|
+
// under ~30s silently reduces the loop back to a single blind attempt.
|
|
50
|
+
if (route === 'generated_answer')
|
|
51
|
+
return 30_000;
|
|
52
|
+
if (route === 'research')
|
|
53
|
+
return 120_000;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/** The request envelope starts before retrieval/routing, so a stuck router can
|
|
57
|
+
* never evade the route-specific deadline that is selected later. */
|
|
58
|
+
export function agentRequestDeadlineMs(requestedMode) {
|
|
59
|
+
return requestedMode === 'research' ? 120_000 : 45_000;
|
|
60
|
+
}
|
|
61
|
+
/** Create the one request-ingress deadline authority used by every stage. */
|
|
62
|
+
export function createAgentRunBudget(input) {
|
|
63
|
+
const nowMs = input.nowMs ?? Date.now;
|
|
64
|
+
const startedAtMs = input.startedAtMs ?? nowMs();
|
|
65
|
+
const mode = input.requestedMode === 'research' ? 'research' : 'ask';
|
|
66
|
+
const hardDeadlineMs = mode === 'research' ? 120_000 : 45_000;
|
|
67
|
+
const timeout = (input.timeoutSignal ?? AbortSignal.timeout)(hardDeadlineMs);
|
|
68
|
+
const hardSignal = input.inheritedSignal
|
|
69
|
+
? AbortSignal.any([input.inheritedSignal, timeout])
|
|
70
|
+
: timeout;
|
|
71
|
+
const elapsedMs = () => Math.max(0, nowMs() - startedAtMs);
|
|
72
|
+
const softTargetMs = (route) => {
|
|
73
|
+
if (mode === 'research')
|
|
74
|
+
return 90_000;
|
|
75
|
+
return agentRouteDeadlineMs(route) ?? 15_000;
|
|
76
|
+
};
|
|
77
|
+
// Narration must still be reachable after a full generation window, and must
|
|
78
|
+
// leave the hard deadline (45s ask / 120s research) room to land.
|
|
79
|
+
const narrationSoftTargetMs = () => (mode === 'research' ? 100_000 : 38_000);
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
startedAtMs,
|
|
82
|
+
hardDeadlineMs,
|
|
83
|
+
hardSignal,
|
|
84
|
+
mode,
|
|
85
|
+
elapsedMs,
|
|
86
|
+
remainingMs: () => Math.max(0, hardDeadlineMs - elapsedMs()),
|
|
87
|
+
softTargetMs,
|
|
88
|
+
mayStartDiscovery: (route) => !hardSignal.aborted && elapsedMs() < softTargetMs(route),
|
|
89
|
+
narrationSoftTargetMs,
|
|
90
|
+
mayStartNarration: () => !hardSignal.aborted && elapsedMs() < narrationSoftTargetMs(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
39
93
|
/**
|
|
40
94
|
* Routes whose gate failure is better answered by switching routes than by
|
|
41
95
|
* re-running the same executor (repair can't add what the route can't produce).
|
|
@@ -250,7 +304,7 @@ export function escalationRouteFor(route, audience) {
|
|
|
250
304
|
if (!target)
|
|
251
305
|
return undefined;
|
|
252
306
|
if (audience === "stakeholder" && ANALYST_ONLY_ROUTES.has(target))
|
|
253
|
-
return "
|
|
307
|
+
return "generated_answer";
|
|
254
308
|
return target;
|
|
255
309
|
}
|
|
256
310
|
/** The handoff action shown on a stakeholder's review-required output. */
|
|
@@ -369,6 +423,7 @@ export class AgentRunEngine {
|
|
|
369
423
|
now;
|
|
370
424
|
budgetModel;
|
|
371
425
|
maxSteps;
|
|
426
|
+
routeTimeoutSignal;
|
|
372
427
|
constructor(options = {}) {
|
|
373
428
|
this.executors = options.executors ?? {};
|
|
374
429
|
this.gates = options.gates ?? {};
|
|
@@ -386,6 +441,7 @@ export class AgentRunEngine {
|
|
|
386
441
|
engineEscalations: options.maxEngineEscalations ?? options.budgets?.engineEscalations,
|
|
387
442
|
};
|
|
388
443
|
this.maxSteps = Math.max(1, options.maxSteps ?? DEFAULT_MAX_STEPS);
|
|
444
|
+
this.routeTimeoutSignal = options.routeTimeoutSignal ?? ((durationMs) => AbortSignal.timeout(durationMs));
|
|
389
445
|
}
|
|
390
446
|
/**
|
|
391
447
|
* Decide the high-level action. A forced `requestedMode` bypasses routing.
|
|
@@ -406,7 +462,8 @@ export class AgentRunEngine {
|
|
|
406
462
|
if (request.requestedMode === "ask"
|
|
407
463
|
&& routed.action !== "converse"
|
|
408
464
|
&& routed.action !== "compose_app"
|
|
409
|
-
&& routed.requiresClarification !== true
|
|
465
|
+
&& routed.requiresClarification !== true
|
|
466
|
+
&& !routed.terminalOutcome) {
|
|
410
467
|
return { ...routed, action: "answer" };
|
|
411
468
|
}
|
|
412
469
|
return routed;
|
|
@@ -440,7 +497,16 @@ export class AgentRunEngine {
|
|
|
440
497
|
}
|
|
441
498
|
const runId = request.runId ?? this.idGenerator();
|
|
442
499
|
const startedAt = this.timestamp();
|
|
500
|
+
const runStartedAtMs = Date.parse(startedAt);
|
|
443
501
|
const requestedMode = request.requestedMode ?? "auto";
|
|
502
|
+
const runBudget = request.runBudget ?? createAgentRunBudget({
|
|
503
|
+
requestedMode,
|
|
504
|
+
startedAtMs: runStartedAtMs,
|
|
505
|
+
inheritedSignal: request.signal,
|
|
506
|
+
timeoutSignal: this.routeTimeoutSignal,
|
|
507
|
+
nowMs: () => this.now().getTime(),
|
|
508
|
+
});
|
|
509
|
+
request = { ...request, runBudget, signal: runBudget.hardSignal };
|
|
444
510
|
const events = [];
|
|
445
511
|
let plan;
|
|
446
512
|
const executedSteps = [];
|
|
@@ -550,15 +616,22 @@ export class AgentRunEngine {
|
|
|
550
616
|
followsUp: true,
|
|
551
617
|
source: "heuristic",
|
|
552
618
|
}
|
|
553
|
-
: await this.decideRoute(request);
|
|
619
|
+
: await awaitWithAbort(this.decideRoute(request), request.signal);
|
|
620
|
+
routeDecision = enforceOrdinaryAnalyticalPlanBoundary(request, routeDecision);
|
|
554
621
|
const defaultRoute = answerAnywayRoute(constrainRouteForAudience(selectRoute(request, routeDecision), audience), request, audience, routeDecision);
|
|
555
|
-
|
|
622
|
+
const authoritativeAsk = routeDecision.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
623
|
+
&& requestedMode !== 'research';
|
|
624
|
+
const activePlanner = authoritativeAsk
|
|
625
|
+
? createDeterministicAgentRunPlanner()
|
|
626
|
+
: this.planner;
|
|
627
|
+
const planningSignal = request.runBudget?.hardSignal ?? request.signal;
|
|
628
|
+
plan = await awaitWithAbort(Promise.resolve(activePlanner.plan({
|
|
556
629
|
request,
|
|
557
630
|
routeDecision,
|
|
558
631
|
defaultRoute,
|
|
559
632
|
maxSteps: this.maxSteps,
|
|
560
633
|
audience,
|
|
561
|
-
});
|
|
634
|
+
})), planningSignal);
|
|
562
635
|
emit({
|
|
563
636
|
type: "plan.created",
|
|
564
637
|
message: plan.rationale,
|
|
@@ -662,6 +735,30 @@ export class AgentRunEngine {
|
|
|
662
735
|
stepStatus = attempt > 0 ? "repaired" : "passed";
|
|
663
736
|
break;
|
|
664
737
|
}
|
|
738
|
+
// A frozen analytical plan has one route and no downstream planner,
|
|
739
|
+
// rematch, route escalation, or whole-answer regeneration authority.
|
|
740
|
+
// Typed server-issued repair is a separate derived run.
|
|
741
|
+
if (authoritativeAsk) {
|
|
742
|
+
stepStatus = 'needs_review';
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
// Ordinary generated Ask freezes one analytical attempt. A failed
|
|
746
|
+
// attempt is terminal review/manual-action work; it cannot hand
|
|
747
|
+
// authority back to an injected LLM replanner for another physical
|
|
748
|
+
// provider send. Explicit Research and authoring modes retain their
|
|
749
|
+
// separately budgeted planner behavior.
|
|
750
|
+
const ordinaryGeneratedAsk = route === 'generated_answer'
|
|
751
|
+
&& (requestedMode === 'ask' || requestedMode === 'auto');
|
|
752
|
+
if (ordinaryGeneratedAsk) {
|
|
753
|
+
stepStatus = 'needs_review';
|
|
754
|
+
emit({
|
|
755
|
+
type: 'replan.decided',
|
|
756
|
+
message: 'Ordinary Ask kept the frozen analytical attempt and stopped for review without replanning.',
|
|
757
|
+
route,
|
|
758
|
+
payload: { decision: 'accept', authority: 'deterministic_fail_closed' },
|
|
759
|
+
});
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
665
762
|
const currentStep = {
|
|
666
763
|
id: stepId,
|
|
667
764
|
index: stepCount,
|
|
@@ -674,7 +771,7 @@ export class AgentRunEngine {
|
|
|
674
771
|
evaluations,
|
|
675
772
|
artifacts: result.artifacts ?? [],
|
|
676
773
|
};
|
|
677
|
-
const decision = await
|
|
774
|
+
const decision = await activePlanner.replan({
|
|
678
775
|
request,
|
|
679
776
|
plan,
|
|
680
777
|
currentStep,
|
|
@@ -779,7 +876,7 @@ export class AgentRunEngine {
|
|
|
779
876
|
});
|
|
780
877
|
continue;
|
|
781
878
|
}
|
|
782
|
-
const outcome = computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion);
|
|
879
|
+
const outcome = computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion, routeDecision.terminalOutcome?.message);
|
|
783
880
|
const step = {
|
|
784
881
|
id: stepId,
|
|
785
882
|
index: stepCount,
|
|
@@ -862,15 +959,19 @@ export class AgentRunEngine {
|
|
|
862
959
|
run.completedAt = this.timestamp();
|
|
863
960
|
run.lifecycle = terminalLifecycle(progress.lifecycle, run.status === "blocked" ? "run.failed" : "run.completed", run.completedAt, events.length);
|
|
864
961
|
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
865
|
-
run.
|
|
962
|
+
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
963
|
+
run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2);
|
|
866
964
|
await checkpointQueue;
|
|
867
965
|
await this.store?.save(run);
|
|
868
966
|
return run;
|
|
869
967
|
}
|
|
870
968
|
catch (err) {
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
969
|
+
const dispatchEvidence = providerDispatchEvidenceFromError(err);
|
|
970
|
+
const message = isOrchestrationBudgetExhausted(err)
|
|
971
|
+
? 'Ask could not complete within its bounded orchestration. Nothing was executed; narrow the metric or dimension and retry.'
|
|
972
|
+
: err instanceof Error && err.name === "TimeoutError"
|
|
973
|
+
? "This analytical run reached its time limit before it finished. A timeout alone does not prove a cross-model join or semantic-modeling problem. Open Trust & Steps to see the last recorded phase; retry the same bounded question or use Research for a longer budget. No result was accepted."
|
|
974
|
+
: err instanceof Error ? err.message : String(err);
|
|
874
975
|
const failedRoute = progress.route;
|
|
875
976
|
const failedPhase = progress.lifecycle.phase;
|
|
876
977
|
emit({
|
|
@@ -931,8 +1032,21 @@ export class AgentRunEngine {
|
|
|
931
1032
|
escalationAttempts: 0,
|
|
932
1033
|
budgetUsage: cascadeBudgetTrace(createCascadeBudgetState(this.budgetModel)),
|
|
933
1034
|
diagnosticReceipt: receipt,
|
|
1035
|
+
...(dispatchEvidence.providerEgressReceipts.length
|
|
1036
|
+
? { providerEgressReceipts: dispatchEvidence.providerEgressReceipts }
|
|
1037
|
+
: {}),
|
|
1038
|
+
telemetry: {
|
|
1039
|
+
...emptyRunTelemetry(durationBetweenMs(startedAt, completedAt), dispatchEvidence.fallbackReason),
|
|
1040
|
+
providerRoundTrips: dispatchEvidence.providerRoundTrips,
|
|
1041
|
+
toolCalls: dispatchEvidence.toolCalls,
|
|
1042
|
+
sqlExecutions: dispatchEvidence.sqlExecutions,
|
|
1043
|
+
repairs: dispatchEvidence.repairs,
|
|
1044
|
+
egressReceipts: dispatchEvidence.providerEgressReceipts.length,
|
|
1045
|
+
},
|
|
934
1046
|
lifecycle: terminalLifecycle(progress.lifecycle, "run.failed", completedAt, events.length),
|
|
935
1047
|
};
|
|
1048
|
+
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1049
|
+
run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2);
|
|
936
1050
|
await checkpointQueue;
|
|
937
1051
|
await this.store?.save(run);
|
|
938
1052
|
return run;
|
|
@@ -985,8 +1099,12 @@ export class AgentRunEngine {
|
|
|
985
1099
|
// If the final step produced no user-facing answer (e.g. it only drafted an
|
|
986
1100
|
// artifact), fall back to the last step that DID answer so the run never
|
|
987
1101
|
// drops a data answer an earlier step already computed.
|
|
988
|
-
const finalHasAnswer =
|
|
1102
|
+
const finalHasAnswer = finalOutcome.status !== "blocked"
|
|
1103
|
+
&& typeof finalResult.answer === "string" && finalResult.answer.trim().length > 0;
|
|
989
1104
|
const answerSource = finalHasAnswer ? finalResult : (input.bestAnswerResult ?? finalResult);
|
|
1105
|
+
const acceptedAnswer = finalOutcome.status === "blocked"
|
|
1106
|
+
? finalOutcome.summary
|
|
1107
|
+
: input.clarifyOutcome?.question ?? answerSource.answer;
|
|
990
1108
|
return {
|
|
991
1109
|
id: input.runId,
|
|
992
1110
|
question: input.request.question,
|
|
@@ -1003,7 +1121,7 @@ export class AgentRunEngine {
|
|
|
1003
1121
|
plan: input.plan,
|
|
1004
1122
|
steps: input.steps,
|
|
1005
1123
|
summary: finalOutcome.summary,
|
|
1006
|
-
answer:
|
|
1124
|
+
answer: acceptedAnswer,
|
|
1007
1125
|
answerKind: answerSource.answerKind ?? "governed",
|
|
1008
1126
|
artifacts,
|
|
1009
1127
|
evaluations: finalStep.evaluations,
|
|
@@ -1014,6 +1132,12 @@ export class AgentRunEngine {
|
|
|
1014
1132
|
? { clarificationOptions: finalResult.clarificationOptions ?? input.routeDecision.clarificationOptions }
|
|
1015
1133
|
: {}),
|
|
1016
1134
|
repairAttempts: finalResult.repairAttempts ?? repairAttempts,
|
|
1135
|
+
...(finalResult.providerEgressReceipts?.length
|
|
1136
|
+
? { providerEgressReceipts: finalResult.providerEgressReceipts }
|
|
1137
|
+
: {}),
|
|
1138
|
+
...(finalResult.telemetry ? {
|
|
1139
|
+
telemetry: withTotalDuration(finalResult.telemetry, durationBetweenMs(input.startedAt, completedAt)),
|
|
1140
|
+
} : {}),
|
|
1017
1141
|
escalationAttempts,
|
|
1018
1142
|
budgetUsage: input.budgetUsage,
|
|
1019
1143
|
...authoringDerivationFromRequest(input.request),
|
|
@@ -1028,14 +1152,209 @@ export class AgentRunEngine {
|
|
|
1028
1152
|
}
|
|
1029
1153
|
async executeRoute(context) {
|
|
1030
1154
|
const executor = this.executors[context.route];
|
|
1031
|
-
if (executor)
|
|
1032
|
-
|
|
1155
|
+
if (executor) {
|
|
1156
|
+
const frozenPlan = context.routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative';
|
|
1157
|
+
const startsAnalyticalWork = context.route === 'certified_answer'
|
|
1158
|
+
|| context.route === 'semantic_answer'
|
|
1159
|
+
|| context.route === 'generated_answer'
|
|
1160
|
+
|| context.route === 'research';
|
|
1161
|
+
if (startsAnalyticalWork
|
|
1162
|
+
&& !frozenPlan
|
|
1163
|
+
&& context.request.runBudget
|
|
1164
|
+
&& !context.request.runBudget.mayStartDiscovery(context.route)) {
|
|
1165
|
+
return softBoundaryResult(context.route, context.request.runBudget);
|
|
1166
|
+
}
|
|
1167
|
+
const signal = context.request.runBudget?.hardSignal ?? context.request.signal;
|
|
1168
|
+
if (signal?.aborted)
|
|
1169
|
+
throw signal.reason ?? routeTimeoutError();
|
|
1170
|
+
const execution = Promise.resolve(executor({
|
|
1171
|
+
...context,
|
|
1172
|
+
request: { ...context.request, ...(signal ? { signal } : {}) },
|
|
1173
|
+
}));
|
|
1174
|
+
return awaitWithAbort(execution, signal);
|
|
1175
|
+
}
|
|
1033
1176
|
return defaultExecutorResult(context.route, context.request, context.routeDecision);
|
|
1034
1177
|
}
|
|
1178
|
+
absoluteDeadlineSignal(deadlineMs, runStartedAtMs, inherited) {
|
|
1179
|
+
const elapsedMs = Math.max(0, this.now().getTime() - runStartedAtMs);
|
|
1180
|
+
const remainingMs = deadlineMs - elapsedMs;
|
|
1181
|
+
const deadlineSignal = remainingMs <= 0
|
|
1182
|
+
? alreadyAbortedSignal(routeTimeoutError())
|
|
1183
|
+
: this.routeTimeoutSignal(remainingMs);
|
|
1184
|
+
return inherited ? AbortSignal.any([inherited, deadlineSignal]) : deadlineSignal;
|
|
1185
|
+
}
|
|
1035
1186
|
timestamp() {
|
|
1036
1187
|
return this.now().toISOString();
|
|
1037
1188
|
}
|
|
1038
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Ordinary analytical Ask cannot delegate meaning to the legacy answer
|
|
1192
|
+
* generator. Retrieval/meaning must first produce the immutable RAP consumed by
|
|
1193
|
+
* every later compiler and executor. This host-side gate protects callers that
|
|
1194
|
+
* inject or deserialize an older router decision as well as the canonical
|
|
1195
|
+
* router producer.
|
|
1196
|
+
*/
|
|
1197
|
+
function enforceOrdinaryAnalyticalPlanBoundary(request, decision) {
|
|
1198
|
+
const ordinaryAsk = request.requestedMode === undefined
|
|
1199
|
+
|| request.requestedMode === 'auto'
|
|
1200
|
+
|| request.requestedMode === 'ask';
|
|
1201
|
+
const terminal = decision.action === 'clarify'
|
|
1202
|
+
|| decision.action === 'block'
|
|
1203
|
+
|| decision.requiresClarification === true
|
|
1204
|
+
|| Boolean(decision.terminalOutcome);
|
|
1205
|
+
const exactSemanticContinuation = Boolean(request.selectedEvidenceId
|
|
1206
|
+
&& decision.meaningResolution?.recommendedRoute === 'semantic'
|
|
1207
|
+
&& decision.meaningResolution.recommendedExecutionId === request.selectedEvidenceId);
|
|
1208
|
+
if (!ordinaryAsk
|
|
1209
|
+
|| terminal
|
|
1210
|
+
|| exactSemanticContinuation
|
|
1211
|
+
|| decision.resolvedAnalyticalPlan?.mode === 'authoritative')
|
|
1212
|
+
return decision;
|
|
1213
|
+
const explicitAuthoring = looksLikeComposeApp(request.question)
|
|
1214
|
+
|| /\b(sql\s+(?:notebook\s+)?cell|notebook\s+cell|write a select|generate a query|dql block|block draft|draft block|create[^.?!]*block|turn[^.?!]*into[^.?!]*block)\b/i.test(request.question);
|
|
1215
|
+
if (explicitAuthoring)
|
|
1216
|
+
return decision;
|
|
1217
|
+
const history = request.history?.length
|
|
1218
|
+
? request.history
|
|
1219
|
+
: conversationHistoryFromContext(request.conversationContext);
|
|
1220
|
+
const questionPlan = buildAnalysisQuestionPlan(request.question);
|
|
1221
|
+
const conversationalKind = classifyConversationalTurn(request.question, history.length > 0 || Boolean(request.conversationContext && Object.keys(request.conversationContext).length > 0));
|
|
1222
|
+
const definitionOnly = questionPlan.mode === 'definition'
|
|
1223
|
+
&& questionPlan.requestedShape.dimensions.length === 0
|
|
1224
|
+
&& questionPlan.requestedShape.filters.length === 0
|
|
1225
|
+
&& questionPlan.timeTerms.length === 0;
|
|
1226
|
+
// Router category/action are advisory and may be absent or forged. A
|
|
1227
|
+
// deterministic host parse owns the no-data lanes too, so a forged
|
|
1228
|
+
// `data_analysis` cannot turn "hi" into SQL and a forged `answer` cannot
|
|
1229
|
+
// turn a glossary definition into an analytical execution.
|
|
1230
|
+
if (ordinaryAsk && (conversationalKind || definitionOnly)) {
|
|
1231
|
+
return {
|
|
1232
|
+
...decision,
|
|
1233
|
+
action: 'converse',
|
|
1234
|
+
category: 'conversational',
|
|
1235
|
+
...(conversationalKind ? { conversationalKind } : {}),
|
|
1236
|
+
confidence: 1,
|
|
1237
|
+
reason: definitionOnly
|
|
1238
|
+
? 'This requests a definition, not a warehouse result, so no analytical execution is needed.'
|
|
1239
|
+
: 'This is a conversational turn and does not request governed data.',
|
|
1240
|
+
requiresClarification: false,
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
const analyticalIntents = new Set([
|
|
1244
|
+
'exact_certified_lookup',
|
|
1245
|
+
'ad_hoc_ranking',
|
|
1246
|
+
'driver_breakdown',
|
|
1247
|
+
'diagnose_change',
|
|
1248
|
+
'segment_compare',
|
|
1249
|
+
'entity_drilldown',
|
|
1250
|
+
'anomaly_investigation',
|
|
1251
|
+
]);
|
|
1252
|
+
const analyticalIntent = analyticalIntents.has(questionPlan.routeIntent)
|
|
1253
|
+
|| Boolean(request.intent && analyticalIntents.has(request.intent));
|
|
1254
|
+
const analyticalShape = questionPlan.mode !== 'clarify'
|
|
1255
|
+
&& questionPlan.mode !== 'definition'
|
|
1256
|
+
&& (questionPlan.requestedShape.measures.length > 0
|
|
1257
|
+
|| questionPlan.requestedShape.dimensions.length > 0
|
|
1258
|
+
|| questionPlan.requestedShape.filters.length > 0
|
|
1259
|
+
|| questionPlan.timeTerms.length > 0
|
|
1260
|
+
|| questionPlan.requestedShape.rankingDirection !== undefined);
|
|
1261
|
+
const governedEvidence = (decision.retrievalEvidence?.candidateCount ?? 0) > 0
|
|
1262
|
+
|| (decision.meaningResolution?.selectedConceptIds.length ?? 0) > 0;
|
|
1263
|
+
const analytical = analyticalIntent || analyticalShape || governedEvidence;
|
|
1264
|
+
if (!analytical)
|
|
1265
|
+
return decision;
|
|
1266
|
+
const message = 'DQL could not freeze an exact analytical plan for the requested metric, grain, filters, ordering, and outputs. Choose a governed identifier or model the missing capability before retrying.';
|
|
1267
|
+
return {
|
|
1268
|
+
...decision,
|
|
1269
|
+
action: 'block',
|
|
1270
|
+
confidence: 1,
|
|
1271
|
+
reason: message,
|
|
1272
|
+
requiresClarification: false,
|
|
1273
|
+
terminalOutcome: {
|
|
1274
|
+
kind: 'modeling_gap',
|
|
1275
|
+
code: 'ANALYTICAL_MODELING_GAP',
|
|
1276
|
+
message,
|
|
1277
|
+
candidateIds: decision.retrievalEvidence?.candidateIds ?? [],
|
|
1278
|
+
},
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
function softBoundaryResult(route, budget) {
|
|
1282
|
+
const seconds = Math.round(budget.softTargetMs(route) / 1_000);
|
|
1283
|
+
return {
|
|
1284
|
+
resolvedRoute: 'clarify',
|
|
1285
|
+
status: 'needs_clarification',
|
|
1286
|
+
trustState: 'blocked',
|
|
1287
|
+
stopReason: 'needs_clarification',
|
|
1288
|
+
summary: `The ${seconds}-second discovery target elapsed before an analytical plan was frozen. No new provider, tool, or retrieval branch was started.`,
|
|
1289
|
+
answer: budget.mode === 'research'
|
|
1290
|
+
? 'Research stopped starting new branches at 90 seconds. Refine the question or retry; any already validated partial findings remain available.'
|
|
1291
|
+
: 'The discovery window ended before DQL could freeze an exact analytical plan. Refine the metric or grain, or retry the same bounded question.',
|
|
1292
|
+
artifacts: [],
|
|
1293
|
+
evaluations: [],
|
|
1294
|
+
nextActions: [{ id: 'retry-after-soft-target', label: 'Retry the same question' }],
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function providerDispatchEvidenceFromError(error) {
|
|
1298
|
+
const empty = {
|
|
1299
|
+
providerEgressReceipts: [],
|
|
1300
|
+
providerRoundTrips: 0,
|
|
1301
|
+
toolCalls: 0,
|
|
1302
|
+
sqlExecutions: 0,
|
|
1303
|
+
repairs: 0,
|
|
1304
|
+
fallbackReason: 'executor_failure',
|
|
1305
|
+
};
|
|
1306
|
+
if (!error || typeof error !== 'object')
|
|
1307
|
+
return empty;
|
|
1308
|
+
const value = error.providerDispatchEvidence;
|
|
1309
|
+
if (!value || typeof value !== 'object')
|
|
1310
|
+
return empty;
|
|
1311
|
+
const record = value;
|
|
1312
|
+
const count = (key) => Number.isInteger(record[key]) && Number(record[key]) >= 0 ? Number(record[key]) : 0;
|
|
1313
|
+
return {
|
|
1314
|
+
providerEgressReceipts: Array.isArray(record.providerEgressReceipts)
|
|
1315
|
+
? record.providerEgressReceipts.flatMap((receipt) => {
|
|
1316
|
+
const normalized = normalizeProviderEgressReceiptV1(receipt);
|
|
1317
|
+
return normalized ? [normalized] : [];
|
|
1318
|
+
})
|
|
1319
|
+
: [],
|
|
1320
|
+
providerRoundTrips: count('providerRoundTrips'),
|
|
1321
|
+
toolCalls: count('toolCalls'),
|
|
1322
|
+
sqlExecutions: count('sqlExecutions'),
|
|
1323
|
+
repairs: count('repairs'),
|
|
1324
|
+
fallbackReason: typeof record.fallbackReason === 'string' ? record.fallbackReason : 'executor_failure',
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function alreadyAbortedSignal(reason) {
|
|
1328
|
+
const controller = new AbortController();
|
|
1329
|
+
controller.abort(reason);
|
|
1330
|
+
return controller.signal;
|
|
1331
|
+
}
|
|
1332
|
+
function routeTimeoutError() {
|
|
1333
|
+
return new DOMException('The absolute agent route deadline elapsed.', 'TimeoutError');
|
|
1334
|
+
}
|
|
1335
|
+
function awaitWithAbort(work, signal) {
|
|
1336
|
+
if (!signal)
|
|
1337
|
+
return work;
|
|
1338
|
+
if (signal.aborted) {
|
|
1339
|
+
// The callee may already have started before its cancellation state was
|
|
1340
|
+
// observed. Consume any eventual rejection while refusing its result.
|
|
1341
|
+
void work.catch(() => undefined);
|
|
1342
|
+
return Promise.reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
|
|
1343
|
+
}
|
|
1344
|
+
return new Promise((resolve, reject) => {
|
|
1345
|
+
let settled = false;
|
|
1346
|
+
const finish = (callback) => {
|
|
1347
|
+
if (settled)
|
|
1348
|
+
return;
|
|
1349
|
+
settled = true;
|
|
1350
|
+
signal.removeEventListener('abort', onAbort);
|
|
1351
|
+
callback();
|
|
1352
|
+
};
|
|
1353
|
+
const onAbort = () => finish(() => reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')));
|
|
1354
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1355
|
+
work.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1039
1358
|
function authoringDerivationFromRequest(request) {
|
|
1040
1359
|
if (request.requestedMode !== 'modeling' && request.requestedMode !== 'skill')
|
|
1041
1360
|
return {};
|
|
@@ -1073,6 +1392,15 @@ function diagnosticFailureFromError(error, phase) {
|
|
|
1073
1392
|
const name = error instanceof Error ? error.name : "";
|
|
1074
1393
|
const message = error instanceof Error ? error.message : String(error);
|
|
1075
1394
|
const lower = `${name} ${message}`.toLowerCase();
|
|
1395
|
+
if (isOrchestrationBudgetExhausted(error)) {
|
|
1396
|
+
return {
|
|
1397
|
+
code: 'orchestration_budget_exhausted',
|
|
1398
|
+
phase,
|
|
1399
|
+
message: 'Ask exhausted its bounded orchestration before a final answer was available.',
|
|
1400
|
+
recoverable: false,
|
|
1401
|
+
safeActions: ['inspect_failure'],
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1076
1404
|
if (name === "TimeoutError" || lower.includes("time limit") || lower.includes("timeout")) {
|
|
1077
1405
|
return {
|
|
1078
1406
|
code: "TIMEOUT",
|
|
@@ -1099,6 +1427,14 @@ function diagnosticFailureFromError(error, phase) {
|
|
|
1099
1427
|
safeActions: ["retry_same_request", "inspect_failure"],
|
|
1100
1428
|
};
|
|
1101
1429
|
}
|
|
1430
|
+
function isOrchestrationBudgetExhausted(error) {
|
|
1431
|
+
if (!error || typeof error !== 'object')
|
|
1432
|
+
return false;
|
|
1433
|
+
if (error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED')
|
|
1434
|
+
return true;
|
|
1435
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1436
|
+
return /(?:run-wide provider dispatch|orchestration) budget exhausted/i.test(message);
|
|
1437
|
+
}
|
|
1102
1438
|
function diagnosticReceiptForRun(run) {
|
|
1103
1439
|
const failureEvaluation = run.evaluations.find((evaluation) => !evaluation.passed && evaluation.severity === "blocking");
|
|
1104
1440
|
const analyticalFailure = run.artifacts
|
|
@@ -1127,6 +1463,8 @@ function diagnosticReceiptForRun(run) {
|
|
|
1127
1463
|
steps: run.steps,
|
|
1128
1464
|
artifacts: run.artifacts,
|
|
1129
1465
|
evaluations: run.evaluations,
|
|
1466
|
+
...(run.repairCapability ? { repairCapability: run.repairCapability } : {}),
|
|
1467
|
+
...(run.providerEgressReceipts?.length ? { providerEgressReceipts: run.providerEgressReceipts } : {}),
|
|
1130
1468
|
...(run.status === "blocked"
|
|
1131
1469
|
? {
|
|
1132
1470
|
failure: {
|
|
@@ -1140,7 +1478,44 @@ function diagnosticReceiptForRun(run) {
|
|
|
1140
1478
|
: {}),
|
|
1141
1479
|
};
|
|
1142
1480
|
}
|
|
1143
|
-
function
|
|
1481
|
+
function diagnosticReceiptV2ForRun(run) {
|
|
1482
|
+
const telemetry = run.telemetry ?? emptyRunTelemetry(durationBetweenMs(run.startedAt, run.completedAt), 'not_recorded');
|
|
1483
|
+
return {
|
|
1484
|
+
version: 2,
|
|
1485
|
+
runId: run.id,
|
|
1486
|
+
route: run.route,
|
|
1487
|
+
status: run.status,
|
|
1488
|
+
telemetry,
|
|
1489
|
+
providerEgressReceiptFingerprints: (run.providerEgressReceipts ?? []).map(receiptFingerprint),
|
|
1490
|
+
...(run.repairCapability ? { repairCapabilityFingerprint: receiptFingerprint(run.repairCapability) } : {}),
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
function emptyRunTelemetry(total, fallbackReason) {
|
|
1494
|
+
return {
|
|
1495
|
+
version: 1,
|
|
1496
|
+
stageDurationsMs: { total },
|
|
1497
|
+
providerRoundTrips: 0,
|
|
1498
|
+
toolCalls: 0,
|
|
1499
|
+
sqlExecutions: 0,
|
|
1500
|
+
repairs: 0,
|
|
1501
|
+
egressReceipts: 0,
|
|
1502
|
+
fallbackReason,
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
function withTotalDuration(telemetry, total) {
|
|
1506
|
+
return {
|
|
1507
|
+
...telemetry,
|
|
1508
|
+
stageDurationsMs: { ...telemetry.stageDurationsMs, total },
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
function durationBetweenMs(startedAt, completedAt) {
|
|
1512
|
+
const duration = Date.parse(completedAt) - Date.parse(startedAt);
|
|
1513
|
+
return Number.isFinite(duration) && duration >= 0 ? Math.min(86_400_000, duration) : 0;
|
|
1514
|
+
}
|
|
1515
|
+
function receiptFingerprint(value) {
|
|
1516
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
1517
|
+
}
|
|
1518
|
+
function attachDiagnosticReceipt(artifacts, receipt, receiptV2) {
|
|
1144
1519
|
if (artifacts.length === 0) {
|
|
1145
1520
|
if (!receipt.failure)
|
|
1146
1521
|
return artifacts;
|
|
@@ -1149,7 +1524,7 @@ function attachDiagnosticReceipt(artifacts, receipt) {
|
|
|
1149
1524
|
kind: "answer",
|
|
1150
1525
|
title: "Agent run diagnostics",
|
|
1151
1526
|
trustState: "blocked",
|
|
1152
|
-
payload: { diagnosticReceipt: receipt },
|
|
1527
|
+
payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}) },
|
|
1153
1528
|
}];
|
|
1154
1529
|
}
|
|
1155
1530
|
const preferredIndex = Math.max(0, artifacts.findIndex((artifact) => artifact.kind === "answer"));
|
|
@@ -1164,11 +1539,12 @@ function attachDiagnosticReceipt(artifacts, receipt) {
|
|
|
1164
1539
|
payload: {
|
|
1165
1540
|
...payload,
|
|
1166
1541
|
diagnosticReceipt: receipt,
|
|
1542
|
+
...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}),
|
|
1167
1543
|
},
|
|
1168
1544
|
};
|
|
1169
1545
|
});
|
|
1170
1546
|
}
|
|
1171
|
-
function computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion) {
|
|
1547
|
+
function computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion, terminalOutcomeMessage) {
|
|
1172
1548
|
const fallback = defaultOutcome(route);
|
|
1173
1549
|
if (isClarify) {
|
|
1174
1550
|
return {
|
|
@@ -1195,10 +1571,21 @@ function computeStepOutcome(route, result, evaluations, request, isClarify, clar
|
|
|
1195
1571
|
trustState,
|
|
1196
1572
|
artifacts,
|
|
1197
1573
|
stopReason,
|
|
1198
|
-
summary:
|
|
1574
|
+
summary: status === "blocked"
|
|
1575
|
+
? terminalOutcomeMessage ?? blockingOutcomeSummary(evaluations, fallback.summary)
|
|
1576
|
+
: result.summary ?? fallback.summary,
|
|
1199
1577
|
...(result.answerTier ? { terminalTier: result.answerTier } : {}),
|
|
1200
1578
|
};
|
|
1201
1579
|
}
|
|
1580
|
+
function blockingOutcomeSummary(evaluations, fallback) {
|
|
1581
|
+
const messages = evaluations
|
|
1582
|
+
.filter((evaluation) => !evaluation.passed && evaluation.severity === 'blocking')
|
|
1583
|
+
.map((evaluation) => evaluation.message.trim())
|
|
1584
|
+
.filter(Boolean);
|
|
1585
|
+
return messages[0] ?? (fallback.includes('Answered')
|
|
1586
|
+
? 'The analytical result did not pass its required validation and was not accepted.'
|
|
1587
|
+
: fallback);
|
|
1588
|
+
}
|
|
1202
1589
|
function consumeRepeatedClarificationSelection(request, routeDecision, result) {
|
|
1203
1590
|
const selectedEvidenceId = request.selectedEvidenceId;
|
|
1204
1591
|
if (!selectedEvidenceId || result.status !== 'needs_clarification')
|
|
@@ -1282,7 +1669,7 @@ export function createDeterministicAgentRunPlanner() {
|
|
|
1282
1669
|
// Honor an explicit escalate target, else the route's default — then clamp for the audience.
|
|
1283
1670
|
const rawEscalation = requested ?? AGENT_RUN_ESCALATION_MAP[currentStep.route];
|
|
1284
1671
|
const escalationRoute = rawEscalation
|
|
1285
|
-
? (audience === "stakeholder" && ANALYST_ONLY_ROUTES.has(rawEscalation) ? "
|
|
1672
|
+
? (audience === "stakeholder" && ANALYST_ONLY_ROUTES.has(rawEscalation) ? "generated_answer" : rawEscalation)
|
|
1286
1673
|
: undefined;
|
|
1287
1674
|
const hint = action?.hint ?? failing.suggestedRepair ?? "Revise and retry.";
|
|
1288
1675
|
if (action?.kind === "escalate" && escalationRoute) {
|
|
@@ -1376,6 +1763,19 @@ function requestedModeToAction(mode) {
|
|
|
1376
1763
|
export function selectRoute(request, decision) {
|
|
1377
1764
|
if (decision.meaningResolutionErrorCode === 'invalid_evidence_reference')
|
|
1378
1765
|
return 'blocked';
|
|
1766
|
+
if (decision.action === 'block')
|
|
1767
|
+
return 'blocked';
|
|
1768
|
+
const authoritativePlan = decision.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1769
|
+
? decision.resolvedAnalyticalPlan
|
|
1770
|
+
: undefined;
|
|
1771
|
+
// Producer invariant: the immutable plan is the authority. A caller cannot
|
|
1772
|
+
// combine an answer decision with blocked capability and suppress both the
|
|
1773
|
+
// typed clarification and modeling/policy diagnostic.
|
|
1774
|
+
if (authoritativePlan?.capability === 'blocked') {
|
|
1775
|
+
if (decision.requiresClarification === true)
|
|
1776
|
+
return 'clarify';
|
|
1777
|
+
return 'blocked';
|
|
1778
|
+
}
|
|
1379
1779
|
const explicitMode = request.requestedMode;
|
|
1380
1780
|
if (explicitMode === 'modeling')
|
|
1381
1781
|
return 'modeling_draft';
|
|
@@ -1384,9 +1784,21 @@ export function selectRoute(request, decision) {
|
|
|
1384
1784
|
if (explicitMode && explicitMode !== 'auto' && explicitMode !== 'ask') {
|
|
1385
1785
|
return selectCascadeRunRoute(request, decision);
|
|
1386
1786
|
}
|
|
1787
|
+
const plan = decision.resolvedAnalyticalPlan;
|
|
1788
|
+
// Research is an explicit request mode, never a phrase/category/depth
|
|
1789
|
+
// inference. Diagnosis and bounded exploration in ordinary Ask stay on the
|
|
1790
|
+
// review-required generated route over the same frozen plan.
|
|
1791
|
+
if (decision.action === 'investigate') {
|
|
1792
|
+
if (plan?.capability === 'certified_execution')
|
|
1793
|
+
return 'certified_answer';
|
|
1794
|
+
if (plan?.capability === 'semantic_execution')
|
|
1795
|
+
return 'semantic_answer';
|
|
1796
|
+
if (plan?.capability === 'governed_relational' || plan?.capability === 'bounded_exploration')
|
|
1797
|
+
return 'generated_answer';
|
|
1798
|
+
return decision.requiresClarification ? 'clarify' : 'blocked';
|
|
1799
|
+
}
|
|
1387
1800
|
if (decision.action !== 'answer')
|
|
1388
1801
|
return selectCascadeRunRoute(request, decision);
|
|
1389
|
-
const plan = decision.resolvedAnalyticalPlan;
|
|
1390
1802
|
if (plan?.mode === 'authoritative' && decision.requiresClarification !== true) {
|
|
1391
1803
|
if (plan.capability === 'certified_execution')
|
|
1392
1804
|
return 'certified_answer';
|
|
@@ -1395,7 +1807,7 @@ export function selectRoute(request, decision) {
|
|
|
1395
1807
|
if (plan.capability === 'governed_relational')
|
|
1396
1808
|
return 'generated_answer';
|
|
1397
1809
|
if (plan.capability === 'bounded_exploration')
|
|
1398
|
-
return '
|
|
1810
|
+
return 'generated_answer';
|
|
1399
1811
|
return 'blocked';
|
|
1400
1812
|
}
|
|
1401
1813
|
// Retrieval + meaning resolution already established a compatible execution
|
|
@@ -1412,8 +1824,9 @@ export function selectRoute(request, decision) {
|
|
|
1412
1824
|
}
|
|
1413
1825
|
function defaultExecutorResult(route, request, decision) {
|
|
1414
1826
|
const fallback = defaultOutcome(route);
|
|
1827
|
+
const terminalMessage = route === 'blocked' ? decision?.terminalOutcome?.message : undefined;
|
|
1415
1828
|
return {
|
|
1416
|
-
summary: fallback.summary,
|
|
1829
|
+
summary: terminalMessage ?? fallback.summary,
|
|
1417
1830
|
answer: route === "clarify" ? decision?.clarifyingQuestion : undefined,
|
|
1418
1831
|
evaluations: defaultEvaluations(route, request, decision),
|
|
1419
1832
|
artifacts: defaultArtifacts(route, {}, request),
|