@f5-sales-demo/xcsh 20.8.6 → 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
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",
|
|
@@ -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 };
|
|
@@ -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,7 +1349,6 @@ 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
|
|
|
@@ -1357,12 +1357,11 @@ export class AgentSession {
|
|
|
1357
1357
|
const previousModel = this.model;
|
|
1358
1358
|
|
|
1359
1359
|
try {
|
|
1360
|
+
await this.setModelRoutingSwitch(targetModel);
|
|
1360
1361
|
this.agent.abort();
|
|
1361
1362
|
|
|
1362
1363
|
this.#routingCoordinator.getStateMachine().setEscalationFloor(targetTier);
|
|
1363
1364
|
this.#routingCoordinator.restoreState({ currentTier: targetTier });
|
|
1364
|
-
|
|
1365
|
-
await this.setModelRoutingSwitch(targetModel);
|
|
1366
1365
|
const effortMap = this.settings.get("routing.tierEffort") as Record<string, string> | undefined;
|
|
1367
1366
|
const { mapTierToEffort } = await import("../routing/effort");
|
|
1368
1367
|
const effort = mapTierToEffort(targetTier, effortMap);
|
|
@@ -2809,264 +2808,21 @@ export class AgentSession {
|
|
|
2809
2808
|
|
|
2810
2809
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
2811
2810
|
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
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;
|
|
2821
2824
|
} else {
|
|
2822
|
-
|
|
2823
|
-
const availableModels =
|
|
2824
|
-
this.#scopedModels.length > 0
|
|
2825
|
-
? this.#scopedModels.map(sm => `${sm.model.provider}/${sm.model.id}`)
|
|
2826
|
-
: this.#modelRegistry.getAvailable().map(m => `${m.provider}/${m.id}`);
|
|
2827
|
-
const startTime = Date.now();
|
|
2828
|
-
const systemTokens = Math.round((this.systemPrompt?.length ?? 0) / 4);
|
|
2829
|
-
const promptTokens = Math.round(expandedText.length / 4);
|
|
2830
|
-
const toolsStr = JSON.stringify(Array.from(this.#toolRegistry.values()));
|
|
2831
|
-
const toolsTokens = Math.round(toolsStr.length / 4);
|
|
2832
|
-
const reserveTokens = (this.settings.get("compaction.reserveTokens") as number) ?? 0;
|
|
2833
|
-
const contextEstimate = {
|
|
2834
|
-
usedTokens: calculateUsedTokens(this.messages) + systemTokens + promptTokens + toolsTokens,
|
|
2835
|
-
reserveTokens,
|
|
2836
|
-
contextWindow: this.model.contextWindow ?? 128000,
|
|
2837
|
-
};
|
|
2838
|
-
const decision = await this.#routingCoordinator.evaluateTurn({
|
|
2839
|
-
anchorModel,
|
|
2840
|
-
mode: routingMode,
|
|
2841
|
-
prompt: expandedText,
|
|
2842
|
-
hasImages: options?.images && options.images.length > 0,
|
|
2843
|
-
priorRejection: this.#routingCoordinator.getState().escalationFloor !== undefined,
|
|
2844
|
-
availableModels,
|
|
2845
|
-
customPools: validateCustomPools(this.settings.get("routing.pools")),
|
|
2846
|
-
disabledPresets: (this.settings.get("routing.disabledPresets") as readonly string[]) ?? [],
|
|
2847
|
-
familyPolicy: (this.settings.get("routing.familyPolicy") as "sticky" | "configured-mixed") ?? "sticky",
|
|
2848
|
-
profilerMode: (this.settings.get("routing.profiler") as any) ?? "hybrid",
|
|
2849
|
-
contextEstimate,
|
|
2850
|
-
getModelContextWindow: (modelId: string) => {
|
|
2851
|
-
const found = this.#modelRegistry
|
|
2852
|
-
.getAll()
|
|
2853
|
-
.find(m => `${m.provider}/${m.id}` === modelId || m.id === modelId);
|
|
2854
|
-
return found?.contextWindow ?? 128000;
|
|
2855
|
-
},
|
|
2856
|
-
runRoutingClassifier: async (utilityModel: string, promptText: string) => {
|
|
2857
|
-
const resolved = this.#modelRegistry
|
|
2858
|
-
.getAll()
|
|
2859
|
-
.find(m => `${m.provider}/${m.id}` === utilityModel || m.id === utilityModel);
|
|
2860
|
-
if (!resolved) throw new Error("Classifier model not found");
|
|
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);
|
|
2879
|
-
if (!apiKey) throw new Error("No API key for classifier model");
|
|
2880
|
-
|
|
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.`;
|
|
2882
|
-
|
|
2883
|
-
const res = await completeSimple(
|
|
2884
|
-
targetModel,
|
|
2885
|
-
{
|
|
2886
|
-
systemPrompt: systemInstruction,
|
|
2887
|
-
messages: [
|
|
2888
|
-
{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() },
|
|
2889
|
-
],
|
|
2890
|
-
},
|
|
2891
|
-
{ apiKey, signal: AbortSignal.timeout(15000) },
|
|
2892
|
-
);
|
|
2893
|
-
const rawText = res.content
|
|
2894
|
-
.filter(c => c.type === "text")
|
|
2895
|
-
.map(c => (c as TextContent).text)
|
|
2896
|
-
.join("");
|
|
2897
|
-
|
|
2898
|
-
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
|
|
2899
|
-
if (jsonMatch) {
|
|
2900
|
-
const parsed = JSON.parse(jsonMatch[0]); // Check validity
|
|
2901
|
-
if (res.usage) {
|
|
2902
|
-
this.settings.getStorage()?.recordModelUsage(`${resolved.provider}/${resolved.id}`);
|
|
2903
|
-
parsed.routingUsage = (res.usage as any).totalTokens ?? (res.usage as any).total ?? 0;
|
|
2904
|
-
}
|
|
2905
|
-
return JSON.stringify(parsed);
|
|
2906
|
-
}
|
|
2907
|
-
throw new Error("Classifier returned malformed JSON");
|
|
2908
|
-
},
|
|
2909
|
-
downshiftAfterTurns: (this.settings.get("routing.downshiftAfterTurns") as number) ?? 2,
|
|
2910
|
-
});
|
|
2911
|
-
const durationMs = Date.now() - startTime;
|
|
2912
|
-
|
|
2913
|
-
this.#emitSessionEvent({
|
|
2914
|
-
type: decision.applied ? "routing_applied" : "routing_decision",
|
|
2915
|
-
epochId: decision.epochId,
|
|
2916
|
-
mode: decision.mode,
|
|
2917
|
-
provider: this.model.provider,
|
|
2918
|
-
poolId: decision.poolId,
|
|
2919
|
-
effectiveTier: decision.effectiveTier,
|
|
2920
|
-
selectedModel: decision.selectedModel,
|
|
2921
|
-
reasons: decision.reasons,
|
|
2922
|
-
delegated: !!decision.delegation,
|
|
2923
|
-
escalated: decision.reasons.some(
|
|
2924
|
-
r => r === "context_capacity_promotion" || r === "escalation_floor_active",
|
|
2925
|
-
),
|
|
2926
|
-
contextTokens: contextEstimate.usedTokens,
|
|
2927
|
-
routingUsage: decision.routingUsage,
|
|
2928
|
-
durationMs,
|
|
2929
|
-
state: this.#routingCoordinator.getState(),
|
|
2930
|
-
} as any).catch(() => {});
|
|
2931
|
-
|
|
2932
|
-
if (decision.selectedModel && decision.applied) {
|
|
2933
|
-
const targetModel = this.#modelRegistry
|
|
2934
|
-
.getAvailable()
|
|
2935
|
-
.find(m => `${m.provider}/${m.id}` === decision.selectedModel || m.id === decision.selectedModel);
|
|
2936
|
-
if (targetModel) {
|
|
2937
|
-
const currentModelId = this.model ? `${this.model.provider}/${this.model.id}` : undefined;
|
|
2938
|
-
const targetModelId = `${targetModel.provider}/${targetModel.id}`;
|
|
2939
|
-
if (currentModelId !== targetModelId) {
|
|
2940
|
-
await this.setModelRoutingSwitch(targetModel);
|
|
2941
|
-
}
|
|
2942
|
-
if (decision.effectiveTier) {
|
|
2943
|
-
const effortMap = this.settings.get("routing.tierEffort") as Record<string, string> | undefined;
|
|
2944
|
-
const { mapTierToEffort } = await import("../routing/effort");
|
|
2945
|
-
const effort = mapTierToEffort(decision.effectiveTier, effortMap);
|
|
2946
|
-
this.setThinkingLevel(effort as any);
|
|
2947
|
-
}
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
|
|
2951
|
-
if (
|
|
2952
|
-
decision.applied &&
|
|
2953
|
-
decision.delegation &&
|
|
2954
|
-
decision.delegation.subtasks.length > 1 &&
|
|
2955
|
-
(this.settings.get("routing.delegation") as string) === "read-only"
|
|
2956
|
-
) {
|
|
2957
|
-
let maxTasks = (this.settings.get("routing.delegationMaxTasks") as number) ?? 3;
|
|
2958
|
-
maxTasks = Math.min(Math.max(1, maxTasks), 3);
|
|
2959
|
-
|
|
2960
|
-
const { results, tokensUsed } = await executeReadOnlyDelegationPlan(
|
|
2961
|
-
decision.delegation,
|
|
2962
|
-
async (subtaskPrompt, signal) => {
|
|
2963
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2964
|
-
|
|
2965
|
-
const { resolveModelPool } = await import("../routing/presets");
|
|
2966
|
-
const pool = resolveModelPool(
|
|
2967
|
-
decision.poolId ?? (this.model ? `${this.model.provider}/${this.model.id}` : ""),
|
|
2968
|
-
this.settings.get("routing.pools") as any,
|
|
2969
|
-
this.settings.get("routing.disabledPresets") as readonly string[],
|
|
2970
|
-
this.settings.get("routing.familyPolicy") as any,
|
|
2971
|
-
);
|
|
2972
|
-
|
|
2973
|
-
let resolvedUtility = this.model;
|
|
2974
|
-
if (pool?.tiers?.utility) {
|
|
2975
|
-
let uId = pool.tiers.utility;
|
|
2976
|
-
if (!uId.includes("/") && pool.provider) {
|
|
2977
|
-
uId = `${pool.provider}/${uId}`;
|
|
2978
|
-
}
|
|
2979
|
-
const found = this.#modelRegistry
|
|
2980
|
-
.getAvailable()
|
|
2981
|
-
.find(m => `${m.provider}/${m.id}` === uId || m.id === uId);
|
|
2982
|
-
if (found) resolvedUtility = found;
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
if (!resolvedUtility) return { result: "Delegation failed: no model.", tokens: 0 };
|
|
2986
|
-
|
|
2987
|
-
const { createAgentSession } = await import("../sdk");
|
|
2988
|
-
const { SessionManager: SDKSessionManager } = await import("./session-manager");
|
|
2989
|
-
const childSettings = await this.settings.cloneForCwd(process.cwd());
|
|
2990
|
-
childSettings.set("routing.delegation", "off");
|
|
2991
|
-
|
|
2992
|
-
// Fix abort race by checking right before creation
|
|
2993
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
2994
|
-
|
|
2995
|
-
const { session: childSession } = await createAgentSession({
|
|
2996
|
-
model: resolvedUtility,
|
|
2997
|
-
sessionManager: SDKSessionManager.inMemory(),
|
|
2998
|
-
settings: childSettings,
|
|
2999
|
-
authStorage: this.#modelRegistry.authStorage,
|
|
3000
|
-
modelRegistry: this.#modelRegistry,
|
|
3001
|
-
toolNames: Array.from(this.#toolRegistry.values())
|
|
3002
|
-
.map(t => t.name)
|
|
3003
|
-
.filter(isDelegationAllowedTool),
|
|
3004
|
-
enableLsp: false,
|
|
3005
|
-
enableMCP: false,
|
|
3006
|
-
});
|
|
3007
|
-
|
|
3008
|
-
try {
|
|
3009
|
-
const onAbort = () => childSession.agent.abort();
|
|
3010
|
-
signal?.addEventListener("abort", onAbort);
|
|
3011
|
-
if (signal?.aborted) {
|
|
3012
|
-
onAbort();
|
|
3013
|
-
return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3014
|
-
}
|
|
3015
|
-
try {
|
|
3016
|
-
await childSession.prompt(subtaskPrompt);
|
|
3017
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3018
|
-
const resultStr = childSession
|
|
3019
|
-
.buildDisplaySessionContext()
|
|
3020
|
-
.messages.filter(m => m.role === "assistant")
|
|
3021
|
-
.map(m =>
|
|
3022
|
-
m.content
|
|
3023
|
-
.filter((c: any) => c.type === "text")
|
|
3024
|
-
.map((c: any) => c.text)
|
|
3025
|
-
.join(""),
|
|
3026
|
-
)
|
|
3027
|
-
.join("\n");
|
|
3028
|
-
const tokens = childSession.buildDisplaySessionContext().usedTokens || 0;
|
|
3029
|
-
return { result: resultStr, tokens };
|
|
3030
|
-
} finally {
|
|
3031
|
-
signal?.removeEventListener("abort", onAbort);
|
|
3032
|
-
}
|
|
3033
|
-
} catch (err) {
|
|
3034
|
-
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3035
|
-
return { result: `Delegation failed: ${String(err)}`, tokens: 0 };
|
|
3036
|
-
} finally {
|
|
3037
|
-
await childSession.dispose();
|
|
3038
|
-
}
|
|
3039
|
-
},
|
|
3040
|
-
maxTasks,
|
|
3041
|
-
{ signal: options?.signal },
|
|
3042
|
-
);
|
|
3043
|
-
|
|
3044
|
-
if (results.length > 0) {
|
|
3045
|
-
let resultsString = JSON.stringify(results, null, 2);
|
|
3046
|
-
if (resultsString.length > 8000) {
|
|
3047
|
-
resultsString = `${resultsString.substring(0, 8000)}\n...[truncated]`;
|
|
3048
|
-
}
|
|
3049
|
-
const delegationOutput = `\n\n<delegation_results>\n${resultsString}\n</delegation_results>`;
|
|
3050
|
-
expandedText += delegationOutput;
|
|
3051
|
-
const textBlock = userContent.find(c => c.type === "text") as Extract<
|
|
3052
|
-
(typeof userContent)[0],
|
|
3053
|
-
{ type: "text" }
|
|
3054
|
-
>;
|
|
3055
|
-
if (textBlock) {
|
|
3056
|
-
textBlock.text += delegationOutput;
|
|
3057
|
-
} else {
|
|
3058
|
-
userContent.push({ type: "text", text: delegationOutput });
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
this.#emitSessionEvent({
|
|
3062
|
-
type: "routing_delegated",
|
|
3063
|
-
epochId: decision.epochId,
|
|
3064
|
-
tasks: decision.delegation.subtasks.length,
|
|
3065
|
-
completed: results.length,
|
|
3066
|
-
tokensUsed,
|
|
3067
|
-
} as any).catch(() => {});
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
2825
|
+
userContent.push({ type: "text", text: delegationOutput });
|
|
3070
2826
|
}
|
|
3071
2827
|
}
|
|
3072
2828
|
|
|
@@ -3087,7 +2843,7 @@ export class AgentSession {
|
|
|
3087
2843
|
|
|
3088
2844
|
async promptCustomMessage<T = unknown>(
|
|
3089
2845
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
3090
|
-
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
|
|
2846
|
+
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "signal">,
|
|
3091
2847
|
): Promise<void> {
|
|
3092
2848
|
const textContent =
|
|
3093
2849
|
typeof message.content === "string"
|
|
@@ -3122,6 +2878,7 @@ export class AgentSession {
|
|
|
3122
2878
|
const delegationOutput = await this.#evaluateAndApplyRouting(
|
|
3123
2879
|
textContent,
|
|
3124
2880
|
Array.isArray(message.content) && message.content.some((c: any) => c.type === "image"),
|
|
2881
|
+
{ signal: options?.signal },
|
|
3125
2882
|
);
|
|
3126
2883
|
if (delegationOutput) {
|
|
3127
2884
|
if (typeof customMessage.content === "string") {
|
|
@@ -3184,6 +2941,7 @@ export class AgentSession {
|
|
|
3184
2941
|
familyPolicy: (this.settings.get("routing.familyPolicy") as "sticky" | "configured-mixed") ?? "sticky",
|
|
3185
2942
|
profilerMode: (this.settings.get("routing.profiler") as any) ?? "hybrid",
|
|
3186
2943
|
contextEstimate,
|
|
2944
|
+
signal: options?.signal,
|
|
3187
2945
|
getModelContextWindow: (modelId: string) => {
|
|
3188
2946
|
const m = this.#modelRegistry
|
|
3189
2947
|
.getAvailable()
|
|
@@ -3218,7 +2976,8 @@ export class AgentSession {
|
|
|
3218
2976
|
if (!needsSwitch) {
|
|
3219
2977
|
if (
|
|
3220
2978
|
targetModel.provider === "openai" ||
|
|
3221
|
-
(targetModel.provider === "litellm" &&
|
|
2979
|
+
(targetModel.provider === "litellm" &&
|
|
2980
|
+
(targetModel.id.includes("openai") || /gpt-/.test(targetModel.id)))
|
|
3222
2981
|
) {
|
|
3223
2982
|
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
3224
2983
|
if ((internalUrl || undefined) !== this.model.baseUrl) {
|
|
@@ -3253,7 +3012,9 @@ export class AgentSession {
|
|
|
3253
3012
|
|
|
3254
3013
|
const { results, tokensUsed } = await executeReadOnlyDelegationPlan(
|
|
3255
3014
|
decision.delegation,
|
|
3256
|
-
async (subtaskPrompt,
|
|
3015
|
+
async (subtaskPrompt, options) => {
|
|
3016
|
+
const signal = options?.signal;
|
|
3017
|
+
const allowedTools = options?.allowedTools ?? isDelegationAllowedTool;
|
|
3257
3018
|
if (signal?.aborted) return { result: "Delegation failed: Aborted", tokens: 0 };
|
|
3258
3019
|
|
|
3259
3020
|
const { resolveModelPool } = await import("../routing/presets");
|
|
@@ -3280,7 +3041,8 @@ export class AgentSession {
|
|
|
3280
3041
|
|
|
3281
3042
|
if (
|
|
3282
3043
|
resolvedUtility.provider === "openai" ||
|
|
3283
|
-
(resolvedUtility.provider === "litellm" &&
|
|
3044
|
+
(resolvedUtility.provider === "litellm" &&
|
|
3045
|
+
(resolvedUtility.id.includes("openai") || /gpt-/.test(resolvedUtility.id)))
|
|
3284
3046
|
) {
|
|
3285
3047
|
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
3286
3048
|
if (internalUrl) resolvedUtility = { ...resolvedUtility, baseUrl: internalUrl } as any;
|
|
@@ -3308,7 +3070,7 @@ export class AgentSession {
|
|
|
3308
3070
|
modelRegistry: this.#modelRegistry,
|
|
3309
3071
|
toolNames: Array.from(this.#toolRegistry.values())
|
|
3310
3072
|
.map(t => t.name)
|
|
3311
|
-
.filter(
|
|
3073
|
+
.filter(allowedTools),
|
|
3312
3074
|
enableLsp: false,
|
|
3313
3075
|
enableMCP: false,
|
|
3314
3076
|
});
|
|
@@ -3333,7 +3095,7 @@ export class AgentSession {
|
|
|
3333
3095
|
.join(""),
|
|
3334
3096
|
)
|
|
3335
3097
|
.join("\n");
|
|
3336
|
-
const tUsage = (
|
|
3098
|
+
const tUsage = childSession.buildDisplaySessionContext().usedTokens || 0;
|
|
3337
3099
|
return { result: resultStr || "No output", tokens: tUsage };
|
|
3338
3100
|
} finally {
|
|
3339
3101
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -3822,7 +3584,7 @@ export class AgentSession {
|
|
|
3822
3584
|
*/
|
|
3823
3585
|
async sendCustomMessage<T = unknown>(
|
|
3824
3586
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
3825
|
-
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
3587
|
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; signal?: AbortSignal },
|
|
3826
3588
|
): Promise<void> {
|
|
3827
3589
|
const clonedContent = Array.isArray(message.content)
|
|
3828
3590
|
? message.content.map((c: any) => ({ ...c }))
|
|
@@ -4367,7 +4129,10 @@ export class AgentSession {
|
|
|
4367
4129
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
4368
4130
|
|
|
4369
4131
|
let targetModel = model;
|
|
4370
|
-
if (
|
|
4132
|
+
if (
|
|
4133
|
+
model.provider === "openai" ||
|
|
4134
|
+
(model.provider === "litellm" && (model.id.includes("openai") || /gpt-/.test(model.id)))
|
|
4135
|
+
) {
|
|
4371
4136
|
const internalUrl = this.settings.get("routing.internalOpenAiUrl") as string | undefined;
|
|
4372
4137
|
if (internalUrl) targetModel = { ...model, baseUrl: internalUrl };
|
|
4373
4138
|
} else if (
|
|
@@ -6981,8 +6746,10 @@ export class AgentSession {
|
|
|
6981
6746
|
#syncRoutingStateFromBranch() {
|
|
6982
6747
|
this.#routingCoordinator.reset();
|
|
6983
6748
|
const entries = this.sessionManager.getBranch();
|
|
6749
|
+
let foundRoutingState = false;
|
|
6984
6750
|
for (const entry of entries) {
|
|
6985
6751
|
if (entry.type === "custom" && entry.customType === "routing_event") {
|
|
6752
|
+
foundRoutingState = true;
|
|
6986
6753
|
const event = entry.data as any;
|
|
6987
6754
|
if (event.state) {
|
|
6988
6755
|
this.#routingCoordinator.restoreState(event.state);
|
|
@@ -7001,6 +6768,16 @@ export class AgentSession {
|
|
|
7001
6768
|
}
|
|
7002
6769
|
}
|
|
7003
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
|
+
}
|
|
7004
6781
|
}
|
|
7005
6782
|
|
|
7006
6783
|
/**
|
|
@@ -7054,6 +6831,7 @@ export class AgentSession {
|
|
|
7054
6831
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
7055
6832
|
}
|
|
7056
6833
|
this.#syncTodoPhasesFromBranch();
|
|
6834
|
+
this.#syncRoutingStateFromBranch();
|
|
7057
6835
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
7058
6836
|
|
|
7059
6837
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
@@ -7600,6 +7378,9 @@ export class AgentSession {
|
|
|
7600
7378
|
export function calculateUsedTokens(messages: any[]): number {
|
|
7601
7379
|
const countTextLength = (content: any): number => {
|
|
7602
7380
|
if (typeof content === "string") {
|
|
7381
|
+
if (content.startsWith("data:image/") || content.startsWith("base64,")) {
|
|
7382
|
+
return 0;
|
|
7383
|
+
}
|
|
7603
7384
|
return content.length;
|
|
7604
7385
|
}
|
|
7605
7386
|
if (Array.isArray(content)) {
|