@f5-sales-demo/xcsh 20.8.5 → 20.8.6
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
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.6",
|
|
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.6",
|
|
65
|
+
"@f5-sales-demo/pi-agent-core": "20.8.6",
|
|
66
|
+
"@f5-sales-demo/pi-ai": "20.8.6",
|
|
67
|
+
"@f5-sales-demo/pi-natives": "20.8.6",
|
|
68
|
+
"@f5-sales-demo/pi-resource-management": "20.8.6",
|
|
69
|
+
"@f5-sales-demo/pi-tui": "20.8.6",
|
|
70
|
+
"@f5-sales-demo/pi-utils": "20.8.6",
|
|
71
71
|
"@sinclair/typebox": "^0.34",
|
|
72
72
|
"@xterm/headless": "^6.0",
|
|
73
73
|
"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.8.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "20.8.6",
|
|
21
|
+
"commit": "ffac88b3a59886c6e6e579bc266e0a62e3d354d5",
|
|
22
|
+
"shortCommit": "ffac88b",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v20.8.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
24
|
+
"tag": "v20.8.6",
|
|
25
|
+
"commitDate": "2026-08-10T05:40:49Z",
|
|
26
|
+
"buildDate": "2026-08-10T06:09:48.444Z",
|
|
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/ffac88b3a59886c6e6e579bc266e0a62e3d354d5",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v20.8.6"
|
|
33
33
|
};
|
package/src/routing/types.ts
CHANGED
|
@@ -1354,6 +1354,7 @@ export class AgentSession {
|
|
|
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 {
|
|
1359
1360
|
this.agent.abort();
|
|
@@ -1387,6 +1388,13 @@ export class AgentSession {
|
|
|
1387
1388
|
} else {
|
|
1388
1389
|
this.#routingCoordinator.getStateMachine().clearEscalationFloor();
|
|
1389
1390
|
}
|
|
1391
|
+
if (previousModel) {
|
|
1392
|
+
this.#setModelWithProviderSessionReset(previousModel, "runtime-switch");
|
|
1393
|
+
this.sessionManager.appendModelChange(
|
|
1394
|
+
`${previousModel.provider}/${previousModel.id}`,
|
|
1395
|
+
"routing_switch_rollback",
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1390
1398
|
} finally {
|
|
1391
1399
|
this.#scheduleAgentContinue({ delayMs: 10 });
|
|
1392
1400
|
}
|
|
@@ -2850,13 +2858,30 @@ export class AgentSession {
|
|
|
2850
2858
|
.getAll()
|
|
2851
2859
|
.find(m => `${m.provider}/${m.id}` === utilityModel || m.id === utilityModel);
|
|
2852
2860
|
if (!resolved) throw new Error("Classifier model not found");
|
|
2853
|
-
|
|
2861
|
+
|
|
2862
|
+
let targetModel = resolved;
|
|
2863
|
+
if (
|
|
2864
|
+
resolved.provider === "openai" ||
|
|
2865
|
+
(resolved.provider === "litellm" && resolved.id.includes("openai"))
|
|
2866
|
+
) {
|
|
2867
|
+
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
2868
|
+
if (internalUrl) targetModel = { ...resolved, baseUrl: internalUrl };
|
|
2869
|
+
} else if (
|
|
2870
|
+
resolved.provider === "anthropic" ||
|
|
2871
|
+
(resolved.provider === "litellm" &&
|
|
2872
|
+
(resolved.id.includes("anthropic") || resolved.id.includes("claude")))
|
|
2873
|
+
) {
|
|
2874
|
+
const internalUrl = this.settings.get("routing.internalAnthropicUrl") as string | undefined;
|
|
2875
|
+
if (internalUrl) targetModel = { ...resolved, baseUrl: internalUrl };
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
const apiKey = await this.#modelRegistry.getApiKey(targetModel, this.sessionId);
|
|
2854
2879
|
if (!apiKey) throw new Error("No API key for classifier model");
|
|
2855
2880
|
|
|
2856
2881
|
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
2882
|
|
|
2858
2883
|
const res = await completeSimple(
|
|
2859
|
-
|
|
2884
|
+
targetModel,
|
|
2860
2885
|
{
|
|
2861
2886
|
systemPrompt: systemInstruction,
|
|
2862
2887
|
messages: [
|
|
@@ -3080,17 +3105,270 @@ export class AgentSession {
|
|
|
3080
3105
|
return;
|
|
3081
3106
|
}
|
|
3082
3107
|
|
|
3108
|
+
const clonedContent = Array.isArray(message.content)
|
|
3109
|
+
? message.content.map((c: any) => ({ ...c }))
|
|
3110
|
+
: message.content;
|
|
3111
|
+
|
|
3083
3112
|
const customMessage: CustomMessage<T> = {
|
|
3084
3113
|
role: "custom",
|
|
3085
3114
|
customType: message.customType,
|
|
3086
|
-
content:
|
|
3115
|
+
content: clonedContent,
|
|
3087
3116
|
display: message.display,
|
|
3088
3117
|
details: message.details,
|
|
3089
3118
|
attribution: message.attribution ?? "agent",
|
|
3090
3119
|
timestamp: Date.now(),
|
|
3091
3120
|
};
|
|
3092
3121
|
|
|
3093
|
-
await this.#
|
|
3122
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
3123
|
+
textContent,
|
|
3124
|
+
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
3125
|
+
);
|
|
3126
|
+
if (delegationOutput) {
|
|
3127
|
+
if (typeof customMessage.content === "string") {
|
|
3128
|
+
customMessage.content += delegationOutput;
|
|
3129
|
+
} else if (Array.isArray(customMessage.content)) {
|
|
3130
|
+
const textBlock = customMessage.content.find((c: any) => c.type === "text") as any;
|
|
3131
|
+
if (textBlock) {
|
|
3132
|
+
textBlock.text += delegationOutput;
|
|
3133
|
+
} else {
|
|
3134
|
+
customMessage.content.push({ type: "text", text: delegationOutput });
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
await this.#promptWithMessage(customMessage, textContent + (delegationOutput || ""), options);
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
async #evaluateAndApplyRouting(
|
|
3143
|
+
promptText: string,
|
|
3144
|
+
hasImages: boolean,
|
|
3145
|
+
options?: { signal?: AbortSignal } | any,
|
|
3146
|
+
): Promise<string | undefined> {
|
|
3147
|
+
const routingMode = (this.settings.get("routing.mode") as import("../routing/types").RoutingMode) ?? "off";
|
|
3148
|
+
if (routingMode === "off" || !this.model) return undefined;
|
|
3149
|
+
|
|
3150
|
+
if (this.#activeRetryFallback) {
|
|
3151
|
+
this.#emitSessionEvent({
|
|
3152
|
+
type: "routing_skipped",
|
|
3153
|
+
epochId: `skip-${Date.now()}`,
|
|
3154
|
+
reasons: ["retry_fallback"],
|
|
3155
|
+
} as any).catch(() => {});
|
|
3156
|
+
return undefined;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
const anchorModel = `${this.model.provider}/${this.model.id}`;
|
|
3160
|
+
const availableModels =
|
|
3161
|
+
this.#scopedModels.length > 0
|
|
3162
|
+
? this.#scopedModels.map(sm => `${sm.model.provider}/${sm.model.id}`)
|
|
3163
|
+
: this.#modelRegistry.getAvailable().map(m => `${m.provider}/${m.id}`);
|
|
3164
|
+
const startTime = Date.now();
|
|
3165
|
+
const systemTokens = Math.round((this.systemPrompt?.length ?? 0) / 4);
|
|
3166
|
+
const promptTokens = Math.round(promptText.length / 4);
|
|
3167
|
+
const toolsStr = JSON.stringify(Array.from(this.#toolRegistry.values()));
|
|
3168
|
+
const toolsTokens = Math.round(toolsStr.length / 4);
|
|
3169
|
+
const reserveTokens = (this.settings.get("compaction.reserveTokens") as number) ?? 0;
|
|
3170
|
+
const contextEstimate = {
|
|
3171
|
+
usedTokens: calculateUsedTokens(this.messages) + systemTokens + promptTokens + toolsTokens,
|
|
3172
|
+
reserveTokens,
|
|
3173
|
+
contextWindow: this.model.contextWindow ?? 128000,
|
|
3174
|
+
};
|
|
3175
|
+
const decision = await this.#routingCoordinator.evaluateTurn({
|
|
3176
|
+
anchorModel,
|
|
3177
|
+
mode: routingMode,
|
|
3178
|
+
prompt: promptText,
|
|
3179
|
+
hasImages,
|
|
3180
|
+
priorRejection: this.#routingCoordinator.getState().escalationFloor !== undefined,
|
|
3181
|
+
availableModels,
|
|
3182
|
+
customPools: validateCustomPools(this.settings.get("routing.pools")),
|
|
3183
|
+
disabledPresets: (this.settings.get("routing.disabledPresets") as readonly string[]) ?? [],
|
|
3184
|
+
familyPolicy: (this.settings.get("routing.familyPolicy") as "sticky" | "configured-mixed") ?? "sticky",
|
|
3185
|
+
profilerMode: (this.settings.get("routing.profiler") as any) ?? "hybrid",
|
|
3186
|
+
contextEstimate,
|
|
3187
|
+
getModelContextWindow: (modelId: string) => {
|
|
3188
|
+
const m = this.#modelRegistry
|
|
3189
|
+
.getAvailable()
|
|
3190
|
+
.find(m => `${m.provider}/${m.id}` === modelId || m.id === modelId);
|
|
3191
|
+
return m?.contextWindow ?? 128000;
|
|
3192
|
+
},
|
|
3193
|
+
});
|
|
3194
|
+
|
|
3195
|
+
this.#emitSessionEvent({
|
|
3196
|
+
type: "routing_evaluated",
|
|
3197
|
+
epochId: decision.epochId,
|
|
3198
|
+
profiler: {
|
|
3199
|
+
latencyMs: Date.now() - startTime,
|
|
3200
|
+
...(decision as any).profiler,
|
|
3201
|
+
},
|
|
3202
|
+
applied: decision.applied,
|
|
3203
|
+
decision: decision.selectedModel,
|
|
3204
|
+
delegation: decision.delegation,
|
|
3205
|
+
} as any).catch(() => {});
|
|
3206
|
+
|
|
3207
|
+
if (decision.selectedModel && decision.applied) {
|
|
3208
|
+
const targetModel = this.#modelRegistry
|
|
3209
|
+
.getAvailable()
|
|
3210
|
+
.find(m => `${m.provider}/${m.id}` === decision.selectedModel || m.id === decision.selectedModel);
|
|
3211
|
+
if (targetModel) {
|
|
3212
|
+
const currentModelId = `${this.model.provider}/${this.model.id}`;
|
|
3213
|
+
const targetModelId = `${targetModel.provider}/${targetModel.id}`;
|
|
3214
|
+
|
|
3215
|
+
let needsSwitch = currentModelId !== targetModelId;
|
|
3216
|
+
|
|
3217
|
+
// Force switch if the current model is the same but needs an internal URL override
|
|
3218
|
+
if (!needsSwitch) {
|
|
3219
|
+
if (
|
|
3220
|
+
targetModel.provider === "openai" ||
|
|
3221
|
+
(targetModel.provider === "litellm" && targetModel.id.includes("openai"))
|
|
3222
|
+
) {
|
|
3223
|
+
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
3224
|
+
if ((internalUrl || undefined) !== this.model.baseUrl) {
|
|
3225
|
+
needsSwitch = true;
|
|
3226
|
+
}
|
|
3227
|
+
} else if (
|
|
3228
|
+
targetModel.provider === "anthropic" ||
|
|
3229
|
+
(targetModel.provider === "litellm" &&
|
|
3230
|
+
(targetModel.id.includes("anthropic") || targetModel.id.includes("claude")))
|
|
3231
|
+
) {
|
|
3232
|
+
const internalUrl = this.settings.get("routing.internalAnthropicUrl") as string | undefined;
|
|
3233
|
+
if ((internalUrl || undefined) !== this.model.baseUrl) {
|
|
3234
|
+
needsSwitch = true;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
if (needsSwitch) {
|
|
3240
|
+
await this.setModelRoutingSwitch(targetModel);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
if (
|
|
3246
|
+
decision.applied &&
|
|
3247
|
+
decision.delegation &&
|
|
3248
|
+
decision.delegation.subtasks.length > 1 &&
|
|
3249
|
+
(this.settings.get("routing.delegation") as string) === "read-only"
|
|
3250
|
+
) {
|
|
3251
|
+
let maxTasks = (this.settings.get("routing.delegationMaxTasks") as number) ?? 3;
|
|
3252
|
+
maxTasks = Math.min(Math.max(1, maxTasks), 3);
|
|
3253
|
+
|
|
3254
|
+
const { results, tokensUsed } = await executeReadOnlyDelegationPlan(
|
|
3255
|
+
decision.delegation,
|
|
3256
|
+
async (subtaskPrompt, signal) => {
|
|
3257
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3258
|
+
|
|
3259
|
+
const { resolveModelPool } = await import("../routing/presets");
|
|
3260
|
+
const pool = resolveModelPool(
|
|
3261
|
+
decision.poolId ?? (this.model ? `${this.model.provider}/${this.model.id}` : ""),
|
|
3262
|
+
this.settings.get("routing.pools") as any,
|
|
3263
|
+
this.settings.get("routing.disabledPresets") as readonly string[],
|
|
3264
|
+
this.settings.get("routing.familyPolicy") as any,
|
|
3265
|
+
);
|
|
3266
|
+
|
|
3267
|
+
let resolvedUtility = this.model;
|
|
3268
|
+
if (pool?.tiers?.utility) {
|
|
3269
|
+
let uId = pool.tiers.utility;
|
|
3270
|
+
if (!uId.includes("/") && pool.provider) {
|
|
3271
|
+
uId = `${pool.provider}/${uId}`;
|
|
3272
|
+
}
|
|
3273
|
+
const found = this.#modelRegistry
|
|
3274
|
+
.getAvailable()
|
|
3275
|
+
.find(m => `${m.provider}/${m.id}` === uId || m.id === uId);
|
|
3276
|
+
if (found) resolvedUtility = found;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
if (!resolvedUtility) return { result: "Delegation failed: Model not resolved", tokens: 0 };
|
|
3280
|
+
|
|
3281
|
+
if (
|
|
3282
|
+
resolvedUtility.provider === "openai" ||
|
|
3283
|
+
(resolvedUtility.provider === "litellm" && resolvedUtility.id.includes("openai"))
|
|
3284
|
+
) {
|
|
3285
|
+
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
3286
|
+
if (internalUrl) resolvedUtility = { ...resolvedUtility, baseUrl: internalUrl } as any;
|
|
3287
|
+
} else if (
|
|
3288
|
+
resolvedUtility.provider === "anthropic" ||
|
|
3289
|
+
(resolvedUtility.provider === "litellm" &&
|
|
3290
|
+
(resolvedUtility.id.includes("anthropic") || resolvedUtility.id.includes("claude")))
|
|
3291
|
+
) {
|
|
3292
|
+
const internalUrl = this.settings.get("routing.internalAnthropicUrl") as string | undefined;
|
|
3293
|
+
if (internalUrl) resolvedUtility = { ...resolvedUtility, baseUrl: internalUrl } as any;
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
const { createAgentSession } = await import("../sdk");
|
|
3297
|
+
const { SessionManager: SDKSessionManager } = await import("./session-manager");
|
|
3298
|
+
const childSettings = await this.settings.cloneForCwd(process.cwd());
|
|
3299
|
+
childSettings.set("routing.delegation", "off");
|
|
3300
|
+
|
|
3301
|
+
if (options?.signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3302
|
+
|
|
3303
|
+
const { session: childSession } = await createAgentSession({
|
|
3304
|
+
model: resolvedUtility!,
|
|
3305
|
+
sessionManager: SDKSessionManager.inMemory(),
|
|
3306
|
+
settings: childSettings,
|
|
3307
|
+
authStorage: this.#modelRegistry.authStorage,
|
|
3308
|
+
modelRegistry: this.#modelRegistry,
|
|
3309
|
+
toolNames: Array.from(this.#toolRegistry.values())
|
|
3310
|
+
.map(t => t.name)
|
|
3311
|
+
.filter(isDelegationAllowedTool),
|
|
3312
|
+
enableLsp: false,
|
|
3313
|
+
enableMCP: false,
|
|
3314
|
+
});
|
|
3315
|
+
|
|
3316
|
+
try {
|
|
3317
|
+
const onAbort = () => childSession.agent.abort();
|
|
3318
|
+
signal?.addEventListener("abort", onAbort);
|
|
3319
|
+
if (signal?.aborted) {
|
|
3320
|
+
onAbort();
|
|
3321
|
+
return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3322
|
+
}
|
|
3323
|
+
try {
|
|
3324
|
+
await childSession.prompt(subtaskPrompt);
|
|
3325
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3326
|
+
const resultStr = childSession
|
|
3327
|
+
.buildDisplaySessionContext()
|
|
3328
|
+
.messages.filter((m: any) => m.role === "assistant")
|
|
3329
|
+
.map((m: any) =>
|
|
3330
|
+
m.content
|
|
3331
|
+
.filter((c: any) => c.type === "text")
|
|
3332
|
+
.map((c: any) => c.text)
|
|
3333
|
+
.join(""),
|
|
3334
|
+
)
|
|
3335
|
+
.join("\n");
|
|
3336
|
+
const tUsage = ((childSession as any).agent?.lastUsage?.totalTokens ?? 0) as number;
|
|
3337
|
+
return { result: resultStr || "No output", tokens: tUsage };
|
|
3338
|
+
} finally {
|
|
3339
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3340
|
+
}
|
|
3341
|
+
} catch (err) {
|
|
3342
|
+
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3343
|
+
return { result: `Delegation failed: ${String(err)}`, tokens: 0 };
|
|
3344
|
+
} finally {
|
|
3345
|
+
await childSession.dispose();
|
|
3346
|
+
}
|
|
3347
|
+
},
|
|
3348
|
+
maxTasks,
|
|
3349
|
+
{ signal: options?.signal },
|
|
3350
|
+
);
|
|
3351
|
+
|
|
3352
|
+
if (results.length > 0) {
|
|
3353
|
+
let resultsString = JSON.stringify(results, null, 2);
|
|
3354
|
+
if (resultsString.length > 8000) {
|
|
3355
|
+
resultsString = `${resultsString.substring(0, 8000)}\n...[truncated]`;
|
|
3356
|
+
}
|
|
3357
|
+
const delegationOutput = `\n\n<delegation_results>\n${resultsString}\n</delegation_results>`;
|
|
3358
|
+
|
|
3359
|
+
this.#emitSessionEvent({
|
|
3360
|
+
type: "routing_delegated",
|
|
3361
|
+
epochId: decision.epochId,
|
|
3362
|
+
tasks: decision.delegation.subtasks.length,
|
|
3363
|
+
completed: results.length,
|
|
3364
|
+
tokensUsed,
|
|
3365
|
+
} as any).catch(() => {});
|
|
3366
|
+
|
|
3367
|
+
return delegationOutput;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
return undefined;
|
|
3094
3372
|
}
|
|
3095
3373
|
|
|
3096
3374
|
async #promptWithMessage(
|
|
@@ -3479,8 +3757,25 @@ export class AgentSession {
|
|
|
3479
3757
|
|
|
3480
3758
|
const prependMessages = queuedMessages.slice(0, -1);
|
|
3481
3759
|
const textContent = this.#getCustomMessageTextContent(message);
|
|
3760
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
3761
|
+
textContent,
|
|
3762
|
+
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
3763
|
+
);
|
|
3764
|
+
if (delegationOutput) {
|
|
3765
|
+
if (typeof message.content === "string") {
|
|
3766
|
+
message.content += delegationOutput;
|
|
3767
|
+
} else if (Array.isArray(message.content)) {
|
|
3768
|
+
const textBlock = message.content.find((c: any) => c.type === "text");
|
|
3769
|
+
if (textBlock) {
|
|
3770
|
+
(textBlock as any).text += delegationOutput;
|
|
3771
|
+
} else {
|
|
3772
|
+
message.content.push({ type: "text", text: delegationOutput });
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3482
3777
|
try {
|
|
3483
|
-
await this.#promptWithMessage(message, textContent, {
|
|
3778
|
+
await this.#promptWithMessage(message, textContent + (delegationOutput || ""), {
|
|
3484
3779
|
prependMessages,
|
|
3485
3780
|
skipPostPromptRecoveryWait: true,
|
|
3486
3781
|
});
|
|
@@ -3529,10 +3824,14 @@ export class AgentSession {
|
|
|
3529
3824
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
3530
3825
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
3531
3826
|
): Promise<void> {
|
|
3827
|
+
const clonedContent = Array.isArray(message.content)
|
|
3828
|
+
? message.content.map((c: any) => ({ ...c }))
|
|
3829
|
+
: message.content;
|
|
3830
|
+
|
|
3532
3831
|
const appMessage: CustomMessage<T> = {
|
|
3533
3832
|
role: "custom",
|
|
3534
3833
|
customType: message.customType,
|
|
3535
|
-
content:
|
|
3834
|
+
content: clonedContent,
|
|
3536
3835
|
display: message.display,
|
|
3537
3836
|
details: message.details,
|
|
3538
3837
|
attribution: message.attribution ?? "agent",
|
|
@@ -3554,7 +3853,33 @@ export class AgentSession {
|
|
|
3554
3853
|
|
|
3555
3854
|
if (options?.deliverAs === "nextTurn") {
|
|
3556
3855
|
if (options?.triggerTurn) {
|
|
3557
|
-
|
|
3856
|
+
let promptText = "";
|
|
3857
|
+
if (typeof message.content === "string") promptText = message.content;
|
|
3858
|
+
else if (Array.isArray(message.content)) {
|
|
3859
|
+
promptText = message.content
|
|
3860
|
+
.filter(c => c.type === "text")
|
|
3861
|
+
.map((c: any) => c.text)
|
|
3862
|
+
.join("");
|
|
3863
|
+
}
|
|
3864
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
3865
|
+
promptText,
|
|
3866
|
+
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
3867
|
+
options,
|
|
3868
|
+
);
|
|
3869
|
+
if (delegationOutput) {
|
|
3870
|
+
promptText += delegationOutput;
|
|
3871
|
+
if (typeof appMessage.content === "string") {
|
|
3872
|
+
appMessage.content += delegationOutput;
|
|
3873
|
+
} else if (Array.isArray(appMessage.content)) {
|
|
3874
|
+
const textBlock = appMessage.content.find(c => c.type === "text") as any;
|
|
3875
|
+
if (textBlock) {
|
|
3876
|
+
textBlock.text += delegationOutput;
|
|
3877
|
+
} else {
|
|
3878
|
+
appMessage.content.push({ type: "text", text: delegationOutput });
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
await this.#promptWithMessage(appMessage, promptText, options as any);
|
|
3558
3883
|
return;
|
|
3559
3884
|
}
|
|
3560
3885
|
this.agent.appendMessage(appMessage);
|
|
@@ -3569,7 +3894,33 @@ export class AgentSession {
|
|
|
3569
3894
|
}
|
|
3570
3895
|
|
|
3571
3896
|
if (options?.triggerTurn) {
|
|
3572
|
-
|
|
3897
|
+
let promptText = "";
|
|
3898
|
+
if (typeof message.content === "string") promptText = message.content;
|
|
3899
|
+
else if (Array.isArray(message.content)) {
|
|
3900
|
+
promptText = message.content
|
|
3901
|
+
.filter(c => c.type === "text")
|
|
3902
|
+
.map((c: any) => c.text)
|
|
3903
|
+
.join("");
|
|
3904
|
+
}
|
|
3905
|
+
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
3906
|
+
promptText,
|
|
3907
|
+
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
3908
|
+
options,
|
|
3909
|
+
);
|
|
3910
|
+
if (delegationOutput) {
|
|
3911
|
+
promptText += delegationOutput;
|
|
3912
|
+
if (typeof appMessage.content === "string") {
|
|
3913
|
+
appMessage.content += delegationOutput;
|
|
3914
|
+
} else if (Array.isArray(appMessage.content)) {
|
|
3915
|
+
const textBlock = appMessage.content.find(c => c.type === "text") as any;
|
|
3916
|
+
if (textBlock) {
|
|
3917
|
+
textBlock.text += delegationOutput;
|
|
3918
|
+
} else {
|
|
3919
|
+
appMessage.content.push({ type: "text", text: delegationOutput });
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
await this.#promptWithMessage(appMessage, promptText, options as any);
|
|
3573
3924
|
return;
|
|
3574
3925
|
}
|
|
3575
3926
|
|
|
@@ -4014,15 +4365,28 @@ export class AgentSession {
|
|
|
4014
4365
|
*/
|
|
4015
4366
|
async setModelRoutingSwitch(model: Model): Promise<void> {
|
|
4016
4367
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
4017
|
-
|
|
4368
|
+
|
|
4369
|
+
let targetModel = model;
|
|
4370
|
+
if (model.provider === "openai" || (model.provider === "litellm" && model.id.includes("openai"))) {
|
|
4371
|
+
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
4372
|
+
if (internalUrl) targetModel = { ...model, baseUrl: internalUrl };
|
|
4373
|
+
} else if (
|
|
4374
|
+
model.provider === "anthropic" ||
|
|
4375
|
+
(model.provider === "litellm" && (model.id.includes("anthropic") || model.id.includes("claude")))
|
|
4376
|
+
) {
|
|
4377
|
+
const internalUrl = this.settings.get("routing.internalAnthropicUrl") as string | undefined;
|
|
4378
|
+
if (internalUrl) targetModel = { ...model, baseUrl: internalUrl };
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
const apiKey = await this.#modelRegistry.getApiKey(targetModel, this.sessionId);
|
|
4018
4382
|
if (!apiKey) {
|
|
4019
|
-
throw new Error(`No API key for ${
|
|
4383
|
+
throw new Error(`No API key for ${targetModel.provider}/${targetModel.id}`);
|
|
4020
4384
|
}
|
|
4021
4385
|
|
|
4022
4386
|
// DO NOT clear active retry fallback - routing is a transient optimization
|
|
4023
|
-
this.#setModelWithProviderSessionReset(
|
|
4024
|
-
this.sessionManager.appendModelChange(`${
|
|
4025
|
-
this.settings.getStorage()?.recordModelUsage(`${
|
|
4387
|
+
this.#setModelWithProviderSessionReset(targetModel, "runtime-switch");
|
|
4388
|
+
this.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, "routing_switch");
|
|
4389
|
+
this.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
|
|
4026
4390
|
|
|
4027
4391
|
this.setThinkingLevel(this.thinkingLevel);
|
|
4028
4392
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|