Skip to content

Commit f7cfcbd

Browse files
authored
🤖 Include tool names in mode switch sentinel (#250)
## Summary When mode switches mid-conversation, the AI now receives information about which tools are available in the new mode. This helps models make better decisions about how to proceed after a mode transition. ## Changes **Extended mode transition sentinel to include tool availability:** - Added optional `toolNames` parameter to `injectModeTransition()` - Reorganized `aiService.ts` to determine tools before message transformations - Mode transition messages now include tool names when available **Example transition message:** ``` [Mode switched from plan to exec. Follow exec mode instructions. Available tools: file_read, bash, propose_plan, web_search.] ``` **Benefits:** - Models understand what tools are newly available/unavailable after mode switch - Handles model-specific tools (e.g., `web_search` only for some models) - Respects tool policy filters automatically - Backward compatible (tools parameter is optional) ## Testing Added comprehensive test coverage: - Mode switch with tools - verifies tool names appear in sentinel - Mode switch without tools parameter - ensures backward compatibility - Mode switch with empty tool list - graceful handling - Existing mode transition tests still pass All 489 tests pass including new test cases. _Generated with `cmux`_
1 parent 737ec28 commit f7cfcbd

File tree

3 files changed

+133
-3
lines changed

3 files changed

+133
-3
lines changed

src/services/aiService.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from "fs/promises";
22
import * as path from "path";
3+
import * as os from "os";
34
import { EventEmitter } from "events";
45
import { convertToModelMessages, wrapLanguageModel, type LanguageModel } from "ai";
56
import { applyToolOutputRedaction } from "@/utils/messages/applyToolOutputRedaction";
@@ -436,6 +437,15 @@ export class AIService extends EventEmitter {
436437
// Extract provider name from modelString (e.g., "anthropic:claude-opus-4-1" -> "anthropic")
437438
const [providerName] = modelString.split(":");
438439

440+
// Get tool names early for mode transition sentinel (stub config, no workspace context needed)
441+
const earlyAllTools = await getToolsForModel(modelString, {
442+
cwd: process.cwd(),
443+
tempDir: os.tmpdir(),
444+
secrets: {},
445+
});
446+
const earlyTools = applyToolPolicy(earlyAllTools, toolPolicy);
447+
const toolNamesForSentinel = Object.keys(earlyTools);
448+
439449
// Filter out assistant messages with only reasoning (no text/tools)
440450
const filteredMessages = filterEmptyAssistantMessages(messages);
441451
log.debug(`Filtered ${messages.length - filteredMessages.length} empty assistant messages`);
@@ -452,7 +462,12 @@ export class AIService extends EventEmitter {
452462
const messagesWithSentinel = addInterruptedSentinel(filteredMessages);
453463

454464
// Inject mode transition context if mode changed from last assistant message
455-
const messagesWithModeContext = injectModeTransition(messagesWithSentinel, mode);
465+
// Include tool names so model knows what tools are available in the new mode
466+
const messagesWithModeContext = injectModeTransition(
467+
messagesWithSentinel,
468+
mode,
469+
toolNamesForSentinel
470+
);
456471

457472
// Apply centralized tool-output redaction BEFORE converting to provider ModelMessages
458473
// This keeps the persisted/UI history intact while trimming heavy fields for the request

src/utils/messages/modelMessageTransform.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,4 +847,105 @@ describe("injectModeTransition", () => {
847847
expect(result.length).toBe(1);
848848
expect(result).toEqual(messages);
849849
});
850+
851+
it("should include tool names in transition message when provided", () => {
852+
const messages: CmuxMessage[] = [
853+
{
854+
id: "user-1",
855+
role: "user",
856+
parts: [{ type: "text", text: "Let's plan a feature" }],
857+
metadata: { timestamp: 1000 },
858+
},
859+
{
860+
id: "assistant-1",
861+
role: "assistant",
862+
parts: [{ type: "text", text: "Here's the plan..." }],
863+
metadata: { timestamp: 2000, mode: "plan" },
864+
},
865+
{
866+
id: "user-2",
867+
role: "user",
868+
parts: [{ type: "text", text: "Now execute it" }],
869+
metadata: { timestamp: 3000 },
870+
},
871+
];
872+
873+
const toolNames = ["file_read", "bash", "file_edit_replace_string", "web_search"];
874+
const result = injectModeTransition(messages, "exec", toolNames);
875+
876+
// Should have 4 messages: user, assistant, mode-transition, user
877+
expect(result.length).toBe(4);
878+
879+
// Third message should be mode transition with tool names
880+
expect(result[2].role).toBe("user");
881+
expect(result[2].metadata?.synthetic).toBe(true);
882+
expect(result[2].parts[0]).toMatchObject({
883+
type: "text",
884+
text: "[Mode switched from plan to exec. Follow exec mode instructions. Available tools: file_read, bash, file_edit_replace_string, web_search.]",
885+
});
886+
});
887+
888+
it("should handle mode transition without tools parameter (backward compatibility)", () => {
889+
const messages: CmuxMessage[] = [
890+
{
891+
id: "user-1",
892+
role: "user",
893+
parts: [{ type: "text", text: "Let's plan" }],
894+
metadata: { timestamp: 1000 },
895+
},
896+
{
897+
id: "assistant-1",
898+
role: "assistant",
899+
parts: [{ type: "text", text: "Planning..." }],
900+
metadata: { timestamp: 2000, mode: "plan" },
901+
},
902+
{
903+
id: "user-2",
904+
role: "user",
905+
parts: [{ type: "text", text: "Execute" }],
906+
metadata: { timestamp: 3000 },
907+
},
908+
];
909+
910+
const result = injectModeTransition(messages, "exec");
911+
912+
// Should have 4 messages with transition, but no tool info
913+
expect(result.length).toBe(4);
914+
expect(result[2].parts[0]).toMatchObject({
915+
type: "text",
916+
text: "[Mode switched from plan to exec. Follow exec mode instructions.]",
917+
});
918+
});
919+
920+
it("should handle mode transition with empty tool list", () => {
921+
const messages: CmuxMessage[] = [
922+
{
923+
id: "user-1",
924+
role: "user",
925+
parts: [{ type: "text", text: "Let's plan" }],
926+
metadata: { timestamp: 1000 },
927+
},
928+
{
929+
id: "assistant-1",
930+
role: "assistant",
931+
parts: [{ type: "text", text: "Planning..." }],
932+
metadata: { timestamp: 2000, mode: "plan" },
933+
},
934+
{
935+
id: "user-2",
936+
role: "user",
937+
parts: [{ type: "text", text: "Execute" }],
938+
metadata: { timestamp: 3000 },
939+
},
940+
];
941+
942+
const result = injectModeTransition(messages, "exec", []);
943+
944+
// Should have 4 messages with transition, but no tool info (empty array handled gracefully)
945+
expect(result.length).toBe(4);
946+
expect(result[2].parts[0]).toMatchObject({
947+
type: "text",
948+
text: "[Mode switched from plan to exec. Follow exec mode instructions.]",
949+
});
950+
});
850951
});

src/utils/messages/modelMessageTransform.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,14 @@ export function addInterruptedSentinel(messages: CmuxMessage[]): CmuxMessage[] {
113113
*
114114
* @param messages The conversation history
115115
* @param currentMode The mode for the upcoming assistant response (e.g., "plan", "exec")
116+
* @param toolNames Optional list of available tool names to include in transition message
116117
* @returns Messages with mode transition context injected if needed
117118
*/
118-
export function injectModeTransition(messages: CmuxMessage[], currentMode?: string): CmuxMessage[] {
119+
export function injectModeTransition(
120+
messages: CmuxMessage[],
121+
currentMode?: string,
122+
toolNames?: string[]
123+
): CmuxMessage[] {
119124
// No mode specified, nothing to do
120125
if (!currentMode) {
121126
return messages;
@@ -160,13 +165,22 @@ export function injectModeTransition(messages: CmuxMessage[], currentMode?: stri
160165
}
161166

162167
// Inject mode transition message right before the last user message
168+
let transitionText = `[Mode switched from ${lastMode} to ${currentMode}. Follow ${currentMode} mode instructions.`;
169+
170+
// Append tool availability if provided
171+
if (toolNames && toolNames.length > 0) {
172+
transitionText += ` Available tools: ${toolNames.join(", ")}.]`;
173+
} else {
174+
transitionText += "]";
175+
}
176+
163177
const transitionMessage: CmuxMessage = {
164178
id: `mode-transition-${Date.now()}`,
165179
role: "user",
166180
parts: [
167181
{
168182
type: "text",
169-
text: `[Mode switched from ${lastMode} to ${currentMode}. Follow ${currentMode} mode instructions.]`,
183+
text: transitionText,
170184
},
171185
],
172186
metadata: {

0 commit comments

Comments
 (0)