Skip to content

Commit 569267a

Browse files
committed
fix: recover from agent transfer errors with retry and error context
Move try-catch inside the runner's retry loop so exceptions (e.g. ADK's JSON.parse failures during transfers) trigger retries instead of killing the session. Add serializeError helper for non-standard error objects, yield transfer chunks to the UI, and send error context in retry messages so the planning agent can choose fallback agents. Add transfer discipline to all sub-agent prompts and orchestration guidance to the planning agent.
1 parent 7cbe08d commit 569267a

File tree

9 files changed

+230
-50
lines changed

9 files changed

+230
-50
lines changed

src/agents/chart-generator.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { z } from 'zod';
2525
import { parseChartArgs } from '../charts/index.js';
2626
import { getAdkModelName, getAgentPrompt, loadSettings } from '../config/index.js';
2727
import { saveMemoriesOnFinalResponse } from '../memory/callbacks.js';
28+
import { TRANSFER_BACK_INSTRUCTION } from './types.js';
2829

2930
const DEFAULT_INSTRUCTION = `You are a Chart Generator Agent specializing in terminal-based data visualizations.
3031
@@ -107,7 +108,8 @@ Use these color names: "red", "green", "blue", "yellow", "cyan", "magenta", "whi
107108
- Choose the most appropriate chart type for the data
108109
- Use descriptive titles that explain what the chart shows
109110
- Use colors to distinguish different data series or categories
110-
- Keep labels concise but meaningful`;
111+
- Keep labels concise but meaningful
112+
${TRANSFER_BACK_INSTRUCTION}`;
111113

112114
/**
113115
* Google ADK FunctionTool for chart rendering.

src/agents/code-executor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { getAdkModelName, getAgentPrompt, loadSettings } from '../config/index.j
2121
import { saveMemoriesOnFinalResponse } from '../memory/callbacks.js';
2222
import { executeCodeAdkTool } from '../tools/adk-tools.js';
2323
import { executeCode } from '../tools/code-execution.js';
24+
import { TRANSFER_BACK_INSTRUCTION } from './types.js';
2425

2526
// Re-export executeCode for backward compatibility
2627
export { executeCode };
@@ -67,7 +68,8 @@ Python standard library including:
6768
### CONSTRAINTS
6869
- NEVER execute code that could be harmful
6970
- NEVER attempt file system operations outside the sandbox
70-
- ALWAYS use print() to output results`;
71+
- ALWAYS use print() to output results
72+
${TRANSFER_BACK_INSTRUCTION}`;
7173

7274
// Load settings with fallback
7375
let settings: AppSettings | null;

src/agents/generic.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { LlmAgent } from '@google/adk';
1818
import type { AppSettings } from '../config/index.js';
1919
import { getAdkModelName, getAgentPrompt, loadSettings } from '../config/index.js';
2020
import { saveMemoriesOnFinalResponse } from '../memory/callbacks.js';
21+
import { TRANSFER_BACK_INSTRUCTION } from './types.js';
2122

2223
const DEFAULT_INSTRUCTION = `You are the Generic Executor Agent, handling knowledge tasks.
2324
@@ -41,8 +42,8 @@ You handle general-purpose tasks. You are the "knowledge worker" for text-based
4142
4243
### CONSTRAINTS
4344
- ALWAYS provide helpful, accurate responses
44-
- ALWAYS transfer your result to your parent agent upon completion
45-
- If asked to do something outside your capabilities, clearly state what agent should be used instead`;
45+
- If asked to do something outside your capabilities, clearly state what agent should be used instead
46+
${TRANSFER_BACK_INSTRUCTION}`;
4647

4748
// Load settings with fallback
4849
let settings: AppSettings | null;

src/agents/mcp.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { getAdkModelName, getAgentPrompt, loadSettings } from '../config/index.j
2121
import { getMcpManager } from '../mcp/index.js';
2222
import { saveMemoriesOnFinalResponse } from '../memory/callbacks.js';
2323
import { createMcpAdkTools } from '../tools/mcp-adk-adapter.js';
24+
import { TRANSFER_BACK_INSTRUCTION } from './types.js';
2425

