@f5-sales-demo/xcsh 20.8.4 → 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
|
@@ -1347,39 +1347,59 @@ export class AgentSession {
|
|
|
1347
1347
|
return;
|
|
1348
1348
|
}
|
|
1349
1349
|
|
|
1350
|
-
if (outcome.safeToContinue) {
|
|
1351
|
-
this.
|
|
1350
|
+
if (!outcome.safeToContinue) {
|
|
1351
|
+
this.#routingCoordinator.getStateMachine().setEscalationFloor(targetTier);
|
|
1352
|
+
return;
|
|
1352
1353
|
}
|
|
1353
1354
|
|
|
1354
|
-
this.#routingCoordinator.
|
|
1355
|
-
|
|
1355
|
+
const previousTier = this.#routingCoordinator.getState().currentTier;
|
|
1356
|
+
const previousFloor = this.#routingCoordinator.getState().escalationFloor;
|
|
1357
|
+
const previousModel = this.model;
|
|
1358
|
+
|
|
1359
|
+
try {
|
|
1360
|
+
this.agent.abort();
|
|
1361
|
+
|
|
1362
|
+
this.#routingCoordinator.getStateMachine().setEscalationFloor(targetTier);
|
|
1356
1363
|
this.#routingCoordinator.restoreState({ currentTier: targetTier });
|
|
1357
|
-
}
|
|
1358
1364
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1365
|
+
await this.setModelRoutingSwitch(targetModel);
|
|
1366
|
+
const effortMap = this.settings.get("routing.tierEffort") as Record<string, string> | undefined;
|
|
1367
|
+
const { mapTierToEffort } = await import("../routing/effort");
|
|
1368
|
+
const effort = mapTierToEffort(targetTier, effortMap);
|
|
1369
|
+
this.setThinkingLevel(effort as any);
|
|
1370
|
+
|
|
1371
|
+
this.#emitSessionEvent(
|
|
1372
|
+
sanitizeRoutingEvent({
|
|
1373
|
+
type: "routing_escalated",
|
|
1374
|
+
epochId: `route-${Date.now()}-esc`,
|
|
1375
|
+
reasons: reasons,
|
|
1376
|
+
mode: routingMode,
|
|
1377
|
+
effectiveTier: targetTier,
|
|
1378
|
+
state: this.#routingCoordinator.getState(),
|
|
1379
|
+
selectedModel: this.model ? `${this.model.provider}/${this.model.id}` : undefined,
|
|
1380
|
+
escalated: true,
|
|
1381
|
+
}),
|
|
1382
|
+
).catch(() => {});
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
console.error("Escalation model swap failed", err);
|
|
1385
|
+
this.#routingCoordinator.restoreState({ currentTier: previousTier });
|
|
1386
|
+
if (previousFloor) {
|
|
1387
|
+
this.#routingCoordinator.getStateMachine().setEscalationFloor(previousFloor);
|
|
1388
|
+
} else {
|
|
1389
|
+
this.#routingCoordinator.getStateMachine().clearEscalationFloor();
|
|
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
|
+
}
|
|
1398
|
+
} finally {
|
|
1379
1399
|
this.#scheduleAgentContinue({ delayMs: 10 });
|
|
1380
1400
|
}
|
|
1381
1401
|
} catch (err) {
|
|
1382
|
-
console.error("Escalation
|
|
1402
|
+
console.error("Escalation setup failed", err);
|
|
1383
1403
|
}
|
|
1384
1404
|
};
|
|
1385
1405
|
this.#trackPostPromptTask(performEscalationModelSwap());
|
|
@@ -2838,13 +2858,30 @@ export class AgentSession {
|
|
|
2838
2858
|
.getAll()
|
|
2839
2859
|
.find(m => `${m.provider}/${m.id}` === utilityModel || m.id === utilityModel);
|
|
2840
2860
|
if (!resolved) throw new Error("Classifier model not found");
|
|
2841
|
-
|
|
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);
|
|
2842
2879
|
if (!apiKey) throw new Error("No API key for classifier model");
|
|
2843
2880
|
|
|
2844
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.`;
|
|
2845
2882
|
|
|
2846
2883
|
const res = await completeSimple(
|
|
2847
|
-
|
|
2884
|
+
targetModel,
|
|
2848
2885
|
{
|
|
2849
2886
|
systemPrompt: systemInstruction,
|
|
2850
2887
|
messages: [
|
|
@@ -3068,17 +3105,270 @@ export class AgentSession {
|
|
|
3068
3105
|
return;
|
|
3069
3106
|
}
|
|
3070
3107
|
|
|
3108
|
+
const clonedContent = Array.isArray(message.content)
|
|
3109
|
+
? message.content.map((c: any) => ({ ...c }))
|
|
3110
|
+
: message.content;
|
|
3111
|
+
|
|
3071
3112
|
const customMessage: CustomMessage<T> = {
|
|
3072
3113
|
role: "custom",
|
|
3073
3114
|
customType: message.customType,
|
|
3074
|
-
content:
|
|
3115
|
+
content: clonedContent,
|
|
3075
3116
|
display: message.display,
|
|
3076
3117
|
details: message.details,
|
|
3077
3118
|
attribution: message.attribution ?? "agent",
|
|
3078
3119
|
timestamp: Date.now(),
|
|
3079
3120
|
};
|
|
3080
3121
|
|
|
3081
|
-
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;
|
|
3082
3372
|
}
|
|
3083
3373
|
|
|
3084
3374
|
async #promptWithMessage(
|
|
@@ -3467,8 +3757,25 @@ export class AgentSession {
|
|
|
3467
3757
|
|
|
3468
3758
|
const prependMessages = queuedMessages.slice(0, -1);
|
|
3469
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
|
+
|
|
3470
3777
|
try {
|
|
3471
|
-
await this.#promptWithMessage(message, textContent, {
|
|
3778
|
+
await this.#promptWithMessage(message, textContent + (delegationOutput || ""), {
|
|
3472
3779
|
prependMessages,
|
|
3473
3780
|
skipPostPromptRecoveryWait: true,
|
|
3474
3781
|
});
|
|
@@ -3517,10 +3824,14 @@ export class AgentSession {
|
|
|
3517
3824
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
3518
3825
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
3519
3826
|
): Promise<void> {
|
|
3827
|
+
const clonedContent = Array.isArray(message.content)
|
|
3828
|
+
? message.content.map((c: any) => ({ ...c }))
|
|
3829
|
+
: message.content;
|
|
3830
|
+
|
|
3520
3831
|
const appMessage: CustomMessage<T> = {
|
|
3521
3832
|
role: "custom",
|
|
3522
3833
|
customType: message.customType,
|
|
3523
|
-
content:
|
|
3834
|
+
content: clonedContent,
|
|
3524
3835
|
display: message.display,
|
|
3525
3836
|
details: message.details,
|
|
3526
3837
|
attribution: message.attribution ?? "agent",
|
|
@@ -3542,7 +3853,33 @@ export class AgentSession {
|
|
|
3542
3853
|
|
|
3543
3854
|
if (options?.deliverAs === "nextTurn") {
|
|
3544
3855
|
if (options?.triggerTurn) {
|
|
3545
|
-
|
|
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);
|
|
3546
3883
|
return;
|
|
3547
3884
|
}
|
|
3548
3885
|
this.agent.appendMessage(appMessage);
|
|
@@ -3557,7 +3894,33 @@ export class AgentSession {
|
|
|
3557
3894
|
}
|
|
3558
3895
|
|
|
3559
3896
|
if (options?.triggerTurn) {
|
|
3560
|
-
|
|
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);
|
|
3561
3924
|
return;
|
|
3562
3925
|
}
|
|
3563
3926
|
|
|
@@ -4002,15 +4365,28 @@ export class AgentSession {
|
|
|
4002
4365
|
*/
|
|
4003
4366
|
async setModelRoutingSwitch(model: Model): Promise<void> {
|
|
4004
4367
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
4005
|
-
|
|
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);
|
|
4006
4382
|
if (!apiKey) {
|
|
4007
|
-
throw new Error(`No API key for ${
|
|
4383
|
+
throw new Error(`No API key for ${targetModel.provider}/${targetModel.id}`);
|
|
4008
4384
|
}
|
|
4009
4385
|
|
|
4010
4386
|
// DO NOT clear active retry fallback - routing is a transient optimization
|
|
4011
|
-
this.#setModelWithProviderSessionReset(
|
|
4012
|
-
this.sessionManager.appendModelChange(`${
|
|
4013
|
-
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}`);
|
|
4014
4390
|
|
|
4015
4391
|
this.setThinkingLevel(this.thinkingLevel);
|
|
4016
4392
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|