Skip to content

Commit 3c0829d

Browse files
authored
feat: enhance sub-agent functionality (#27)
* feat: enhance sub-agent functionality with context memory and error handling * feat: improve agent memory formatting and enhance supervisor message structure * refactor: streamline content handling in getStringContent and improve error message structure * chore: add changeset
1 parent ed15f63 commit 3c0829d

6 files changed

Lines changed: 321 additions & 112 deletions

File tree

.changeset/fuzzy-guests-repair.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@voltagent/core": patch
3+
---
4+
5+
fix: improve sub-agent context sharing for sequential task execution - #30
6+
7+
Enhanced the Agent system to properly handle context sharing between sub-agents, enabling reliable sequential task execution. The changes include:
8+
9+
- Adding `contextMessages` parameter to `getSystemMessage` method
10+
- Refactoring `prepareAgentsMemory` to properly format conversation history
11+
- Ensuring conversation context is correctly passed between delegated tasks
12+
- Enhancing system prompts to better handle sequential workflows
13+
14+
This fixes issues where the second agent in a sequence would not have access to the first agent's output, causing failures in multi-step workflows.

packages/core/src/agent/index.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ type MockModelType = { modelId: string; [key: string]: any };
2727
function getStringContent(content: any): string {
2828
if (typeof content === "string") {
2929
return content;
30-
} else if (Array.isArray(content)) {
30+
}
31+
if (Array.isArray(content)) {
3132
return content
3233
.map((part) => {
3334
if (typeof part === "string") {
3435
return part;
35-
} else if (part && typeof part === "object" && "type" in part) {
36+
}
37+
if (part && typeof part === "object" && "type" in part) {
3638
if (part.type === "text" && "text" in part) {
3739
return part.text;
3840
}

packages/core/src/agent/index.ts

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,11 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
143143
protected async getSystemMessage({
144144
input,
145145
historyEntryId,
146+
contextMessages,
146147
}: {
147148
input?: string | BaseMessage[];
148149
historyEntryId: string;
150+
contextMessages: BaseMessage[];
149151
}): Promise<BaseMessage> {
150152
let description = this.description;
151153

@@ -172,7 +174,7 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
172174

173175
try {
174176
const context = await this.retriever.retrieve(input);
175-
if (context && context.trim()) {
177+
if (context?.trim()) {
176178
description = `${description}\n\nRelevant Context:\n${context}`;
177179

178180
// Update the event
@@ -199,7 +201,16 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
199201

200202
// If the agent has sub-agents, generate supervisor system message
201203
if (this.subAgentManager.hasSubAgents()) {
202-
description = this.subAgentManager.generateSupervisorSystemMessage(description);
204+
// Fetch recent agent history for the sub-agents
205+
const agentsMemory = await this.prepareAgentsMemory(contextMessages);
206+
207+
// Generate the supervisor message with the agents memory inserted
208+
description = this.subAgentManager.generateSupervisorSystemMessage(description, agentsMemory);
209+
210+
return {
211+
role: "system",
212+
content: description,
213+
};
203214
}
204215

205216
return {
@@ -208,6 +219,32 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
208219
};
209220
}
210221

222+
/**
223+
* Prepare agents memory for the supervisor system message
224+
* This fetches and formats recent interactions with sub-agents
225+
*/
226+
private async prepareAgentsMemory(contextMessages: BaseMessage[]): Promise<string> {
227+
try {
228+
// Get all sub-agents
229+
const subAgents = this.subAgentManager.getSubAgents();
230+
if (subAgents.length === 0) return "";
231+
232+
// Format the agent histories into a readable format
233+
const formattedMemory = contextMessages
234+
.filter((p) => p.role !== "system")
235+
.filter((p) => p.role === "assistant" && !p.content.toString().includes("toolCallId"))
236+
.map((message) => {
237+
return `${message.role}: ${message.content}`;
238+
})
239+
.join("\n\n");
240+
241+
return formattedMemory || "No previous agent interactions found.";
242+
} catch (error) {
243+
console.warn("Error preparing agents memory:", error);
244+
return "Error retrieving agent history.";
245+
}
246+
}
247+
211248
/**
212249
* Add input to messages array based on type
213250
*/
@@ -449,7 +486,7 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
449486
};
450487

451488
/**
452-
* Tool event creator
489+
* Fix delete operator usage for better performance
453490
*/
454491
private addToolEvent = async (
455492
context: OperationContext,
@@ -466,19 +503,21 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
466503
...(data.metadata || {}),
467504
};
468505

469-
if (data.toolId) {
470-
metadata.toolId = data.toolId;
471-
delete data.toolId;
506+
// Extract data fields to use while avoiding parameter reassignment
507+
const { toolId, input, output, error, errorMessage } = data;
508+
509+
if (toolId) {
510+
metadata.toolId = toolId;
472511
}
473512

474513
const eventData: Partial<StandardEventData> = {
475514
affectedNodeId: toolNodeId,
476515
status: status as any,
477516
timestamp: new Date().toISOString(),
478-
input: data.input,
479-
output: data.output,
480-
error: data.error,
481-
errorMessage: data.errorMessage,
517+
input,
518+
output,
519+
error,
520+
errorMessage,
482521
metadata,
483522
};
484523

@@ -543,14 +582,16 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
543582
...(data.metadata || {}),
544583
};
545584

546-
if (data.usage) {
547-
metadata.usage = data.usage;
548-
delete data.usage;
585+
// Extract data fields to use while avoiding parameter reassignment
586+
const { usage, ...standardData } = data;
587+
588+
if (usage) {
589+
metadata.usage = usage;
549590
}
550591

551-
// Create new data
552-
const standardData: Partial<StandardEventData> = {
553-
...data,
592+
// Create new data with metadata
593+
const eventData: Partial<StandardEventData> = {
594+
...standardData,
554595
metadata,
555596
};
556597

@@ -560,7 +601,7 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
560601
status,
561602
NodeType.AGENT,
562603
this.id,
563-
standardData,
604+
eventData,
564605
"agent",
565606
context,
566607
);
@@ -603,6 +644,7 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
603644
const systemMessage = await this.getSystemMessage({
604645
input,
605646
historyEntryId: context.historyEntry.id,
647+
contextMessages,
606648
});
607649

608650
// Combine messages
@@ -783,6 +825,7 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
783825
const systemMessage = await this.getSystemMessage({
784826
input,
785827
historyEntryId: context.historyEntry.id,
828+
contextMessages,
786829
});
787830

788831
// Combine messages
@@ -988,6 +1031,7 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
9881031
const systemMessage = await this.getSystemMessage({
9891032
input,
9901033
historyEntryId: context.historyEntry.id,
1034+
contextMessages,
9911035
});
9921036

9931037
// Combine messages
@@ -1107,6 +1151,7 @@ export class Agent<TProvider extends { llm: LLMProvider<any> }> {
11071151
const systemMessage = await this.getSystemMessage({
11081152
input,
11091153
historyEntryId: context.historyEntry.id,
1154+
contextMessages,
11101155
});
11111156

11121157
// Combine messages

packages/core/src/agent/subagent/index.spec.ts

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { SubAgentManager } from ".";
1+
import { SubAgentManager } from "./index";
2+
import type { Agent } from "../index";
23
import type { AgentHandoffOptions } from "../types";
34

45
// Creating a Mock Agent class
@@ -109,24 +110,36 @@ describe("SubAgentManager", () => {
109110
});
110111

111112
describe("generateSupervisorSystemMessage", () => {
112-
it("should return original description when no sub-agents", () => {
113+
it("should return the original description if no sub-agents", () => {
114+
const subAgentManager = new SubAgentManager("TestAgent");
113115
const description = "Original description";
116+
117+
// Call with empty agentsMemory string (default parameter)
114118
expect(subAgentManager.generateSupervisorSystemMessage(description)).toBe(description);
115119
});
116120

117-
it("should return supervisor message when has sub-agents", () => {
118-
subAgentManager.addSubAgent(mockAgent1);
119-
subAgentManager.addSubAgent(mockAgent2);
120-
121+
it("should generate a supervisor message when sub-agents exist", () => {
122+
const subAgentAgent1 = {
123+
id: "agent1",
124+
name: "Agent 1",
125+
description: "First agent",
126+
} as Agent<any>;
127+
const subAgentAgent2 = {
128+
id: "agent2",
129+
name: "Agent 2",
130+
description: "Second agent",
131+
} as Agent<any>;
132+
133+
const subAgentManager = new SubAgentManager("TestAgent", [subAgentAgent1, subAgentAgent2]);
121134
const description = "Original description";
135+
136+
// Call with empty agentsMemory string (default parameter)
122137
const result = subAgentManager.generateSupervisorSystemMessage(description);
123138

124-
expect(result).toContain("supervisor agent");
125-
expect(result).toContain("Math Agent");
126-
expect(result).toContain("Writing Agent");
127-
expect(result).toContain(
128-
"Provide a final answer to the User when you have a response from all agents.",
129-
);
139+
expect(result).toContain("You are a supervisor agent");
140+
expect(result).toContain("Agent 1: First agent");
141+
expect(result).toContain("Agent 2: Second agent");
142+
expect(result).toContain("<agents_memory>");
130143
});
131144
});
132145

@@ -199,7 +212,10 @@ describe("SubAgentManager", () => {
199212
targetAgents: ["non-existent-agent"],
200213
context: {},
201214
}),
202-
).rejects.toThrow("No valid target agents found");
215+
).resolves.toMatchObject({
216+
error: "Failed to delegate task: No valid target agents found. Available agents: ",
217+
status: "error",
218+
});
203219
});
204220

205221
it("should execute and return results when valid agents exist", async () => {
@@ -209,8 +225,20 @@ describe("SubAgentManager", () => {
209225
const handoffToMultipleSpy = jest
210226
.spyOn(subAgentManager, "handoffToMultiple")
211227
.mockResolvedValue([
212-
{ result: "Result 1", conversationId: "conv1", messages: [] },
213-
{ result: "Result 2", conversationId: "conv2", messages: [] },
228+
{
229+
result: "Result 1",
230+
conversationId: "conv1",
231+
messages: [],
232+
error: undefined,
233+
status: "success",
234+
},
235+
{
236+
result: "Result 2",
237+
conversationId: "conv2",
238+
messages: [],
239+
error: undefined,
240+
status: "success",
241+
},
214242
]);
215243

216244
const tool = subAgentManager.createDelegateTool();
@@ -223,8 +251,20 @@ describe("SubAgentManager", () => {
223251

224252
expect(handoffToMultipleSpy).toHaveBeenCalled();
225253
expect(result).toEqual([
226-
{ agentName: "Math Agent", response: "Result 1", conversationId: "conv1" },
227-
{ agentName: "Writing Agent", response: "Result 2", conversationId: "conv2" },
254+
{
255+
agentName: "Math Agent",
256+
response: "Result 1",
257+
conversationId: "conv1",
258+
error: undefined,
259+
status: "success",
260+
},
261+
{
262+
agentName: "Writing Agent",
263+
response: "Result 2",
264+
conversationId: "conv2",
265+
error: undefined,
266+
status: "success",
267+
},
228268
]);
229269
});
230270
});

0 commit comments

Comments
 (0)