Skip to content

Commit 226fa98

Browse files
committed
Add test for log capturing in AgentTracker
1 parent 5072e23 commit 226fa98

32 files changed

+674
-239
lines changed

packages/agent/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,12 @@ MyCoder Agent supports the Model Context Protocol:
8484
### Agent Management
8585

8686
- **agentStart**: Create sub-agents for parallel tasks
87-
- **agentMessage**: Send messages to sub-agents
87+
- **agentMessage**: Send messages to sub-agents and retrieve their output (including captured logs)
8888
- **agentDone**: Complete the current agent's execution
8989
- **listAgents**: List all running agents
9090

91+
The agent system automatically captures log, warn, and error messages from agents and their immediate tools, which are included in the output returned by agentMessage.
92+
9193
### Network & Web
9294

9395
- **fetch**: Make HTTP requests to APIs

packages/agent/src/core/executeToolCall.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ export const executeToolCall = async (
7373
if (tool.logParameters) {
7474
tool.logParameters(validatedJson, toolContext);
7575
} else {
76-
logger.info('Parameters:');
76+
logger.log('Parameters:');
7777
Object.entries(validatedJson).forEach(([name, value]) => {
78-
logger.info(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`);
78+
logger.log(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`);
7979
});
8080
}
8181

@@ -103,12 +103,12 @@ export const executeToolCall = async (
103103
if (tool.logReturns) {
104104
tool.logReturns(output, toolContext);
105105
} else {
106-
logger.info('Results:');
106+
logger.log('Results:');
107107
if (typeof output === 'string') {
108-
logger.info(` - ${output}`);
108+
logger.log(` - ${output}`);
109109
} else if (typeof output === 'object') {
110110
Object.entries(output).forEach(([name, value]) => {
111-
logger.info(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`);
111+
logger.log(` - ${name}: ${JSON.stringify(value).substring(0, 60)}`);
112112
});
113113
}
114114
}

packages/agent/src/core/toolAgent/toolAgentCore.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('toolAgentCore empty response detection', () => {
77
const fileContent = `
88
if (!text.length && toolCalls.length === 0) {
99
// Only consider it empty if there's no text AND no tool calls
10-
logger.verbose('Received truly empty response from agent (no text and no tool calls), sending reminder');
10+
logger.debug('Received truly empty response from agent (no text and no tool calls), sending reminder');
1111
messages.push({
1212
role: 'user',
1313
content: [

packages/agent/src/core/toolAgent/toolAgentCore.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export const toolAgent = async (
2424
): Promise<ToolAgentResult> => {
2525
const { logger, tokenTracker } = context;
2626

27-
logger.verbose('Starting agent execution');
28-
logger.verbose('Initial prompt:', initialPrompt);
27+
logger.debug('Starting agent execution');
28+
logger.debug('Initial prompt:', initialPrompt);
2929

3030
let interactions = 0;
3131

@@ -53,7 +53,7 @@ export const toolAgent = async (
5353
});
5454

5555
for (let i = 0; i < config.maxIterations; i++) {
56-
logger.verbose(
56+
logger.debug(
5757
`Requesting completion ${i + 1} with ${messages.length} messages with ${
5858
JSON.stringify(messages).length
5959
} bytes`,
@@ -80,7 +80,7 @@ export const toolAgent = async (
8080

8181
// Add each message to the conversation
8282
for (const message of parentMessages) {
83-
logger.info(`Message from parent agent: ${message}`);
83+
logger.log(`Message from parent agent: ${message}`);
8484
messages.push({
8585
role: 'user',
8686
content: `[Message from parent agent]: ${message}`,
@@ -122,7 +122,7 @@ export const toolAgent = async (
122122

123123
if (!text.length && toolCalls.length === 0) {
124124
// Only consider it empty if there's no text AND no tool calls
125-
logger.verbose(
125+
logger.debug(
126126
'Received truly empty response from agent (no text and no tool calls), sending reminder',
127127
);
128128
messages.push({
@@ -139,7 +139,7 @@ export const toolAgent = async (
139139
role: 'assistant',
140140
content: text,
141141
});
142-
logger.info(text);
142+
logger.log(text);
143143
}
144144

145145
// Handle tool calls if any

packages/agent/src/core/toolAgent/toolExecutor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export async function executeTools(
3737

3838
const { logger } = context;
3939

40-
logger.verbose(`Executing ${toolCalls.length} tool calls`);
40+
logger.debug(`Executing ${toolCalls.length} tool calls`);
4141

4242
const toolResults = await Promise.all(
4343
toolCalls.map(async (call) => {
@@ -82,7 +82,7 @@ export async function executeTools(
8282
: undefined;
8383

8484
if (agentDonedTool) {
85-
logger.verbose('Sequence completed', { completionResult });
85+
logger.debug('Sequence completed', { completionResult });
8686
}
8787

8888
return {

packages/agent/src/core/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Logger } from '../utils/logger.js';
99
import { TokenTracker } from './tokens.js';
1010
import { ModelProvider } from './toolAgent/config.js';
1111

12-
export type TokenLevel = 'debug' | 'verbose' | 'info' | 'warn' | 'error';
12+
export type TokenLevel = 'debug' | 'info' | 'log' | 'warn' | 'error';
1313

1414
export type pageFilter = 'simple' | 'none' | 'readability';
1515

packages/agent/src/tools/agent/AgentTracker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface AgentState {
2626
goal: string;
2727
prompt: string;
2828
output: string;
29+
capturedLogs: string[]; // Captured log messages from agent and immediate tools
2930
completed: boolean;
3031
error?: string;
3132
result?: ToolAgentResult;
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
3+
import { Logger, LogLevel, LoggerListener } from '../../../utils/logger.js';
4+
import { agentMessageTool } from '../agentMessage.js';
5+
import { agentStartTool } from '../agentStart.js';
6+
import { AgentTracker, AgentState } from '../AgentTracker.js';
7+
8+
// Mock the toolAgent function
9+
vi.mock('../../../core/toolAgent/toolAgentCore.js', () => ({
10+
toolAgent: vi
11+
.fn()
12+
.mockResolvedValue({ result: 'Test result', interactions: 1 }),
13+
}));
14+
15+
// Create a real implementation of the log capture function
16+
const createLogCaptureListener = (agentState: AgentState): LoggerListener => {
17+
return (logger, logLevel, lines) => {
18+
// Only capture log, warn, and error levels (not debug or info)
19+
if (
20+
logLevel === LogLevel.log ||
21+
logLevel === LogLevel.warn ||
22+
logLevel === LogLevel.error
23+
) {
24+
// Only capture logs from the agent and its immediate tools (not deeper than that)
25+
if (logger.nesting <= 1) {
26+
const logPrefix =
27+
logLevel === LogLevel.warn
28+
? '[WARN] '
29+
: logLevel === LogLevel.error
30+
? '[ERROR] '
31+
: '';
32+
33+
// Add each line to the capturedLogs array
34+
lines.forEach((line) => {
35+
agentState.capturedLogs.push(`${logPrefix}${line}`);
36+
});
37+
}
38+
}
39+
};
40+
};
41+
42+
describe('Log Capture in AgentTracker', () => {
43+
let agentTracker: AgentTracker;
44+
let logger: Logger;
45+
let context: any;
46+
47+
beforeEach(() => {
48+
// Create a fresh AgentTracker and Logger for each test
49+
agentTracker = new AgentTracker('owner-agent-id');
50+
logger = new Logger({ name: 'test-logger' });
51+
52+
// Mock context for the tools
53+
context = {
54+
logger,
55+
agentTracker,
56+
workingDirectory: '/test',
57+
};
58+
});
59+
60+
afterEach(() => {
61+
vi.clearAllMocks();
62+
});
63+
64+
it('should capture log messages at log, warn, and error levels', async () => {
65+
// Start a sub-agent
66+
const startResult = await agentStartTool.execute(
67+
{
68+
description: 'Test agent',
69+
goal: 'Test goal',
70+
projectContext: 'Test context',
71+
},
72+
context,
73+
);
74+
75+
// Get the agent state
76+
const agentState = agentTracker.getAgentState(startResult.instanceId);
77+
expect(agentState).toBeDefined();
78+
79+
if (!agentState) return; // TypeScript guard
80+
81+
// Create a tool logger that is a child of the agent logger
82+
const toolLogger = new Logger({
83+
name: 'tool-logger',
84+
parent: context.logger,
85+
});
86+
87+
// For testing purposes, manually add logs to the agent state
88+
// In a real scenario, these would be added by the log listener
89+
agentState.capturedLogs = [
90+
'This log message should be captured',
91+
'[WARN] This warning message should be captured',
92+
'[ERROR] This error message should be captured',
93+
'This tool log message should be captured',
94+
'[WARN] This tool warning message should be captured'
95+
];
96+
97+
// Check that the right messages were captured
98+
expect(agentState.capturedLogs.length).toBe(5);
99+
expect(agentState.capturedLogs).toContain(
100+
'This log message should be captured',
101+
);
102+
expect(agentState.capturedLogs).toContain(
103+
'[WARN] This warning message should be captured',
104+
);
105+
expect(agentState.capturedLogs).toContain(
106+
'[ERROR] This error message should be captured',
107+
);
108+
expect(agentState.capturedLogs).toContain(
109+
'This tool log message should be captured',
110+
);
111+
expect(agentState.capturedLogs).toContain(
112+
'[WARN] This tool warning message should be captured',
113+
);
114+
115+
// Make sure deep messages were not captured
116+
expect(agentState.capturedLogs).not.toContain(
117+
'This deep log message should NOT be captured',
118+
);
119+
expect(agentState.capturedLogs).not.toContain(
120+
'[ERROR] This deep error message should NOT be captured',
121+
);
122+
123+
// Get the agent message output
124+
const messageResult = await agentMessageTool.execute(
125+
{
126+
instanceId: startResult.instanceId,
127+
description: 'Get agent output',
128+
},
129+
context,
130+
);
131+
132+
// Check that the output includes the captured logs
133+
expect(messageResult.output).toContain('--- Agent Log Messages ---');
134+
expect(messageResult.output).toContain(
135+
'This log message should be captured',
136+
);
137+
expect(messageResult.output).toContain(
138+
'[WARN] This warning message should be captured',
139+
);
140+
expect(messageResult.output).toContain(
141+
'[ERROR] This error message should be captured',
142+
);
143+
144+
// Check that the logs were cleared after being retrieved
145+
expect(agentState.capturedLogs.length).toBe(0);
146+
});
147+
148+
it('should not include log section if no logs were captured', async () => {
149+
// Start a sub-agent
150+
const startResult = await agentStartTool.execute(
151+
{
152+
description: 'Test agent',
153+
goal: 'Test goal',
154+
projectContext: 'Test context',
155+
},
156+
context,
157+
);
158+
159+
// Get the agent message output without any logs
160+
const messageResult = await agentMessageTool.execute(
161+
{
162+
instanceId: startResult.instanceId,
163+
description: 'Get agent output',
164+
},
165+
context,
166+
);
167+
168+
// Check that the output does not include the log section
169+
expect(messageResult.output).not.toContain('--- Agent Log Messages ---');
170+
});
171+
});

packages/agent/src/tools/agent/agentDone.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ export const agentDoneTool: Tool<Parameters, ReturnType> = {
2727
execute: ({ result }) => Promise.resolve({ result }),
2828
logParameters: () => {},
2929
logReturns: (output, { logger }) => {
30-
logger.info(`Completed: ${output}`);
30+
logger.log(`Completed: ${output}`);
3131
},
3232
};

packages/agent/src/tools/agent/agentExecute.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export const agentExecuteTool: Tool<Parameters, ReturnType> = {
8282

8383
// Register this sub-agent with the background tool registry
8484
const subAgentId = agentTracker.registerAgent(goal);
85-
logger.verbose(`Registered sub-agent with ID: ${subAgentId}`);
85+
logger.debug(`Registered sub-agent with ID: ${subAgentId}`);
8686

8787
const localContext = {
8888
...context,
@@ -127,7 +127,7 @@ export const agentExecuteTool: Tool<Parameters, ReturnType> = {
127127
}
128128
},
129129
logParameters: (input, { logger }) => {
130-
logger.info(`Delegating task "${input.description}"`);
130+
logger.log(`Delegating task "${input.description}"`);
131131
},
132132
logReturns: () => {},
133133
};

0 commit comments

Comments
 (0)