@f5-sales-demo/xcsh 20.8.5 → 20.9.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/package.json +9 -8
- package/src/config/settings-schema.ts +22 -0
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/routing/classifier.ts +6 -1
- package/src/routing/coordinator.ts +2 -0
- package/src/routing/delegation.ts +8 -2
- package/src/routing/types.ts +3 -0
- package/src/session/agent-session.ts +403 -258
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "20.
|
|
4
|
+
"version": "20.9.0",
|
|
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",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"check:bundle": "bun scripts/check-bundle.ts",
|
|
41
41
|
"pretest": "bun ../../scripts/check-installed-dependencies.ts",
|
|
42
42
|
"test": "bun run generate-build-info && bun scripts/bun-test-guarded.ts --max-concurrency 1",
|
|
43
|
+
"bench:routing-matrix": "bun scripts/bench-routing-matrix.ts",
|
|
43
44
|
"fix": "biome check --write --unsafe . && bun run format-prompts && bun run generate-docs-index && bun run generate-api-spec-index && bun run generate-build-info",
|
|
44
45
|
"fmt": "biome format --write . && bun run format-prompts",
|
|
45
46
|
"format-prompts": "bun scripts/format-prompts.ts",
|
|
@@ -61,13 +62,13 @@
|
|
|
61
62
|
"dependencies": {
|
|
62
63
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
63
64
|
"@mozilla/readability": "^0.6",
|
|
64
|
-
"@f5-sales-demo/xcsh-stats": "20.
|
|
65
|
-
"@f5-sales-demo/pi-agent-core": "20.
|
|
66
|
-
"@f5-sales-demo/pi-ai": "20.
|
|
67
|
-
"@f5-sales-demo/pi-natives": "20.
|
|
68
|
-
"@f5-sales-demo/pi-resource-management": "20.
|
|
69
|
-
"@f5-sales-demo/pi-tui": "20.
|
|
70
|
-
"@f5-sales-demo/pi-utils": "20.
|
|
65
|
+
"@f5-sales-demo/xcsh-stats": "20.9.0",
|
|
66
|
+
"@f5-sales-demo/pi-agent-core": "20.9.0",
|
|
67
|
+
"@f5-sales-demo/pi-ai": "20.9.0",
|
|
68
|
+
"@f5-sales-demo/pi-natives": "20.9.0",
|
|
69
|
+
"@f5-sales-demo/pi-resource-management": "20.9.0",
|
|
70
|
+
"@f5-sales-demo/pi-tui": "20.9.0",
|
|
71
|
+
"@f5-sales-demo/pi-utils": "20.9.0",
|
|
71
72
|
"@sinclair/typebox": "^0.34",
|
|
72
73
|
"@xterm/headless": "^6.0",
|
|
73
74
|
"ajv": "^8.20",
|
|
@@ -627,6 +627,28 @@ export const SETTINGS_SCHEMA = {
|
|
|
627
627
|
default: [],
|
|
628
628
|
},
|
|
629
629
|
|
|
630
|
+
"routing.internalOpenAiUrl": {
|
|
631
|
+
type: "string",
|
|
632
|
+
default: "",
|
|
633
|
+
ui: {
|
|
634
|
+
tab: "model",
|
|
635
|
+
label: "Internal OpenAI URL",
|
|
636
|
+
description: "Internal FQDN for OpenAI routing lane",
|
|
637
|
+
submenu: true,
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
"routing.internalAnthropicUrl": {
|
|
642
|
+
type: "string",
|
|
643
|
+
default: "",
|
|
644
|
+
ui: {
|
|
645
|
+
tab: "model",
|
|
646
|
+
label: "Internal Anthropic URL",
|
|
647
|
+
description: "Internal FQDN for Anthropic routing lane",
|
|
648
|
+
submenu: true,
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
|
|
630
652
|
// Sampling
|
|
631
653
|
temperature: {
|
|
632
654
|
type: "number",
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "20.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "20.9.0",
|
|
21
|
+
"commit": "c120e6e1b6530d1ba068a89905dcbd1df72d7b2c",
|
|
22
|
+
"shortCommit": "c120e6e",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v20.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
24
|
+
"tag": "v20.9.0",
|
|
25
|
+
"commitDate": "2026-08-10T17:23:39Z",
|
|
26
|
+
"buildDate": "2026-08-10T17:52:55.552Z",
|
|
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.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/c120e6e1b6530d1ba068a89905dcbd1df72d7b2c",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.9.0"
|
|
33
33
|
};
|
|
@@ -86,7 +86,12 @@ export async function classifyTaskHybrid(options: HybridClassifierOptions): Prom
|
|
|
86
86
|
if (err instanceof Error && err.name === "AbortError") {
|
|
87
87
|
throw err;
|
|
88
88
|
}
|
|
89
|
-
const isSyntax =
|
|
89
|
+
const isSyntax =
|
|
90
|
+
err instanceof SyntaxError ||
|
|
91
|
+
(err instanceof Error &&
|
|
92
|
+
(err.message.includes("malformed") ||
|
|
93
|
+
err.message.includes("JSON") ||
|
|
94
|
+
err.message.includes("Unexpected token")));
|
|
90
95
|
return {
|
|
91
96
|
...baseProfile,
|
|
92
97
|
reasons: [...baseProfile.reasons, isSyntax ? "classifier_fallback_malformed" : "classifier_fallback_timeout"],
|
|
@@ -28,6 +28,8 @@ export interface EvaluateTurnOptions {
|
|
|
28
28
|
downshiftAfterTurns?: number;
|
|
29
29
|
getModelContextWindow?: (modelId: string) => number;
|
|
30
30
|
runRoutingClassifier?: (utilityModel: string, prompt: string) => Promise<string>;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
ttftStartNs?: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export class RoutingCoordinator {
|
|
@@ -41,7 +41,10 @@ export function validateDelegationPlan(plan: ReadOnlyDelegationPlan, maxTasks =
|
|
|
41
41
|
|
|
42
42
|
export async function executeReadOnlyDelegationPlan(
|
|
43
43
|
plan: ReadOnlyDelegationPlan,
|
|
44
|
-
executor: (
|
|
44
|
+
executor: (
|
|
45
|
+
subtaskPrompt: string,
|
|
46
|
+
options?: { signal?: AbortSignal; allowedTools?: (toolName: string) => boolean },
|
|
47
|
+
) => Promise<{ result: string; tokens: number }>,
|
|
45
48
|
maxTasks = 3,
|
|
46
49
|
options?: { signal?: AbortSignal },
|
|
47
50
|
): Promise<{ results: Array<{ id: string; result: string }>; tokensUsed: number }> {
|
|
@@ -65,7 +68,10 @@ export async function executeReadOnlyDelegationPlan(
|
|
|
65
68
|
]
|
|
66
69
|
.filter(Boolean)
|
|
67
70
|
.join("\n");
|
|
68
|
-
const { result, tokens } = await executor(promptPayload,
|
|
71
|
+
const { result, tokens } = await executor(promptPayload, {
|
|
72
|
+
signal: options?.signal,
|
|
73
|
+
allowedTools: isDelegationAllowedTool,
|
|
74
|
+
});
|
|
69
75
|
return { id: task.id, result, tokens };
|
|
70
76
|
} catch (err) {
|
|
71
77
|
return { id: task.id, result: `Failed: ${String(err)}`, tokens: 0 };
|
package/src/routing/types.ts
CHANGED
|
@@ -43,7 +43,6 @@ import type {
|
|
|
43
43
|
} from "@f5-sales-demo/pi-ai";
|
|
44
44
|
import {
|
|
45
45
|
calculateRateLimitBackoffMs,
|
|
46
|
-
completeSimple,
|
|
47
46
|
getSupportedEfforts,
|
|
48
47
|
isContextOverflow,
|
|
49
48
|
isUsageLimitError,
|
|
@@ -445,6 +444,7 @@ export class AgentSession {
|
|
|
445
444
|
* distinguishable from launch configuration (#2459). Defaults to "config": settings, a remembered
|
|
446
445
|
* role, or a resumed session are all config, and only --model or a live switch is not.
|
|
447
446
|
*/
|
|
447
|
+
#initialModelResolutionSource: ModelResolutionSource = "config";
|
|
448
448
|
#modelResolutionSource: ModelResolutionSource = "config";
|
|
449
449
|
#promptTemplates: PromptTemplate[];
|
|
450
450
|
#slashCommands: FileSlashCommand[];
|
|
@@ -596,7 +596,8 @@ export class AgentSession {
|
|
|
596
596
|
this.#pythonKernelOwnerId = config.pythonKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
597
597
|
this.#scopedModels = config.scopedModels ?? [];
|
|
598
598
|
this.#thinkingLevel = config.thinkingLevel;
|
|
599
|
-
this.#
|
|
599
|
+
this.#initialModelResolutionSource = config.modelResolutionSource ?? "config";
|
|
600
|
+
this.#modelResolutionSource = this.#initialModelResolutionSource;
|
|
600
601
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
601
602
|
this.#slashCommands = config.slashCommands ?? [];
|
|
602
603
|
this.#extensionRunner = config.extensionRunner;
|
|
@@ -1348,20 +1349,19 @@ export class AgentSession {
|
|
|
1348
1349
|
}
|
|
1349
1350
|
|
|
1350
1351
|
if (!outcome.safeToContinue) {
|
|
1351
|
-
this.#routingCoordinator.getStateMachine().setEscalationFloor(targetTier);
|
|
1352
1352
|
return;
|
|
1353
1353
|
}
|
|
1354
1354
|
|
|
1355
1355
|
const previousTier = this.#routingCoordinator.getState().currentTier;
|
|
1356
1356
|
const previousFloor = this.#routingCoordinator.getState().escalationFloor;
|
|
1357
|
+
const previousModel = this.model;
|
|
1357
1358
|
|
|
1358
1359
|
try {
|
|
1360
|
+
await this.setModelRoutingSwitch(targetModel);
|
|
1359
1361
|
this.agent.abort();
|
|
1360
1362
|
|
|
1361
1363
|
this.#routingCoordinator.getStateMachine().setEscalationFloor(targetTier);
|
|
1362
1364
|
this.#routingCoordinator.restoreState({ currentTier: targetTier });
|
|
1363
|
-
|
|
1364
|
-
await this.setModelRoutingSwitch(targetModel);
|
|
1365
1365
|
const effortMap = this.settings.get("routing.tierEffort") as Record<string, string> | undefined;
|
|
1366
1366
|
const { mapTierToEffort } = await import("../routing/effort");
|
|
1367
1367
|
const effort = mapTierToEffort(targetTier, effortMap);
|
|
@@ -1387,6 +1387,13 @@ export class AgentSession {
|
|
|
1387
1387
|
} else {
|
|
1388
1388
|
this.#routingCoordinator.getStateMachine().clearEscalationFloor();
|
|
1389
1389
|
}
|
|
1390
|
+
if (previousModel) {
|
|
1391
|
+
this.#setModelWithProviderSessionReset(previousModel, "runtime-switch");
|
|
1392
|
+
this.sessionManager.appendModelChange(
|
|
1393
|
+
`${previousModel.provider}/${previousModel.id}`,
|
|
1394
|
+
"routing_switch_rollback",
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1390
1397
|
} finally {
|
|
1391
1398
|
this.#scheduleAgentContinue({ delayMs: 10 });
|
|
1392
1399
|
}
|
|
@@ -2801,247 +2808,21 @@ export class AgentSession {
|
|
|
2801
2808
|
|
|
2802
2809
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
2803
2810
|
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2811
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
2812
|
+
expandedText,
|
|
2813
|
+
options?.images ? options.images.length > 0 : false,
|
|
2814
|
+
{ signal: options?.signal },
|
|
2815
|
+
);
|
|
2816
|
+
if (delegationOutput) {
|
|
2817
|
+
expandedText += delegationOutput;
|
|
2818
|
+
const textBlock = userContent.find(c => c.type === "text") as Extract<
|
|
2819
|
+
(typeof userContent)[0],
|
|
2820
|
+
{ type: "text" }
|
|
2821
|
+
>;
|
|
2822
|
+
if (textBlock) {
|
|
2823
|
+
textBlock.text += delegationOutput;
|
|
2813
2824
|
} else {
|
|
2814
|
-
|
|
2815
|
-
const availableModels =
|
|
2816
|
-
this.#scopedModels.length > 0
|
|
2817
|
-
? this.#scopedModels.map(sm => `${sm.model.provider}/${sm.model.id}`)
|
|
2818
|
-
: this.#modelRegistry.getAvailable().map(m => `${m.provider}/${m.id}`);
|
|
2819
|
-
const startTime = Date.now();
|
|
2820
|
-
const systemTokens = Math.round((this.systemPrompt?.length ?? 0) / 4);
|
|
2821
|
-
const promptTokens = Math.round(expandedText.length / 4);
|
|
2822
|
-
const toolsStr = JSON.stringify(Array.from(this.#toolRegistry.values()));
|
|
2823
|
-
const toolsTokens = Math.round(toolsStr.length / 4);
|
|
2824
|
-
const reserveTokens = (this.settings.get("compaction.reserveTokens") as number) ?? 0;
|
|
2825
|
-
const contextEstimate = {
|
|
2826
|
-
usedTokens: calculateUsedTokens(this.messages) + systemTokens + promptTokens + toolsTokens,
|
|
2827
|
-
reserveTokens,
|
|
2828
|
-
contextWindow: this.model.contextWindow ?? 128000,
|
|
2829
|
-
};
|
|
2830
|
-
const decision = await this.#routingCoordinator.evaluateTurn({
|
|
2831
|
-
anchorModel,
|
|
2832
|
-
mode: routingMode,
|
|
2833
|
-
prompt: expandedText,
|
|
2834
|
-
hasImages: options?.images && options.images.length > 0,
|
|
2835
|
-
priorRejection: this.#routingCoordinator.getState().escalationFloor !== undefined,
|
|
2836
|
-
availableModels,
|
|
2837
|
-
customPools: validateCustomPools(this.settings.get("routing.pools")),
|
|
2838
|
-
disabledPresets: (this.settings.get("routing.disabledPresets") as readonly string[]) ?? [],
|
|
2839
|
-
familyPolicy: (this.settings.get("routing.familyPolicy") as "sticky" | "configured-mixed") ?? "sticky",
|
|
2840
|
-
profilerMode: (this.settings.get("routing.profiler") as any) ?? "hybrid",
|
|
2841
|
-
contextEstimate,
|
|
2842
|
-
getModelContextWindow: (modelId: string) => {
|
|
2843
|
-
const found = this.#modelRegistry
|
|
2844
|
-
.getAll()
|
|
2845
|
-
.find(m => `${m.provider}/${m.id}` === modelId || m.id === modelId);
|
|
2846
|
-
return found?.contextWindow ?? 128000;
|
|
2847
|
-
},
|
|
2848
|
-
runRoutingClassifier: async (utilityModel: string, promptText: string) => {
|
|
2849
|
-
const resolved = this.#modelRegistry
|
|
2850
|
-
.getAll()
|
|
2851
|
-
.find(m => `${m.provider}/${m.id}` === utilityModel || m.id === utilityModel);
|
|
2852
|
-
if (!resolved) throw new Error("Classifier model not found");
|
|
2853
|
-
const apiKey = await this.#modelRegistry.getApiKey(resolved, this.sessionId);
|
|
2854
|
-
if (!apiKey) throw new Error("No API key for classifier model");
|
|
2855
|
-
|
|
2856
|
-
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.`;
|
|
2857
|
-
|
|
2858
|
-
const res = await completeSimple(
|
|
2859
|
-
resolved,
|
|
2860
|
-
{
|
|
2861
|
-
systemPrompt: systemInstruction,
|
|
2862
|
-
messages: [
|
|
2863
|
-
{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() },
|
|
2864
|
-
],
|
|
2865
|
-
},
|
|
2866
|
-
{ apiKey, signal: AbortSignal.timeout(15000) },
|
|
2867
|
-
);
|
|
2868
|
-
const rawText = res.content
|
|
2869
|
-
.filter(c => c.type === "text")
|
|
2870
|
-
.map(c => (c as TextContent).text)
|
|
2871
|
-
.join("");
|
|
2872
|
-
|
|
2873
|
-
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
|
|
2874
|
-
if (jsonMatch) {
|
|
2875
|
-
const parsed = JSON.parse(jsonMatch[0]); // Check validity
|
|
2876
|
-
if (res.usage) {
|
|
2877
|
-
this.settings.getStorage()?.recordModelUsage(`${resolved.provider}/${resolved.id}`);
|
|
2878
|
-
parsed.routingUsage = (res.usage as any).totalTokens ?? (res.usage as any).total ?? 0;
|
|
2879
|
-
}
|
|
2880
|
-
return JSON.stringify(parsed);
|
|
2881
|
-
}
|
|
2882
|
-
throw new Error("Classifier returned malformed JSON");
|
|
2883
|
-
},
|
|
2884
|
-
downshiftAfterTurns: (this.settings.get("routing.downshiftAfterTurns") as number) ?? 2,
|
|
2885
|
-
});
|
|
2886
|
-
const durationMs = Date.now() - startTime;
|
|
2887
|
-
|
|
2888
|
-
this.#emitSessionEvent({
|
|
2889
|
-
type: decision.applied ? "routing_applied" : "routing_decision",
|
|
2890
|
-
epochId: decision.epochId,
|
|
2891
|
-
mode: decision.mode,
|
|
2892
|
-
provider: this.model.provider,
|
|
2893
|
-
poolId: decision.poolId,
|
|
2894
|
-
effectiveTier: decision.effectiveTier,
|
|
2895
|
-
selectedModel: decision.selectedModel,
|
|
2896
|
-
reasons: decision.reasons,
|
|
2897
|
-
delegated: !!decision.delegation,
|
|
2898
|
-
escalated: decision.reasons.some(
|
|
2899
|
-
r => r === "context_capacity_promotion" || r === "escalation_floor_active",
|
|
2900
|
-
),
|
|
2901
|
-
contextTokens: contextEstimate.usedTokens,
|
|
2902
|
-
routingUsage: decision.routingUsage,
|
|
2903
|
-
durationMs,
|
|
2904
|
-
state: this.#routingCoordinator.getState(),
|
|
2905
|
-
} as any).catch(() => {});
|
|
2906
|
-
|
|
2907
|
-
if (decision.selectedModel && decision.applied) {
|
|
2908
|
-
const targetModel = this.#modelRegistry
|
|
2909
|
-
.getAvailable()
|
|
2910
|
-
.find(m => `${m.provider}/${m.id}` === decision.selectedModel || m.id === decision.selectedModel);
|
|
2911
|
-
if (targetModel) {
|
|
2912
|
-
const currentModelId = this.model ? `${this.model.provider}/${this.model.id}` : undefined;
|
|
2913
|
-
const targetModelId = `${targetModel.provider}/${targetModel.id}`;
|
|
2914
|
-
if (currentModelId !== targetModelId) {
|
|
2915
|
-
await this.setModelRoutingSwitch(targetModel);
|
|
2916
|
-
}
|
|
2917
|
-
if (decision.effectiveTier) {
|
|
2918
|
-
const effortMap = this.settings.get("routing.tierEffort") as Record<string, string> | undefined;
|
|
2919
|
-
const { mapTierToEffort } = await import("../routing/effort");
|
|
2920
|
-
const effort = mapTierToEffort(decision.effectiveTier, effortMap);
|
|
2921
|
-
this.setThinkingLevel(effort as any);
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
if (
|
|
2927
|
-
decision.applied &&
|
|
2928
|
-
decision.delegation &&
|
|
2929
|
-
decision.delegation.subtasks.length > 1 &&
|
|
2930
|
-
(this.settings.get("routing.delegation") as string) === "read-only"
|
|
2931
|
-
) {
|
|
2932
|
-
let maxTasks = (this.settings.get("routing.delegationMaxTasks") as number) ?? 3;
|
|
2933
|
-
maxTasks = Math.min(Math.max(1, maxTasks), 3);
|
|
2934
|
-
|
|
2935
|
-
const { results, tokensUsed } = await executeReadOnlyDelegationPlan(
|
|
2936
|
-
decision.delegation,
|
|
2937
|
-
async (subtaskPrompt, signal) => {
|
|
2938
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2939
|
-
|
|
2940
|
-
const { resolveModelPool } = await import("../routing/presets");
|
|
2941
|
-
const pool = resolveModelPool(
|
|
2942
|
-
decision.poolId ?? (this.model ? `${this.model.provider}/${this.model.id}` : ""),
|
|
2943
|
-
this.settings.get("routing.pools") as any,
|
|
2944
|
-
this.settings.get("routing.disabledPresets") as readonly string[],
|
|
2945
|
-
this.settings.get("routing.familyPolicy") as any,
|
|
2946
|
-
);
|
|
2947
|
-
|
|
2948
|
-
let resolvedUtility = this.model;
|
|
2949
|
-
if (pool?.tiers?.utility) {
|
|
2950
|
-
let uId = pool.tiers.utility;
|
|
2951
|
-
if (!uId.includes("/") && pool.provider) {
|
|
2952
|
-
uId = `${pool.provider}/${uId}`;
|
|
2953
|
-
}
|
|
2954
|
-
const found = this.#modelRegistry
|
|
2955
|
-
.getAvailable()
|
|
2956
|
-
.find(m => `${m.provider}/${m.id}` === uId || m.id === uId);
|
|
2957
|
-
if (found) resolvedUtility = found;
|
|
2958
|
-
}
|
|
2959
|
-
|
|
2960
|
-
if (!resolvedUtility) return { result: "Delegation failed: no model.", tokens: 0 };
|
|
2961
|
-
|
|
2962
|
-
const { createAgentSession } = await import("../sdk");
|
|
2963
|
-
const { SessionManager: SDKSessionManager } = await import("./session-manager");
|
|
2964
|
-
const childSettings = await this.settings.cloneForCwd(process.cwd());
|
|
2965
|
-
childSettings.set("routing.delegation", "off");
|
|
2966
|
-
|
|
2967
|
-
// Fix abort race by checking right before creation
|
|
2968
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2969
|
-
|
|
2970
|
-
const { session: childSession } = await createAgentSession({
|
|
2971
|
-
model: resolvedUtility,
|
|
2972
|
-
sessionManager: SDKSessionManager.inMemory(),
|
|
2973
|
-
settings: childSettings,
|
|
2974
|
-
authStorage: this.#modelRegistry.authStorage,
|
|
2975
|
-
modelRegistry: this.#modelRegistry,
|
|
2976
|
-
toolNames: Array.from(this.#toolRegistry.values())
|
|
2977
|
-
.map(t => t.name)
|
|
2978
|
-
.filter(isDelegationAllowedTool),
|
|
2979
|
-
enableLsp: false,
|
|
2980
|
-
enableMCP: false,
|
|
2981
|
-
});
|
|
2982
|
-
|
|
2983
|
-
try {
|
|
2984
|
-
const onAbort = () => childSession.agent.abort();
|
|
2985
|
-
signal?.addEventListener("abort", onAbort);
|
|
2986
|
-
if (signal?.aborted) {
|
|
2987
|
-
onAbort();
|
|
2988
|
-
return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2989
|
-
}
|
|
2990
|
-
try {
|
|
2991
|
-
await childSession.prompt(subtaskPrompt);
|
|
2992
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2993
|
-
const resultStr = childSession
|
|
2994
|
-
.buildDisplaySessionContext()
|
|
2995
|
-
.messages.filter(m => m.role === "assistant")
|
|
2996
|
-
.map(m =>
|
|
2997
|
-
m.content
|
|
2998
|
-
.filter((c: any) => c.type === "text")
|
|
2999
|
-
.map((c: any) => c.text)
|
|
3000
|
-
.join(""),
|
|
3001
|
-
)
|
|
3002
|
-
.join("\n");
|
|
3003
|
-
const tokens = childSession.buildDisplaySessionContext().usedTokens || 0;
|
|
3004
|
-
return { result: resultStr, tokens };
|
|
3005
|
-
} finally {
|
|
3006
|
-
signal?.removeEventListener("abort", onAbort);
|
|
3007
|
-
}
|
|
3008
|
-
} catch (err) {
|
|
3009
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3010
|
-
return { result: `Delegation failed: ${String(err)}`, tokens: 0 };
|
|
3011
|
-
} finally {
|
|
3012
|
-
await childSession.dispose();
|
|
3013
|
-
}
|
|
3014
|
-
},
|
|
3015
|
-
maxTasks,
|
|
3016
|
-
{ signal: options?.signal },
|
|
3017
|
-
);
|
|
3018
|
-
|
|
3019
|
-
if (results.length > 0) {
|
|
3020
|
-
let resultsString = JSON.stringify(results, null, 2);
|
|
3021
|
-
if (resultsString.length > 8000) {
|
|
3022
|
-
resultsString = `${resultsString.substring(0, 8000)}\n...[truncated]`;
|
|
3023
|
-
}
|
|
3024
|
-
const delegationOutput = `\n\n<delegation_results>\n${resultsString}\n</delegation_results>`;
|
|
3025
|
-
expandedText += delegationOutput;
|
|
3026
|
-
const textBlock = userContent.find(c => c.type === "text") as Extract<
|
|
3027
|
-
(typeof userContent)[0],
|
|
3028
|
-
{ type: "text" }
|
|
3029
|
-
>;
|
|
3030
|
-
if (textBlock) {
|
|
3031
|
-
textBlock.text += delegationOutput;
|
|
3032
|
-
} else {
|
|
3033
|
-
userContent.push({ type: "text", text: delegationOutput });
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
this.#emitSessionEvent({
|
|
3037
|
-
type: "routing_delegated",
|
|
3038
|
-
epochId: decision.epochId,
|
|
3039
|
-
tasks: decision.delegation.subtasks.length,
|
|
3040
|
-
completed: results.length,
|
|
3041
|
-
tokensUsed,
|
|
3042
|
-
} as any).catch(() => {});
|
|
3043
|
-
}
|
|
3044
|
-
}
|
|
2825
|
+
userContent.push({ type: "text", text: delegationOutput });
|
|
3045
2826
|
}
|
|
3046
2827
|
}
|
|
3047
2828
|
|
|
@@ -3062,7 +2843,7 @@ export class AgentSession {
|
|
|
3062
2843
|
|
|
3063
2844
|
async promptCustomMessage<T = unknown>(
|
|
3064
2845
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
3065
|
-
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
|
|
2846
|
+
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "signal">,
|
|
3066
2847
|
): Promise<void> {
|
|
3067
2848
|
const textContent =
|
|
3068
2849
|
typeof message.content === "string"
|
|
@@ -3080,17 +2861,276 @@ export class AgentSession {
|
|
|
3080
2861
|
return;
|
|
3081
2862
|
}
|
|
3082
2863
|
|
|
2864
|
+
const clonedContent = Array.isArray(message.content)
|
|
2865
|
+
? message.content.map((c: any) => ({ ...c }))
|
|
2866
|
+
: message.content;
|
|
2867
|
+
|
|
3083
2868
|
const customMessage: CustomMessage<T> = {
|
|
3084
2869
|
role: "custom",
|
|
3085
2870
|
customType: message.customType,
|
|
3086
|
-
content:
|
|
2871
|
+
content: clonedContent,
|
|
3087
2872
|
display: message.display,
|
|
3088
2873
|
details: message.details,
|
|
3089
2874
|
attribution: message.attribution ?? "agent",
|
|
3090
2875
|
timestamp: Date.now(),
|
|
3091
2876
|
};
|
|
3092
2877
|
|
|
3093
|
-
await this.#
|
|
2878
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
2879
|
+
textContent,
|
|
2880
|
+
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
2881
|
+
{ signal: options?.signal },
|
|
2882
|
+
);
|
|
2883
|
+
if (delegationOutput) {
|
|
2884
|
+
if (typeof customMessage.content === "string") {
|
|
2885
|
+
customMessage.content += delegationOutput;
|
|
2886
|
+
} else if (Array.isArray(customMessage.content)) {
|
|
2887
|
+
const textBlock = customMessage.content.find((c: any) => c.type === "text") as any;
|
|
2888
|
+
if (textBlock) {
|
|
2889
|
+
textBlock.text += delegationOutput;
|
|
2890
|
+
} else {
|
|
2891
|
+
customMessage.content.push({ type: "text", text: delegationOutput });
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
await this.#promptWithMessage(customMessage, textContent + (delegationOutput || ""), options);
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
async #evaluateAndApplyRouting(
|
|
2900
|
+
promptText: string,
|
|
2901
|
+
hasImages: boolean,
|
|
2902
|
+
options?: { signal?: AbortSignal } | any,
|
|
2903
|
+
): Promise<string | undefined> {
|
|
2904
|
+
const routingMode = (this.settings.get("routing.mode") as import("../routing/types").RoutingMode) ?? "off";
|
|
2905
|
+
if (routingMode === "off" || !this.model) return undefined;
|
|
2906
|
+
|
|
2907
|
+
if (this.#activeRetryFallback) {
|
|
2908
|
+
this.#emitSessionEvent({
|
|
2909
|
+
type: "routing_skipped",
|
|
2910
|
+
epochId: `skip-${Date.now()}`,
|
|
2911
|
+
reasons: ["retry_fallback"],
|
|
2912
|
+
} as any).catch(() => {});
|
|
2913
|
+
return undefined;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
const anchorModel = `${this.model.provider}/${this.model.id}`;
|
|
2917
|
+
const availableModels =
|
|
2918
|
+
this.#scopedModels.length > 0
|
|
2919
|
+
? this.#scopedModels.map(sm => `${sm.model.provider}/${sm.model.id}`)
|
|
2920
|
+
: this.#modelRegistry.getAvailable().map(m => `${m.provider}/${m.id}`);
|
|
2921
|
+
const startTime = Date.now();
|
|
2922
|
+
const systemTokens = Math.round((this.systemPrompt?.length ?? 0) / 4);
|
|
2923
|
+
const promptTokens = Math.round(promptText.length / 4);
|
|
2924
|
+
const toolsStr = JSON.stringify(Array.from(this.#toolRegistry.values()));
|
|
2925
|
+
const toolsTokens = Math.round(toolsStr.length / 4);
|
|
2926
|
+
const reserveTokens = (this.settings.get("compaction.reserveTokens") as number) ?? 0;
|
|
2927
|
+
const contextEstimate = {
|
|
2928
|
+
usedTokens: calculateUsedTokens(this.messages) + systemTokens + promptTokens + toolsTokens,
|
|
2929
|
+
reserveTokens,
|
|
2930
|
+
contextWindow: this.model.contextWindow ?? 128000,
|
|
2931
|
+
};
|
|
2932
|
+
const decision = await this.#routingCoordinator.evaluateTurn({
|
|
2933
|
+
anchorModel,
|
|
2934
|
+
mode: routingMode,
|
|
2935
|
+
prompt: promptText,
|
|
2936
|
+
hasImages,
|
|
2937
|
+
priorRejection: this.#routingCoordinator.getState().escalationFloor !== undefined,
|
|
2938
|
+
availableModels,
|
|
2939
|
+
customPools: validateCustomPools(this.settings.get("routing.pools")),
|
|
2940
|
+
disabledPresets: (this.settings.get("routing.disabledPresets") as readonly string[]) ?? [],
|
|
2941
|
+
familyPolicy: (this.settings.get("routing.familyPolicy") as "sticky" | "configured-mixed") ?? "sticky",
|
|
2942
|
+
profilerMode: (this.settings.get("routing.profiler") as any) ?? "hybrid",
|
|
2943
|
+
contextEstimate,
|
|
2944
|
+
signal: options?.signal,
|
|
2945
|
+
getModelContextWindow: (modelId: string) => {
|
|
2946
|
+
const m = this.#modelRegistry
|
|
2947
|
+
.getAvailable()
|
|
2948
|
+
.find(m => `${m.provider}/${m.id}` === modelId || m.id === modelId);
|
|
2949
|
+
return m?.contextWindow ?? 128000;
|
|
2950
|
+
},
|
|
2951
|
+
});
|
|
2952
|
+
|
|
2953
|
+
this.#emitSessionEvent({
|
|
2954
|
+
type: "routing_evaluated",
|
|
2955
|
+
epochId: decision.epochId,
|
|
2956
|
+
profiler: {
|
|
2957
|
+
latencyMs: Date.now() - startTime,
|
|
2958
|
+
...(decision as any).profiler,
|
|
2959
|
+
},
|
|
2960
|
+
applied: decision.applied,
|
|
2961
|
+
decision: decision.selectedModel,
|
|
2962
|
+
delegation: decision.delegation,
|
|
2963
|
+
} as any).catch(() => {});
|
|
2964
|
+
|
|
2965
|
+
if (decision.selectedModel && decision.applied) {
|
|
2966
|
+
const targetModel = this.#modelRegistry
|
|
2967
|
+
.getAvailable()
|
|
2968
|
+
.find(m => `${m.provider}/${m.id}` === decision.selectedModel || m.id === decision.selectedModel);
|
|
2969
|
+
if (targetModel) {
|
|
2970
|
+
const currentModelId = `${this.model.provider}/${this.model.id}`;
|
|
2971
|
+
const targetModelId = `${targetModel.provider}/${targetModel.id}`;
|
|
2972
|
+
|
|
2973
|
+
let needsSwitch = currentModelId !== targetModelId;
|
|
2974
|
+
|
|
2975
|
+
// Force switch if the current model is the same but needs an internal URL override
|
|
2976
|
+
if (!needsSwitch) {
|
|
2977
|
+
if (
|
|
2978
|
+
targetModel.provider === "openai" ||
|
|
2979
|
+
(targetModel.provider === "litellm" &&
|
|
2980
|
+
(targetModel.id.includes("openai") || /gpt-/.test(targetModel.id)))
|
|
2981
|
+
) {
|
|
2982
|
+
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
2983
|
+
if ((internalUrl || undefined) !== this.model.baseUrl) {
|
|
2984
|
+
needsSwitch = true;
|
|
2985
|
+
}
|
|
2986
|
+
} else if (
|
|
2987
|
+
targetModel.provider === "anthropic" ||
|
|
2988
|
+
(targetModel.provider === "litellm" &&
|
|
2989
|
+
(targetModel.id.includes("anthropic") || targetModel.id.includes("claude")))
|
|
2990
|
+
) {
|
|
2991
|
+
const internalUrl = this.settings.get("routing.internalAnthropicUrl") as string | undefined;
|
|
2992
|
+
if ((internalUrl || undefined) !== this.model.baseUrl) {
|
|
2993
|
+
needsSwitch = true;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
if (needsSwitch) {
|
|
2999
|
+
await this.setModelRoutingSwitch(targetModel);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
if (
|
|
3005
|
+
decision.applied &&
|
|
3006
|
+
decision.delegation &&
|
|
3007
|
+
decision.delegation.subtasks.length > 1 &&
|
|
3008
|
+
(this.settings.get("routing.delegation") as string) === "read-only"
|
|
3009
|
+
) {
|
|
3010
|
+
let maxTasks = (this.settings.get("routing.delegationMaxTasks") as number) ?? 3;
|
|
3011
|
+
maxTasks = Math.min(Math.max(1, maxTasks), 3);
|
|
3012
|
+
|
|
3013
|
+
const { results, tokensUsed } = await executeReadOnlyDelegationPlan(
|
|
3014
|
+
decision.delegation,
|
|
3015
|
+
async (subtaskPrompt, options) => {
|
|
3016
|
+
const signal = options?.signal;
|
|
3017
|
+
const allowedTools = options?.allowedTools ?? isDelegationAllowedTool;
|
|
3018
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3019
|
+
|
|
3020
|
+
const { resolveModelPool } = await import("../routing/presets");
|
|
3021
|
+
const pool = resolveModelPool(
|
|
3022
|
+
decision.poolId ?? (this.model ? `${this.model.provider}/${this.model.id}` : ""),
|
|
3023
|
+
this.settings.get("routing.pools") as any,
|
|
3024
|
+
this.settings.get("routing.disabledPresets") as readonly string[],
|
|
3025
|
+
this.settings.get("routing.familyPolicy") as any,
|
|
3026
|
+
);
|
|
3027
|
+
|
|
3028
|
+
let resolvedUtility = this.model;
|
|
3029
|
+
if (pool?.tiers?.utility) {
|
|
3030
|
+
let uId = pool.tiers.utility;
|
|
3031
|
+
if (!uId.includes("/") && pool.provider) {
|
|
3032
|
+
uId = `${pool.provider}/${uId}`;
|
|
3033
|
+
}
|
|
3034
|
+
const found = this.#modelRegistry
|
|
3035
|
+
.getAvailable()
|
|
3036
|
+
.find(m => `${m.provider}/${m.id}` === uId || m.id === uId);
|
|
3037
|
+
if (found) resolvedUtility = found;
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
if (!resolvedUtility) return { result: "Delegation failed: Model not resolved", tokens: 0 };
|
|
3041
|
+
|
|
3042
|
+
if (
|
|
3043
|
+
resolvedUtility.provider === "openai" ||
|
|
3044
|
+
(resolvedUtility.provider === "litellm" &&
|
|
3045
|
+
(resolvedUtility.id.includes("openai") || /gpt-/.test(resolvedUtility.id)))
|
|
3046
|
+
) {
|
|
3047
|
+
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
3048
|
+
if (internalUrl) resolvedUtility = { ...resolvedUtility, baseUrl: internalUrl } as any;
|
|
3049
|
+
} else if (
|
|
3050
|
+
resolvedUtility.provider === "anthropic" ||
|
|
3051
|
+
(resolvedUtility.provider === "litellm" &&
|
|
3052
|
+
(resolvedUtility.id.includes("anthropic") || resolvedUtility.id.includes("claude")))
|
|
3053
|
+
) {
|
|
3054
|
+
const internalUrl = this.settings.get("routing.internalAnthropicUrl") as string | undefined;
|
|
3055
|
+
if (internalUrl) resolvedUtility = { ...resolvedUtility, baseUrl: internalUrl } as any;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
const { createAgentSession } = await import("../sdk");
|
|
3059
|
+
const { SessionManager: SDKSessionManager } = await import("./session-manager");
|
|
3060
|
+
const childSettings = await this.settings.cloneForCwd(process.cwd());
|
|
3061
|
+
childSettings.set("routing.delegation", "off");
|
|
3062
|
+
|
|
3063
|
+
if (options?.signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3064
|
+
|
|
3065
|
+
const { session: childSession } = await createAgentSession({
|
|
3066
|
+
model: resolvedUtility!,
|
|
3067
|
+
sessionManager: SDKSessionManager.inMemory(),
|
|
3068
|
+
settings: childSettings,
|
|
3069
|
+
authStorage: this.#modelRegistry.authStorage,
|
|
3070
|
+
modelRegistry: this.#modelRegistry,
|
|
3071
|
+
toolNames: Array.from(this.#toolRegistry.values())
|
|
3072
|
+
.map(t => t.name)
|
|
3073
|
+
.filter(allowedTools),
|
|
3074
|
+
enableLsp: false,
|
|
3075
|
+
enableMCP: false,
|
|
3076
|
+
});
|
|
3077
|
+
|
|
3078
|
+
try {
|
|
3079
|
+
const onAbort = () => childSession.agent.abort();
|
|
3080
|
+
signal?.addEventListener("abort", onAbort);
|
|
3081
|
+
if (signal?.aborted) {
|
|
3082
|
+
onAbort();
|
|
3083
|
+
return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3084
|
+
}
|
|
3085
|
+
try {
|
|
3086
|
+
await childSession.prompt(subtaskPrompt);
|
|
3087
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3088
|
+
const resultStr = childSession
|
|
3089
|
+
.buildDisplaySessionContext()
|
|
3090
|
+
.messages.filter((m: any) => m.role === "assistant")
|
|
3091
|
+
.map((m: any) =>
|
|
3092
|
+
m.content
|
|
3093
|
+
.filter((c: any) => c.type === "text")
|
|
3094
|
+
.map((c: any) => c.text)
|
|
3095
|
+
.join(""),
|
|
3096
|
+
)
|
|
3097
|
+
.join("\n");
|
|
3098
|
+
const tUsage = childSession.buildDisplaySessionContext().usedTokens || 0;
|
|
3099
|
+
return { result: resultStr || "No output", tokens: tUsage };
|
|
3100
|
+
} finally {
|
|
3101
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3102
|
+
}
|
|
3103
|
+
} catch (err) {
|
|
3104
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3105
|
+
return { result: `Delegation failed: ${String(err)}`, tokens: 0 };
|
|
3106
|
+
} finally {
|
|
3107
|
+
await childSession.dispose();
|
|
3108
|
+
}
|
|
3109
|
+
},
|
|
3110
|
+
maxTasks,
|
|
3111
|
+
{ signal: options?.signal },
|
|
3112
|
+
);
|
|
3113
|
+
|
|
3114
|
+
if (results.length > 0) {
|
|
3115
|
+
let resultsString = JSON.stringify(results, null, 2);
|
|
3116
|
+
if (resultsString.length > 8000) {
|
|
3117
|
+
resultsString = `${resultsString.substring(0, 8000)}\n...[truncated]`;
|
|
3118
|
+
}
|
|
3119
|
+
const delegationOutput = `\n\n<delegation_results>\n${resultsString}\n</delegation_results>`;
|
|
3120
|
+
|
|
3121
|
+
this.#emitSessionEvent({
|
|
3122
|
+
type: "routing_delegated",
|
|
3123
|
+
epochId: decision.epochId,
|
|
3124
|
+
tasks: decision.delegation.subtasks.length,
|
|
3125
|
+
completed: results.length,
|
|
3126
|
+
tokensUsed,
|
|
3127
|
+
} as any).catch(() => {});
|
|
3128
|
+
|
|
3129
|
+
return delegationOutput;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
return undefined;
|
|
3094
3134
|
}
|
|
3095
3135
|
|
|
3096
3136
|
async #promptWithMessage(
|
|
@@ -3479,8 +3519,25 @@ export class AgentSession {
|
|
|
3479
3519
|
|
|
3480
3520
|
const prependMessages = queuedMessages.slice(0, -1);
|
|
3481
3521
|
const textContent = this.#getCustomMessageTextContent(message);
|
|
3522
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
3523
|
+
textContent,
|
|
3524
|
+
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
3525
|
+
);
|
|
3526
|
+
if (delegationOutput) {
|
|
3527
|
+
if (typeof message.content === "string") {
|
|
3528
|
+
message.content += delegationOutput;
|
|
3529
|
+
} else if (Array.isArray(message.content)) {
|
|
3530
|
+
const textBlock = message.content.find((c: any) => c.type === "text");
|
|
3531
|
+
if (textBlock) {
|
|
3532
|
+
(textBlock as any).text += delegationOutput;
|
|
3533
|
+
} else {
|
|
3534
|
+
message.content.push({ type: "text", text: delegationOutput });
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3482
3539
|
try {
|
|
3483
|
-
await this.#promptWithMessage(message, textContent, {
|
|
3540
|
+
await this.#promptWithMessage(message, textContent + (delegationOutput || ""), {
|
|
3484
3541
|
prependMessages,
|
|
3485
3542
|
skipPostPromptRecoveryWait: true,
|
|
3486
3543
|
});
|
|
@@ -3527,12 +3584,16 @@ export class AgentSession {
|
|
|
3527
3584
|
*/
|
|
3528
3585
|
async sendCustomMessage<T = unknown>(
|
|
3529
3586
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
3530
|
-
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
3587
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; signal?: AbortSignal },
|
|
3531
3588
|
): Promise<void> {
|
|
3589
|
+
const clonedContent = Array.isArray(message.content)
|
|
3590
|
+
? message.content.map((c: any) => ({ ...c }))
|
|
3591
|
+
: message.content;
|
|
3592
|
+
|
|
3532
3593
|
const appMessage: CustomMessage<T> = {
|
|
3533
3594
|
role: "custom",
|
|
3534
3595
|
customType: message.customType,
|
|
3535
|
-
content:
|
|
3596
|
+
content: clonedContent,
|
|
3536
3597
|
display: message.display,
|
|
3537
3598
|
details: message.details,
|
|
3538
3599
|
attribution: message.attribution ?? "agent",
|
|
@@ -3554,7 +3615,33 @@ export class AgentSession {
|
|
|
3554
3615
|
|
|
3555
3616
|
if (options?.deliverAs === "nextTurn") {
|
|
3556
3617
|
if (options?.triggerTurn) {
|
|
3557
|
-
|
|
3618
|
+
let promptText = "";
|
|
3619
|
+
if (typeof message.content === "string") promptText = message.content;
|
|
3620
|
+
else if (Array.isArray(message.content)) {
|
|
3621
|
+
promptText = message.content
|
|
3622
|
+
.filter(c => c.type === "text")
|
|
3623
|
+
.map((c: any) => c.text)
|
|
3624
|
+
.join("");
|
|
3625
|
+
}
|
|
3626
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
3627
|
+
promptText,
|
|
3628
|
+
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
3629
|
+
options,
|
|
3630
|
+
);
|
|
3631
|
+
if (delegationOutput) {
|
|
3632
|
+
promptText += delegationOutput;
|
|
3633
|
+
if (typeof appMessage.content === "string") {
|
|
3634
|
+
appMessage.content += delegationOutput;
|
|
3635
|
+
} else if (Array.isArray(appMessage.content)) {
|
|
3636
|
+
const textBlock = appMessage.content.find(c => c.type === "text") as any;
|
|
3637
|
+
if (textBlock) {
|
|
3638
|
+
textBlock.text += delegationOutput;
|
|
3639
|
+
} else {
|
|
3640
|
+
appMessage.content.push({ type: "text", text: delegationOutput });
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
await this.#promptWithMessage(appMessage, promptText, options as any);
|
|
3558
3645
|
return;
|
|
3559
3646
|
}
|
|
3560
3647
|
this.agent.appendMessage(appMessage);
|
|
@@ -3569,7 +3656,33 @@ export class AgentSession {
|
|
|
3569
3656
|
}
|
|
3570
3657
|
|
|
3571
3658
|
if (options?.triggerTurn) {
|
|
3572
|
-
|
|
3659
|
+
let promptText = "";
|
|
3660
|
+
if (typeof message.content === "string") promptText = message.content;
|
|
3661
|
+
else if (Array.isArray(message.content)) {
|
|
3662
|
+
promptText = message.content
|
|
3663
|
+
.filter(c => c.type === "text")
|
|
3664
|
+
.map((c: any) => c.text)
|
|
3665
|
+
.join("");
|
|
3666
|
+
}
|
|
3667
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
3668
|
+
promptText,
|
|
3669
|
+
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
3670
|
+
options,
|
|
3671
|
+
);
|
|
3672
|
+
if (delegationOutput) {
|
|
3673
|
+
promptText += delegationOutput;
|
|
3674
|
+
if (typeof appMessage.content === "string") {
|
|
3675
|
+
appMessage.content += delegationOutput;
|
|
3676
|
+
} else if (Array.isArray(appMessage.content)) {
|
|
3677
|
+
const textBlock = appMessage.content.find(c => c.type === "text") as any;
|
|
3678
|
+
if (textBlock) {
|
|
3679
|
+
textBlock.text += delegationOutput;
|
|
3680
|
+
} else {
|
|
3681
|
+
appMessage.content.push({ type: "text", text: delegationOutput });
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
await this.#promptWithMessage(appMessage, promptText, options as any);
|
|
3573
3686
|
return;
|
|
3574
3687
|
}
|
|
3575
3688
|
|
|
@@ -4014,15 +4127,31 @@ export class AgentSession {
|
|
|
4014
4127
|
*/
|
|
4015
4128
|
async setModelRoutingSwitch(model: Model): Promise<void> {
|
|
4016
4129
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
4017
|
-
|
|
4130
|
+
|
|
4131
|
+
let targetModel = model;
|
|
4132
|
+
if (
|
|
4133
|
+
model.provider === "openai" ||
|
|
4134
|
+
(model.provider === "litellm" && (model.id.includes("openai") || /gpt-/.test(model.id)))
|
|
4135
|
+
) {
|
|
4136
|
+
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
4137
|
+
if (internalUrl) targetModel = { ...model, baseUrl: internalUrl };
|
|
4138
|
+
} else if (
|
|
4139
|
+
model.provider === "anthropic" ||
|
|
4140
|
+
(model.provider === "litellm" && (model.id.includes("anthropic") || model.id.includes("claude")))
|
|
4141
|
+
) {
|
|
4142
|
+
const internalUrl = this.settings.get("routing.internalAnthropicUrl") as string | undefined;
|
|
4143
|
+
if (internalUrl) targetModel = { ...model, baseUrl: internalUrl };
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
const apiKey = await this.#modelRegistry.getApiKey(targetModel, this.sessionId);
|
|
4018
4147
|
if (!apiKey) {
|
|
4019
|
-
throw new Error(`No API key for ${
|
|
4148
|
+
throw new Error(`No API key for ${targetModel.provider}/${targetModel.id}`);
|
|
4020
4149
|
}
|
|
4021
4150
|
|
|
4022
4151
|
// DO NOT clear active retry fallback - routing is a transient optimization
|
|
4023
|
-
this.#setModelWithProviderSessionReset(
|
|
4024
|
-
this.sessionManager.appendModelChange(`${
|
|
4025
|
-
this.settings.getStorage()?.recordModelUsage(`${
|
|
4152
|
+
this.#setModelWithProviderSessionReset(targetModel, "runtime-switch");
|
|
4153
|
+
this.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, "routing_switch");
|
|
4154
|
+
this.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
|
|
4026
4155
|
|
|
4027
4156
|
this.setThinkingLevel(this.thinkingLevel);
|
|
4028
4157
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
@@ -6617,8 +6746,10 @@ export class AgentSession {
|
|
|
6617
6746
|
#syncRoutingStateFromBranch() {
|
|
6618
6747
|
this.#routingCoordinator.reset();
|
|
6619
6748
|
const entries = this.sessionManager.getBranch();
|
|
6749
|
+
let foundRoutingState = false;
|
|
6620
6750
|
for (const entry of entries) {
|
|
6621
6751
|
if (entry.type === "custom" && entry.customType === "routing_event") {
|
|
6752
|
+
foundRoutingState = true;
|
|
6622
6753
|
const event = entry.data as any;
|
|
6623
6754
|
if (event.state) {
|
|
6624
6755
|
this.#routingCoordinator.restoreState(event.state);
|
|
@@ -6637,6 +6768,16 @@ export class AgentSession {
|
|
|
6637
6768
|
}
|
|
6638
6769
|
}
|
|
6639
6770
|
}
|
|
6771
|
+
if (!foundRoutingState) {
|
|
6772
|
+
this.#modelResolutionSource = this.#initialModelResolutionSource;
|
|
6773
|
+
const hasServiceTierEntry = this.sessionManager
|
|
6774
|
+
.getBranch()
|
|
6775
|
+
.some(entry => entry.type === "service_tier_change");
|
|
6776
|
+
if (!hasServiceTierEntry) {
|
|
6777
|
+
const configuredServiceTier = this.settings?.get("serviceTier");
|
|
6778
|
+
this.agent.serviceTier = configuredServiceTier === "none" ? undefined : configuredServiceTier;
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6640
6781
|
}
|
|
6641
6782
|
|
|
6642
6783
|
/**
|
|
@@ -6690,6 +6831,7 @@ export class AgentSession {
|
|
|
6690
6831
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
6691
6832
|
}
|
|
6692
6833
|
this.#syncTodoPhasesFromBranch();
|
|
6834
|
+
this.#syncRoutingStateFromBranch();
|
|
6693
6835
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
6694
6836
|
|
|
6695
6837
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
@@ -7236,6 +7378,9 @@ export class AgentSession {
|
|
|
7236
7378
|
export function calculateUsedTokens(messages: any[]): number {
|
|
7237
7379
|
const countTextLength = (content: any): number => {
|
|
7238
7380
|
if (typeof content === "string") {
|
|
7381
|
+
if (content.startsWith("data:image/") || content.startsWith("base64,")) {
|
|
7382
|
+
return 0;
|
|
7383
|
+
}
|
|
7239
7384
|
return content.length;
|
|
7240
7385
|
}
|
|
7241
7386
|
if (Array.isArray(content)) {
|