Skip to content

Commit fbe8355

Browse files
committed
fix(code): fix subagent sessions
1 parent 25fb4cf commit fbe8355

5 files changed

Lines changed: 230 additions & 19 deletions

File tree

apps/code/src/main/services/agent/service.ts

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,56 @@ const onAgentLog: OnLogCallback = (level, scope, message, data) => {
178178
}
179179
};
180180

181+
const HAIKU_EXPLORE_AGENT_OVERRIDE = {
182+
description:
183+
'Fast agent for exploring and understanding codebases. Use this when you need to find files by pattern (eg. "src/components/**/*.tsx"), search for code or keywords (eg. "where is the auth middleware?"), or answer questions about how the codebase works (eg. "how does the session service handle reconnects?"). When calling this agent, specify a thoroughness level: "quick" for targeted lookups, "medium" for broader exploration, or "very thorough" for comprehensive analysis across multiple locations.',
184+
model: "haiku",
185+
prompt: `You are a fast, read-only codebase exploration agent.
186+
187+
Your job is to find files, search code, read the most relevant sources, and report findings clearly.
188+
189+
Rules:
190+
- Never create, modify, delete, move, or copy files.
191+
- Never use shell redirection or any command that changes system state.
192+
- Use Glob for broad file pattern matching.
193+
- Use Grep for searching file contents.
194+
- Use Read when you know the exact file path to inspect.
195+
- Use Bash only for safe read-only commands like ls, git status, git log, git diff, find, cat, head, and tail.
196+
- Adapt your search approach based on the thoroughness level specified by the caller.
197+
- Return file paths as absolute paths in your final response.
198+
- Avoid using emojis.
199+
- Wherever possible, spawn multiple parallel tool calls for grepping and reading files.
200+
- Search efficiently, then read only the most relevant files.
201+
- Return findings directly in your final response — do not create files.`,
202+
tools: [
203+
"Bash",
204+
"Glob",
205+
"Grep",
206+
"Read",
207+
"WebFetch",
208+
"WebSearch",
209+
"NotebookRead",
210+
"TodoWrite",
211+
],
212+
};
213+
214+
function buildClaudeCodeOptions(args: {
215+
additionalDirectories?: string[];
216+
effort?: EffortLevel;
217+
plugins: { type: "local"; path: string }[];
218+
}) {
219+
return {
220+
...(args.additionalDirectories?.length && {
221+
additionalDirectories: args.additionalDirectories,
222+
}),
223+
...(args.effort && { effort: args.effort }),
224+
plugins: args.plugins,
225+
agents: {
226+
"ph-explore": HAIKU_EXPLORE_AGENT_OVERRIDE,
227+
},
228+
};
229+
}
230+
181231
interface SessionConfig {
182232
taskId: string;
183233
taskRunId: string;
@@ -631,6 +681,11 @@ When creating pull requests, add the following footer at the end of the PR descr
631681
},
632682
...externalPlugins,
633683
];
684+
const claudeCodeOptions = buildClaudeCodeOptions({
685+
additionalDirectories,
686+
effort,
687+
plugins,
688+
});
634689

635690
let configOptions: SessionConfigOption[] | undefined;
636691
let agentSessionId: string;
@@ -679,13 +734,7 @@ When creating pull requests, add the following footer at the end of the PR descr
679734
...(permissionMode && { permissionMode }),
680735
...(model != null && { model }),
681736
claudeCode: {
682-
options: {
683-
...(additionalDirectories?.length && {
684-
additionalDirectories,
685-
}),
686-
...(effort && { effort }),
687-
plugins,
688-
},
737+
options: claudeCodeOptions,
689738
},
690739
},
691740
});
@@ -712,11 +761,7 @@ When creating pull requests, add the following footer at the end of the PR descr
712761
...(permissionMode && { permissionMode }),
713762
...(model != null && { model }),
714763
claudeCode: {
715-
options: {
716-
...(additionalDirectories?.length && { additionalDirectories }),
717-
...(effort && { effort }),
718-
plugins,
719-
},
764+
options: claudeCodeOptions,
720765
},
721766
},
722767
});

