@elyracode/coding-agent 0.5.3 → 0.5.5

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/core/agent-session.d.ts +1 -1
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +3 -3
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/compaction/compaction.d.ts +2 -2
  7. package/dist/core/compaction/compaction.d.ts.map +1 -1
  8. package/dist/core/compaction/compaction.js +21 -1
  9. package/dist/core/compaction/compaction.js.map +1 -1
  10. package/dist/core/extensions/runner.d.ts.map +1 -1
  11. package/dist/core/extensions/runner.js +2 -0
  12. package/dist/core/extensions/runner.js.map +1 -1
  13. package/dist/core/memory.d.ts +1 -1
  14. package/dist/core/memory.d.ts.map +1 -1
  15. package/dist/core/memory.js.map +1 -1
  16. package/dist/core/smart-router.d.ts +1 -1
  17. package/dist/core/smart-router.d.ts.map +1 -1
  18. package/dist/core/smart-router.js.map +1 -1
  19. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  20. package/dist/modes/interactive/interactive-mode.js +1 -0
  21. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  22. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  23. package/examples/extensions/custom-provider-anthropic/package.json +2 -2
  24. package/examples/extensions/custom-provider-gitlab-duo/package.json +2 -2
  25. package/examples/extensions/sandbox/package-lock.json +2 -2
  26. package/examples/extensions/sandbox/package.json +2 -2
  27. package/examples/extensions/with-deps/package-lock.json +2 -2
  28. package/examples/extensions/with-deps/package.json +2 -2
  29. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/core/memory.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport type { AgentMessage } from \"@elyracode/agent-core\";\nimport { type AssistantMessage, completeSimple, type Model } from \"@elyracode/ai\";\n\nimport { getAgentDir } from \"../config.js\";\n\n/** Directory for memory files */\nexport function getMemoryDir(): string {\n\treturn join(getAgentDir(), \"memory\");\n}\n\n/** Get the memory file path for a given project cwd */\nexport function getMemoryPath(cwd: string): string {\n\tconst safePath = `--${cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\")}--`;\n\treturn join(getMemoryDir(), `${safePath}.md`);\n}\n\n/** Load existing memory for a project, or return undefined if none exists */\nexport function loadMemory(cwd: string): string | undefined {\n\tconst path = getMemoryPath(cwd);\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst content = readFileSync(path, \"utf-8\").trim();\n\t\treturn content || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Save memory content for a project */\nexport function saveMemory(cwd: string, content: string): void {\n\tconst dir = getMemoryDir();\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(getMemoryPath(cwd), content, \"utf-8\");\n}\n\n/** Extract the concatenated text from an AssistantMessage's content blocks. */\nfunction extractText(message: AssistantMessage): string {\n\treturn message.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/** The prompt used to generate a session memory entry */\nconst MEMORY_GENERATION_PROMPT = `You are analyzing a coding session to extract persistent knowledge for future sessions.\n\nGiven the conversation below, extract ONLY information that would be valuable in future sessions working on the same codebase. Focus on:\n\n1. **Architecture**: Project structure, key patterns, module boundaries\n2. **Decisions**: Technical decisions made and their rationale\n3. **Gotchas**: Bugs found, edge cases, things that didn't work\n4. **Conventions**: Coding style preferences, naming conventions, tooling setup\n5. **State**: What was accomplished, what's left unfinished\n\nRules:\n- Be extremely concise. Each point should be one line.\n- Skip generic knowledge (language features, common patterns)\n- Only include project-specific knowledge the user would have to re-explain\n- If the session was trivial (simple question, no real work), respond with \"SKIP\"\n- Output as a markdown list grouped by category\n- Max 20 bullet points total`;\n\n/** The prompt used to merge new memory with existing */\nconst MEMORY_MERGE_PROMPT = `You are maintaining a persistent knowledge base for a coding project.\n\nBelow is the EXISTING memory, followed by NEW observations from a recent session.\n\nMerge them into a single, deduplicated knowledge base:\n- Remove outdated entries that the new session contradicts\n- Deduplicate similar entries\n- Keep the most recent/accurate version of conflicting info\n- Preserve important architectural and decision context\n- Stay concise: max 30 bullet points total\n- Output as a markdown list grouped by category (Architecture, Decisions, Gotchas, Conventions, State)\n- If the new observations say \"SKIP\", return the existing memory unchanged`;\n\n/**\n * Generate a memory summary from a completed session's messages.\n * If existing memory exists, merges the new observations with it.\n */\nexport async function generateMemory(options: {\n\tmessages: AgentMessage[];\n\tmodel: Model<any>;\n\tapiKey: string;\n\theaders?: Record<string, string>;\n\texistingMemory?: string;\n\tcwd: string;\n}): Promise<string | undefined> {\n\tconst { messages, model, apiKey, headers, existingMemory, cwd } = options;\n\n\t// Skip very short sessions (less than 2 assistant messages)\n\tconst assistantMessages = messages.filter(\n\t\t(m): m is AssistantMessage => \"role\" in m && (m as { role: string }).role === \"assistant\",\n\t);\n\tif (assistantMessages.length < 2) return existingMemory;\n\n\t// Serialize conversation to text (simplified -- just user and assistant text)\n\tconst conversationLines: string[] = [];\n\tfor (const msg of messages) {\n\t\tif (!(\"role\" in msg)) continue;\n\t\tconst m = msg as { role: string; content: unknown };\n\t\tif (m.role === \"user\" && Array.isArray(m.content)) {\n\t\t\tconst text = (m.content as { type: string; text?: string }[])\n\t\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t\t.map((c) => c.text ?? \"\")\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (text) conversationLines.push(`User: ${text}`);\n\t\t} else if (m.role === \"user\" && typeof m.content === \"string\") {\n\t\t\tconversationLines.push(`User: ${m.content}`);\n\t\t} else if (m.role === \"assistant\" && Array.isArray(m.content)) {\n\t\t\tconst blocks = m.content as { type: string; text?: string; name?: string }[];\n\t\t\tconst text = blocks\n\t\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t\t.map((c) => c.text ?? \"\")\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (text) conversationLines.push(`Assistant: ${text.slice(0, 2000)}`);\n\t\t\t// Include tool call names\n\t\t\tconst toolCalls = blocks.filter((c) => c.type === \"toolCall\");\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tconst toolNames = toolCalls.map((t) => t.name).join(\", \");\n\t\t\t\tconversationLines.push(` [Tools used: ${toolNames}]`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Truncate conversation to fit in context (keep last ~50 exchanges)\n\tconst conversation = conversationLines.slice(-100).join(\"\\n\");\n\tconst now = Date.now();\n\n\ttry {\n\t\tif (existingMemory) {\n\t\t\t// Merge mode: generate new observations, then merge with existing\n\t\t\tconst newObsResult = await completeSimple(\n\t\t\t\tmodel,\n\t\t\t\t{\n\t\t\t\t\tsystemPrompt: MEMORY_GENERATION_PROMPT,\n\t\t\t\t\tmessages: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\t\tcontent: `Project: ${cwd}\\n\\n<conversation>\\n${conversation}\\n</conversation>`,\n\t\t\t\t\t\t\ttimestamp: now,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{ apiKey, headers },\n\t\t\t);\n\n\t\t\tconst newObservations = extractText(newObsResult).trim();\n\t\t\tif (!newObservations || newObservations === \"SKIP\") {\n\t\t\t\treturn existingMemory;\n\t\t\t}\n\n\t\t\tconst mergedResult = await completeSimple(\n\t\t\t\tmodel,\n\t\t\t\t{\n\t\t\t\t\tsystemPrompt: MEMORY_MERGE_PROMPT,\n\t\t\t\t\tmessages: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\t\tcontent: `EXISTING MEMORY:\\n${existingMemory}\\n\\nNEW OBSERVATIONS:\\n${newObservations}`,\n\t\t\t\t\t\t\ttimestamp: now,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{ apiKey, headers },\n\t\t\t);\n\n\t\t\tconst merged = extractText(mergedResult).trim();\n\t\t\treturn merged || existingMemory;\n\t\t} else {\n\t\t\t// Fresh memory generation\n\t\t\tconst result = await completeSimple(\n\t\t\t\tmodel,\n\t\t\t\t{\n\t\t\t\t\tsystemPrompt: MEMORY_GENERATION_PROMPT,\n\t\t\t\t\tmessages: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\t\tcontent: `Project: ${cwd}\\n\\n<conversation>\\n${conversation}\\n</conversation>`,\n\t\t\t\t\t\t\ttimestamp: now,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{ apiKey, headers },\n\t\t\t);\n\n\t\t\tconst text = extractText(result).trim();\n\t\t\tif (!text || text === \"SKIP\") {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\treturn text;\n\t\t}\n\t} catch {\n\t\t// Don't fail the session shutdown if memory generation fails\n\t\treturn existingMemory;\n\t}\n}\n"]}
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/core/memory.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\nimport type { AgentMessage } from \"@elyracode/agent-core\";\nimport { type AssistantMessage, completeSimple, type Model } from \"@elyracode/ai\";\n\nimport { getAgentDir } from \"../config.js\";\n\n/** Directory for memory files */\nexport function getMemoryDir(): string {\n\treturn join(getAgentDir(), \"memory\");\n}\n\n/** Get the memory file path for a given project cwd */\nexport function getMemoryPath(cwd: string): string {\n\tconst safePath = `--${cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\")}--`;\n\treturn join(getMemoryDir(), `${safePath}.md`);\n}\n\n/** Load existing memory for a project, or return undefined if none exists */\nexport function loadMemory(cwd: string): string | undefined {\n\tconst path = getMemoryPath(cwd);\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst content = readFileSync(path, \"utf-8\").trim();\n\t\treturn content || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** Save memory content for a project */\nexport function saveMemory(cwd: string, content: string): void {\n\tconst dir = getMemoryDir();\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(getMemoryPath(cwd), content, \"utf-8\");\n}\n\n/** Extract the concatenated text from an AssistantMessage's content blocks. */\nfunction extractText(message: AssistantMessage): string {\n\treturn message.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/** The prompt used to generate a session memory entry */\nconst MEMORY_GENERATION_PROMPT = `You are analyzing a coding session to extract persistent knowledge for future sessions.\n\nGiven the conversation below, extract ONLY information that would be valuable in future sessions working on the same codebase. Focus on:\n\n1. **Architecture**: Project structure, key patterns, module boundaries\n2. **Decisions**: Technical decisions made and their rationale\n3. **Gotchas**: Bugs found, edge cases, things that didn't work\n4. **Conventions**: Coding style preferences, naming conventions, tooling setup\n5. **State**: What was accomplished, what's left unfinished\n\nRules:\n- Be extremely concise. Each point should be one line.\n- Skip generic knowledge (language features, common patterns)\n- Only include project-specific knowledge the user would have to re-explain\n- If the session was trivial (simple question, no real work), respond with \"SKIP\"\n- Output as a markdown list grouped by category\n- Max 20 bullet points total`;\n\n/** The prompt used to merge new memory with existing */\nconst MEMORY_MERGE_PROMPT = `You are maintaining a persistent knowledge base for a coding project.\n\nBelow is the EXISTING memory, followed by NEW observations from a recent session.\n\nMerge them into a single, deduplicated knowledge base:\n- Remove outdated entries that the new session contradicts\n- Deduplicate similar entries\n- Keep the most recent/accurate version of conflicting info\n- Preserve important architectural and decision context\n- Stay concise: max 30 bullet points total\n- Output as a markdown list grouped by category (Architecture, Decisions, Gotchas, Conventions, State)\n- If the new observations say \"SKIP\", return the existing memory unchanged`;\n\n/**\n * Generate a memory summary from a completed session's messages.\n * If existing memory exists, merges the new observations with it.\n */\nexport async function generateMemory(options: {\n\tmessages: readonly AgentMessage[];\n\tmodel: Model<any>;\n\tapiKey: string;\n\theaders?: Record<string, string>;\n\texistingMemory?: string;\n\tcwd: string;\n}): Promise<string | undefined> {\n\tconst { messages, model, apiKey, headers, existingMemory, cwd } = options;\n\n\t// Skip very short sessions (less than 2 assistant messages)\n\tconst assistantMessages = messages.filter(\n\t\t(m): m is AssistantMessage => \"role\" in m && (m as { role: string }).role === \"assistant\",\n\t);\n\tif (assistantMessages.length < 2) return existingMemory;\n\n\t// Serialize conversation to text (simplified -- just user and assistant text)\n\tconst conversationLines: string[] = [];\n\tfor (const msg of messages) {\n\t\tif (!(\"role\" in msg)) continue;\n\t\tconst m = msg as { role: string; content: unknown };\n\t\tif (m.role === \"user\" && Array.isArray(m.content)) {\n\t\t\tconst text = (m.content as { type: string; text?: string }[])\n\t\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t\t.map((c) => c.text ?? \"\")\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (text) conversationLines.push(`User: ${text}`);\n\t\t} else if (m.role === \"user\" && typeof m.content === \"string\") {\n\t\t\tconversationLines.push(`User: ${m.content}`);\n\t\t} else if (m.role === \"assistant\" && Array.isArray(m.content)) {\n\t\t\tconst blocks = m.content as { type: string; text?: string; name?: string }[];\n\t\t\tconst text = blocks\n\t\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t\t.map((c) => c.text ?? \"\")\n\t\t\t\t.join(\"\\n\");\n\t\t\tif (text) conversationLines.push(`Assistant: ${text.slice(0, 2000)}`);\n\t\t\t// Include tool call names\n\t\t\tconst toolCalls = blocks.filter((c) => c.type === \"toolCall\");\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tconst toolNames = toolCalls.map((t) => t.name).join(\", \");\n\t\t\t\tconversationLines.push(` [Tools used: ${toolNames}]`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Truncate conversation to fit in context (keep last ~50 exchanges)\n\tconst conversation = conversationLines.slice(-100).join(\"\\n\");\n\tconst now = Date.now();\n\n\ttry {\n\t\tif (existingMemory) {\n\t\t\t// Merge mode: generate new observations, then merge with existing\n\t\t\tconst newObsResult = await completeSimple(\n\t\t\t\tmodel,\n\t\t\t\t{\n\t\t\t\t\tsystemPrompt: MEMORY_GENERATION_PROMPT,\n\t\t\t\t\tmessages: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\t\tcontent: `Project: ${cwd}\\n\\n<conversation>\\n${conversation}\\n</conversation>`,\n\t\t\t\t\t\t\ttimestamp: now,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{ apiKey, headers },\n\t\t\t);\n\n\t\t\tconst newObservations = extractText(newObsResult).trim();\n\t\t\tif (!newObservations || newObservations === \"SKIP\") {\n\t\t\t\treturn existingMemory;\n\t\t\t}\n\n\t\t\tconst mergedResult = await completeSimple(\n\t\t\t\tmodel,\n\t\t\t\t{\n\t\t\t\t\tsystemPrompt: MEMORY_MERGE_PROMPT,\n\t\t\t\t\tmessages: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\t\tcontent: `EXISTING MEMORY:\\n${existingMemory}\\n\\nNEW OBSERVATIONS:\\n${newObservations}`,\n\t\t\t\t\t\t\ttimestamp: now,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{ apiKey, headers },\n\t\t\t);\n\n\t\t\tconst merged = extractText(mergedResult).trim();\n\t\t\treturn merged || existingMemory;\n\t\t} else {\n\t\t\t// Fresh memory generation\n\t\t\tconst result = await completeSimple(\n\t\t\t\tmodel,\n\t\t\t\t{\n\t\t\t\t\tsystemPrompt: MEMORY_GENERATION_PROMPT,\n\t\t\t\t\tmessages: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\t\tcontent: `Project: ${cwd}\\n\\n<conversation>\\n${conversation}\\n</conversation>`,\n\t\t\t\t\t\t\ttimestamp: now,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{ apiKey, headers },\n\t\t\t);\n\n\t\t\tconst text = extractText(result).trim();\n\t\t\tif (!text || text === \"SKIP\") {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\treturn text;\n\t\t}\n\t} catch {\n\t\t// Don't fail the session shutdown if memory generation fails\n\t\treturn existingMemory;\n\t}\n}\n"]}
@@ -15,7 +15,7 @@ export interface SmartRouterConfig {
15
15
  }
