@duckcodeailabs/dql-agent 1.13.4 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-run-engine.d.ts +87 -7
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +559 -40
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +42 -25
- package/dist/agent-run-store.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-orchestration.d.ts +256 -0
- package/dist/analytical-orchestration.d.ts.map +1 -0
- package/dist/analytical-orchestration.js +419 -0
- package/dist/analytical-orchestration.js.map +1 -0
- package/dist/analytical-result-facts.d.ts +54 -1
- package/dist/analytical-result-facts.d.ts.map +1 -1
- package/dist/analytical-result-facts.js +183 -0
- package/dist/analytical-result-facts.js.map +1 -1
- package/dist/answer-loop.d.ts +44 -3
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +771 -38
- 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/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +3 -0
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +6 -0
- package/dist/conversation/turn-trust.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 +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -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 +30 -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/catalog.d.ts +24 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +227 -42
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +408 -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 +460 -31
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +38 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +1060 -53
- 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'
|
|
@@ -35,7 +37,70 @@ export function compareResolvedPlanShadow(decision, actualRoute) {
|
|
|
35
37
|
matches: plannedRoute === actualRoute,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
40
|
+
/** Stable host-issued reason for a user cancellation. Provider AbortErrors do
|
|
41
|
+
* not carry this code and therefore remain distinguishable executor failures. */
|
|
42
|
+
export const AGENT_RUN_USER_CANCEL_CODE = "RUN_CANCELLED";
|
|
43
|
+
export function createAgentRunCancellationError() {
|
|
44
|
+
return Object.assign(new Error("Stopped by user."), { code: AGENT_RUN_USER_CANCEL_CODE });
|
|
45
|
+
}
|
|
46
|
+
export function isAgentRunUserCancellation(error) {
|
|
47
|
+
if (!error || typeof error !== "object")
|
|
48
|
+
return false;
|
|
49
|
+
return error.code === AGENT_RUN_USER_CANCEL_CODE;
|
|
50
|
+
}
|
|
38
51
|
const DEFAULT_MAX_STEPS = 4;
|
|
52
|
+
export function agentRouteDeadlineMs(route) {
|
|
53
|
+
if (route === 'certified_answer' || route === 'semantic_answer')
|
|
54
|
+
return 5_000;
|
|
55
|
+
if (route === 'clarify')
|
|
56
|
+
return 10_000;
|
|
57
|
+
// Generation may now take a genuine tool round (look something up, then use
|
|
58
|
+
// it) instead of a single blind shot, so the discovery window has to cover it.
|
|
59
|
+
// Subscription-CLI providers cost roughly 10-15s per dispatch, so a window
|
|
60
|
+
// under ~30s silently reduces the loop back to a single blind attempt.
|
|
61
|
+
if (route === 'generated_answer')
|
|
62
|
+
return 30_000;
|
|
63
|
+
if (route === 'research')
|
|
64
|
+
return 120_000;
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/** The request envelope starts before retrieval/routing, so a stuck router can
|
|
68
|
+
* never evade the route-specific deadline that is selected later. */
|
|
69
|
+
export function agentRequestDeadlineMs(requestedMode) {
|
|
70
|
+
return requestedMode === 'research' ? 120_000 : 45_000;
|
|
71
|
+
}
|
|
72
|
+
/** Create the one request-ingress deadline authority used by every stage. */
|
|
73
|
+
export function createAgentRunBudget(input) {
|
|
74
|
+
const nowMs = input.nowMs ?? Date.now;
|
|
75
|
+
const startedAtMs = input.startedAtMs ?? nowMs();
|
|
76
|
+
const mode = input.requestedMode === 'research' ? 'research' : 'ask';
|
|
77
|
+
const hardDeadlineMs = mode === 'research' ? 120_000 : 45_000;
|
|
78
|
+
const timeout = (input.timeoutSignal ?? AbortSignal.timeout)(hardDeadlineMs);
|
|
79
|
+
const hardSignal = input.inheritedSignal
|
|
80
|
+
? AbortSignal.any([input.inheritedSignal, timeout])
|
|
81
|
+
: timeout;
|
|
82
|
+
const elapsedMs = () => Math.max(0, nowMs() - startedAtMs);
|
|
83
|
+
const softTargetMs = (route) => {
|
|
84
|
+
if (mode === 'research')
|
|
85
|
+
return 90_000;
|
|
86
|
+
return agentRouteDeadlineMs(route) ?? 15_000;
|
|
87
|
+
};
|
|
88
|
+
// Narration must still be reachable after a full generation window, and must
|
|
89
|
+
// leave the hard deadline (45s ask / 120s research) room to land.
|
|
90
|
+
const narrationSoftTargetMs = () => (mode === 'research' ? 100_000 : 38_000);
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
startedAtMs,
|
|
93
|
+
hardDeadlineMs,
|
|
94
|
+
hardSignal,
|
|
95
|
+
mode,
|
|
96
|
+
elapsedMs,
|
|
97
|
+
remainingMs: () => Math.max(0, hardDeadlineMs - elapsedMs()),
|
|
98
|
+
softTargetMs,
|
|
99
|
+
mayStartDiscovery: (route) => !hardSignal.aborted && elapsedMs() < softTargetMs(route),
|
|
100
|
+
narrationSoftTargetMs,
|
|
101
|
+
mayStartNarration: () => !hardSignal.aborted && elapsedMs() < narrationSoftTargetMs(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
39
104
|
/**
|
|
40
105
|
* Routes whose gate failure is better answered by switching routes than by
|
|
41
106
|
* re-running the same executor (repair can't add what the route can't produce).
|
|
@@ -250,7 +315,7 @@ export function escalationRouteFor(route, audience) {
|
|
|
250
315
|
if (!target)
|
|
251
316
|
return undefined;
|
|
252
317
|
if (audience === "stakeholder" && ANALYST_ONLY_ROUTES.has(target))
|
|
253
|
-
return "
|
|
318
|
+
return "generated_answer";
|
|
254
319
|
return target;
|
|
255
320
|
}
|
|
256
321
|
/** The handoff action shown on a stakeholder's review-required output. */
|
|
@@ -369,6 +434,7 @@ export class AgentRunEngine {
|
|
|
369
434
|
now;
|
|
370
435
|
budgetModel;
|
|
371
436
|
maxSteps;
|
|
437
|
+
routeTimeoutSignal;
|
|
372
438
|
constructor(options = {}) {
|
|
373
439
|
this.executors = options.executors ?? {};
|
|
374
440
|
this.gates = options.gates ?? {};
|
|
@@ -386,6 +452,7 @@ export class AgentRunEngine {
|
|
|
386
452
|
engineEscalations: options.maxEngineEscalations ?? options.budgets?.engineEscalations,
|
|
387
453
|
};
|
|
388
454
|
this.maxSteps = Math.max(1, options.maxSteps ?? DEFAULT_MAX_STEPS);
|
|
455
|
+
this.routeTimeoutSignal = options.routeTimeoutSignal ?? ((durationMs) => AbortSignal.timeout(durationMs));
|
|
389
456
|
}
|
|
390
457
|
/**
|
|
391
458
|
* Decide the high-level action. A forced `requestedMode` bypasses routing.
|
|
@@ -406,7 +473,8 @@ export class AgentRunEngine {
|
|
|
406
473
|
if (request.requestedMode === "ask"
|
|
407
474
|
&& routed.action !== "converse"
|
|
408
475
|
&& routed.action !== "compose_app"
|
|
409
|
-
&& routed.requiresClarification !== true
|
|
476
|
+
&& routed.requiresClarification !== true
|
|
477
|
+
&& !routed.terminalOutcome) {
|
|
410
478
|
return { ...routed, action: "answer" };
|
|
411
479
|
}
|
|
412
480
|
return routed;
|
|
@@ -440,7 +508,16 @@ export class AgentRunEngine {
|
|
|
440
508
|
}
|
|
441
509
|
const runId = request.runId ?? this.idGenerator();
|
|
442
510
|
const startedAt = this.timestamp();
|
|
511
|
+
const runStartedAtMs = Date.parse(startedAt);
|
|
443
512
|
const requestedMode = request.requestedMode ?? "auto";
|
|
513
|
+
const runBudget = request.runBudget ?? createAgentRunBudget({
|
|
514
|
+
requestedMode,
|
|
515
|
+
startedAtMs: runStartedAtMs,
|
|
516
|
+
inheritedSignal: request.signal,
|
|
517
|
+
timeoutSignal: this.routeTimeoutSignal,
|
|
518
|
+
nowMs: () => this.now().getTime(),
|
|
519
|
+
});
|
|
520
|
+
request = { ...request, runBudget, signal: runBudget.hardSignal };
|
|
444
521
|
const events = [];
|
|
445
522
|
let plan;
|
|
446
523
|
const executedSteps = [];
|
|
@@ -550,15 +627,22 @@ export class AgentRunEngine {
|
|
|
550
627
|
followsUp: true,
|
|
551
628
|
source: "heuristic",
|
|
552
629
|
}
|
|
553
|
-
: await this.decideRoute(request);
|
|
630
|
+
: await awaitWithAbort(this.decideRoute(request), request.signal);
|
|
631
|
+
routeDecision = enforceOrdinaryAnalyticalPlanBoundary(request, routeDecision);
|
|
554
632
|
const defaultRoute = answerAnywayRoute(constrainRouteForAudience(selectRoute(request, routeDecision), audience), request, audience, routeDecision);
|
|
555
|
-
|
|
633
|
+
const authoritativeAsk = routeDecision.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
634
|
+
&& requestedMode !== 'research';
|
|
635
|
+
const activePlanner = authoritativeAsk
|
|
636
|
+
? createDeterministicAgentRunPlanner()
|
|
637
|
+
: this.planner;
|
|
638
|
+
const planningSignal = request.runBudget?.hardSignal ?? request.signal;
|
|
639
|
+
plan = await awaitWithAbort(Promise.resolve(activePlanner.plan({
|
|
556
640
|
request,
|
|
557
641
|
routeDecision,
|
|
558
642
|
defaultRoute,
|
|
559
643
|
maxSteps: this.maxSteps,
|
|
560
644
|
audience,
|
|
561
|
-
});
|
|
645
|
+
})), planningSignal);
|
|
562
646
|
emit({
|
|
563
647
|
type: "plan.created",
|
|
564
648
|
message: plan.rationale,
|
|
@@ -643,6 +727,11 @@ export class AgentRunEngine {
|
|
|
643
727
|
emitAnswerDelta: onAnswerDelta,
|
|
644
728
|
});
|
|
645
729
|
result = consumeRepeatedClarificationSelection(request, routeDecision, result);
|
|
730
|
+
if (result.analyticalTurnPlan)
|
|
731
|
+
progress.analyticalTurnPlan = result.analyticalTurnPlan;
|
|
732
|
+
if (result.analyticalTaskOutcomes)
|
|
733
|
+
progress.analyticalTaskOutcomes = result.analyticalTaskOutcomes;
|
|
734
|
+
persistProgress();
|
|
646
735
|
evaluations = this.evaluate({ route, request, routeDecision, result, attempt });
|
|
647
736
|
for (const evaluation of evaluations) {
|
|
648
737
|
emit({
|
|
@@ -662,6 +751,13 @@ export class AgentRunEngine {
|
|
|
662
751
|
stepStatus = attempt > 0 ? "repaired" : "passed";
|
|
663
752
|
break;
|
|
664
753
|
}
|
|
754
|
+
// A frozen analytical plan has one route and no downstream planner,
|
|
755
|
+
// rematch, route escalation, or whole-answer regeneration authority.
|
|
756
|
+
// Typed server-issued repair is a separate derived run.
|
|
757
|
+
if (authoritativeAsk) {
|
|
758
|
+
stepStatus = 'needs_review';
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
665
761
|
const currentStep = {
|
|
666
762
|
id: stepId,
|
|
667
763
|
index: stepCount,
|
|
@@ -674,7 +770,7 @@ export class AgentRunEngine {
|
|
|
674
770
|
evaluations,
|
|
675
771
|
artifacts: result.artifacts ?? [],
|
|
676
772
|
};
|
|
677
|
-
const decision = await
|
|
773
|
+
const decision = await activePlanner.replan({
|
|
678
774
|
request,
|
|
679
775
|
plan,
|
|
680
776
|
currentStep,
|
|
@@ -779,7 +875,7 @@ export class AgentRunEngine {
|
|
|
779
875
|
});
|
|
780
876
|
continue;
|
|
781
877
|
}
|
|
782
|
-
const outcome = computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion);
|
|
878
|
+
const outcome = computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion, routeDecision.terminalOutcome?.message);
|
|
783
879
|
const step = {
|
|
784
880
|
id: stepId,
|
|
785
881
|
index: stepCount,
|
|
@@ -862,15 +958,102 @@ export class AgentRunEngine {
|
|
|
862
958
|
run.completedAt = this.timestamp();
|
|
863
959
|
run.lifecycle = terminalLifecycle(progress.lifecycle, run.status === "blocked" ? "run.failed" : "run.completed", run.completedAt, events.length);
|
|
864
960
|
run.diagnosticReceipt = diagnosticReceiptForRun(run);
|
|
865
|
-
run.
|
|
961
|
+
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
962
|
+
run.artifacts = attachDiagnosticReceipt(run.artifacts, run.diagnosticReceipt, run.diagnosticReceiptV2);
|
|
866
963
|
await checkpointQueue;
|
|
867
964
|
await this.store?.save(run);
|
|
868
965
|
return run;
|
|
869
966
|
}
|
|
870
967
|
catch (err) {
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
968
|
+
const dispatchEvidence = providerDispatchEvidenceFromError(err);
|
|
969
|
+
const userCancelled = isAgentRunUserCancellation(request.signal?.reason) || isAgentRunUserCancellation(err);
|
|
970
|
+
if (userCancelled) {
|
|
971
|
+
const message = "Stopped by user.";
|
|
972
|
+
const cancelledRoute = progress.route ?? "cancelled";
|
|
973
|
+
const cancelledPhase = progress.lifecycle.phase;
|
|
974
|
+
emit({
|
|
975
|
+
type: "run.cancelled",
|
|
976
|
+
message,
|
|
977
|
+
route: cancelledRoute,
|
|
978
|
+
status: "cancelled",
|
|
979
|
+
trustState: "not_applicable",
|
|
980
|
+
});
|
|
981
|
+
const completedAt = this.timestamp();
|
|
982
|
+
const failure = {
|
|
983
|
+
code: AGENT_RUN_USER_CANCEL_CODE,
|
|
984
|
+
phase: cancelledPhase,
|
|
985
|
+
message,
|
|
986
|
+
recoverable: false,
|
|
987
|
+
safeActions: [],
|
|
988
|
+
};
|
|
989
|
+
const evaluations = [
|
|
990
|
+
...progress.evaluations,
|
|
991
|
+
{
|
|
992
|
+
id: "run-cancelled",
|
|
993
|
+
label: "Run cancelled",
|
|
994
|
+
passed: true,
|
|
995
|
+
severity: "info",
|
|
996
|
+
message,
|
|
997
|
+
},
|
|
998
|
+
];
|
|
999
|
+
const receipt = {
|
|
1000
|
+
version: 1,
|
|
1001
|
+
runId,
|
|
1002
|
+
phase: failure.phase,
|
|
1003
|
+
route: cancelledRoute,
|
|
1004
|
+
plan,
|
|
1005
|
+
steps: executedSteps,
|
|
1006
|
+
artifacts: progress.artifacts,
|
|
1007
|
+
evaluations,
|
|
1008
|
+
failure,
|
|
1009
|
+
};
|
|
1010
|
+
const run = {
|
|
1011
|
+
id: runId,
|
|
1012
|
+
question: submittedQuestion,
|
|
1013
|
+
requestedMode,
|
|
1014
|
+
route: cancelledRoute,
|
|
1015
|
+
status: "cancelled",
|
|
1016
|
+
trustState: "not_applicable",
|
|
1017
|
+
stopReason: "cancelled",
|
|
1018
|
+
startedAt,
|
|
1019
|
+
completedAt,
|
|
1020
|
+
selectedObject: request.selectedObject,
|
|
1021
|
+
executionTarget: request.executionTarget,
|
|
1022
|
+
routeDecision,
|
|
1023
|
+
plan,
|
|
1024
|
+
steps: executedSteps,
|
|
1025
|
+
summary: message,
|
|
1026
|
+
artifacts: progress.artifacts,
|
|
1027
|
+
evaluations,
|
|
1028
|
+
events,
|
|
1029
|
+
nextActions: [],
|
|
1030
|
+
repairAttempts: 0,
|
|
1031
|
+
escalationAttempts: 0,
|
|
1032
|
+
budgetUsage: cascadeBudgetTrace(createCascadeBudgetState(this.budgetModel)),
|
|
1033
|
+
diagnosticReceipt: receipt,
|
|
1034
|
+
...(dispatchEvidence.providerEgressReceipts.length
|
|
1035
|
+
? { providerEgressReceipts: dispatchEvidence.providerEgressReceipts }
|
|
1036
|
+
: {}),
|
|
1037
|
+
telemetry: {
|
|
1038
|
+
...emptyRunTelemetry(durationBetweenMs(startedAt, completedAt), "cancelled"),
|
|
1039
|
+
providerRoundTrips: dispatchEvidence.providerRoundTrips,
|
|
1040
|
+
toolCalls: dispatchEvidence.toolCalls,
|
|
1041
|
+
sqlExecutions: dispatchEvidence.sqlExecutions,
|
|
1042
|
+
repairs: dispatchEvidence.repairs,
|
|
1043
|
+
egressReceipts: dispatchEvidence.providerEgressReceipts.length,
|
|
1044
|
+
},
|
|
1045
|
+
lifecycle: terminalLifecycle(progress.lifecycle, "run.cancelled", completedAt, events.length),
|
|
1046
|
+
};
|
|
1047
|
+
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1048
|
+
await checkpointQueue;
|
|
1049
|
+
await this.store?.save(run);
|
|
1050
|
+
return run;
|
|
1051
|
+
}
|
|
1052
|
+
const message = isOrchestrationBudgetExhausted(err)
|
|
1053
|
+
? 'Ask could not complete within its bounded orchestration. Nothing was executed; narrow the metric or dimension and retry.'
|
|
1054
|
+
: err instanceof Error && err.name === "TimeoutError"
|
|
1055
|
+
? "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."
|
|
1056
|
+
: err instanceof Error ? err.message : String(err);
|
|
874
1057
|
const failedRoute = progress.route;
|
|
875
1058
|
const failedPhase = progress.lifecycle.phase;
|
|
876
1059
|
emit({
|
|
@@ -931,8 +1114,21 @@ export class AgentRunEngine {
|
|
|
931
1114
|
escalationAttempts: 0,
|
|
932
1115
|
budgetUsage: cascadeBudgetTrace(createCascadeBudgetState(this.budgetModel)),
|
|
933
1116
|
diagnosticReceipt: receipt,
|
|
1117
|
+
...(dispatchEvidence.providerEgressReceipts.length
|
|
1118
|
+
? { providerEgressReceipts: dispatchEvidence.providerEgressReceipts }
|
|
1119
|
+
: {}),
|
|
1120
|
+
telemetry: {
|
|
1121
|
+
...emptyRunTelemetry(durationBetweenMs(startedAt, completedAt), dispatchEvidence.fallbackReason),
|
|
1122
|
+
providerRoundTrips: dispatchEvidence.providerRoundTrips,
|
|
1123
|
+
toolCalls: dispatchEvidence.toolCalls,
|
|
1124
|
+
sqlExecutions: dispatchEvidence.sqlExecutions,
|
|
1125
|
+
repairs: dispatchEvidence.repairs,
|
|
1126
|
+
egressReceipts: dispatchEvidence.providerEgressReceipts.length,
|
|
1127
|
+
},
|
|
934
1128
|
lifecycle: terminalLifecycle(progress.lifecycle, "run.failed", completedAt, events.length),
|
|
935
1129
|
};
|
|
1130
|
+
run.diagnosticReceiptV2 = diagnosticReceiptV2ForRun(run);
|
|
1131
|
+
run.artifacts = attachDiagnosticReceipt(retainedArtifacts, receipt, run.diagnosticReceiptV2);
|
|
936
1132
|
await checkpointQueue;
|
|
937
1133
|
await this.store?.save(run);
|
|
938
1134
|
return run;
|
|
@@ -985,8 +1181,12 @@ export class AgentRunEngine {
|
|
|
985
1181
|
// If the final step produced no user-facing answer (e.g. it only drafted an
|
|
986
1182
|
// artifact), fall back to the last step that DID answer so the run never
|
|
987
1183
|
// drops a data answer an earlier step already computed.
|
|
988
|
-
const finalHasAnswer =
|
|
1184
|
+
const finalHasAnswer = finalOutcome.status !== "blocked"
|
|
1185
|
+
&& typeof finalResult.answer === "string" && finalResult.answer.trim().length > 0;
|
|
989
1186
|
const answerSource = finalHasAnswer ? finalResult : (input.bestAnswerResult ?? finalResult);
|
|
1187
|
+
const acceptedAnswer = finalOutcome.status === "blocked"
|
|
1188
|
+
? finalOutcome.summary
|
|
1189
|
+
: input.clarifyOutcome?.question ?? answerSource.answer;
|
|
990
1190
|
return {
|
|
991
1191
|
id: input.runId,
|
|
992
1192
|
question: input.request.question,
|
|
@@ -1003,7 +1203,7 @@ export class AgentRunEngine {
|
|
|
1003
1203
|
plan: input.plan,
|
|
1004
1204
|
steps: input.steps,
|
|
1005
1205
|
summary: finalOutcome.summary,
|
|
1006
|
-
answer:
|
|
1206
|
+
answer: acceptedAnswer,
|
|
1007
1207
|
answerKind: answerSource.answerKind ?? "governed",
|
|
1008
1208
|
artifacts,
|
|
1009
1209
|
evaluations: finalStep.evaluations,
|
|
@@ -1014,8 +1214,16 @@ export class AgentRunEngine {
|
|
|
1014
1214
|
? { clarificationOptions: finalResult.clarificationOptions ?? input.routeDecision.clarificationOptions }
|
|
1015
1215
|
: {}),
|
|
1016
1216
|
repairAttempts: finalResult.repairAttempts ?? repairAttempts,
|
|
1217
|
+
...(finalResult.providerEgressReceipts?.length
|
|
1218
|
+
? { providerEgressReceipts: finalResult.providerEgressReceipts }
|
|
1219
|
+
: {}),
|
|
1220
|
+
...(finalResult.telemetry ? {
|
|
1221
|
+
telemetry: withTotalDuration(finalResult.telemetry, durationBetweenMs(input.startedAt, completedAt)),
|
|
1222
|
+
} : {}),
|
|
1017
1223
|
escalationAttempts,
|
|
1018
1224
|
budgetUsage: input.budgetUsage,
|
|
1225
|
+
...(finalResult.analyticalTurnPlan ? { analyticalTurnPlan: finalResult.analyticalTurnPlan } : {}),
|
|
1226
|
+
...(finalResult.analyticalTaskOutcomes ? { analyticalTaskOutcomes: finalResult.analyticalTaskOutcomes } : {}),
|
|
1019
1227
|
...authoringDerivationFromRequest(input.request),
|
|
1020
1228
|
};
|
|
1021
1229
|
}
|
|
@@ -1028,14 +1236,229 @@ export class AgentRunEngine {
|
|
|
1028
1236
|
}
|
|
1029
1237
|
async executeRoute(context) {
|
|
1030
1238
|
const executor = this.executors[context.route];
|
|
1031
|
-
if (executor)
|
|
1032
|
-
|
|
1239
|
+
if (executor) {
|
|
1240
|
+
const frozenPlan = context.routeDecision?.resolvedAnalyticalPlan?.mode === 'authoritative';
|
|
1241
|
+
const startsAnalyticalWork = context.route === 'certified_answer'
|
|
1242
|
+
|| context.route === 'semantic_answer'
|
|
1243
|
+
|| context.route === 'generated_answer'
|
|
1244
|
+
|| context.route === 'research';
|
|
1245
|
+
if (startsAnalyticalWork
|
|
1246
|
+
&& !frozenPlan
|
|
1247
|
+
&& context.request.runBudget
|
|
1248
|
+
&& !context.request.runBudget.mayStartDiscovery(context.route)) {
|
|
1249
|
+
return softBoundaryResult(context.route, context.request.runBudget);
|
|
1250
|
+
}
|
|
1251
|
+
const signal = context.request.runBudget?.hardSignal ?? context.request.signal;
|
|
1252
|
+
if (signal?.aborted)
|
|
1253
|
+
throw signal.reason ?? routeTimeoutError();
|
|
1254
|
+
const execution = Promise.resolve(executor({
|
|
1255
|
+
...context,
|
|
1256
|
+
request: { ...context.request, ...(signal ? { signal } : {}) },
|
|
1257
|
+
}));
|
|
1258
|
+
return awaitWithAbort(execution, signal);
|
|
1259
|
+
}
|
|
1033
1260
|
return defaultExecutorResult(context.route, context.request, context.routeDecision);
|
|
1034
1261
|
}
|
|
1262
|
+
absoluteDeadlineSignal(deadlineMs, runStartedAtMs, inherited) {
|
|
1263
|
+
const elapsedMs = Math.max(0, this.now().getTime() - runStartedAtMs);
|
|
1264
|
+
const remainingMs = deadlineMs - elapsedMs;
|
|
1265
|
+
const deadlineSignal = remainingMs <= 0
|
|
1266
|
+
? alreadyAbortedSignal(routeTimeoutError())
|
|
1267
|
+
: this.routeTimeoutSignal(remainingMs);
|
|
1268
|
+
return inherited ? AbortSignal.any([inherited, deadlineSignal]) : deadlineSignal;
|
|
1269
|
+
}
|
|
1035
1270
|
timestamp() {
|
|
1036
1271
|
return this.now().toISOString();
|
|
1037
1272
|
}
|
|
1038
1273
|
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Ordinary analytical Ask cannot delegate meaning to the legacy answer
|
|
1276
|
+
* generator. Retrieval/meaning must first produce the immutable RAP consumed by
|
|
1277
|
+
* every later compiler and executor. This host-side gate protects callers that
|
|
1278
|
+
* inject or deserialize an older router decision as well as the canonical
|
|
1279
|
+
* router producer.
|
|
1280
|
+
*/
|
|
1281
|
+
function enforceOrdinaryAnalyticalPlanBoundary(request, decision) {
|
|
1282
|
+
const ordinaryAsk = request.requestedMode === undefined
|
|
1283
|
+
|| request.requestedMode === 'auto'
|
|
1284
|
+
|| request.requestedMode === 'ask';
|
|
1285
|
+
const terminal = decision.action === 'clarify'
|
|
1286
|
+
|| decision.action === 'block'
|
|
1287
|
+
|| decision.requiresClarification === true
|
|
1288
|
+
|| Boolean(decision.terminalOutcome);
|
|
1289
|
+
// A modeling/coverage gap is a pre-freeze discovery result, not permission to
|
|
1290
|
+
// terminate an ordinary Ask. Keep the diagnostic as a typed reason in the
|
|
1291
|
+
// route decision, clear the blocked RAP, and let the answer executor continue
|
|
1292
|
+
// through the governed relational and review-required generated lanes. Policy
|
|
1293
|
+
// blocks and genuine user ambiguity remain terminal. This is the key
|
|
1294
|
+
// governed-first-but-not-governed-only boundary (AGT-028, EXP-001).
|
|
1295
|
+
if (ordinaryAsk
|
|
1296
|
+
&& decision.terminalOutcome?.kind === 'modeling_gap'
|
|
1297
|
+
&& decision.requiresClarification !== true
|
|
1298
|
+
&& !request.selectedEvidenceId) {
|
|
1299
|
+
return {
|
|
1300
|
+
...decision,
|
|
1301
|
+
action: 'answer',
|
|
1302
|
+
confidence: Math.min(decision.confidence, 0.55),
|
|
1303
|
+
reason: `${decision.terminalOutcome.message} Continuing through DBT-grounded relational and review-required generated analysis before asking for a modeling change.`,
|
|
1304
|
+
terminalOutcome: undefined,
|
|
1305
|
+
resolvedAnalyticalPlan: undefined,
|
|
1306
|
+
requiresClarification: false,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
const exactSemanticContinuation = Boolean(request.selectedEvidenceId
|
|
1310
|
+
&& decision.meaningResolution?.recommendedRoute === 'semantic'
|
|
1311
|
+
&& decision.meaningResolution.recommendedExecutionId === request.selectedEvidenceId);
|
|
1312
|
+
if (!ordinaryAsk
|
|
1313
|
+
|| terminal
|
|
1314
|
+
|| exactSemanticContinuation
|
|
1315
|
+
|| decision.resolvedAnalyticalPlan?.mode === 'authoritative')
|
|
1316
|
+
return decision;
|
|
1317
|
+
const explicitAuthoring = looksLikeComposeApp(request.question)
|
|
1318
|
+
|| /\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);
|
|
1319
|
+
if (explicitAuthoring)
|
|
1320
|
+
return decision;
|
|
1321
|
+
const history = request.history?.length
|
|
1322
|
+
? request.history
|
|
1323
|
+
: conversationHistoryFromContext(request.conversationContext);
|
|
1324
|
+
const questionPlan = buildAnalysisQuestionPlan(request.question);
|
|
1325
|
+
const conversationalKind = classifyConversationalTurn(request.question, history.length > 0 || Boolean(request.conversationContext && Object.keys(request.conversationContext).length > 0));
|
|
1326
|
+
const definitionOnly = questionPlan.mode === 'definition'
|
|
1327
|
+
&& questionPlan.requestedShape.dimensions.length === 0
|
|
1328
|
+
&& questionPlan.requestedShape.filters.length === 0
|
|
1329
|
+
&& questionPlan.timeTerms.length === 0;
|
|
1330
|
+
// Router category/action are advisory and may be absent or forged. A
|
|
1331
|
+
// deterministic host parse owns the no-data lanes too, so a forged
|
|
1332
|
+
// `data_analysis` cannot turn "hi" into SQL and a forged `answer` cannot
|
|
1333
|
+
// turn a glossary definition into an analytical execution.
|
|
1334
|
+
if (ordinaryAsk && (conversationalKind || definitionOnly)) {
|
|
1335
|
+
return {
|
|
1336
|
+
...decision,
|
|
1337
|
+
action: 'converse',
|
|
1338
|
+
category: 'conversational',
|
|
1339
|
+
...(conversationalKind ? { conversationalKind } : {}),
|
|
1340
|
+
confidence: 1,
|
|
1341
|
+
reason: definitionOnly
|
|
1342
|
+
? 'This requests a definition, not a warehouse result, so no analytical execution is needed.'
|
|
1343
|
+
: 'This is a conversational turn and does not request governed data.',
|
|
1344
|
+
requiresClarification: false,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
const analyticalIntents = new Set([
|
|
1348
|
+
'exact_certified_lookup',
|
|
1349
|
+
'ad_hoc_ranking',
|
|
1350
|
+
'driver_breakdown',
|
|
1351
|
+
'diagnose_change',
|
|
1352
|
+
'segment_compare',
|
|
1353
|
+
'entity_drilldown',
|
|
1354
|
+
'anomaly_investigation',
|
|
1355
|
+
]);
|
|
1356
|
+
const analyticalIntent = analyticalIntents.has(questionPlan.routeIntent)
|
|
1357
|
+
|| Boolean(request.intent && analyticalIntents.has(request.intent));
|
|
1358
|
+
const analyticalShape = questionPlan.mode !== 'clarify'
|
|
1359
|
+
&& questionPlan.mode !== 'definition'
|
|
1360
|
+
&& (questionPlan.requestedShape.measures.length > 0
|
|
1361
|
+
|| questionPlan.requestedShape.dimensions.length > 0
|
|
1362
|
+
|| questionPlan.requestedShape.filters.length > 0
|
|
1363
|
+
|| questionPlan.timeTerms.length > 0
|
|
1364
|
+
|| questionPlan.requestedShape.rankingDirection !== undefined);
|
|
1365
|
+
const governedEvidence = (decision.retrievalEvidence?.candidateCount ?? 0) > 0
|
|
1366
|
+
|| (decision.meaningResolution?.selectedConceptIds.length ?? 0) > 0;
|
|
1367
|
+
const analytical = analyticalIntent || analyticalShape || governedEvidence;
|
|
1368
|
+
if (!analytical)
|
|
1369
|
+
return decision;
|
|
1370
|
+
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.';
|
|
1371
|
+
return {
|
|
1372
|
+
...decision,
|
|
1373
|
+
action: 'block',
|
|
1374
|
+
confidence: 1,
|
|
1375
|
+
reason: message,
|
|
1376
|
+
requiresClarification: false,
|
|
1377
|
+
terminalOutcome: {
|
|
1378
|
+
kind: 'modeling_gap',
|
|
1379
|
+
code: 'ANALYTICAL_MODELING_GAP',
|
|
1380
|
+
message,
|
|
1381
|
+
candidateIds: decision.retrievalEvidence?.candidateIds ?? [],
|
|
1382
|
+
},
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
function softBoundaryResult(route, budget) {
|
|
1386
|
+
const seconds = Math.round(budget.softTargetMs(route) / 1_000);
|
|
1387
|
+
return {
|
|
1388
|
+
resolvedRoute: 'clarify',
|
|
1389
|
+
status: 'needs_clarification',
|
|
1390
|
+
trustState: 'blocked',
|
|
1391
|
+
stopReason: 'needs_clarification',
|
|
1392
|
+
summary: `The ${seconds}-second discovery target elapsed before an analytical plan was frozen. No new provider, tool, or retrieval branch was started.`,
|
|
1393
|
+
answer: budget.mode === 'research'
|
|
1394
|
+
? 'Research stopped starting new branches at 90 seconds. Refine the question or retry; any already validated partial findings remain available.'
|
|
1395
|
+
: 'The discovery window ended before DQL could freeze an exact analytical plan. Refine the metric or grain, or retry the same bounded question.',
|
|
1396
|
+
artifacts: [],
|
|
1397
|
+
evaluations: [],
|
|
1398
|
+
nextActions: [{ id: 'retry-after-soft-target', label: 'Retry the same question' }],
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function providerDispatchEvidenceFromError(error) {
|
|
1402
|
+
const empty = {
|
|
1403
|
+
providerEgressReceipts: [],
|
|
1404
|
+
providerRoundTrips: 0,
|
|
1405
|
+
toolCalls: 0,
|
|
1406
|
+
sqlExecutions: 0,
|
|
1407
|
+
repairs: 0,
|
|
1408
|
+
fallbackReason: 'executor_failure',
|
|
1409
|
+
};
|
|
1410
|
+
if (!error || typeof error !== 'object')
|
|
1411
|
+
return empty;
|
|
1412
|
+
const value = error.providerDispatchEvidence;
|
|
1413
|
+
if (!value || typeof value !== 'object')
|
|
1414
|
+
return empty;
|
|
1415
|
+
const record = value;
|
|
1416
|
+
const count = (key) => Number.isInteger(record[key]) && Number(record[key]) >= 0 ? Number(record[key]) : 0;
|
|
1417
|
+
return {
|
|
1418
|
+
providerEgressReceipts: Array.isArray(record.providerEgressReceipts)
|
|
1419
|
+
? record.providerEgressReceipts.flatMap((receipt) => {
|
|
1420
|
+
const normalized = normalizeProviderEgressReceiptV1(receipt);
|
|
1421
|
+
return normalized ? [normalized] : [];
|
|
1422
|
+
})
|
|
1423
|
+
: [],
|
|
1424
|
+
providerRoundTrips: count('providerRoundTrips'),
|
|
1425
|
+
toolCalls: count('toolCalls'),
|
|
1426
|
+
sqlExecutions: count('sqlExecutions'),
|
|
1427
|
+
repairs: count('repairs'),
|
|
1428
|
+
fallbackReason: typeof record.fallbackReason === 'string' ? record.fallbackReason : 'executor_failure',
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
function alreadyAbortedSignal(reason) {
|
|
1432
|
+
const controller = new AbortController();
|
|
1433
|
+
controller.abort(reason);
|
|
1434
|
+
return controller.signal;
|
|
1435
|
+
}
|
|
1436
|
+
function routeTimeoutError() {
|
|
1437
|
+
return new DOMException('The absolute agent route deadline elapsed.', 'TimeoutError');
|
|
1438
|
+
}
|
|
1439
|
+
function awaitWithAbort(work, signal) {
|
|
1440
|
+
if (!signal)
|
|
1441
|
+
return work;
|
|
1442
|
+
if (signal.aborted) {
|
|
1443
|
+
// The callee may already have started before its cancellation state was
|
|
1444
|
+
// observed. Consume any eventual rejection while refusing its result.
|
|
1445
|
+
void work.catch(() => undefined);
|
|
1446
|
+
return Promise.reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
|
|
1447
|
+
}
|
|
1448
|
+
return new Promise((resolve, reject) => {
|
|
1449
|
+
let settled = false;
|
|
1450
|
+
const finish = (callback) => {
|
|
1451
|
+
if (settled)
|
|
1452
|
+
return;
|
|
1453
|
+
settled = true;
|
|
1454
|
+
signal.removeEventListener('abort', onAbort);
|
|
1455
|
+
callback();
|
|
1456
|
+
};
|
|
1457
|
+
const onAbort = () => finish(() => reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')));
|
|
1458
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1459
|
+
work.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1039
1462
|
function authoringDerivationFromRequest(request) {
|
|
1040
1463
|
if (request.requestedMode !== 'modeling' && request.requestedMode !== 'skill')
|
|
1041
1464
|
return {};
|
|
@@ -1073,6 +1496,15 @@ function diagnosticFailureFromError(error, phase) {
|
|
|
1073
1496
|
const name = error instanceof Error ? error.name : "";
|
|
1074
1497
|
const message = error instanceof Error ? error.message : String(error);
|
|
1075
1498
|
const lower = `${name} ${message}`.toLowerCase();
|
|
1499
|
+
if (isOrchestrationBudgetExhausted(error)) {
|
|
1500
|
+
return {
|
|
1501
|
+
code: 'orchestration_budget_exhausted',
|
|
1502
|
+
phase,
|
|
1503
|
+
message: 'Ask exhausted its bounded orchestration before a final answer was available.',
|
|
1504
|
+
recoverable: false,
|
|
1505
|
+
safeActions: ['inspect_failure'],
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1076
1508
|
if (name === "TimeoutError" || lower.includes("time limit") || lower.includes("timeout")) {
|
|
1077
1509
|
return {
|
|
1078
1510
|
code: "TIMEOUT",
|
|
@@ -1082,13 +1514,13 @@ function diagnosticFailureFromError(error, phase) {
|
|
|
1082
1514
|
safeActions: ["retry_same_plan"],
|
|
1083
1515
|
};
|
|
1084
1516
|
}
|
|
1085
|
-
if (
|
|
1517
|
+
if (isAgentRunUserCancellation(error)) {
|
|
1086
1518
|
return {
|
|
1087
1519
|
code: "RUN_CANCELLED",
|
|
1088
1520
|
phase,
|
|
1089
|
-
message,
|
|
1090
|
-
recoverable:
|
|
1091
|
-
safeActions: [
|
|
1521
|
+
message: "Stopped by user.",
|
|
1522
|
+
recoverable: false,
|
|
1523
|
+
safeActions: [],
|
|
1092
1524
|
};
|
|
1093
1525
|
}
|
|
1094
1526
|
return {
|
|
@@ -1099,6 +1531,14 @@ function diagnosticFailureFromError(error, phase) {
|
|
|
1099
1531
|
safeActions: ["retry_same_request", "inspect_failure"],
|
|
1100
1532
|
};
|
|
1101
1533
|
}
|
|
1534
|
+
function isOrchestrationBudgetExhausted(error) {
|
|
1535
|
+
if (!error || typeof error !== 'object')
|
|
1536
|
+
return false;
|
|
1537
|
+
if (error.code === 'PROVIDER_DISPATCH_BUDGET_EXHAUSTED')
|
|
1538
|
+
return true;
|
|
1539
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1540
|
+
return /(?:run-wide provider dispatch|orchestration) budget exhausted/i.test(message);
|
|
1541
|
+
}
|
|
1102
1542
|
function diagnosticReceiptForRun(run) {
|
|
1103
1543
|
const failureEvaluation = run.evaluations.find((evaluation) => !evaluation.passed && evaluation.severity === "blocking");
|
|
1104
1544
|
const analyticalFailure = run.artifacts
|
|
@@ -1118,7 +1558,7 @@ function diagnosticReceiptForRun(run) {
|
|
|
1118
1558
|
return {
|
|
1119
1559
|
version: 1,
|
|
1120
1560
|
runId: run.id,
|
|
1121
|
-
phase: run.lifecycle?.phase ?? (run.status === "blocked" ? "run.failed" : "run.completed"),
|
|
1561
|
+
phase: run.lifecycle?.phase ?? (run.status === "blocked" ? "run.failed" : run.status === "cancelled" ? "run.cancelled" : "run.completed"),
|
|
1122
1562
|
route: run.route,
|
|
1123
1563
|
...(run.routeDecision?.resolvedAnalyticalPlan
|
|
1124
1564
|
? { resolvedAnalyticalPlan: run.routeDecision.resolvedAnalyticalPlan }
|
|
@@ -1127,20 +1567,59 @@ function diagnosticReceiptForRun(run) {
|
|
|
1127
1567
|
steps: run.steps,
|
|
1128
1568
|
artifacts: run.artifacts,
|
|
1129
1569
|
evaluations: run.evaluations,
|
|
1130
|
-
...(run.
|
|
1570
|
+
...(run.repairCapability ? { repairCapability: run.repairCapability } : {}),
|
|
1571
|
+
...(run.providerEgressReceipts?.length ? { providerEgressReceipts: run.providerEgressReceipts } : {}),
|
|
1572
|
+
...(run.status === "blocked" || run.status === "cancelled"
|
|
1131
1573
|
? {
|
|
1132
1574
|
failure: {
|
|
1133
|
-
code: failureCode,
|
|
1134
|
-
phase: run.lifecycle?.phase ?? "run.failed",
|
|
1135
|
-
message: failureEvaluation?.message ?? run.summary,
|
|
1136
|
-
recoverable: Boolean(run.nextActions.length),
|
|
1137
|
-
safeActions: run.nextActions.map((action) => action.id),
|
|
1575
|
+
code: run.status === "cancelled" ? AGENT_RUN_USER_CANCEL_CODE : failureCode,
|
|
1576
|
+
phase: run.lifecycle?.phase ?? (run.status === "cancelled" ? "run.cancelled" : "run.failed"),
|
|
1577
|
+
message: run.status === "cancelled" ? "Stopped by user." : failureEvaluation?.message ?? run.summary,
|
|
1578
|
+
recoverable: run.status === "cancelled" ? false : Boolean(run.nextActions.length),
|
|
1579
|
+
safeActions: run.status === "cancelled" ? [] : run.nextActions.map((action) => action.id),
|
|
1138
1580
|
},
|
|
1139
1581
|
}
|
|
1140
1582
|
: {}),
|
|
1141
1583
|
};
|
|
1142
1584
|
}
|
|
1143
|
-
function
|
|
1585
|
+
function diagnosticReceiptV2ForRun(run) {
|
|
1586
|
+
const telemetry = run.telemetry ?? emptyRunTelemetry(durationBetweenMs(run.startedAt, run.completedAt), 'not_recorded');
|
|
1587
|
+
return {
|
|
1588
|
+
version: 2,
|
|
1589
|
+
runId: run.id,
|
|
1590
|
+
route: run.route,
|
|
1591
|
+
status: run.status,
|
|
1592
|
+
telemetry,
|
|
1593
|
+
providerEgressReceiptFingerprints: (run.providerEgressReceipts ?? []).map(receiptFingerprint),
|
|
1594
|
+
...(run.repairCapability ? { repairCapabilityFingerprint: receiptFingerprint(run.repairCapability) } : {}),
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function emptyRunTelemetry(total, fallbackReason) {
|
|
1598
|
+
return {
|
|
1599
|
+
version: 1,
|
|
1600
|
+
stageDurationsMs: { total },
|
|
1601
|
+
providerRoundTrips: 0,
|
|
1602
|
+
toolCalls: 0,
|
|
1603
|
+
sqlExecutions: 0,
|
|
1604
|
+
repairs: 0,
|
|
1605
|
+
egressReceipts: 0,
|
|
1606
|
+
fallbackReason,
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
function withTotalDuration(telemetry, total) {
|
|
1610
|
+
return {
|
|
1611
|
+
...telemetry,
|
|
1612
|
+
stageDurationsMs: { ...telemetry.stageDurationsMs, total },
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
function durationBetweenMs(startedAt, completedAt) {
|
|
1616
|
+
const duration = Date.parse(completedAt) - Date.parse(startedAt);
|
|
1617
|
+
return Number.isFinite(duration) && duration >= 0 ? Math.min(86_400_000, duration) : 0;
|
|
1618
|
+
}
|
|
1619
|
+
function receiptFingerprint(value) {
|
|
1620
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
1621
|
+
}
|
|
1622
|
+
function attachDiagnosticReceipt(artifacts, receipt, receiptV2) {
|
|
1144
1623
|
if (artifacts.length === 0) {
|
|
1145
1624
|
if (!receipt.failure)
|
|
1146
1625
|
return artifacts;
|
|
@@ -1149,7 +1628,7 @@ function attachDiagnosticReceipt(artifacts, receipt) {
|
|
|
1149
1628
|
kind: "answer",
|
|
1150
1629
|
title: "Agent run diagnostics",
|
|
1151
1630
|
trustState: "blocked",
|
|
1152
|
-
payload: { diagnosticReceipt: receipt },
|
|
1631
|
+
payload: { diagnosticReceipt: receipt, ...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}) },
|
|
1153
1632
|
}];
|
|
1154
1633
|
}
|
|
1155
1634
|
const preferredIndex = Math.max(0, artifacts.findIndex((artifact) => artifact.kind === "answer"));
|
|
@@ -1164,11 +1643,12 @@ function attachDiagnosticReceipt(artifacts, receipt) {
|
|
|
1164
1643
|
payload: {
|
|
1165
1644
|
...payload,
|
|
1166
1645
|
diagnosticReceipt: receipt,
|
|
1646
|
+
...(receiptV2 ? { diagnosticReceiptV2: receiptV2 } : {}),
|
|
1167
1647
|
},
|
|
1168
1648
|
};
|
|
1169
1649
|
});
|
|
1170
1650
|
}
|
|
1171
|
-
function computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion) {
|
|
1651
|
+
function computeStepOutcome(route, result, evaluations, request, isClarify, clarifyQuestion, terminalOutcomeMessage) {
|
|
1172
1652
|
const fallback = defaultOutcome(route);
|
|
1173
1653
|
if (isClarify) {
|
|
1174
1654
|
return {
|
|
@@ -1195,10 +1675,21 @@ function computeStepOutcome(route, result, evaluations, request, isClarify, clar
|
|
|
1195
1675
|
trustState,
|
|
1196
1676
|
artifacts,
|
|
1197
1677
|
stopReason,
|
|
1198
|
-
summary:
|
|
1678
|
+
summary: status === "blocked"
|
|
1679
|
+
? terminalOutcomeMessage ?? blockingOutcomeSummary(evaluations, fallback.summary)
|
|
1680
|
+
: result.summary ?? fallback.summary,
|
|
1199
1681
|
...(result.answerTier ? { terminalTier: result.answerTier } : {}),
|
|
1200
1682
|
};
|
|
1201
1683
|
}
|
|
1684
|
+
function blockingOutcomeSummary(evaluations, fallback) {
|
|
1685
|
+
const messages = evaluations
|
|
1686
|
+
.filter((evaluation) => !evaluation.passed && evaluation.severity === 'blocking')
|
|
1687
|
+
.map((evaluation) => evaluation.message.trim())
|
|
1688
|
+
.filter(Boolean);
|
|
1689
|
+
return messages[0] ?? (fallback.includes('Answered')
|
|
1690
|
+
? 'The analytical result did not pass its required validation and was not accepted.'
|
|
1691
|
+
: fallback);
|
|
1692
|
+
}
|
|
1202
1693
|
function consumeRepeatedClarificationSelection(request, routeDecision, result) {
|
|
1203
1694
|
const selectedEvidenceId = request.selectedEvidenceId;
|
|
1204
1695
|
if (!selectedEvidenceId || result.status !== 'needs_clarification')
|
|
@@ -1282,7 +1773,7 @@ export function createDeterministicAgentRunPlanner() {
|
|
|
1282
1773
|
// Honor an explicit escalate target, else the route's default — then clamp for the audience.
|
|
1283
1774
|
const rawEscalation = requested ?? AGENT_RUN_ESCALATION_MAP[currentStep.route];
|
|
1284
1775
|
const escalationRoute = rawEscalation
|
|
1285
|
-
? (audience === "stakeholder" && ANALYST_ONLY_ROUTES.has(rawEscalation) ? "
|
|
1776
|
+
? (audience === "stakeholder" && ANALYST_ONLY_ROUTES.has(rawEscalation) ? "generated_answer" : rawEscalation)
|
|
1286
1777
|
: undefined;
|
|
1287
1778
|
const hint = action?.hint ?? failing.suggestedRepair ?? "Revise and retry.";
|
|
1288
1779
|
if (action?.kind === "escalate" && escalationRoute) {
|
|
@@ -1376,6 +1867,19 @@ function requestedModeToAction(mode) {
|
|
|
1376
1867
|
export function selectRoute(request, decision) {
|
|
1377
1868
|
if (decision.meaningResolutionErrorCode === 'invalid_evidence_reference')
|
|
1378
1869
|
return 'blocked';
|
|
1870
|
+
if (decision.action === 'block')
|
|
1871
|
+
return 'blocked';
|
|
1872
|
+
const authoritativePlan = decision.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
1873
|
+
? decision.resolvedAnalyticalPlan
|
|
1874
|
+
: undefined;
|
|
1875
|
+
// Producer invariant: the immutable plan is the authority. A caller cannot
|
|
1876
|
+
// combine an answer decision with blocked capability and suppress both the
|
|
1877
|
+
// typed clarification and modeling/policy diagnostic.
|
|
1878
|
+
if (authoritativePlan?.capability === 'blocked') {
|
|
1879
|
+
if (decision.requiresClarification === true)
|
|
1880
|
+
return 'clarify';
|
|
1881
|
+
return 'blocked';
|
|
1882
|
+
}
|
|
1379
1883
|
const explicitMode = request.requestedMode;
|
|
1380
1884
|
if (explicitMode === 'modeling')
|
|
1381
1885
|
return 'modeling_draft';
|
|
@@ -1384,9 +1888,21 @@ export function selectRoute(request, decision) {
|
|
|
1384
1888
|
if (explicitMode && explicitMode !== 'auto' && explicitMode !== 'ask') {
|
|
1385
1889
|
return selectCascadeRunRoute(request, decision);
|
|
1386
1890
|
}
|
|
1891
|
+
const plan = decision.resolvedAnalyticalPlan;
|
|
1892
|
+
// Research is an explicit request mode, never a phrase/category/depth
|
|
1893
|
+
// inference. Diagnosis and bounded exploration in ordinary Ask stay on the
|
|
1894
|
+
// review-required generated route over the same frozen plan.
|
|
1895
|
+
if (decision.action === 'investigate') {
|
|
1896
|
+
if (plan?.capability === 'certified_execution')
|
|
1897
|
+
return 'certified_answer';
|
|
1898
|
+
if (plan?.capability === 'semantic_execution')
|
|
1899
|
+
return 'semantic_answer';
|
|
1900
|
+
if (plan?.capability === 'governed_relational' || plan?.capability === 'bounded_exploration')
|
|
1901
|
+
return 'generated_answer';
|
|
1902
|
+
return decision.requiresClarification ? 'clarify' : 'blocked';
|
|
1903
|
+
}
|
|
1387
1904
|
if (decision.action !== 'answer')
|
|
1388
1905
|
return selectCascadeRunRoute(request, decision);
|
|
1389
|
-
const plan = decision.resolvedAnalyticalPlan;
|
|
1390
1906
|
if (plan?.mode === 'authoritative' && decision.requiresClarification !== true) {
|
|
1391
1907
|
if (plan.capability === 'certified_execution')
|
|
1392
1908
|
return 'certified_answer';
|
|
@@ -1395,7 +1911,7 @@ export function selectRoute(request, decision) {
|
|
|
1395
1911
|
if (plan.capability === 'governed_relational')
|
|
1396
1912
|
return 'generated_answer';
|
|
1397
1913
|
if (plan.capability === 'bounded_exploration')
|
|
1398
|
-
return '
|
|
1914
|
+
return 'generated_answer';
|
|
1399
1915
|
return 'blocked';
|
|
1400
1916
|
}
|
|
1401
1917
|
// Retrieval + meaning resolution already established a compatible execution
|
|
@@ -1412,8 +1928,9 @@ export function selectRoute(request, decision) {
|
|
|
1412
1928
|
}
|
|
1413
1929
|
function defaultExecutorResult(route, request, decision) {
|
|
1414
1930
|
const fallback = defaultOutcome(route);
|
|
1931
|
+
const terminalMessage = route === 'blocked' ? decision?.terminalOutcome?.message : undefined;
|
|
1415
1932
|
return {
|
|
1416
|
-
summary: fallback.summary,
|
|
1933
|
+
summary: terminalMessage ?? fallback.summary,
|
|
1417
1934
|
answer: route === "clarify" ? decision?.clarifyingQuestion : undefined,
|
|
1418
1935
|
evaluations: defaultEvaluations(route, request, decision),
|
|
1419
1936
|
artifacts: defaultArtifacts(route, {}, request),
|
|
@@ -1594,6 +2111,8 @@ function defaultArtifacts(route, result, request) {
|
|
|
1594
2111
|
}
|
|
1595
2112
|
}
|
|
1596
2113
|
function stopReasonFor(route, status, trustState, artifacts) {
|
|
2114
|
+
if (status === "cancelled")
|
|
2115
|
+
return "cancelled";
|
|
1597
2116
|
if (status === "blocked" || trustState === "blocked")
|
|
1598
2117
|
return "blocked";
|
|
1599
2118
|
if (route === "conversation")
|
|
@@ -1611,7 +2130,7 @@ function stopReasonFor(route, status, trustState, artifacts) {
|
|
|
1611
2130
|
return "generated_review_required";
|
|
1612
2131
|
}
|
|
1613
2132
|
function defaultNextActions(route, status) {
|
|
1614
|
-
if (status === "blocked")
|
|
2133
|
+
if (status === "blocked" || status === "cancelled")
|
|
1615
2134
|
return [];
|
|
1616
2135
|
if (route === "certified_answer") {
|
|
1617
2136
|
return [
|