apps/code/src/main/services/auth-proxy/service.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,111 @@ export class AuthProxyService {
154154
req.on("data", (chunk: Buffer) => chunks.push(chunk));
155155
req.on("end", () => {
156156
fetchOptions.body = Buffer.concat(chunks);
157+
this.logRequestBodySummary(fetchOptions.body as Buffer);
157158
this.forwardRequest(targetUrl.toString(), fetchOptions, res);
158159
});
159160
} else {
160161
this.forwardRequest(targetUrl.toString(), fetchOptions, res);
161162
}
162163
}
163164

165+
private logRequestBodySummary(body: Buffer): void {
166+
try {
167+
const json = JSON.parse(body.toString("utf-8"));
168+
const totalBodyBytes = body.byteLength;
169+
170+
// Summarize system prompt
171+
const system = json.system;
172+
let systemSummary: unknown;
173+
if (typeof system === "string") {
174+
systemSummary = {
175+
type: "string",
176+
charLength: system.length,
177+
preview: system.slice(0, 300),
178+
};
179+
} else if (Array.isArray(system)) {
180+
systemSummary = system.map(
181+
(block: Record<string, unknown>, i: number) => {
182+
const text = block.text as string | undefined;
183+
return {
184+
index: i,
185+
type: block.type,
186+
cacheControl: block.cache_control,
187+
charLength: text?.length ?? 0,
188+
preview: text?.slice(0, 200),
189+
tail: (text?.length ?? 0) > 200 ? text?.slice(-100) : undefined,
190+
};
191+
},
192+
);
193+
}
194+
195+
// Summarize messages
196+
const messages = json.messages;
197+
const messageSummary = Array.isArray(messages)
198+
? messages.map((msg: Record<string, unknown>, i: number) => {
199+
const content = msg.content;
200+
if (typeof content === "string") {
201+
return {
202+
index: i,
203+
role: msg.role,
204+
type: "string",
205+
charLength: content.length,
206+
preview: content.slice(0, 200),
207+
};
208+
}
209+
if (Array.isArray(content)) {
210+
return {
211+
index: i,
212+
role: msg.role,
213+
blocks: content.map((block: Record<string, unknown>) => {
214+
const text = (block.text ?? block.content) as
215+
| string
216+
| undefined;
217+
return {
218+
type: block.type,
219+
...(block.name ? { name: block.name } : {}),
220+
...(block.tool_use_id
221+
? { toolUseId: block.tool_use_id }
222+
: {}),
223+
cacheControl: block.cache_control,
224+
charLength: text?.length ?? 0,
225+
...(text ? { preview: text.slice(0, 200) } : {}),
226+
};
227+
}),
228+
};
229+
}
230+
return { index: i, role: msg.role, type: typeof content };
231+
})
232+
: [];
233+
234+
// Summarize tools
235+
const tools = json.tools;
236+
const toolsSummary = Array.isArray(tools)
237+
? {
238+
count: tools.length,
239+
names: tools.map((t: Record<string, unknown>) => t.name),
240+
}
241+
: undefined;
242+
243+
log.info("API request body summary", {
244+
totalBodyBytes,
245+
model: json.model,
246+
maxTokens: json.max_tokens,
247+
systemBlockCount: Array.isArray(system)
248+
? system.length
249+
: system
250+
? 1
251+
: 0,
252+
systemSummary,
253+
messageCount: Array.isArray(messages) ? messages.length : 0,
254+
messageSummary,
255+
toolsSummary,
256+
});
257+
} catch {
258+
// Best-effort logging, don't break the proxy
259+
}
260+
}
261+
164262
private async forwardRequest(
165263
url: string,
166264
options: RequestInit,

packages/agent/src/adapters/claude/claude-agent.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ import {
6767
getEffortOptions,
6868
resolveModelPreference,
6969
supports1MContext,
70-
supportsMcpInjection,
7170
toSdkModelId,
7271
} from "./session/models";
7372
import {
@@ -781,6 +780,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
781780
const meta = params._meta as NewSessionMeta | undefined;
782781
const taskId = meta?.persistence?.taskId;
783782
const effort = meta?.claudeCode?.options?.effort as EffortLevel | undefined;
783+
const agentOverrides = meta?.claudeCode?.options?.agents as
784+
| Record<
785+
string,
786+
{
787+
model?: string;
788+
tools?: string[];
789+
disallowedTools?: string[];
790+
}
791+
>
792+
| undefined;
784793

785794
// We want to create a new session id unless it is resume,
786795
// but not resume + forkSession.
@@ -798,18 +807,23 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
798807
const settingsManager = new SettingsManager(cwd);
799808
await settingsManager.initialize();
800809

801-
const earlyModelId =
802-
settingsManager.getSettings().model || meta?.model || "";
803-
const mcpServers = supportsMcpInjection(earlyModelId)
804-
? parseMcpServers(params)
805-
: {};
810+
const mcpServers = parseMcpServers(params);
806811
const systemPrompt = buildSystemPrompt(meta?.systemPrompt);
807812

808813
this.logger.info(isResume ? "Resuming session" : "Creating new session", {
809814
sessionId,
810815
taskId,
811816
taskRunId: meta?.taskRunId,
812817
cwd,
818+
hasAgentOverrides: !!agentOverrides,
819+
agentOverrideNames: agentOverrides ? Object.keys(agentOverrides) : [],
820+
exploreOverride: agentOverrides?.Explore
821+
? {
822+
model: agentOverrides.Explore.model,
823+
tools: agentOverrides.Explore.tools,
824+
disallowedTools: agentOverrides.Explore.disallowedTools,
825+
}
826+
: undefined,
813827
});
814828

815829
const permissionMode: CodeExecutionMode =

packages/agent/src/adapters/claude/hooks.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,56 @@ export const createPostToolUseHook =
7171
return { continue: true };
7272
};
7373

74+
/**
75+
* Rewrites Agent tool calls targeting built-in subagent types to use our custom
76+
* definitions instead. This works around a Claude Agent SDK bug where
77+
* `options.agents` cannot override built-in agent definitions because the
78+
* built-ins appear first in the agents array and `Array.find()` returns the
79+
* first match.
80+
*
81+
* By giving our custom agent a different name (e.g. "ph-explore") and rewriting
82+
* the subagent_type in the tool input, we sidestep the collision entirely.
83+
*
84+
* https://github.com/anthropics/claude-agent-sdk-typescript/issues/267
85+
*/
86+
const SUBAGENT_REWRITES: Record<string, string> = {
87+
Explore: "ph-explore",
88+
};
89+
90+
export const createSubagentRewriteHook =
91+
(logger: Logger): HookCallback =>
92+
async (input: HookInput, _toolUseID: string | undefined) => {
93+
if (input.hook_event_name !== "PreToolUse") {
94+
return { continue: true };
95+
}
96+
97+
if (input.tool_name !== "Agent") {
98+
return { continue: true };
99+
}
100+
101+
const toolInput = input.tool_input as Record<string, unknown> | undefined;
102+
const subagentType = toolInput?.subagent_type;
103+
if (typeof subagentType !== "string" || !SUBAGENT_REWRITES[subagentType]) {
104+
return { continue: true };
105+
}
106+
107+
const target = SUBAGENT_REWRITES[subagentType];
108+
logger.info(
109+
`[SubagentRewriteHook] Rewriting subagent_type: ${subagentType}${target}`,
110+
);
111+
112+
return {
113+
continue: true,
114+
hookSpecificOutput: {
115+
hookEventName: "PreToolUse" as const,
116+
updatedInput: {
117+
...toolInput,
118+
subagent_type: target,
119+
},
120+
},
121+
};
122+
};
123+
74124
export const createPreToolUseHook =
75125
(settingsManager: SettingsManager, logger: Logger): HookCallback =>
76126
async (input: HookInput, _toolUseID: string | undefined) => {

packages/agent/src/adapters/claude/session/options.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { Logger } from "../../../utils/logger";
1414
import {
1515
createPostToolUseHook,
1616
createPreToolUseHook,
17+
createSubagentRewriteHook,
1718
type OnModeChange,
1819
} from "../hooks";
1920
import type { CodeExecutionMode } from "../tools";
@@ -117,7 +118,10 @@ function buildHooks(
117118
PreToolUse: [
118119
...(userHooks?.PreToolUse || []),
119120
{
120-
hooks: [createPreToolUseHook(settingsManager, logger)],
121+
hooks: [
122+
createPreToolUseHook(settingsManager, logger),
123+
createSubagentRewriteHook(logger),
124+
],
121125
},
122126
],
123127
};

0 commit comments

Comments
 (0)