2526
const DEFAULT_INSTRUCTION = `You are an MCP tools specialist. You MUST use the tools provided to you.
2627
@@ -49,7 +50,10 @@ After calling tools and getting results, format your response as:
4950
[Summarize what you found from the tool calls]
5051
5152
## Status
52-
Success / Partial / Could Not Complete`;
53+
Success / Partial / Could Not Complete
54+
55+
### CONSTRAINTS
56+
${TRANSFER_BACK_INSTRUCTION}`;
5357

5458
// Load settings with fallback
5559
let settings: AppSettings | null;

src/agents/planning.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,21 @@ PLAN:
6767
2. [Task] → [agent_name]
6868
\`\`\`
6969
70-
**STEP 2: EXECUTE ONE STEP AT A TIME**
70+
**STEP 2: EXECUTE**
7171
- Delegate to the agent for step 1
7272
- Wait for response
7373
- Check if successful
74+
- For sequential multi-agent tasks, you can instruct an agent to transfer directly to the next agent:
75+
Example: "Research this topic, then transfer to code_executor_agent to analyze the data"
76+
- For simpler tasks, just delegate and the agent will return results to you.
7477
7578
**STEP 3: HANDLE FAILURES IMMEDIATELY**
7679
If an agent returns error or no useful result:
7780
→ IMMEDIATELY try the fallback agent. Do NOT retry the same agent.
81+
If you receive a message about a previous error:
82+
- Analyze the error to understand what failed
83+
- Choose a different agent or approach
84+
- Do NOT retry the same agent that failed
7885
7986
**STEP 4: SYNTHESIZE AND RETURN**
8087
When all steps complete, combine results and transfer to parent.
@@ -91,7 +98,8 @@ When the user request is missing details:
9198
- NEVER delegate without stating which step you're on
9299
- NEVER retry a failed agent—use the fallback instead
93100
- NEVER call tools directly—you have no tools
94-
- ALWAYS transfer final result to parent agent when done`;
101+
- ALWAYS transfer final result to parent agent when done
102+
- Sub-agents transfer back to you by default. You can chain agents by telling a sub-agent to transfer to another specific agent upon completion.`;
95103

96104
// Load settings with fallback
97105
let settings: AppSettings | null;

src/agents/research.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { AppSettings } from '../config/index.js';
1818
import { getAdkModelName, getAgentPrompt, loadSettings } from '../config/index.js';
1919
import { saveMemoriesOnFinalResponse } from '../memory/callbacks.js';
2020
import { braveSearchAdkTool, readWebpageAdkTool } from '../tools/adk-tools.js';
21+
import { TRANSFER_BACK_INSTRUCTION } from './types.js';
2122

2223
const DEFAULT_INSTRUCTION = `You are the Research Specialist, an expert in gathering comprehensive information from the web.
2324
@@ -65,7 +66,8 @@ Structure your research report as:
6566
- NEVER fabricate information or URLs.
6667
- NEVER present speculation as fact.
6768
- ALWAYS cite sources for factual claims.
68-
- Maximum 5 page reads per research task.`;
69+
- Maximum 5 page reads per research task.
70+
${TRANSFER_BACK_INSTRUCTION}`;
6971

7072
// Load settings with fallback
7173
let settings: AppSettings | null;

src/agents/runner.ts

Lines changed: 67 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@ import type { AgentStreamChunk } from './types.js';
2424

2525
const APP_NAME = 'Solenoid';
2626

27+
/**
28+
* Serialize any error value into a readable string.
29+
* ADK sometimes throws non-standard error objects (e.g. `{}`) that lose
30+
* information with naive stringification.
31+
*/
32+
function serializeError(error: unknown): string {
33+
if (error instanceof Error) return error.message || error.constructor.name;
34+
if (typeof error === 'string') return error;
35+
try {
36+
const str = String(error);
37+
if (str !== '[object Object]') return str;
38+
} catch {
39+
// fall through
40+
}
41+
try {
42+
const json = JSON.stringify(error);
43+
if (json && json !== '{}') return json;
44+
} catch {
45+
// fall through
46+
}
47+
return 'Unknown error (non-serializable)';
48+
}
49+
2750
/**
2851
* Debug: Log the agent hierarchy
2952
*/
@@ -101,24 +124,34 @@ export async function* runAgent(
101124
return lastErrorMessage ?? lastErrorCode ?? 'empty response';
102125
}
103126

