@f5-sales-demo/xcsh 20.8.2 → 20.8.3
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/package.json +8 -8
- package/src/config/settings-schema.ts +15 -0
- package/src/index.ts +7 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/routing/classifier.ts +21 -6
- package/src/routing/coordinator.ts +1 -0
- package/src/routing/delegation.ts +23 -12
- package/src/routing/events.ts +3 -0
- package/src/routing/presets.ts +36 -20
- package/src/routing/profiler.ts +3 -1
- package/src/routing/resolver.ts +3 -1
- package/src/routing/types.ts +4 -2
- package/src/session/agent-session.ts +341 -183
- package/src/session/session-manager.ts +5 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "20.8.
|
|
4
|
+
"version": "20.8.3",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
63
63
|
"@mozilla/readability": "^0.6",
|
|
64
|
-
"@f5-sales-demo/xcsh-stats": "20.8.
|
|
65
|
-
"@f5-sales-demo/pi-agent-core": "20.8.
|
|
66
|
-
"@f5-sales-demo/pi-ai": "20.8.
|
|
67
|
-
"@f5-sales-demo/pi-natives": "20.8.
|
|
68
|
-
"@f5-sales-demo/pi-resource-management": "20.8.
|
|
69
|
-
"@f5-sales-demo/pi-tui": "20.8.
|
|
70
|
-
"@f5-sales-demo/pi-utils": "20.8.
|
|
64
|
+
"@f5-sales-demo/xcsh-stats": "20.8.3",
|
|
65
|
+
"@f5-sales-demo/pi-agent-core": "20.8.3",
|
|
66
|
+
"@f5-sales-demo/pi-ai": "20.8.3",
|
|
67
|
+
"@f5-sales-demo/pi-natives": "20.8.3",
|
|
68
|
+
"@f5-sales-demo/pi-resource-management": "20.8.3",
|
|
69
|
+
"@f5-sales-demo/pi-tui": "20.8.3",
|
|
70
|
+
"@f5-sales-demo/pi-utils": "20.8.3",
|
|
71
71
|
"@sinclair/typebox": "^0.34",
|
|
72
72
|
"@xterm/headless": "^6.0",
|
|
73
73
|
"ajv": "^8.20",
|
|
@@ -602,6 +602,21 @@ export const SETTINGS_SCHEMA = {
|
|
|
602
602
|
},
|
|
603
603
|
},
|
|
604
604
|
|
|
605
|
+
"routing.tierEffort": {
|
|
606
|
+
type: "record",
|
|
607
|
+
default: {
|
|
608
|
+
utility: "low",
|
|
609
|
+
balanced: "medium",
|
|
610
|
+
frontier: "high",
|
|
611
|
+
},
|
|
612
|
+
ui: {
|
|
613
|
+
tab: "model",
|
|
614
|
+
label: "Tier Effort Mapping",
|
|
615
|
+
description: "Map routing tiers to thinking effort levels",
|
|
616
|
+
submenu: true,
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
|
|
605
620
|
"routing.pools": {
|
|
606
621
|
type: "record",
|
|
607
622
|
default: {},
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,13 @@ export * from "./modes";
|
|
|
38
38
|
export * from "./modes/components";
|
|
39
39
|
// Theme utilities for custom tools
|
|
40
40
|
export * from "./modes/theme/theme";
|
|
41
|
+
export * from "./routing/classifier";
|
|
42
|
+
export * from "./routing/coordinator";
|
|
43
|
+
export * from "./routing/delegation";
|
|
44
|
+
export * from "./routing/presets";
|
|
45
|
+
export * from "./routing/profiler";
|
|
46
|
+
export * from "./routing/state-machine";
|
|
47
|
+
export type * from "./routing/types";
|
|
41
48
|
// SDK for programmatic usage
|
|
42
49
|
export * from "./sdk";
|
|
43
50
|
export * from "./session/agent-session";
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "20.8.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "20.8.3",
|
|
21
|
+
"commit": "a53c648993e3b94f865921929f85bdf491447000",
|
|
22
|
+
"shortCommit": "a53c648",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v20.8.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
24
|
+
"tag": "v20.8.3",
|
|
25
|
+
"commitDate": "2026-08-09T05:36:30Z",
|
|
26
|
+
"buildDate": "2026-08-09T06:08:18.709Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.8.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/a53c648993e3b94f865921929f85bdf491447000",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.8.3"
|
|
33
33
|
};
|
|
@@ -28,16 +28,31 @@ export async function classifyTaskHybrid(options: HybridClassifierOptions): Prom
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
|
-
|
|
31
|
+
let utilityModel = options.pool.tiers.utility;
|
|
32
|
+
if (!utilityModel.includes("/") && options.pool.provider) {
|
|
33
|
+
utilityModel = `${options.pool.provider}/${utilityModel}`;
|
|
34
|
+
}
|
|
32
35
|
const rawOutput = await runner(utilityModel, options.prompt);
|
|
33
36
|
|
|
34
37
|
const parsed = JSON.parse(rawOutput);
|
|
35
|
-
|
|
38
|
+
let score = typeof parsed.complexityScore === "number" ? parsed.complexityScore : 40;
|
|
39
|
+
// Enforce hard capability and validated outcome floors computed by deterministic rules
|
|
40
|
+
score = Math.max(score, baseProfile.complexityScore);
|
|
36
41
|
const confidence = typeof parsed.confidence === "number" ? parsed.confidence : 0;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
let delegation = parsed.delegation;
|
|
43
|
+
if (Array.isArray(delegation)) {
|
|
44
|
+
delegation = { subtasks: delegation };
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
delegation &&
|
|
48
|
+
typeof delegation === "object" &&
|
|
49
|
+
Array.isArray(delegation.subtasks) &&
|
|
50
|
+
delegation.subtasks.length > 0
|
|
51
|
+
) {
|
|
52
|
+
// keep delegation
|
|
53
|
+
} else {
|
|
54
|
+
delegation = undefined;
|
|
55
|
+
}
|
|
41
56
|
const routingUsage = typeof parsed.routingUsage === "number" ? parsed.routingUsage : undefined;
|
|
42
57
|
|
|
43
58
|
if (confidence < 0.75) {
|
|
@@ -22,8 +22,8 @@ export function validateDelegationPlan(plan: ReadOnlyDelegationPlan, maxTasks =
|
|
|
22
22
|
return { valid: false, error: "Invalid plan: missing subtasks array" };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
if (plan.subtasks.length
|
|
26
|
-
return { valid: false, error: "Plan
|
|
25
|
+
if (plan.subtasks.length <= 1) {
|
|
26
|
+
return { valid: false, error: "Plan requires >1 subtasks for delegation" };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
if (plan.subtasks.length > maxTasks) {
|
|
@@ -41,16 +41,19 @@ export function validateDelegationPlan(plan: ReadOnlyDelegationPlan, maxTasks =
|
|
|
41
41
|
|
|
42
42
|
export async function executeReadOnlyDelegationPlan(
|
|
43
43
|
plan: ReadOnlyDelegationPlan,
|
|
44
|
-
executor: (subtaskPrompt: string) => Promise<string>,
|
|
44
|
+
executor: (subtaskPrompt: string, signal?: AbortSignal) => Promise<{ result: string; tokens: number }>,
|
|
45
45
|
maxTasks = 3,
|
|
46
|
-
|
|
46
|
+
options?: { signal?: AbortSignal },
|
|
47
|
+
): Promise<{ results: Array<{ id: string; result: string }>; tokensUsed: number }> {
|
|
47
48
|
const validation = validateDelegationPlan(plan, maxTasks);
|
|
48
49
|
if (!validation.valid) {
|
|
49
|
-
return [];
|
|
50
|
+
return { results: [], tokensUsed: 0 };
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
const
|
|
53
|
-
|
|
53
|
+
const tasks = plan.subtasks.slice(0, maxTasks).map(async task => {
|
|
54
|
+
if (options?.signal?.aborted) {
|
|
55
|
+
return { id: task.id, result: "Failed: Aborted", tokens: 0 };
|
|
56
|
+
}
|
|
54
57
|
try {
|
|
55
58
|
const promptPayload = [
|
|
56
59
|
`Task: ${task.title}`,
|
|
@@ -62,11 +65,19 @@ export async function executeReadOnlyDelegationPlan(
|
|
|
62
65
|
]
|
|
63
66
|
.filter(Boolean)
|
|
64
67
|
.join("\n");
|
|
65
|
-
const
|
|
66
|
-
|
|
68
|
+
const { result, tokens } = await executor(promptPayload, options?.signal);
|
|
69
|
+
return { id: task.id, result, tokens };
|
|
67
70
|
} catch (err) {
|
|
68
|
-
|
|
71
|
+
return { id: task.id, result: `Failed: ${String(err)}`, tokens: 0 };
|
|
69
72
|
}
|
|
70
|
-
}
|
|
71
|
-
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const settled = await Promise.all(tasks);
|
|
76
|
+
let tokensUsed = 0;
|
|
77
|
+
const results = settled.map(s => {
|
|
78
|
+
tokensUsed += s.tokens;
|
|
79
|
+
return { id: s.id, result: s.result };
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { results, tokensUsed };
|
|
72
83
|
}
|
package/src/routing/events.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface RoutingEvent {
|
|
|
15
15
|
escalated?: boolean;
|
|
16
16
|
contextTokens?: number;
|
|
17
17
|
durationMs?: number;
|
|
18
|
+
state?: Partial<import("./state-machine").RoutingState>;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export function sanitizeRoutingEvent(event: Record<string, unknown>): RoutingEvent {
|
|
@@ -33,6 +34,7 @@ export function sanitizeRoutingEvent(event: Record<string, unknown>): RoutingEve
|
|
|
33
34
|
"escalated",
|
|
34
35
|
"contextTokens",
|
|
35
36
|
"durationMs",
|
|
37
|
+
"state",
|
|
36
38
|
]);
|
|
37
39
|
|
|
38
40
|
const sanitized: Record<string, unknown> = {};
|
|
@@ -57,6 +59,7 @@ export function sanitizeRoutingEvent(event: Record<string, unknown>): RoutingEve
|
|
|
57
59
|
escalated: sanitized.escalated as boolean | undefined,
|
|
58
60
|
contextTokens: sanitized.contextTokens as number | undefined,
|
|
59
61
|
durationMs: sanitized.durationMs as number | undefined,
|
|
62
|
+
state: sanitized.state as Partial<import("./state-machine").RoutingState> | undefined,
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
65
|
|
package/src/routing/presets.ts
CHANGED
|
@@ -5,36 +5,36 @@ export const BUILTIN_ROUTING_PRESETS: Record<string, RoutingPoolConfig> = {
|
|
|
5
5
|
id: "openai/gpt-5.6",
|
|
6
6
|
provider: "openai",
|
|
7
7
|
tiers: {
|
|
8
|
-
utility: "gpt-4
|
|
9
|
-
balanced: "gpt-4
|
|
10
|
-
frontier: "gpt-5-
|
|
8
|
+
utility: "gpt-5.4-mini",
|
|
9
|
+
balanced: "gpt-5.4",
|
|
10
|
+
frontier: "gpt-5.6-sol",
|
|
11
11
|
},
|
|
12
12
|
},
|
|
13
13
|
"anthropic/claude": {
|
|
14
14
|
id: "anthropic/claude",
|
|
15
15
|
provider: "anthropic",
|
|
16
16
|
tiers: {
|
|
17
|
-
utility: "claude-3
|
|
18
|
-
balanced: "claude-3
|
|
19
|
-
frontier: "claude-
|
|
17
|
+
utility: "claude-3-haiku-20240307",
|
|
18
|
+
balanced: "claude-3-5-sonnet-20241022",
|
|
19
|
+
frontier: "claude-opus-4-0",
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
"litellm/openai": {
|
|
23
23
|
id: "litellm/openai",
|
|
24
24
|
provider: "litellm",
|
|
25
25
|
tiers: {
|
|
26
|
-
utility: "
|
|
27
|
-
balanced: "
|
|
28
|
-
frontier: "sol",
|
|
26
|
+
utility: "gpt-5.4-mini",
|
|
27
|
+
balanced: "gpt-5.4",
|
|
28
|
+
frontier: "gpt-5.6-sol",
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
31
|
"litellm/anthropic": {
|
|
32
32
|
id: "litellm/anthropic",
|
|
33
33
|
provider: "litellm",
|
|
34
34
|
tiers: {
|
|
35
|
-
utility: "claude-3-5-haiku",
|
|
36
|
-
balanced: "claude-3-
|
|
37
|
-
frontier: "claude-opus-4-
|
|
35
|
+
utility: "claude-3-5-haiku-20241022",
|
|
36
|
+
balanced: "claude-3-5-sonnet-20241022",
|
|
37
|
+
frontier: "claude-opus-4-0",
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
};
|
|
@@ -68,7 +68,8 @@ export function validateCustomPools(pools: any): Record<string, RoutingPoolConfi
|
|
|
68
68
|
|
|
69
69
|
const allowMixed = typeof p.allowMixed === "boolean" ? p.allowMixed : false;
|
|
70
70
|
if (!allowMixed) {
|
|
71
|
-
const getPrefix = (model: string) =>
|
|
71
|
+
const getPrefix = (model: string) =>
|
|
72
|
+
model.includes("/") ? model.split("/")[0] : typeof p.provider === "string" ? p.provider : "";
|
|
72
73
|
const uPrefix = getPrefix(p.tiers.utility);
|
|
73
74
|
const bPrefix = getPrefix(p.tiers.balanced);
|
|
74
75
|
const fPrefix = getPrefix(p.tiers.frontier);
|
|
@@ -118,11 +119,24 @@ export function resolveModelPool(
|
|
|
118
119
|
for (const [poolId, pool] of Object.entries(customPools)) {
|
|
119
120
|
if (!pool?.tiers) continue;
|
|
120
121
|
if (disabledPresets.includes(poolId) || disabledPresets.includes(pool.id)) continue;
|
|
121
|
-
if (familyPolicy === "sticky"
|
|
122
|
+
if (familyPolicy === "sticky") {
|
|
123
|
+
if (pool.allowMixed) continue;
|
|
124
|
+
if (provider) {
|
|
125
|
+
const pProv = pool.provider ?? provider;
|
|
126
|
+
const uProv = pool.tiers.utility.includes("/") ? pool.tiers.utility.split("/")[0] : pProv;
|
|
127
|
+
const bProv = pool.tiers.balanced.includes("/") ? pool.tiers.balanced.split("/")[0] : pProv;
|
|
128
|
+
const fProv = pool.tiers.frontier.includes("/") ? pool.tiers.frontier.split("/")[0] : pProv;
|
|
129
|
+
if (uProv !== provider || bProv !== provider || fProv !== provider) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
122
134
|
if (provider && pool.provider && pool.provider !== provider && pool.id !== anchorModel) {
|
|
123
135
|
continue;
|
|
124
136
|
}
|
|
125
137
|
|
|
138
|
+
const qualify = (tier: string) => (tier.includes("/") ? tier : `${pool.provider ?? provider}/${tier}`);
|
|
139
|
+
|
|
126
140
|
if (
|
|
127
141
|
pool.id === anchorModel ||
|
|
128
142
|
pool.tiers.utility === anchorModel ||
|
|
@@ -131,9 +145,9 @@ export function resolveModelPool(
|
|
|
131
145
|
pool.tiers.utility === modelName ||
|
|
132
146
|
pool.tiers.balanced === modelName ||
|
|
133
147
|
pool.tiers.frontier === modelName ||
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
148
|
+
qualify(pool.tiers.utility) === anchorModel ||
|
|
149
|
+
qualify(pool.tiers.balanced) === anchorModel ||
|
|
150
|
+
qualify(pool.tiers.frontier) === anchorModel
|
|
137
151
|
) {
|
|
138
152
|
return pool;
|
|
139
153
|
}
|
|
@@ -146,14 +160,16 @@ export function resolveModelPool(
|
|
|
146
160
|
continue; // Provider mismatch!
|
|
147
161
|
}
|
|
148
162
|
|
|
163
|
+
const qualify = (tier: string) => (tier.includes("/") ? tier : `${pool.provider}/${tier}`);
|
|
164
|
+
|
|
149
165
|
if (
|
|
150
166
|
presetId === anchorModel ||
|
|
151
167
|
pool.tiers.utility === modelName ||
|
|
152
168
|
pool.tiers.balanced === modelName ||
|
|
153
169
|
pool.tiers.frontier === modelName ||
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
qualify(pool.tiers.utility) === anchorModel ||
|
|
171
|
+
qualify(pool.tiers.balanced) === anchorModel ||
|
|
172
|
+
qualify(pool.tiers.frontier) === anchorModel
|
|
157
173
|
) {
|
|
158
174
|
return pool;
|
|
159
175
|
}
|
package/src/routing/profiler.ts
CHANGED
|
@@ -19,13 +19,15 @@ const COMPLEX_KEYWORDS = [
|
|
|
19
19
|
"refactor the session",
|
|
20
20
|
"multi-region",
|
|
21
21
|
"system design",
|
|
22
|
+
"ambiguity",
|
|
23
|
+
"target",
|
|
22
24
|
];
|
|
23
25
|
|
|
24
26
|
const SIMPLE_READ_PATTERNS = [/^(summarize|read|cat|view|explain)\b/i, /\bfix typo\b/i, /\btypo in line\b/i];
|
|
25
27
|
|
|
26
28
|
export function profileTaskDeterministic(input: ProfilerInput): TaskProfile {
|
|
27
29
|
const reasons: RoutingReasonCode[] = [];
|
|
28
|
-
let score =
|
|
30
|
+
let score = 30; // base score (default balanced)
|
|
29
31
|
|
|
30
32
|
const promptLower = input.prompt.toLowerCase().trim();
|
|
31
33
|
|
package/src/routing/resolver.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface ResolveTierResult {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface ResolveTierOptions {
|
|
12
|
-
contextEstimate?: { usedTokens: number; contextWindow: number };
|
|
12
|
+
contextEstimate?: { usedTokens: number; contextWindow: number; reserveTokens?: number };
|
|
13
13
|
getModelContextWindow?: (modelId: string) => number;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -43,6 +43,7 @@ export function resolveTierModel(
|
|
|
43
43
|
!checkCandidateContextEligible({
|
|
44
44
|
estimatedInputTokens: options.contextEstimate.usedTokens,
|
|
45
45
|
candidateContextWindow: candidateWin,
|
|
46
|
+
reserveTokens: options.contextEstimate.reserveTokens,
|
|
46
47
|
})
|
|
47
48
|
) {
|
|
48
49
|
return false;
|
|
@@ -64,6 +65,7 @@ export function resolveTierModel(
|
|
|
64
65
|
!checkCandidateContextEligible({
|
|
65
66
|
estimatedInputTokens: options.contextEstimate.usedTokens,
|
|
66
67
|
candidateContextWindow: candidateWin,
|
|
68
|
+
reserveTokens: options.contextEstimate.reserveTokens,
|
|
67
69
|
})
|
|
68
70
|
) {
|
|
69
71
|
return false;
|
package/src/routing/types.ts
CHANGED
|
@@ -26,7 +26,8 @@ export type RoutingReasonCode =
|
|
|
26
26
|
| "lint_failure"
|
|
27
27
|
| "context_capacity_promotion"
|
|
28
28
|
| "downshift_hysteresis_pending"
|
|
29
|
-
| "escalation_floor_active"
|
|
29
|
+
| "escalation_floor_active"
|
|
30
|
+
| "retry_fallback";
|
|
30
31
|
|
|
31
32
|
export interface ReadOnlyDelegationSubtask {
|
|
32
33
|
id: string;
|
|
@@ -84,7 +85,7 @@ export interface RoutingDecision {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export interface RoutingOutcomeEvidence {
|
|
87
|
-
kind: "test_failure" | "build_failure" | "lint_failure" | "user_feedback";
|
|
88
|
+
kind: "test_failure" | "build_failure" | "lint_failure" | "user_feedback" | "retry_fallback";
|
|
88
89
|
summary: string;
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -105,4 +106,5 @@ export interface RoutingSettings {
|
|
|
105
106
|
|
|
106
107
|
pools: Record<string, RoutingPoolConfig>;
|
|
107
108
|
disabledPresets: string[];
|
|
109
|
+
tierEffort?: Record<string, string>;
|
|
108
110
|
}
|
|
@@ -294,6 +294,8 @@ export interface PromptOptions {
|
|
|
294
294
|
* the provider `onPayload` seam (per-turn, no client-side tool registration).
|
|
295
295
|
*/
|
|
296
296
|
serverTools?: Record<string, unknown>[];
|
|
297
|
+
/** Optional abort signal for early cancellation */
|
|
298
|
+
signal?: AbortSignal;
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
/** Result from cycleModel() */
|
|
@@ -756,6 +758,17 @@ export class AgentSession {
|
|
|
756
758
|
void this.#queueExtensionEvent(event);
|
|
757
759
|
return;
|
|
758
760
|
}
|
|
761
|
+
|
|
762
|
+
if (
|
|
763
|
+
event.type === "routing_decision" ||
|
|
764
|
+
event.type === "routing_applied" ||
|
|
765
|
+
event.type === "routing_escalated" ||
|
|
766
|
+
event.type === "routing_skipped" ||
|
|
767
|
+
(event.type as string) === "routing_floor_cleared"
|
|
768
|
+
) {
|
|
769
|
+
this.sessionManager.appendCustomEntry("routing_event", event);
|
|
770
|
+
}
|
|
771
|
+
|
|
759
772
|
await this.#emitExtensionEvent(event);
|
|
760
773
|
this.#emit(event);
|
|
761
774
|
}
|
|
@@ -1284,34 +1297,98 @@ export class AgentSession {
|
|
|
1284
1297
|
this.#routingCoordinator.restoreState(state);
|
|
1285
1298
|
}
|
|
1286
1299
|
public recordRoutingOutcome(outcome: RoutingOutcome): void {
|
|
1300
|
+
const routingMode = this.settings.get("routing.mode") as import("../routing/types").RoutingMode;
|
|
1287
1301
|
if (outcome.status === "rejected") {
|
|
1288
1302
|
const hasUserFeedback = outcome.evidence.some(e => e.kind === "user_feedback");
|
|
1289
1303
|
const hasTestFailure = outcome.evidence.some(e => e.kind === "test_failure");
|
|
1290
1304
|
const hasBuildFailure = outcome.evidence.some(e => e.kind === "build_failure");
|
|
1291
1305
|
const hasLintFailure = outcome.evidence.some(e => e.kind === "lint_failure");
|
|
1306
|
+
const hasRetryFallback = outcome.evidence.some(e => e.kind === "retry_fallback");
|
|
1292
1307
|
|
|
1293
1308
|
const reasons: import("../routing/types").RoutingReasonCode[] = [];
|
|
1294
1309
|
if (hasUserFeedback) reasons.push("prior_rejection");
|
|
1295
1310
|
if (hasTestFailure) reasons.push("test_failure");
|
|
1296
1311
|
if (hasBuildFailure) reasons.push("build_failure");
|
|
1297
1312
|
if (hasLintFailure) reasons.push("lint_failure");
|
|
1313
|
+
if (hasRetryFallback) reasons.push("retry_fallback");
|
|
1298
1314
|
|
|
1299
|
-
|
|
1315
|
+
if (reasons.length === 0) return;
|
|
1300
1316
|
|
|
1301
1317
|
const routingState = this.#routingCoordinator.getState();
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1318
|
+
let targetTier: import("../routing/types").RoutingTier = "frontier";
|
|
1319
|
+
if (outcome.safeToContinue) {
|
|
1320
|
+
if (routingState.currentTier === "utility") targetTier = "balanced";
|
|
1321
|
+
else if (routingState.currentTier === "balanced") targetTier = "frontier";
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (routingMode === "auto") {
|
|
1325
|
+
const performEscalationModelSwap = async () => {
|
|
1326
|
+
try {
|
|
1327
|
+
const { resolveModelPool } = await import("../routing/presets");
|
|
1328
|
+
const poolId = this.settings.get("routing.pools") as any;
|
|
1329
|
+
const disabledPresets = this.settings.get("routing.disabledPresets") as readonly string[];
|
|
1330
|
+
const familyPolicy = this.settings.get("routing.familyPolicy") as any;
|
|
1331
|
+
const pool = resolveModelPool(
|
|
1332
|
+
this.model ? `${this.model.provider}/${this.model.id}` : "",
|
|
1333
|
+
poolId,
|
|
1334
|
+
disabledPresets,
|
|
1335
|
+
familyPolicy,
|
|
1336
|
+
);
|
|
1337
|
+
if (!pool) return;
|
|
1338
|
+
const modelId = pool.tiers[targetTier];
|
|
1339
|
+
if (!modelId) return;
|
|
1340
|
+
const uId = modelId.includes("/") ? modelId : `${pool.provider}/${modelId}`;
|
|
1341
|
+
const targetModel = this.#modelRegistry
|
|
1342
|
+
.getAvailable()
|
|
1343
|
+
.find(m => `${m.provider}/${m.id}` === uId || m.id === uId);
|
|
1344
|
+
|
|
1345
|
+
if (!targetModel) {
|
|
1346
|
+
console.warn(`Escalation aborted: target model ${uId} not available in registry.`);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
if (outcome.safeToContinue) {
|
|
1351
|
+
this.agent.abort();
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
this.#routingCoordinator.getStateMachine().setEscalationFloor(targetTier);
|
|
1355
|
+
if (outcome.safeToContinue) {
|
|
1356
|
+
this.#routingCoordinator.restoreState({ currentTier: targetTier });
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
await this.setModelRoutingSwitch(targetModel);
|
|
1360
|
+
const effortMap = this.settings.get("routing.tierEffort") as Record<string, string> | undefined;
|
|
1361
|
+
const { mapTierToEffort } = await import("../routing/effort");
|
|
1362
|
+
const effort = mapTierToEffort(targetTier, effortMap);
|
|
1363
|
+
this.setThinkingLevel(effort as any);
|
|
1364
|
+
|
|
1365
|
+
this.#emitSessionEvent(
|
|
1366
|
+
sanitizeRoutingEvent({
|
|
1367
|
+
type: "routing_escalated",
|
|
1368
|
+
epochId: `route-${Date.now()}-esc`,
|
|
1369
|
+
reasons: reasons,
|
|
1370
|
+
mode: routingMode,
|
|
1371
|
+
effectiveTier: targetTier,
|
|
1372
|
+
state: this.#routingCoordinator.getState(),
|
|
1373
|
+
selectedModel: this.model ? `${this.model.provider}/${this.model.id}` : undefined,
|
|
1374
|
+
escalated: true,
|
|
1375
|
+
}),
|
|
1376
|
+
).catch(() => {});
|
|
1377
|
+
|
|
1378
|
+
if (outcome.safeToContinue) {
|
|
1379
|
+
this.#scheduleAgentContinue({ delayMs: 10 });
|
|
1380
|
+
}
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
console.error("Escalation model swap failed", err);
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
this.#trackPostPromptTask(performEscalationModelSwap());
|
|
1386
|
+
}
|
|
1313
1387
|
} else {
|
|
1314
|
-
|
|
1388
|
+
if (routingMode === "auto") {
|
|
1389
|
+
this.#routingCoordinator.getStateMachine().clearEscalationFloor();
|
|
1390
|
+
this.#emitSessionEvent({ type: "routing_floor_cleared" } as any).catch(() => {});
|
|
1391
|
+
}
|
|
1315
1392
|
}
|
|
1316
1393
|
}
|
|
1317
1394
|
/**
|
|
@@ -2405,7 +2482,9 @@ export class AgentSession {
|
|
|
2405
2482
|
}
|
|
2406
2483
|
|
|
2407
2484
|
buildDisplaySessionContext(): SessionContext {
|
|
2408
|
-
|
|
2485
|
+
const context = deobfuscateSessionContext(this.sessionManager.buildSessionContext(), this.#obfuscator);
|
|
2486
|
+
context.usedTokens = calculateUsedTokens(context.messages);
|
|
2487
|
+
return context;
|
|
2409
2488
|
}
|
|
2410
2489
|
|
|
2411
2490
|
/** Convert session messages using the same pre-LLM pipeline as the active session. */
|
|
@@ -2713,172 +2792,242 @@ export class AgentSession {
|
|
|
2713
2792
|
// Evaluate dynamic model routing if enabled
|
|
2714
2793
|
const routingMode = (this.settings.get("routing.mode") as RoutingMode) ?? "off";
|
|
2715
2794
|
if (routingMode !== "off" && this.model) {
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
resolved
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2795
|
+
if (this.#activeRetryFallback) {
|
|
2796
|
+
this.#emitSessionEvent({
|
|
2797
|
+
type: "routing_skipped",
|
|
2798
|
+
epochId: `skip-${Date.now()}`,
|
|
2799
|
+
reasons: ["retry_fallback"],
|
|
2800
|
+
} as any).catch(() => {});
|
|
2801
|
+
} else {
|
|
2802
|
+
const anchorModel = `${this.model.provider}/${this.model.id}`;
|
|
2803
|
+
const availableModels =
|
|
2804
|
+
this.#scopedModels.length > 0
|
|
2805
|
+
? this.#scopedModels.map(sm => `${sm.model.provider}/${sm.model.id}`)
|
|
2806
|
+
: this.#modelRegistry.getAvailable().map(m => `${m.provider}/${m.id}`);
|
|
2807
|
+
const startTime = Date.now();
|
|
2808
|
+
const systemTokens = Math.round((this.systemPrompt?.length ?? 0) / 4);
|
|
2809
|
+
const promptTokens = Math.round(expandedText.length / 4);
|
|
2810
|
+
const toolsStr = JSON.stringify(Array.from(this.#toolRegistry.values()));
|
|
2811
|
+
const toolsTokens = Math.round(toolsStr.length / 4);
|
|
2812
|
+
const reserveTokens = (this.settings.get("compaction.reserveTokens") as number) ?? 0;
|
|
2813
|
+
const contextEstimate = {
|
|
2814
|
+
usedTokens: calculateUsedTokens(this.messages) + systemTokens + promptTokens + toolsTokens,
|
|
2815
|
+
reserveTokens,
|
|
2816
|
+
contextWindow: this.model.contextWindow ?? 128000,
|
|
2817
|
+
};
|
|
2818
|
+
const decision = await this.#routingCoordinator.evaluateTurn({
|
|
2819
|
+
anchorModel,
|
|
2820
|
+
mode: routingMode,
|
|
2821
|
+
prompt: expandedText,
|
|
2822
|
+
hasImages: options?.images && options.images.length > 0,
|
|
2823
|
+
priorRejection: this.#routingCoordinator.getState().escalationFloor !== undefined,
|
|
2824
|
+
availableModels,
|
|
2825
|
+
customPools: validateCustomPools(this.settings.get("routing.pools")),
|
|
2826
|
+
disabledPresets: (this.settings.get("routing.disabledPresets") as readonly string[]) ?? [],
|
|
2827
|
+
familyPolicy: (this.settings.get("routing.familyPolicy") as "sticky" | "configured-mixed") ?? "sticky",
|
|
2828
|
+
profilerMode: (this.settings.get("routing.profiler") as any) ?? "hybrid",
|
|
2829
|
+
contextEstimate,
|
|
2830
|
+
getModelContextWindow: (modelId: string) => {
|
|
2831
|
+
const found = this.#modelRegistry
|
|
2832
|
+
.getAll()
|
|
2833
|
+
.find(m => `${m.provider}/${m.id}` === modelId || m.id === modelId);
|
|
2834
|
+
return found?.contextWindow ?? 128000;
|
|
2835
|
+
},
|
|
2836
|
+
runRoutingClassifier: async (utilityModel: string, promptText: string) => {
|
|
2837
|
+
const resolved = this.#modelRegistry
|
|
2838
|
+
.getAll()
|
|
2839
|
+
.find(m => `${m.provider}/${m.id}` === utilityModel || m.id === utilityModel);
|
|
2840
|
+
if (!resolved) throw new Error("Classifier model not found");
|
|
2841
|
+
const apiKey = await this.#modelRegistry.getApiKey(resolved, this.sessionId);
|
|
2842
|
+
if (!apiKey) throw new Error("No API key for classifier model");
|
|
2843
|
+
|
|
2844
|
+
const systemInstruction = `You are a routing classifier. Analyze the user's prompt and output a JSON object: { "complexityScore": number, "confidence": number, "delegation"?: { "reason": string, "subtasks": { "id": string, "title": string, "description": string, "targetFilesOrPaths": string[], "desiredTier"?: string }[] } }. Score complexity from 0 to 100 based on required reasoning, context width, and coding effort. Return ONLY valid JSON without markdown blocks.`;
|
|
2845
|
+
|
|
2846
|
+
const res = await completeSimple(
|
|
2847
|
+
resolved,
|
|
2848
|
+
{
|
|
2849
|
+
systemPrompt: systemInstruction,
|
|
2850
|
+
messages: [
|
|
2851
|
+
{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() },
|
|
2852
|
+
],
|
|
2853
|
+
},
|
|
2854
|
+
{ apiKey, signal: AbortSignal.timeout(15000) },
|
|
2855
|
+
);
|
|
2856
|
+
const rawText = res.content
|
|
2857
|
+
.filter(c => c.type === "text")
|
|
2858
|
+
.map(c => (c as TextContent).text)
|
|
2859
|
+
.join("");
|
|
2769
2860
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2861
|
+
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
|
|
2862
|
+
if (jsonMatch) {
|
|
2863
|
+
const parsed = JSON.parse(jsonMatch[0]); // Check validity
|
|
2864
|
+
if (res.usage) {
|
|
2865
|
+
this.settings.getStorage()?.recordModelUsage(`${resolved.provider}/${resolved.id}`);
|
|
2866
|
+
parsed.routingUsage = (res.usage as any).totalTokens ?? (res.usage as any).total ?? 0;
|
|
2867
|
+
}
|
|
2868
|
+
return JSON.stringify(parsed);
|
|
2869
|
+
}
|
|
2870
|
+
throw new Error("Classifier returned malformed JSON");
|
|
2871
|
+
},
|
|
2872
|
+
downshiftAfterTurns: (this.settings.get("routing.downshiftAfterTurns") as number) ?? 2,
|
|
2873
|
+
});
|
|
2874
|
+
const durationMs = Date.now() - startTime;
|
|
2875
|
+
|
|
2876
|
+
this.#emitSessionEvent({
|
|
2877
|
+
type: decision.applied ? "routing_applied" : "routing_decision",
|
|
2878
|
+
epochId: decision.epochId,
|
|
2879
|
+
mode: decision.mode,
|
|
2880
|
+
provider: this.model.provider,
|
|
2881
|
+
poolId: decision.poolId,
|
|
2882
|
+
effectiveTier: decision.effectiveTier,
|
|
2883
|
+
selectedModel: decision.selectedModel,
|
|
2884
|
+
reasons: decision.reasons,
|
|
2885
|
+
delegated: !!decision.delegation,
|
|
2886
|
+
escalated: decision.reasons.some(
|
|
2887
|
+
r => r === "context_capacity_promotion" || r === "escalation_floor_active",
|
|
2888
|
+
),
|
|
2889
|
+
contextTokens: contextEstimate.usedTokens,
|
|
2890
|
+
routingUsage: decision.routingUsage,
|
|
2891
|
+
durationMs,
|
|
2892
|
+
state: this.#routingCoordinator.getState(),
|
|
2893
|
+
} as any).catch(() => {});
|
|
2894
|
+
|
|
2895
|
+
if (decision.selectedModel && decision.applied) {
|
|
2896
|
+
const targetModel = this.#modelRegistry
|
|
2897
|
+
.getAvailable()
|
|
2898
|
+
.find(m => `${m.provider}/${m.id}` === decision.selectedModel || m.id === decision.selectedModel);
|
|
2899
|
+
if (targetModel) {
|
|
2900
|
+
const currentModelId = this.model ? `${this.model.provider}/${this.model.id}` : undefined;
|
|
2901
|
+
const targetModelId = `${targetModel.provider}/${targetModel.id}`;
|
|
2902
|
+
if (currentModelId !== targetModelId) {
|
|
2903
|
+
await this.setModelRoutingSwitch(targetModel);
|
|
2904
|
+
}
|
|
2905
|
+
if (decision.effectiveTier) {
|
|
2906
|
+
const effortMap = this.settings.get("routing.tierEffort") as Record<string, string> | undefined;
|
|
2907
|
+
const { mapTierToEffort } = await import("../routing/effort");
|
|
2908
|
+
const effort = mapTierToEffort(decision.effectiveTier, effortMap);
|
|
2909
|
+
this.setThinkingLevel(effort as any);
|
|
2776
2910
|
}
|
|
2777
|
-
return JSON.stringify(parsed);
|
|
2778
2911
|
}
|
|
2779
|
-
throw new Error("Classifier returned malformed JSON");
|
|
2780
|
-
},
|
|
2781
|
-
downshiftAfterTurns: (this.settings.get("routing.downshiftAfterTurns") as number) ?? 2,
|
|
2782
|
-
});
|
|
2783
|
-
const durationMs = Date.now() - startTime;
|
|
2784
|
-
|
|
2785
|
-
this.sessionManager.appendCustomEntry("routing_state", this.#routingCoordinator.getState());
|
|
2786
|
-
|
|
2787
|
-
this.#emitSessionEvent({
|
|
2788
|
-
type: decision.applied ? "routing_applied" : "routing_decision",
|
|
2789
|
-
epochId: decision.epochId,
|
|
2790
|
-
mode: decision.mode,
|
|
2791
|
-
provider: this.model.provider,
|
|
2792
|
-
poolId: decision.poolId,
|
|
2793
|
-
effectiveTier: decision.effectiveTier,
|
|
2794
|
-
selectedModel: decision.selectedModel,
|
|
2795
|
-
reasons: decision.reasons,
|
|
2796
|
-
delegated: !!decision.delegation,
|
|
2797
|
-
escalated: decision.reasons.some(
|
|
2798
|
-
r => r === "context_capacity_promotion" || r === "escalation_floor_active",
|
|
2799
|
-
),
|
|
2800
|
-
contextTokens: contextEstimate.usedTokens,
|
|
2801
|
-
routingUsage: decision.routingUsage,
|
|
2802
|
-
durationMs,
|
|
2803
|
-
} as any).catch(() => {});
|
|
2804
|
-
|
|
2805
|
-
if (decision.applied && decision.selectedModel && decision.selectedModel !== anchorModel) {
|
|
2806
|
-
const targetModel = this.#modelRegistry
|
|
2807
|
-
.getAvailable()
|
|
2808
|
-
.find(m => `${m.provider}/${m.id}` === decision.selectedModel || m.id === decision.selectedModel);
|
|
2809
|
-
if (targetModel) {
|
|
2810
|
-
await this.setModelRoutingSwitch(targetModel);
|
|
2811
2912
|
}
|
|
2812
|
-
}
|
|
2813
2913
|
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2914
|
+
if (
|
|
2915
|
+
decision.applied &&
|
|
2916
|
+
decision.delegation &&
|
|
2917
|
+
decision.delegation.subtasks.length > 1 &&
|
|
2918
|
+
(this.settings.get("routing.delegation") as string) === "read-only"
|
|
2919
|
+
) {
|
|
2920
|
+
let maxTasks = (this.settings.get("routing.delegationMaxTasks") as number) ?? 3;
|
|
2921
|
+
maxTasks = Math.min(Math.max(1, maxTasks), 3);
|
|
2922
|
+
|
|
2923
|
+
const { results, tokensUsed } = await executeReadOnlyDelegationPlan(
|
|
2924
|
+
decision.delegation,
|
|
2925
|
+
async (subtaskPrompt, signal) => {
|
|
2926
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2927
|
+
|
|
2928
|
+
const { resolveModelPool } = await import("../routing/presets");
|
|
2929
|
+
const pool = resolveModelPool(
|
|
2930
|
+
decision.poolId ?? (this.model ? `${this.model.provider}/${this.model.id}` : ""),
|
|
2931
|
+
this.settings.get("routing.pools") as any,
|
|
2932
|
+
this.settings.get("routing.disabledPresets") as readonly string[],
|
|
2933
|
+
this.settings.get("routing.familyPolicy") as any,
|
|
2934
|
+
);
|
|
2935
|
+
|
|
2936
|
+
let resolvedUtility = this.model;
|
|
2937
|
+
if (pool?.tiers?.utility) {
|
|
2938
|
+
let uId = pool.tiers.utility;
|
|
2939
|
+
if (!uId.includes("/") && pool.provider) {
|
|
2940
|
+
uId = `${pool.provider}/${uId}`;
|
|
2941
|
+
}
|
|
2942
|
+
const found = this.#modelRegistry
|
|
2828
2943
|
.getAvailable()
|
|
2829
|
-
.find(m => `${m.provider}/${m.id}` ===
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
const { createAgentSession } = await import("../sdk");
|
|
2834
|
-
const { SessionManager: SDKSessionManager } = await import("./session-manager");
|
|
2835
|
-
const childSettings = await this.settings.cloneForCwd(process.cwd());
|
|
2836
|
-
childSettings.set("routing.delegation", "off");
|
|
2837
|
-
const { session: childSession } = await createAgentSession({
|
|
2838
|
-
model: resolvedUtility,
|
|
2839
|
-
sessionManager: SDKSessionManager.inMemory(),
|
|
2840
|
-
settings: childSettings,
|
|
2841
|
-
authStorage: this.#modelRegistry.authStorage,
|
|
2842
|
-
modelRegistry: this.#modelRegistry,
|
|
2843
|
-
toolNames: Array.from(this.#toolRegistry.values())
|
|
2844
|
-
.map(t => t.name)
|
|
2845
|
-
.filter(isDelegationAllowedTool),
|
|
2846
|
-
enableLsp: false,
|
|
2847
|
-
enableMCP: false,
|
|
2848
|
-
});
|
|
2944
|
+
.find(m => `${m.provider}/${m.id}` === uId || m.id === uId);
|
|
2945
|
+
if (found) resolvedUtility = found;
|
|
2946
|
+
}
|
|
2849
2947
|
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2948
|
+
if (!resolvedUtility) return { result: "Delegation failed: no model.", tokens: 0 };
|
|
2949
|
+
|
|
2950
|
+
const { createAgentSession } = await import("../sdk");
|
|
2951
|
+
const { SessionManager: SDKSessionManager } = await import("./session-manager");
|
|
2952
|
+
const childSettings = await this.settings.cloneForCwd(process.cwd());
|
|
2953
|
+
childSettings.set("routing.delegation", "off");
|
|
2954
|
+
|
|
2955
|
+
// Fix abort race by checking right before creation
|
|
2956
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2957
|
+
|
|
2958
|
+
const { session: childSession } = await createAgentSession({
|
|
2959
|
+
model: resolvedUtility,
|
|
2960
|
+
sessionManager: SDKSessionManager.inMemory(),
|
|
2961
|
+
settings: childSettings,
|
|
2962
|
+
authStorage: this.#modelRegistry.authStorage,
|
|
2963
|
+
modelRegistry: this.#modelRegistry,
|
|
2964
|
+
toolNames: Array.from(this.#toolRegistry.values())
|
|
2965
|
+
.map(t => t.name)
|
|
2966
|
+
.filter(isDelegationAllowedTool),
|
|
2967
|
+
enableLsp: false,
|
|
2968
|
+
enableMCP: false,
|
|
2969
|
+
});
|
|
2970
|
+
|
|
2971
|
+
try {
|
|
2972
|
+
const onAbort = () => childSession.agent.abort();
|
|
2973
|
+
signal?.addEventListener("abort", onAbort);
|
|
2974
|
+
if (signal?.aborted) {
|
|
2975
|
+
onAbort();
|
|
2976
|
+
return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2977
|
+
}
|
|
2978
|
+
try {
|
|
2979
|
+
await childSession.prompt(subtaskPrompt);
|
|
2980
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2981
|
+
const resultStr = childSession
|
|
2982
|
+
.buildDisplaySessionContext()
|
|
2983
|
+
.messages.filter(m => m.role === "assistant")
|
|
2984
|
+
.map(m =>
|
|
2985
|
+
m.content
|
|
2986
|
+
.filter((c: any) => c.type === "text")
|
|
2987
|
+
.map((c: any) => c.text)
|
|
2988
|
+
.join(""),
|
|
2989
|
+
)
|
|
2990
|
+
.join("\n");
|
|
2991
|
+
const tokens = childSession.buildDisplaySessionContext().usedTokens || 0;
|
|
2992
|
+
return { result: resultStr, tokens };
|
|
2993
|
+
} finally {
|
|
2994
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2995
|
+
}
|
|
2996
|
+
} catch (err) {
|
|
2997
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2998
|
+
return { result: `Delegation failed: ${String(err)}`, tokens: 0 };
|
|
2999
|
+
} finally {
|
|
3000
|
+
await childSession.dispose();
|
|
3001
|
+
}
|
|
3002
|
+
},
|
|
3003
|
+
maxTasks,
|
|
3004
|
+
{ signal: options?.signal },
|
|
3005
|
+
);
|
|
3006
|
+
|
|
3007
|
+
if (results.length > 0) {
|
|
3008
|
+
let resultsString = JSON.stringify(results, null, 2);
|
|
3009
|
+
if (resultsString.length > 8000) {
|
|
3010
|
+
resultsString = `${resultsString.substring(0, 8000)}\n...[truncated]`;
|
|
3011
|
+
}
|
|
3012
|
+
const delegationOutput = `\n\n<delegation_results>\n${resultsString}\n</delegation_results>`;
|
|
3013
|
+
expandedText += delegationOutput;
|
|
3014
|
+
const textBlock = userContent.find(c => c.type === "text") as Extract<
|
|
3015
|
+
(typeof userContent)[0],
|
|
3016
|
+
{ type: "text" }
|
|
3017
|
+
>;
|
|
3018
|
+
if (textBlock) {
|
|
3019
|
+
textBlock.text += delegationOutput;
|
|
3020
|
+
} else {
|
|
3021
|
+
userContent.push({ type: "text", text: delegationOutput });
|
|
2866
3022
|
}
|
|
2867
|
-
},
|
|
2868
|
-
maxTasks,
|
|
2869
|
-
);
|
|
2870
3023
|
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
if (textBlock) {
|
|
2879
|
-
textBlock.text += delegationOutput;
|
|
2880
|
-
} else {
|
|
2881
|
-
userContent.push({ type: "text", text: delegationOutput });
|
|
3024
|
+
this.#emitSessionEvent({
|
|
3025
|
+
type: "routing_delegated",
|
|
3026
|
+
epochId: decision.epochId,
|
|
3027
|
+
tasks: decision.delegation.subtasks.length,
|
|
3028
|
+
completed: results.length,
|
|
3029
|
+
tokensUsed,
|
|
3030
|
+
} as any).catch(() => {});
|
|
2882
3031
|
}
|
|
2883
3032
|
}
|
|
2884
3033
|
}
|
|
@@ -5707,11 +5856,6 @@ export class AgentSession {
|
|
|
5707
5856
|
this.#activeRetryFallback.lastAppliedFallbackThinkingLevel = nextThinkingLevel;
|
|
5708
5857
|
}
|
|
5709
5858
|
|
|
5710
|
-
this.recordRoutingOutcome({
|
|
5711
|
-
status: "rejected",
|
|
5712
|
-
evidence: [{ kind: "test_failure", summary: `Fallback applied from ${currentSelector} to ${selector.raw}` }],
|
|
5713
|
-
});
|
|
5714
|
-
|
|
5715
5859
|
await this.#emitSessionEvent({
|
|
5716
5860
|
type: "retry_fallback_applied",
|
|
5717
5861
|
from: currentSelector,
|
|
@@ -6459,13 +6603,27 @@ export class AgentSession {
|
|
|
6459
6603
|
}
|
|
6460
6604
|
|
|
6461
6605
|
#syncRoutingStateFromBranch() {
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
)
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6606
|
+
this.#routingCoordinator.reset();
|
|
6607
|
+
const entries = this.sessionManager.getBranch();
|
|
6608
|
+
for (const entry of entries) {
|
|
6609
|
+
if (entry.type === "custom" && entry.customType === "routing_event") {
|
|
6610
|
+
const event = entry.data as any;
|
|
6611
|
+
if (event.state) {
|
|
6612
|
+
this.#routingCoordinator.restoreState(event.state);
|
|
6613
|
+
} else {
|
|
6614
|
+
if (event.type === "routing_escalated") {
|
|
6615
|
+
if (event.effectiveTier) {
|
|
6616
|
+
this.#routingCoordinator.getStateMachine().setEscalationFloor(event.effectiveTier);
|
|
6617
|
+
}
|
|
6618
|
+
} else if (event.type === "routing_floor_cleared") {
|
|
6619
|
+
this.#routingCoordinator.getStateMachine().clearEscalationFloor();
|
|
6620
|
+
} else if (event.type === "routing_applied" || event.type === "routing_decision") {
|
|
6621
|
+
if (event.effectiveTier) {
|
|
6622
|
+
this.#routingCoordinator.restoreState({ currentTier: event.effectiveTier });
|
|
6623
|
+
}
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6469
6627
|
}
|
|
6470
6628
|
}
|
|
6471
6629
|
|
|
@@ -261,6 +261,8 @@ export interface SessionContext {
|
|
|
261
261
|
mode: string;
|
|
262
262
|
/** Mode-specific data from the last mode_change entry */
|
|
263
263
|
modeData?: Record<string, unknown>;
|
|
264
|
+
/** Number of tokens used across all provider requests on this branch */
|
|
265
|
+
usedTokens: number;
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
export interface SessionInfo {
|
|
@@ -532,6 +534,7 @@ export function buildSessionContext(
|
|
|
532
534
|
selectedMCPToolNames: [],
|
|
533
535
|
hasPersistedMCPToolSelection: false,
|
|
534
536
|
mode: "none",
|
|
537
|
+
usedTokens: 0,
|
|
535
538
|
};
|
|
536
539
|
}
|
|
537
540
|
if (leafId) {
|
|
@@ -552,6 +555,7 @@ export function buildSessionContext(
|
|
|
552
555
|
selectedMCPToolNames: [],
|
|
553
556
|
hasPersistedMCPToolSelection: false,
|
|
554
557
|
mode: "none",
|
|
558
|
+
usedTokens: 0,
|
|
555
559
|
};
|
|
556
560
|
}
|
|
557
561
|
|
|
@@ -704,6 +708,7 @@ export function buildSessionContext(
|
|
|
704
708
|
hasPersistedMCPToolSelection,
|
|
705
709
|
mode,
|
|
706
710
|
modeData,
|
|
711
|
+
usedTokens: 0, // Will be computed by the session object later
|
|
707
712
|
};
|
|
708
713
|
}
|
|
709
714
|
|