@elyracode/coding-agent 0.9.7 → 0.9.9
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/CHANGELOG.md +26 -0
- package/dist/cli/doctor-cli.d.ts +8 -0
- package/dist/cli/doctor-cli.d.ts.map +1 -0
- package/dist/cli/doctor-cli.js +81 -0
- package/dist/cli/doctor-cli.js.map +1 -0
- package/dist/core/agent-session.d.ts +35 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +85 -24
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/settings-manager.d.ts +11 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skill-extraction.d.ts +34 -0
- package/dist/core/skill-extraction.d.ts.map +1 -0
- package/dist/core/skill-extraction.js +136 -0
- package/dist/core/skill-extraction.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +7 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/smart-router.d.ts +37 -5
- package/dist/core/smart-router.d.ts.map +1 -1
- package/dist/core/smart-router.js +98 -19
- package/dist/core/smart-router.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +228 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +45 -3
- package/dist/package-manager-cli.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,
|
|
1
|
+
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CA0DrE,CAAC"}
|
|
@@ -47,7 +47,13 @@ export const BUILTIN_SLASH_COMMANDS = [
|
|
|
47
47
|
{ name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" },
|
|
48
48
|
{ name: "packages", description: "Browse and install available extensions" },
|
|
49
49
|
{ name: "ext", description: "Browse and install available extensions (alias for /packages)" },
|
|
50
|
-
{
|
|
50
|
+
{
|
|
51
|
+
name: "goal",
|
|
52
|
+
description: "Set a goal: agent keeps working until the command exits with code 0 (--budget <usd> caps spend)",
|
|
53
|
+
},
|
|
54
|
+
{ name: "learn", description: "Extract a reusable skill from the current session" },
|
|
55
|
+
{ name: "replay", description: "Replay the last turn on a new branch with a different model" },
|
|
56
|
+
{ name: "route", description: "Show what smart routing would do for the next turn" },
|
|
51
57
|
{ name: "update", description: "Update Elyra and installed extensions to the latest version and restart" },
|
|
52
58
|
{ name: "quit", description: `Quit ${APP_NAME}` },
|
|
53
59
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"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","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"help\", description: \"Show help with example prompts and useful commands\" },\n\t{ name: \"skills\", description: \"List loaded skills, or install from registry: /skills install <name>\" },\n\t{ name: \"init\", description: \"Set up Elyra for this project (.elyra/ directory, extensions, blueprints)\" },\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"rewind\", description: \"Rewind session and file state to a previous point\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"cost\", description: \"Show session cost and token usage\" },\n\t{\n\t\tname: \"diff\",\n\t\tdescription: \"Show changes (git diff). Flags: --session, --cached, --editor (open in $EDITOR), or a path\",\n\t},\n\t{\n\t\tname: \"review\",\n\t\tdescription: \"Get an independent code review of your changes from the model. Flags: --session, or a path\",\n\t},\n\t{\n\t\tname: \"bisect\",\n\t\tdescription:\n\t\t\t\"Find which agent turn introduced a problem. /bisect <command> runs it automatically (like git bisect run)\",\n\t},\n\t{ name: \"pin\", description: \"Pin a file to context (survives compaction): /pin <path>\" },\n\t{ name: \"memory\", description: \"View or manage project memory\" },\n\t{ name: \"unpin\", description: \"Unpin a file from context: /unpin <path>\" },\n\t{ name: \"pins\", description: \"List pinned context files\" },\n\t{ name: \"blueprint\", description: \"Apply a session blueprint: /blueprint [name]\" },\n\t{ name: \"snippet\", description: \"Send a saved instruction snippet: /snippet [name]\" },\n\t{ name: \"theme\", description: \"Switch color theme (opens selector, or /theme <name>)\" },\n\t{ name: \"footer\", description: \"Toggle minimal footer (hide token stats and context usage)\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"packages\", description: \"Browse and install available extensions\" },\n\t{ name: \"ext\", description: \"Browse and install available extensions (alias for /packages)\" },\n\t{\n\t\tname: \"goal\",\n\t\tdescription: \"Set a goal: agent keeps working until the command exits with code 0 (--budget <usd> caps spend)\",\n\t},\n\t{ name: \"learn\", description: \"Extract a reusable skill from the current session\" },\n\t{ name: \"replay\", description: \"Replay the last turn on a new branch with a different model\" },\n\t{ name: \"route\", description: \"Show what smart routing would do for the next turn\" },\n\t{ name: \"update\", description: \"Update Elyra and installed extensions to the latest version and restart\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n];\n"]}
|
|
@@ -12,32 +12,64 @@ export interface SmartRouterConfig {
|
|
|
12
12
|
availableModels: Model<Api>[];
|
|
13
13
|
/** The user's preferred/current model -- used as the Tier 2 default and fallback. */
|
|
14
14
|
preferredModel: Model<Api>;
|
|
15
|
+
/**
|
|
16
|
+
* User-pinned models per tier (from settings). When set, a pin wins over
|
|
17
|
+
* the heuristic ranking for its tier — the router decides the tier, the
|
|
18
|
+
* user decides the model. Pins are still subject to the modality filter.
|
|
19
|
+
*/
|
|
20
|
+
pinnedModels?: Partial<Record<ComplexityTier, Model<Api>>>;
|
|
15
21
|
}
|
|
16
22
|
export interface TurnAnalysis {
|
|
17
23
|
/** The messages in the conversation so far. */
|
|
18
24
|
messages: readonly AgentMessage[];
|
|
19
25
|
/** Current thinking level. */
|
|
20
26
|
thinkingLevel: ThinkingLevel;
|
|
27
|
+
/**
|
|
28
|
+
* Fraction (0..1+) of an active goal budget already spent, when a
|
|
29
|
+
* budgeted /goal is running. Used to cap the tier as funds run low.
|
|
30
|
+
*/
|
|
31
|
+
budgetUsedFraction?: number;
|
|
21
32
|
}
|
|
22
33
|
export type ComplexityTier = "fast" | "balanced" | "powerful";
|
|
23
34
|
export interface SmartRouterResult {
|
|
24
35
|
model: Model<Api>;
|
|
25
36
|
tier: ComplexityTier;
|
|
26
37
|
reason: string;
|
|
38
|
+
/** False when the selected model is the one already in use. */
|
|
39
|
+
changed: boolean;
|
|
27
40
|
}
|
|
41
|
+
/** True when any user message in the conversation contains an image. */
|
|
42
|
+
export declare function conversationHasImages(messages: readonly AgentMessage[]): boolean;
|
|
28
43
|
/** Classify the complexity tier for the next turn. */
|
|
29
44
|
export declare function classifyComplexity(analysis: TurnAnalysis, previousTier?: ComplexityTier): {
|
|
30
45
|
tier: ComplexityTier;
|
|
31
46
|
reason: string;
|
|
32
47
|
};
|
|
33
|
-
/**
|
|
34
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Budget-aware tier capping: as an active goal budget depletes, cap the
|
|
50
|
+
* tier so the loop spends the expensive turns early and rations later
|
|
51
|
+
* instead of hitting the wall at full price.
|
|
52
|
+
*/
|
|
53
|
+
export declare function applyBudgetCap(result: {
|
|
54
|
+
tier: ComplexityTier;
|
|
55
|
+
reason: string;
|
|
56
|
+
}, budgetUsedFraction: number | undefined): {
|
|
57
|
+
tier: ComplexityTier;
|
|
58
|
+
reason: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Select the best model for a given complexity tier from available models.
|
|
62
|
+
* A user-pinned model for the tier wins over the heuristic ranking.
|
|
63
|
+
* When the conversation contains images, only vision-capable models are
|
|
64
|
+
* considered so routing can never break a multimodal session.
|
|
65
|
+
*/
|
|
66
|
+
export declare function selectModelForTier(tier: ComplexityTier, config: SmartRouterConfig, requiresImageInput?: boolean): Model<Api>;
|
|
35
67
|
/**
|
|
36
68
|
* Analyze the turn and return a model recommendation.
|
|
37
69
|
*
|
|
38
|
-
* Returns undefined when
|
|
39
|
-
*
|
|
40
|
-
*
|
|
70
|
+
* Returns undefined only when smart routing is disabled. When the selected
|
|
71
|
+
* model equals the preferred model, the result is still returned with
|
|
72
|
+
* `changed: false` so callers can track the classified tier for hysteresis.
|
|
41
73
|
*/
|
|
42
74
|
export declare function routeNextTurn(analysis: TurnAnalysis, config: SmartRouterConfig, previousTier?: ComplexityTier): SmartRouterResult | undefined;
|
|
43
75
|
//# sourceMappingURL=smart-router.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smart-router.d.ts","sourceRoot":"","sources":["../../src/core/smart-router.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,GAAG,EAAoB,KAAK,EAA4C,MAAM,eAAe,CAAC;AAO5G,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAC9B,qFAAqF;IACrF,cAAc,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"smart-router.d.ts","sourceRoot":"","sources":["../../src/core/smart-router.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,GAAG,EAAoB,KAAK,EAA4C,MAAM,eAAe,CAAC;AAO5G,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,eAAe,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAC9B,qFAAqF;IACrF,cAAc,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,YAAY;IAC5B,+CAA+C;IAC/C,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAClC,8BAA8B;IAC9B,aAAa,EAAE,aAAa,CAAC;IAC7B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,OAAO,EAAE,OAAO,CAAC;CACjB;AA4ID,wEAAwE;AACxE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAAG,OAAO,CAOhF;AAqBD,sDAAsD;AACtD,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,YAAY,EACtB,YAAY,CAAC,EAAE,cAAc,GAC3B;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAwF1C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC7B,MAAM,EAAE;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAChD,kBAAkB,EAAE,MAAM,GAAG,SAAS,GACpC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAU1C;AAuDD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,iBAAiB,EACzB,kBAAkB,UAAQ,GACxB,KAAK,CAAC,GAAG,CAAC,CA0BZ;AAMD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC5B,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,iBAAiB,EACzB,YAAY,CAAC,EAAE,cAAc,GAC3B,iBAAiB,GAAG,SAAS,CAQ/B"}
|
|
@@ -53,7 +53,12 @@ const COMPLEX_KEYWORDS = [
|
|
|
53
53
|
];
|
|
54
54
|
/** Short user message threshold (characters). */
|
|
55
55
|
const SHORT_MESSAGE_THRESHOLD = 100;
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* User-turn count threshold for complex multi-step tasks.
|
|
58
|
+
* Counts only user messages: raw message counts inflate immediately because
|
|
59
|
+
* every tool result is its own message, which made earlier versions escalate
|
|
60
|
+
* to powerful within two or three turns.
|
|
61
|
+
*/
|
|
57
62
|
const LONG_CONVERSATION_THRESHOLD = 20;
|
|
58
63
|
/** File edit count threshold per turn for complexity escalation. */
|
|
59
64
|
const MANY_EDITS_THRESHOLD = 3;
|
|
@@ -61,6 +66,10 @@ const MANY_EDITS_THRESHOLD = 3;
|
|
|
61
66
|
const LARGE_TOOL_OUTPUT_THRESHOLD = 5000;
|
|
62
67
|
/** Conversation length below which read-only de-escalation to fast is allowed. */
|
|
63
68
|
const READ_ONLY_DEESCALATION_MAX_MESSAGES = 10;
|
|
69
|
+
/** Above this fraction of a goal budget, cap routing at the balanced tier. */
|
|
70
|
+
const BUDGET_CAP_BALANCED_FRACTION = 0.75;
|
|
71
|
+
/** Above this fraction of a goal budget, cap routing at the fast tier. */
|
|
72
|
+
const BUDGET_CAP_FAST_FRACTION = 0.9;
|
|
64
73
|
// ============================================================================
|
|
65
74
|
// Helpers
|
|
66
75
|
// ============================================================================
|
|
@@ -116,6 +125,18 @@ function containsComplexKeyword(text) {
|
|
|
116
125
|
const lower = text.toLowerCase();
|
|
117
126
|
return COMPLEX_KEYWORDS.some((kw) => lower.includes(kw));
|
|
118
127
|
}
|
|
128
|
+
/** True when any user message in the conversation contains an image. */
|
|
129
|
+
export function conversationHasImages(messages) {
|
|
130
|
+
for (const msg of messages) {
|
|
131
|
+
if (!isUserMessage(msg))
|
|
132
|
+
continue;
|
|
133
|
+
if (typeof msg.content === "string")
|
|
134
|
+
continue;
|
|
135
|
+
if (msg.content.some((c) => c.type === "image"))
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
119
140
|
// ============================================================================
|
|
120
141
|
// Tier classification
|
|
121
142
|
// ============================================================================
|
|
@@ -141,9 +162,10 @@ export function classifyComplexity(analysis, previousTier) {
|
|
|
141
162
|
if (thinkingLevel === "high" || thinkingLevel === "xhigh") {
|
|
142
163
|
return { tier: "powerful", reason: `thinking level is set to ${thinkingLevel}` };
|
|
143
164
|
}
|
|
144
|
-
// Long conversation -> complex
|
|
145
|
-
|
|
146
|
-
|
|
165
|
+
// Long conversation (counted in user turns, not raw messages) -> complex
|
|
166
|
+
const userTurnCount = messages.filter(isUserMessage).length;
|
|
167
|
+
if (userTurnCount > LONG_CONVERSATION_THRESHOLD) {
|
|
168
|
+
return { tier: "powerful", reason: `long conversation (${userTurnCount} user turns)` };
|
|
147
169
|
}
|
|
148
170
|
// Find the last assistant message and its tool results
|
|
149
171
|
let lastAssistantIndex = -1;
|
|
@@ -205,6 +227,23 @@ export function classifyComplexity(analysis, previousTier) {
|
|
|
205
227
|
}
|
|
206
228
|
return applyHysteresis({ tier: "balanced", reason: "default" }, previousTier);
|
|
207
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Budget-aware tier capping: as an active goal budget depletes, cap the
|
|
232
|
+
* tier so the loop spends the expensive turns early and rations later
|
|
233
|
+
* instead of hitting the wall at full price.
|
|
234
|
+
*/
|
|
235
|
+
export function applyBudgetCap(result, budgetUsedFraction) {
|
|
236
|
+
if (budgetUsedFraction === undefined)
|
|
237
|
+
return result;
|
|
238
|
+
const pct = Math.round(budgetUsedFraction * 100);
|
|
239
|
+
if (budgetUsedFraction >= BUDGET_CAP_FAST_FRACTION && result.tier !== "fast") {
|
|
240
|
+
return { tier: "fast", reason: `${result.reason} (capped to fast: ${pct}% of goal budget spent)` };
|
|
241
|
+
}
|
|
242
|
+
if (budgetUsedFraction >= BUDGET_CAP_BALANCED_FRACTION && result.tier === "powerful") {
|
|
243
|
+
return { tier: "balanced", reason: `${result.reason} (capped to balanced: ${pct}% of goal budget spent)` };
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
208
247
|
/**
|
|
209
248
|
* Hysteresis: prevent oscillation by not dropping from powerful directly to fast.
|
|
210
249
|
* If the previous turn was powerful and the current classification is fast,
|
|
@@ -219,19 +258,61 @@ function applyHysteresis(result, previousTier) {
|
|
|
219
258
|
// ============================================================================
|
|
220
259
|
// Model selection
|
|
221
260
|
// ============================================================================
|
|
222
|
-
/**
|
|
223
|
-
|
|
261
|
+
/** Combined per-token cost used to rank models within a tier. */
|
|
262
|
+
function combinedCost(model) {
|
|
263
|
+
return model.cost.input + model.cost.output;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Rank candidates within a tier. Lower rank wins. Ordering:
|
|
267
|
+
* 1. Same provider as the preferred model (consistent auth, billing, behavior)
|
|
268
|
+
* 2. For balanced/powerful: reasoning-capable models first
|
|
269
|
+
* 3. Paid models before free ones (free tiers are rate-limited/unreliable)
|
|
270
|
+
* 4. Cheaper combined cost first
|
|
271
|
+
* 5. Larger context window as the final tiebreak
|
|
272
|
+
*/
|
|
273
|
+
function compareCandidates(a, b, tier, preferred) {
|
|
274
|
+
const aSameProvider = a.provider === preferred.provider ? 0 : 1;
|
|
275
|
+
const bSameProvider = b.provider === preferred.provider ? 0 : 1;
|
|
276
|
+
if (aSameProvider !== bSameProvider)
|
|
277
|
+
return aSameProvider - bSameProvider;
|
|
278
|
+
if (tier !== "fast") {
|
|
279
|
+
const aReasoning = a.reasoning ? 0 : 1;
|
|
280
|
+
const bReasoning = b.reasoning ? 0 : 1;
|
|
281
|
+
if (aReasoning !== bReasoning)
|
|
282
|
+
return aReasoning - bReasoning;
|
|
283
|
+
}
|
|
284
|
+
const aFree = combinedCost(a) === 0 ? 1 : 0;
|
|
285
|
+
const bFree = combinedCost(b) === 0 ? 1 : 0;
|
|
286
|
+
if (aFree !== bFree)
|
|
287
|
+
return aFree - bFree;
|
|
288
|
+
const costDiff = combinedCost(a) - combinedCost(b);
|
|
289
|
+
if (costDiff !== 0)
|
|
290
|
+
return costDiff;
|
|
291
|
+
return b.contextWindow - a.contextWindow;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Select the best model for a given complexity tier from available models.
|
|
295
|
+
* A user-pinned model for the tier wins over the heuristic ranking.
|
|
296
|
+
* When the conversation contains images, only vision-capable models are
|
|
297
|
+
* considered so routing can never break a multimodal session.
|
|
298
|
+
*/
|
|
299
|
+
export function selectModelForTier(tier, config, requiresImageInput = false) {
|
|
224
300
|
const { availableModels, preferredModel } = config;
|
|
301
|
+
// User pin wins, as long as it is modality-compatible.
|
|
302
|
+
const pinned = config.pinnedModels?.[tier];
|
|
303
|
+
if (pinned && (!requiresImageInput || pinned.input.includes("image"))) {
|
|
304
|
+
return pinned;
|
|
305
|
+
}
|
|
306
|
+
const candidates = requiresImageInput ? availableModels.filter((m) => m.input.includes("image")) : availableModels;
|
|
225
307
|
// Partition models into tiers
|
|
226
|
-
const tierModels =
|
|
308
|
+
const tierModels = candidates.filter((m) => modelTier(m) === tier);
|
|
227
309
|
if (tierModels.length === 0) {
|
|
228
|
-
// Requested tier has no models, fall back to preferred
|
|
310
|
+
// Requested tier has no (compatible) models, fall back to preferred
|
|
229
311
|
return preferredModel;
|
|
230
312
|
}
|
|
231
|
-
// Pick the model with the largest context window (best value within tier)
|
|
232
313
|
let best = tierModels[0];
|
|
233
314
|
for (let i = 1; i < tierModels.length; i++) {
|
|
234
|
-
if (tierModels[i]
|
|
315
|
+
if (compareCandidates(tierModels[i], best, tier, preferredModel) < 0) {
|
|
235
316
|
best = tierModels[i];
|
|
236
317
|
}
|
|
237
318
|
}
|
|
@@ -243,18 +324,16 @@ export function selectModelForTier(tier, config) {
|
|
|
243
324
|
/**
|
|
244
325
|
* Analyze the turn and return a model recommendation.
|
|
245
326
|
*
|
|
246
|
-
* Returns undefined when
|
|
247
|
-
*
|
|
248
|
-
*
|
|
327
|
+
* Returns undefined only when smart routing is disabled. When the selected
|
|
328
|
+
* model equals the preferred model, the result is still returned with
|
|
329
|
+
* `changed: false` so callers can track the classified tier for hysteresis.
|
|
249
330
|
*/
|
|
250
331
|
export function routeNextTurn(analysis, config, previousTier) {
|
|
251
332
|
if (!config.enabled)
|
|
252
333
|
return undefined;
|
|
253
|
-
const { tier, reason } = classifyComplexity(analysis, previousTier);
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return undefined;
|
|
258
|
-
return { model, tier, reason };
|
|
334
|
+
const { tier, reason } = applyBudgetCap(classifyComplexity(analysis, previousTier), analysis.budgetUsedFraction);
|
|
335
|
+
const requiresImageInput = conversationHasImages(analysis.messages);
|
|
336
|
+
const model = selectModelForTier(tier, config, requiresImageInput);
|
|
337
|
+
return { model, tier, reason, changed: !modelsAreEqual(model, config.preferredModel) };
|
|
259
338
|
}
|
|
260
339
|
//# sourceMappingURL=smart-router.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smart-router.js","sourceRoot":"","sources":["../../src/core/smart-router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Smart model routing for automatic tier-based model selection.\n *\n * Analyzes conversation signals after each turn and selects the optimal model\n * tier (fast/balanced/powerful) for the next turn based on task complexity.\n */\n\nimport type { AgentMessage, ThinkingLevel } from \"@elyracode/agent-core\";\nimport type { Api, AssistantMessage, Model, ToolCall, ToolResultMessage, UserMessage } from \"@elyracode/ai\";\nimport { modelsAreEqual } from \"@elyracode/ai\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface SmartRouterConfig {\n\tenabled: boolean;\n\t/** Available models the router can pick from. Must have at least one. */\n\tavailableModels: Model<Api>[];\n\t/** The user's preferred/current model -- used as the Tier 2 default and fallback. */\n\tpreferredModel: Model<Api>;\n}\n\nexport interface TurnAnalysis {\n\t/** The messages in the conversation so far. */\n\tmessages: readonly AgentMessage[];\n\t/** Current thinking level. */\n\tthinkingLevel: ThinkingLevel;\n}\n\nexport type ComplexityTier = \"fast\" | \"balanced\" | \"powerful\";\n\nexport interface SmartRouterResult {\n\tmodel: Model<Api>;\n\ttier: ComplexityTier;\n\treason: string;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Output cost thresholds ($/million tokens) for tier classification. */\nconst TIER_1_MAX_COST = 3;\nconst TIER_3_MIN_COST = 20;\n\n/** Tool names considered read-only (no side effects). */\nconst READ_ONLY_TOOLS: ReadonlySet<string> = new Set([\"read\", \"grep\", \"find\", \"ls\"]);\n\n/** Keywords in user messages that signal complex work. */\nconst COMPLEX_KEYWORDS: readonly string[] = [\n\t\"refactor\",\n\t\"architect\",\n\t\"redesign\",\n\t\"migrate\",\n\t\"rewrite\",\n\t\"debug\",\n\t\"fix the bug\",\n\t\"security audit\",\n\t\"optimize\",\n\t\"performance\",\n\t\"investigate\",\n\t\"diagnose\",\n\t\"troubleshoot\",\n\t\"root cause\",\n\t\"race condition\",\n\t\"deadlock\",\n\t\"memory leak\",\n\t\"regression\",\n\t\"breaking change\",\n\t\"backwards compatible\",\n\t\"design pattern\",\n\t\"abstraction\",\n\t\"inheritance\",\n\t\"polymorphism\",\n\t\"dependency injection\",\n\t\"restructure\",\n\t\"consolidate\",\n\t\"decompose\",\n\t\"decouple\",\n\t\"integrate\",\n\t\"implement from scratch\",\n\t\"build a system\",\n\t\"complex\",\n\t\"tricky\",\n\t\"subtle\",\n];\n\n/** Short user message threshold (characters). */\nconst SHORT_MESSAGE_THRESHOLD = 100;\n\n/** Message count threshold for complex multi-step tasks. */\nconst LONG_CONVERSATION_THRESHOLD = 20;\n\n/** File edit count threshold per turn for complexity escalation. */\nconst MANY_EDITS_THRESHOLD = 3;\n\n/** Character count threshold for large tool output. */\nconst LARGE_TOOL_OUTPUT_THRESHOLD = 5000;\n\n/** Conversation length below which read-only de-escalation to fast is allowed. */\nconst READ_ONLY_DEESCALATION_MAX_MESSAGES = 10;\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isUserMessage(msg: AgentMessage): msg is UserMessage {\n\treturn \"role\" in msg && (msg as UserMessage).role === \"user\";\n}\n\nfunction isAssistantMessage(msg: AgentMessage): msg is AssistantMessage {\n\treturn \"role\" in msg && (msg as AssistantMessage).role === \"assistant\";\n}\n\nfunction isToolResultMessage(msg: AgentMessage): msg is ToolResultMessage {\n\treturn \"role\" in msg && (msg as ToolResultMessage).role === \"toolResult\";\n}\n\n/** Extract the plain-text content of a user message. */\nfunction getUserMessageText(msg: UserMessage): string {\n\tif (typeof msg.content === \"string\") return msg.content;\n\treturn msg.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\" \");\n}\n\n/** Extract tool calls from an assistant message's content. */\nfunction getToolCalls(msg: AssistantMessage): ToolCall[] {\n\treturn msg.content.filter((c): c is ToolCall => c.type === \"toolCall\");\n}\n\n/** True when all tool calls in a message are read-only. */\nfunction allToolCallsReadOnly(toolCalls: ToolCall[]): boolean {\n\treturn toolCalls.length > 0 && toolCalls.every((tc) => READ_ONLY_TOOLS.has(tc.name));\n}\n\n/** Count write/edit tool calls in an assistant message. */\nfunction countFileEditCalls(toolCalls: ToolCall[]): number {\n\treturn toolCalls.filter((tc) => tc.name === \"write\" || tc.name === \"edit\").length;\n}\n\n/**\n * Collect tool result messages that follow an assistant message.\n * Walks forward from the assistant message index, collecting consecutive\n * tool results (there may be other message types interleaved in custom\n * AgentMessage streams, so we stop at the next user or assistant message).\n */\nfunction getToolResultsAfter(messages: readonly AgentMessage[], assistantIndex: number): ToolResultMessage[] {\n\tconst results: ToolResultMessage[] = [];\n\tfor (let i = assistantIndex + 1; i < messages.length; i++) {\n\t\tconst msg = messages[i];\n\t\tif (isUserMessage(msg) || isAssistantMessage(msg)) break;\n\t\tif (isToolResultMessage(msg)) results.push(msg);\n\t}\n\treturn results;\n}\n\n/** True when the text contains any of the complex keywords (case-insensitive). */\nfunction containsComplexKeyword(text: string): boolean {\n\tconst lower = text.toLowerCase();\n\treturn COMPLEX_KEYWORDS.some((kw) => lower.includes(kw));\n}\n\n// ============================================================================\n// Tier classification\n// ============================================================================\n\n/** Classify a model into its cost tier. */\nfunction modelTier(model: Model<Api>): ComplexityTier {\n\tconst cost = model.cost.output;\n\tif (cost < TIER_1_MAX_COST) return \"fast\";\n\tif (cost < TIER_3_MIN_COST) return \"balanced\";\n\treturn \"powerful\";\n}\n\n/** Measure the total text length of a tool result message. */\nfunction toolResultTextLength(tr: ToolResultMessage): number {\n\treturn tr.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.reduce((sum, c) => sum + c.text.length, 0);\n}\n\n/** Classify the complexity tier for the next turn. */\nexport function classifyComplexity(\n\tanalysis: TurnAnalysis,\n\tpreviousTier?: ComplexityTier,\n): { tier: ComplexityTier; reason: string } {\n\tconst { messages, thinkingLevel } = analysis;\n\n\t// High thinking level -> complex\n\tif (thinkingLevel === \"high\" || thinkingLevel === \"xhigh\") {\n\t\treturn { tier: \"powerful\", reason: `thinking level is set to ${thinkingLevel}` };\n\t}\n\n\t// Long conversation -> complex\n\tif (messages.length > LONG_CONVERSATION_THRESHOLD) {\n\t\treturn { tier: \"powerful\", reason: `long conversation (${messages.length} messages)` };\n\t}\n\n\t// Find the last assistant message and its tool results\n\tlet lastAssistantIndex = -1;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tif (isAssistantMessage(messages[i])) {\n\t\t\tlastAssistantIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Find the last user message\n\tlet lastUserMessage: UserMessage | undefined;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tif (isUserMessage(messages[i])) {\n\t\t\tlastUserMessage = messages[i] as UserMessage;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Check for complex signals from the last assistant turn\n\tif (lastAssistantIndex >= 0) {\n\t\tconst lastAssistant = messages[lastAssistantIndex] as AssistantMessage;\n\t\tconst toolCalls = getToolCalls(lastAssistant);\n\t\tconst toolResults = getToolResultsAfter(messages, lastAssistantIndex);\n\n\t\t// Errors in last turn -> complex\n\t\tconst hasErrors = toolResults.some((tr) => tr.isError);\n\t\tif (hasErrors) {\n\t\t\treturn { tier: \"powerful\", reason: \"tool errors in last turn\" };\n\t\t}\n\n\t\t// Many file edits in last turn -> complex\n\t\tif (countFileEditCalls(toolCalls) > MANY_EDITS_THRESHOLD) {\n\t\t\treturn applyHysteresis(\n\t\t\t\t{ tier: \"powerful\", reason: `many file edits in last turn (${countFileEditCalls(toolCalls)})` },\n\t\t\t\tpreviousTier,\n\t\t\t);\n\t\t}\n\n\t\t// Large tool outputs suggest complex operations\n\t\tif (toolResults.some((r) => toolResultTextLength(r) > LARGE_TOOL_OUTPUT_THRESHOLD)) {\n\t\t\treturn applyHysteresis({ tier: \"powerful\", reason: \"large tool output detected\" }, previousTier);\n\t\t}\n\n\t\t// All read-only tools and no errors -> check for simple\n\t\tif (allToolCallsReadOnly(toolCalls) && !hasErrors) {\n\t\t\t// If the last user message is also short and simple, use fast tier.\n\t\t\t// Only de-escalate in short conversations; in longer ones a read-only\n\t\t\t// turn in the middle of complex work shouldn't drop to fast.\n\t\t\tif (lastUserMessage && messages.length < READ_ONLY_DEESCALATION_MAX_MESSAGES) {\n\t\t\t\tconst text = getUserMessageText(lastUserMessage);\n\t\t\t\tif (text.length < SHORT_MESSAGE_THRESHOLD && !containsComplexKeyword(text)) {\n\t\t\t\t\treturn applyHysteresis(\n\t\t\t\t\t\t{ tier: \"fast\", reason: \"read-only tools with short user message\" },\n\t\t\t\t\t\tpreviousTier,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check user message for complex keywords\n\tif (lastUserMessage) {\n\t\tconst text = getUserMessageText(lastUserMessage);\n\t\tif (containsComplexKeyword(text)) {\n\t\t\treturn applyHysteresis({ tier: \"powerful\", reason: \"complex keywords in user message\" }, previousTier);\n\t\t}\n\n\t\t// Short question with no prior assistant context -> fast\n\t\tif (lastAssistantIndex < 0 && text.length < SHORT_MESSAGE_THRESHOLD) {\n\t\t\treturn applyHysteresis({ tier: \"fast\", reason: \"short initial message\" }, previousTier);\n\t\t}\n\t}\n\n\treturn applyHysteresis({ tier: \"balanced\", reason: \"default\" }, previousTier);\n}\n\n/**\n * Hysteresis: prevent oscillation by not dropping from powerful directly to fast.\n * If the previous turn was powerful and the current classification is fast,\n * dampen to balanced instead.\n */\nfunction applyHysteresis(\n\tresult: { tier: ComplexityTier; reason: string },\n\tpreviousTier: ComplexityTier | undefined,\n): { tier: ComplexityTier; reason: string } {\n\tif (previousTier === \"powerful\" && result.tier === \"fast\") {\n\t\treturn { tier: \"balanced\", reason: `${result.reason} (dampened from fast: previous turn was powerful)` };\n\t}\n\treturn result;\n}\n\n// ============================================================================\n// Model selection\n// ============================================================================\n\n/** Select the best model for a given complexity tier from available models. */\nexport function selectModelForTier(tier: ComplexityTier, config: SmartRouterConfig): Model<Api> {\n\tconst { availableModels, preferredModel } = config;\n\n\t// Partition models into tiers\n\tconst tierModels = availableModels.filter((m) => modelTier(m) === tier);\n\n\tif (tierModels.length === 0) {\n\t\t// Requested tier has no models, fall back to preferred\n\t\treturn preferredModel;\n\t}\n\n\t// Pick the model with the largest context window (best value within tier)\n\tlet best = tierModels[0];\n\tfor (let i = 1; i < tierModels.length; i++) {\n\t\tif (tierModels[i].contextWindow > best.contextWindow) {\n\t\t\tbest = tierModels[i];\n\t\t}\n\t}\n\treturn best;\n}\n\n// ============================================================================\n// Main entry\n// ============================================================================\n\n/**\n * Analyze the turn and return a model recommendation.\n *\n * Returns undefined when:\n * - Smart routing is disabled\n * - The selected model is the same as the preferred model (no change needed)\n */\nexport function routeNextTurn(\n\tanalysis: TurnAnalysis,\n\tconfig: SmartRouterConfig,\n\tpreviousTier?: ComplexityTier,\n): SmartRouterResult | undefined {\n\tif (!config.enabled) return undefined;\n\n\tconst { tier, reason } = classifyComplexity(analysis, previousTier);\n\tconst model = selectModelForTier(tier, config);\n\n\t// No change needed if the router picks the same model the user already has\n\tif (modelsAreEqual(model, config.preferredModel)) return undefined;\n\n\treturn { model, tier, reason };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"smart-router.js","sourceRoot":"","sources":["../../src/core/smart-router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Smart model routing for automatic tier-based model selection.\n *\n * Analyzes conversation signals after each turn and selects the optimal model\n * tier (fast/balanced/powerful) for the next turn based on task complexity.\n */\n\nimport type { AgentMessage, ThinkingLevel } from \"@elyracode/agent-core\";\nimport type { Api, AssistantMessage, Model, ToolCall, ToolResultMessage, UserMessage } from \"@elyracode/ai\";\nimport { modelsAreEqual } from \"@elyracode/ai\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface SmartRouterConfig {\n\tenabled: boolean;\n\t/** Available models the router can pick from. Must have at least one. */\n\tavailableModels: Model<Api>[];\n\t/** The user's preferred/current model -- used as the Tier 2 default and fallback. */\n\tpreferredModel: Model<Api>;\n\t/**\n\t * User-pinned models per tier (from settings). When set, a pin wins over\n\t * the heuristic ranking for its tier — the router decides the tier, the\n\t * user decides the model. Pins are still subject to the modality filter.\n\t */\n\tpinnedModels?: Partial<Record<ComplexityTier, Model<Api>>>;\n}\n\nexport interface TurnAnalysis {\n\t/** The messages in the conversation so far. */\n\tmessages: readonly AgentMessage[];\n\t/** Current thinking level. */\n\tthinkingLevel: ThinkingLevel;\n\t/**\n\t * Fraction (0..1+) of an active goal budget already spent, when a\n\t * budgeted /goal is running. Used to cap the tier as funds run low.\n\t */\n\tbudgetUsedFraction?: number;\n}\n\nexport type ComplexityTier = \"fast\" | \"balanced\" | \"powerful\";\n\nexport interface SmartRouterResult {\n\tmodel: Model<Api>;\n\ttier: ComplexityTier;\n\treason: string;\n\t/** False when the selected model is the one already in use. */\n\tchanged: boolean;\n}\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Output cost thresholds ($/million tokens) for tier classification. */\nconst TIER_1_MAX_COST = 3;\nconst TIER_3_MIN_COST = 20;\n\n/** Tool names considered read-only (no side effects). */\nconst READ_ONLY_TOOLS: ReadonlySet<string> = new Set([\"read\", \"grep\", \"find\", \"ls\"]);\n\n/** Keywords in user messages that signal complex work. */\nconst COMPLEX_KEYWORDS: readonly string[] = [\n\t\"refactor\",\n\t\"architect\",\n\t\"redesign\",\n\t\"migrate\",\n\t\"rewrite\",\n\t\"debug\",\n\t\"fix the bug\",\n\t\"security audit\",\n\t\"optimize\",\n\t\"performance\",\n\t\"investigate\",\n\t\"diagnose\",\n\t\"troubleshoot\",\n\t\"root cause\",\n\t\"race condition\",\n\t\"deadlock\",\n\t\"memory leak\",\n\t\"regression\",\n\t\"breaking change\",\n\t\"backwards compatible\",\n\t\"design pattern\",\n\t\"abstraction\",\n\t\"inheritance\",\n\t\"polymorphism\",\n\t\"dependency injection\",\n\t\"restructure\",\n\t\"consolidate\",\n\t\"decompose\",\n\t\"decouple\",\n\t\"integrate\",\n\t\"implement from scratch\",\n\t\"build a system\",\n\t\"complex\",\n\t\"tricky\",\n\t\"subtle\",\n];\n\n/** Short user message threshold (characters). */\nconst SHORT_MESSAGE_THRESHOLD = 100;\n\n/**\n * User-turn count threshold for complex multi-step tasks.\n * Counts only user messages: raw message counts inflate immediately because\n * every tool result is its own message, which made earlier versions escalate\n * to powerful within two or three turns.\n */\nconst LONG_CONVERSATION_THRESHOLD = 20;\n\n/** File edit count threshold per turn for complexity escalation. */\nconst MANY_EDITS_THRESHOLD = 3;\n\n/** Character count threshold for large tool output. */\nconst LARGE_TOOL_OUTPUT_THRESHOLD = 5000;\n\n/** Conversation length below which read-only de-escalation to fast is allowed. */\nconst READ_ONLY_DEESCALATION_MAX_MESSAGES = 10;\n\n/** Above this fraction of a goal budget, cap routing at the balanced tier. */\nconst BUDGET_CAP_BALANCED_FRACTION = 0.75;\n\n/** Above this fraction of a goal budget, cap routing at the fast tier. */\nconst BUDGET_CAP_FAST_FRACTION = 0.9;\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isUserMessage(msg: AgentMessage): msg is UserMessage {\n\treturn \"role\" in msg && (msg as UserMessage).role === \"user\";\n}\n\nfunction isAssistantMessage(msg: AgentMessage): msg is AssistantMessage {\n\treturn \"role\" in msg && (msg as AssistantMessage).role === \"assistant\";\n}\n\nfunction isToolResultMessage(msg: AgentMessage): msg is ToolResultMessage {\n\treturn \"role\" in msg && (msg as ToolResultMessage).role === \"toolResult\";\n}\n\n/** Extract the plain-text content of a user message. */\nfunction getUserMessageText(msg: UserMessage): string {\n\tif (typeof msg.content === \"string\") return msg.content;\n\treturn msg.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\" \");\n}\n\n/** Extract tool calls from an assistant message's content. */\nfunction getToolCalls(msg: AssistantMessage): ToolCall[] {\n\treturn msg.content.filter((c): c is ToolCall => c.type === \"toolCall\");\n}\n\n/** True when all tool calls in a message are read-only. */\nfunction allToolCallsReadOnly(toolCalls: ToolCall[]): boolean {\n\treturn toolCalls.length > 0 && toolCalls.every((tc) => READ_ONLY_TOOLS.has(tc.name));\n}\n\n/** Count write/edit tool calls in an assistant message. */\nfunction countFileEditCalls(toolCalls: ToolCall[]): number {\n\treturn toolCalls.filter((tc) => tc.name === \"write\" || tc.name === \"edit\").length;\n}\n\n/**\n * Collect tool result messages that follow an assistant message.\n * Walks forward from the assistant message index, collecting consecutive\n * tool results (there may be other message types interleaved in custom\n * AgentMessage streams, so we stop at the next user or assistant message).\n */\nfunction getToolResultsAfter(messages: readonly AgentMessage[], assistantIndex: number): ToolResultMessage[] {\n\tconst results: ToolResultMessage[] = [];\n\tfor (let i = assistantIndex + 1; i < messages.length; i++) {\n\t\tconst msg = messages[i];\n\t\tif (isUserMessage(msg) || isAssistantMessage(msg)) break;\n\t\tif (isToolResultMessage(msg)) results.push(msg);\n\t}\n\treturn results;\n}\n\n/** True when the text contains any of the complex keywords (case-insensitive). */\nfunction containsComplexKeyword(text: string): boolean {\n\tconst lower = text.toLowerCase();\n\treturn COMPLEX_KEYWORDS.some((kw) => lower.includes(kw));\n}\n\n/** True when any user message in the conversation contains an image. */\nexport function conversationHasImages(messages: readonly AgentMessage[]): boolean {\n\tfor (const msg of messages) {\n\t\tif (!isUserMessage(msg)) continue;\n\t\tif (typeof msg.content === \"string\") continue;\n\t\tif (msg.content.some((c) => c.type === \"image\")) return true;\n\t}\n\treturn false;\n}\n\n// ============================================================================\n// Tier classification\n// ============================================================================\n\n/** Classify a model into its cost tier. */\nfunction modelTier(model: Model<Api>): ComplexityTier {\n\tconst cost = model.cost.output;\n\tif (cost < TIER_1_MAX_COST) return \"fast\";\n\tif (cost < TIER_3_MIN_COST) return \"balanced\";\n\treturn \"powerful\";\n}\n\n/** Measure the total text length of a tool result message. */\nfunction toolResultTextLength(tr: ToolResultMessage): number {\n\treturn tr.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.reduce((sum, c) => sum + c.text.length, 0);\n}\n\n/** Classify the complexity tier for the next turn. */\nexport function classifyComplexity(\n\tanalysis: TurnAnalysis,\n\tpreviousTier?: ComplexityTier,\n): { tier: ComplexityTier; reason: string } {\n\tconst { messages, thinkingLevel } = analysis;\n\n\t// High thinking level -> complex\n\tif (thinkingLevel === \"high\" || thinkingLevel === \"xhigh\") {\n\t\treturn { tier: \"powerful\", reason: `thinking level is set to ${thinkingLevel}` };\n\t}\n\n\t// Long conversation (counted in user turns, not raw messages) -> complex\n\tconst userTurnCount = messages.filter(isUserMessage).length;\n\tif (userTurnCount > LONG_CONVERSATION_THRESHOLD) {\n\t\treturn { tier: \"powerful\", reason: `long conversation (${userTurnCount} user turns)` };\n\t}\n\n\t// Find the last assistant message and its tool results\n\tlet lastAssistantIndex = -1;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tif (isAssistantMessage(messages[i])) {\n\t\t\tlastAssistantIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Find the last user message\n\tlet lastUserMessage: UserMessage | undefined;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tif (isUserMessage(messages[i])) {\n\t\t\tlastUserMessage = messages[i] as UserMessage;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Check for complex signals from the last assistant turn\n\tif (lastAssistantIndex >= 0) {\n\t\tconst lastAssistant = messages[lastAssistantIndex] as AssistantMessage;\n\t\tconst toolCalls = getToolCalls(lastAssistant);\n\t\tconst toolResults = getToolResultsAfter(messages, lastAssistantIndex);\n\n\t\t// Errors in last turn -> complex\n\t\tconst hasErrors = toolResults.some((tr) => tr.isError);\n\t\tif (hasErrors) {\n\t\t\treturn { tier: \"powerful\", reason: \"tool errors in last turn\" };\n\t\t}\n\n\t\t// Many file edits in last turn -> complex\n\t\tif (countFileEditCalls(toolCalls) > MANY_EDITS_THRESHOLD) {\n\t\t\treturn applyHysteresis(\n\t\t\t\t{ tier: \"powerful\", reason: `many file edits in last turn (${countFileEditCalls(toolCalls)})` },\n\t\t\t\tpreviousTier,\n\t\t\t);\n\t\t}\n\n\t\t// Large tool outputs suggest complex operations\n\t\tif (toolResults.some((r) => toolResultTextLength(r) > LARGE_TOOL_OUTPUT_THRESHOLD)) {\n\t\t\treturn applyHysteresis({ tier: \"powerful\", reason: \"large tool output detected\" }, previousTier);\n\t\t}\n\n\t\t// All read-only tools and no errors -> check for simple\n\t\tif (allToolCallsReadOnly(toolCalls) && !hasErrors) {\n\t\t\t// If the last user message is also short and simple, use fast tier.\n\t\t\t// Only de-escalate in short conversations; in longer ones a read-only\n\t\t\t// turn in the middle of complex work shouldn't drop to fast.\n\t\t\tif (lastUserMessage && messages.length < READ_ONLY_DEESCALATION_MAX_MESSAGES) {\n\t\t\t\tconst text = getUserMessageText(lastUserMessage);\n\t\t\t\tif (text.length < SHORT_MESSAGE_THRESHOLD && !containsComplexKeyword(text)) {\n\t\t\t\t\treturn applyHysteresis(\n\t\t\t\t\t\t{ tier: \"fast\", reason: \"read-only tools with short user message\" },\n\t\t\t\t\t\tpreviousTier,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check user message for complex keywords\n\tif (lastUserMessage) {\n\t\tconst text = getUserMessageText(lastUserMessage);\n\t\tif (containsComplexKeyword(text)) {\n\t\t\treturn applyHysteresis({ tier: \"powerful\", reason: \"complex keywords in user message\" }, previousTier);\n\t\t}\n\n\t\t// Short question with no prior assistant context -> fast\n\t\tif (lastAssistantIndex < 0 && text.length < SHORT_MESSAGE_THRESHOLD) {\n\t\t\treturn applyHysteresis({ tier: \"fast\", reason: \"short initial message\" }, previousTier);\n\t\t}\n\t}\n\n\treturn applyHysteresis({ tier: \"balanced\", reason: \"default\" }, previousTier);\n}\n\n/**\n * Budget-aware tier capping: as an active goal budget depletes, cap the\n * tier so the loop spends the expensive turns early and rations later\n * instead of hitting the wall at full price.\n */\nexport function applyBudgetCap(\n\tresult: { tier: ComplexityTier; reason: string },\n\tbudgetUsedFraction: number | undefined,\n): { tier: ComplexityTier; reason: string } {\n\tif (budgetUsedFraction === undefined) return result;\n\tconst pct = Math.round(budgetUsedFraction * 100);\n\tif (budgetUsedFraction >= BUDGET_CAP_FAST_FRACTION && result.tier !== \"fast\") {\n\t\treturn { tier: \"fast\", reason: `${result.reason} (capped to fast: ${pct}% of goal budget spent)` };\n\t}\n\tif (budgetUsedFraction >= BUDGET_CAP_BALANCED_FRACTION && result.tier === \"powerful\") {\n\t\treturn { tier: \"balanced\", reason: `${result.reason} (capped to balanced: ${pct}% of goal budget spent)` };\n\t}\n\treturn result;\n}\n\n/**\n * Hysteresis: prevent oscillation by not dropping from powerful directly to fast.\n * If the previous turn was powerful and the current classification is fast,\n * dampen to balanced instead.\n */\nfunction applyHysteresis(\n\tresult: { tier: ComplexityTier; reason: string },\n\tpreviousTier: ComplexityTier | undefined,\n): { tier: ComplexityTier; reason: string } {\n\tif (previousTier === \"powerful\" && result.tier === \"fast\") {\n\t\treturn { tier: \"balanced\", reason: `${result.reason} (dampened from fast: previous turn was powerful)` };\n\t}\n\treturn result;\n}\n\n// ============================================================================\n// Model selection\n// ============================================================================\n\n/** Combined per-token cost used to rank models within a tier. */\nfunction combinedCost(model: Model<Api>): number {\n\treturn model.cost.input + model.cost.output;\n}\n\n/**\n * Rank candidates within a tier. Lower rank wins. Ordering:\n * 1. Same provider as the preferred model (consistent auth, billing, behavior)\n * 2. For balanced/powerful: reasoning-capable models first\n * 3. Paid models before free ones (free tiers are rate-limited/unreliable)\n * 4. Cheaper combined cost first\n * 5. Larger context window as the final tiebreak\n */\nfunction compareCandidates(a: Model<Api>, b: Model<Api>, tier: ComplexityTier, preferred: Model<Api>): number {\n\tconst aSameProvider = a.provider === preferred.provider ? 0 : 1;\n\tconst bSameProvider = b.provider === preferred.provider ? 0 : 1;\n\tif (aSameProvider !== bSameProvider) return aSameProvider - bSameProvider;\n\n\tif (tier !== \"fast\") {\n\t\tconst aReasoning = a.reasoning ? 0 : 1;\n\t\tconst bReasoning = b.reasoning ? 0 : 1;\n\t\tif (aReasoning !== bReasoning) return aReasoning - bReasoning;\n\t}\n\n\tconst aFree = combinedCost(a) === 0 ? 1 : 0;\n\tconst bFree = combinedCost(b) === 0 ? 1 : 0;\n\tif (aFree !== bFree) return aFree - bFree;\n\n\tconst costDiff = combinedCost(a) - combinedCost(b);\n\tif (costDiff !== 0) return costDiff;\n\n\treturn b.contextWindow - a.contextWindow;\n}\n\n/**\n * Select the best model for a given complexity tier from available models.\n * A user-pinned model for the tier wins over the heuristic ranking.\n * When the conversation contains images, only vision-capable models are\n * considered so routing can never break a multimodal session.\n */\nexport function selectModelForTier(\n\ttier: ComplexityTier,\n\tconfig: SmartRouterConfig,\n\trequiresImageInput = false,\n): Model<Api> {\n\tconst { availableModels, preferredModel } = config;\n\n\t// User pin wins, as long as it is modality-compatible.\n\tconst pinned = config.pinnedModels?.[tier];\n\tif (pinned && (!requiresImageInput || pinned.input.includes(\"image\"))) {\n\t\treturn pinned;\n\t}\n\n\tconst candidates = requiresImageInput ? availableModels.filter((m) => m.input.includes(\"image\")) : availableModels;\n\n\t// Partition models into tiers\n\tconst tierModels = candidates.filter((m) => modelTier(m) === tier);\n\n\tif (tierModels.length === 0) {\n\t\t// Requested tier has no (compatible) models, fall back to preferred\n\t\treturn preferredModel;\n\t}\n\n\tlet best = tierModels[0];\n\tfor (let i = 1; i < tierModels.length; i++) {\n\t\tif (compareCandidates(tierModels[i], best, tier, preferredModel) < 0) {\n\t\t\tbest = tierModels[i];\n\t\t}\n\t}\n\treturn best;\n}\n\n// ============================================================================\n// Main entry\n// ============================================================================\n\n/**\n * Analyze the turn and return a model recommendation.\n *\n * Returns undefined only when smart routing is disabled. When the selected\n * model equals the preferred model, the result is still returned with\n * `changed: false` so callers can track the classified tier for hysteresis.\n */\nexport function routeNextTurn(\n\tanalysis: TurnAnalysis,\n\tconfig: SmartRouterConfig,\n\tpreviousTier?: ComplexityTier,\n): SmartRouterResult | undefined {\n\tif (!config.enabled) return undefined;\n\n\tconst { tier, reason } = applyBudgetCap(classifyComplexity(analysis, previousTier), analysis.budgetUsedFraction);\n\tconst requiresImageInput = conversationHasImages(analysis.messages);\n\tconst model = selectModelForTier(tier, config, requiresImageInput);\n\n\treturn { model, tier, reason, changed: !modelsAreEqual(model, config.preferredModel) };\n}\n"]}
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAuYnE,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACxC;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBAoT/D"}
|
package/dist/main.js
CHANGED
|
@@ -10,6 +10,7 @@ import { modelsAreEqual } from "@elyracode/ai";
|
|
|
10
10
|
import { ProcessTerminal, setKeybindings, TUI } from "@elyracode/tui";
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
import { parseArgs, printHelp } from "./cli/args.js";
|
|
13
|
+
import { handleDoctorCommand } from "./cli/doctor-cli.js";
|
|
13
14
|
import { processFileArguments } from "./cli/file-processor.js";
|
|
14
15
|
import { buildInitialMessage } from "./cli/initial-message.js";
|
|
15
16
|
import { listModels } from "./cli/list-models.js";
|
|
@@ -330,6 +331,9 @@ export async function main(args, options) {
|
|
|
330
331
|
if (await handleConfigCommand(args)) {
|
|
331
332
|
return;
|
|
332
333
|
}
|
|
334
|
+
if (await handleDoctorCommand(args)) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
333
337
|
const parsed = parseArgs(args);
|
|
334
338
|
if (parsed.diagnostics.length > 0) {
|
|
335
339
|
for (const d of parsed.diagnostics) {
|