104-
try {
105-
while (attempt <= MAX_RETRIES && !gotFinalContent) {
106-
if (attempt > 0) {
107-
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt - 1); // 1s, 2s, 4s, 8s, 16s
108-
const reason = retryReason();
109-
agentLogger.info(
110-
`[Runner] Retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_RETRIES + 1}): ${reason}`
111-
);
112-
yield {
113-
type: 'status',
114-
content: `Retrying (${attempt}/${MAX_RETRIES}): ${reason}`,
115-
};
116-
await new Promise((resolve) => setTimeout(resolve, delayMs));
117-
}
127+
while (attempt <= MAX_RETRIES && !gotFinalContent) {
128+
if (attempt > 0) {
129+
const delayMs = BASE_DELAY_MS * Math.pow(2, attempt - 1); // 1s, 2s, 4s, 8s, 16s
130+
const reason = retryReason();
131+
agentLogger.info(
132+
`[Runner] Retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_RETRIES + 1}): ${reason}`
133+
);
134+
yield {
135+
type: 'status',
136+
content: `Retrying (${attempt}/${MAX_RETRIES}): ${reason}`,
137+
};
138+
await new Promise((resolve) => setTimeout(resolve, delayMs));
139+
}
118140

119-
const message =
120-
attempt === 0 ? userMessage : createUserContent('Please continue with your response.');
141+
let message: Content;
142+
if (attempt === 0) {
143+
message = userMessage;
144+
} else if (lastErrorMessage && !lastErrorCode) {
145+
// Exception occurred (no ADK error code) — give the model error context
146+
message = createUserContent(
147+
`The previous attempt encountered an error: ${lastErrorMessage}. Please try an alternative approach or a different agent.`
148+
);
149+
} else {
150+
// Empty response or ADK error code — nudge the model to continue
151+
message = createUserContent('Please continue with your response.');
152+
}
121153

154+
try {
122155
let eventIndex = 0;
123156
for await (const event of runner.runAsync({
124157
userId: 'default_user',
@@ -132,9 +165,10 @@ export async function* runAgent(
132165
);
133166

134167
if (event.actions?.transferToAgent) {
135-
agentLogger.debug(
136-
`[Runner] *** TRANSFER DETECTED: ${event.author} -> ${event.actions.transferToAgent} ***`
168+
agentLogger.info(
169+
`[Runner] *** TRANSFER: ${event.author} -> ${event.actions.transferToAgent} ***`
137170
);
171+
yield { type: 'transfer', transferTo: event.actions.transferToAgent };
138172
}
139173

140174
const partTypes =
@@ -199,26 +233,27 @@ export async function* runAgent(
199233
break;
200234
}
201235
}
202-
203-
if (!gotFinalContent) {
204-
attempt++;
205-
}
236+
} catch (error) {
237+
const serialized = serializeError(error);
238+
lastErrorMessage = serialized;
239+
lastErrorCode = undefined;
240+
agentLogger.error(
241+
{ errorType: error?.constructor?.name, attempt: attempt + 1, message: serialized },
242+
'[Runner] Exception during runAsync — will retry'
243+
);
206244
}
207245

208246
if (!gotFinalContent) {
209-
const reason = retryReason();
210-
agentLogger.error(`[Runner] All ${MAX_RETRIES + 1} attempts exhausted — ${reason}`);
211-
yield {
212-
type: 'text',
213-
content: `The model failed after ${MAX_RETRIES + 1} attempts: ${reason}`,
214-
};
215-
yield { type: 'done' };
247+
attempt++;
216248
}
217-
} catch (error) {
218-
agentLogger.error({ error }, '[Runner] Error during agent execution');
249+
}
250+
251+
if (!gotFinalContent) {
252+
const reason = retryReason();
253+
agentLogger.error(`[Runner] All ${MAX_RETRIES + 1} attempts exhausted — ${reason}`);
219254
yield {
220255
type: 'text',
221-
content: `Error: ${error instanceof Error ? error.message : String(error)}`,
256+
content: `The model failed after ${MAX_RETRIES + 1} attempts: ${reason}`,
222257
};
223258
yield { type: 'done' };
224259
}

src/agents/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,12 @@ export interface SessionState {
6969
export interface AgentRunner {
7070
run(input: string, sessionId?: string): AsyncGenerator<AgentStreamChunk, void, unknown>;
7171
}
72+
73+
/**
74+
* Shared instruction block appended to every sub-agent's CONSTRAINTS section.
75+
* Ensures sub-agents transfer results back to the planning agent by default,
76+
* while allowing explicit chaining overrides from the planner.
77+
*/
78+
export const TRANSFER_BACK_INSTRUCTION = `- When your task is complete, transfer back to planning_agent with your results.
79+
- EXCEPTION: If the planning agent explicitly told you to transfer to a specific agent next, follow that instruction.
80+
- Do NOT independently decide to transfer to sibling agents — let the planning agent orchestrate the sequence.`;

0 commit comments

Comments
 (0)