16
16
  export interface TurnAnalysis {
17
17
  /** The messages in the conversation so far. */
18
- messages: AgentMessage[];
18
+ messages: readonly AgentMessage[];
19
19
  /** Current thinking level. */
20
20
  thinkingLevel: ThinkingLevel;
21
21
  }
@@ -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;CAC3B;AAED,MAAM,WAAW,YAAY;IAC5B,+CAA+C;IAC/C,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,8BAA8B;IAC9B,aAAa,EAAE,aAAa,CAAC;CAC7B;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;CACf;AA4GD,sDAAsD;AACtD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,GAAG;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA0EnG;AAMD,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,KAAK,CAAC,GAAG,CAAC,CAmB9F;AAMD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,SAAS,CAU9G","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: 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];\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// ============================================================================\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: 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/** Classify the complexity tier for the next turn. */\nexport function classifyComplexity(analysis: TurnAnalysis): { 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 { tier: \"powerful\", reason: `many file edits in last turn (${countFileEditCalls(toolCalls)})` };\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\tif (lastUserMessage) {\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 { tier: \"fast\", reason: \"read-only tools with short user message\" };\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 { tier: \"powerful\", reason: \"complex keywords in user message\" };\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 { tier: \"fast\", reason: \"short initial message\" };\n\t\t}\n\t}\n\n\treturn { tier: \"balanced\", reason: \"default\" };\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(analysis: TurnAnalysis, config: SmartRouterConfig): SmartRouterResult | undefined {\n\tif (!config.enabled) return undefined;\n\n\tconst { tier, reason } = classifyComplexity(analysis);\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.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;CAC3B;AAED,MAAM,WAAW,YAAY;IAC5B,+CAA+C;IAC/C,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAClC,8BAA8B;IAC9B,aAAa,EAAE,aAAa,CAAC;CAC7B;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;CACf;AA4GD,sDAAsD;AACtD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,GAAG;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA0EnG;AAMD,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,KAAK,CAAC,GAAG,CAAC,CAmB9F;AAMD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,SAAS,CAU9G","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];\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// ============================================================================\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/** Classify the complexity tier for the next turn. */\nexport function classifyComplexity(analysis: TurnAnalysis): { 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 { tier: \"powerful\", reason: `many file edits in last turn (${countFileEditCalls(toolCalls)})` };\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\tif (lastUserMessage) {\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 { tier: \"fast\", reason: \"read-only tools with short user message\" };\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 { tier: \"powerful\", reason: \"complex keywords in user message\" };\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 { tier: \"fast\", reason: \"short initial message\" };\n\t\t}\n\t}\n\n\treturn { tier: \"balanced\", reason: \"default\" };\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(analysis: TurnAnalysis, config: SmartRouterConfig): SmartRouterResult | undefined {\n\tif (!config.enabled) return undefined;\n\n\tconst { tier, reason } = classifyComplexity(analysis);\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 +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: 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];\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// ============================================================================\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: 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/** Classify the complexity tier for the next turn. */\nexport function classifyComplexity(analysis: TurnAnalysis): { 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 { tier: \"powerful\", reason: `many file edits in last turn (${countFileEditCalls(toolCalls)})` };\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\tif (lastUserMessage) {\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 { tier: \"fast\", reason: \"read-only tools with short user message\" };\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 { tier: \"powerful\", reason: \"complex keywords in user message\" };\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 { tier: \"fast\", reason: \"short initial message\" };\n\t\t}\n\t}\n\n\treturn { tier: \"balanced\", reason: \"default\" };\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(analysis: TurnAnalysis, config: SmartRouterConfig): SmartRouterResult | undefined {\n\tif (!config.enabled) return undefined;\n\n\tconst { tier, reason } = classifyComplexity(analysis);\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}\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];\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// ============================================================================\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/** Classify the complexity tier for the next turn. */\nexport function classifyComplexity(analysis: TurnAnalysis): { 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 { tier: \"powerful\", reason: `many file edits in last turn (${countFileEditCalls(toolCalls)})` };\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\tif (lastUserMessage) {\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 { tier: \"fast\", reason: \"read-only tools with short user message\" };\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 { tier: \"powerful\", reason: \"complex keywords in user message\" };\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 { tier: \"fast\", reason: \"short initial message\" };\n\t\t}\n\t}\n\n\treturn { tier: \"balanced\", reason: \"default\" };\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(analysis: TurnAnalysis, config: SmartRouterConfig): SmartRouterResult | undefined {\n\tif (!config.enabled) return undefined;\n\n\tconst { tier, reason } = classifyComplexity(analysis);\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"]}