Skip to content

Commit 4532fb4

Browse files
committed
add respawn tool
1 parent 3ca28a5 commit 4532fb4

File tree

8 files changed

+153
-13
lines changed

8 files changed

+153
-13
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
"@vitest/browser": "^3.0.5",
5555
"chalk": "^5",
5656
"dotenv": "^16",
57-
"eslint-plugin-promise": "^7.2.1",
5857
"playwright": "^1.50.1",
5958
"semver": "^7.7.1",
6059
"source-map-support": "^0.5",
@@ -76,6 +75,7 @@
7675
"eslint-config-prettier": "^9",
7776
"eslint-plugin-import": "^2",
7877
"eslint-plugin-prettier": "^5",
78+
"eslint-plugin-promise": "^7.2.1",
7979
"eslint-plugin-unused-imports": "^4",
8080
"prettier": "^3",
8181
"rimraf": "^5",

pnpm-lock.yaml

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/toolAgent.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,29 @@ async function executeTools(
115115
tools: Tool[],
116116
messages: Message[],
117117
logger: Logger,
118-
): Promise<ToolCallResult> {
118+
): Promise<ToolCallResult & { respawn?: { context: string } }> {
119119
if (toolCalls.length === 0) {
120120
return { sequenceCompleted: false, toolResults: [] };
121121
}
122122

123123
logger.verbose(`Executing ${toolCalls.length} tool calls`);
124124

125+
// Check for respawn tool call
126+
const respawnCall = toolCalls.find(call => call.name === 'respawn');
127+
if (respawnCall) {
128+
return {
129+
sequenceCompleted: false,
130+
toolResults: [{
131+
type: 'tool_result',
132+
tool_use_id: respawnCall.id,
133+
content: 'Respawn initiated',
134+
}],
135+
respawn: {
136+
context: respawnCall.input.respawnContext
137+
}
138+
};
139+
}
140+
125141
const results = await Promise.all(
126142
toolCalls.map(async (call) => {
127143
let toolResult = '';
@@ -242,13 +258,24 @@ export const toolAgent = async (
242258
logger.info(assistantMessage);
243259
}
244260

245-
const { sequenceCompleted, completionResult } = await executeTools(
261+
const { sequenceCompleted, completionResult, respawn } = await executeTools(
246262
toolCalls,
247263
tools,
248264
messages,
249265
logger,
250266
);
251267

268+
if (respawn) {
269+
logger.info('Respawning agent with new context');
270+
// Reset messages to just the new context
271+
messages.length = 0;
272+
messages.push({
273+
role: 'user',
274+
content: [{ type: 'text', text: respawn.context }],
275+
});
276+
continue;
277+
}
278+
252279
if (sequenceCompleted) {
253280
const result = {
254281
result:

src/tools/getTools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { shellStartTool } from './system/shellStart.js';
99
import { shellMessageTool } from './system/shellMessage.js';
1010
import { browseMessageTool } from './browser/browseMessage.js';
1111
import { browseStartTool } from './browser/browseStart.js';
12+
import { respawnTool } from './system/respawn.js';
1213

1314
export function getTools(): Tool[] {
1415
return [
@@ -22,5 +23,6 @@ export function getTools(): Tool[] {
2223
shellMessageTool,
2324
browseStartTool,
2425
browseMessageTool,
26+
respawnTool,
2527
] as Tool[];
2628
}

src/tools/io/updateFile.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import * as fs from 'fs/promises';
1+
import * as fsPromises from 'fs/promises';
2+
import * as fs from 'fs';
23
import * as path from 'path';
34
import { Tool } from '../../core/types.js';
45
import { z } from 'zod';
@@ -47,32 +48,33 @@ export const updateFileTool: Tool<Parameters, ReturnType> = {
4748
const absolutePath = path.resolve(path.normalize(filePath));
4849
logger.verbose(`Updating file: ${absolutePath}`);
4950

50-
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
51+
await fsPromises.mkdir(path.dirname(absolutePath), { recursive: true });
5152

5253
if (operation.command === 'update') {
53-
const content = await fs.readFile(absolutePath, 'utf8');
54+
const content = await fsPromises.readFile(absolutePath, 'utf8');
5455
const occurrences = content.split(operation.oldStr).length - 1;
5556
if (occurrences !== 1) {
5657
throw new Error(
5758
`Found ${occurrences} occurrences of oldStr, expected exactly 1`,
5859
);
5960
}
60-
await fs.writeFile(
61+
await fsPromises.writeFile(
6162
absolutePath,
6263
content.replace(operation.oldStr, operation.newStr),
6364
'utf8',
6465
);
6566
} else if (operation.command === 'append') {
66-
await fs.appendFile(absolutePath, operation.content, 'utf8');
67+
await fsPromises.appendFile(absolutePath, operation.content, 'utf8');
6768
} else {
68-
await fs.writeFile(absolutePath, operation.content, 'utf8');
69+
await fsPromises.writeFile(absolutePath, operation.content, 'utf8');
6970
}
7071

7172
logger.verbose(`Operation complete: ${operation.command}`);
7273
return { path: filePath, operation: operation.command };
7374
},
7475
logParameters: (input, { logger }) => {
75-
logger.info(`Modifying "${input.path}", ${input.description}`);
76+
const isFile = fs.existsSync(input.path);
77+
logger.info(`${isFile ? 'Modifying' : 'Creating' } "${input.path}", ${input.description}`);
7678
},
7779
logReturns: () => {},
7880
};

src/tools/system/respawn.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Tool, ToolContext } from '../../core/types.js';
2+
3+
export interface RespawnInput {
4+
respawnContext: string;
5+
}
6+
7+
export const respawnTool: Tool = {
8+
name: 'respawn',
9+
description: 'Resets the agent context to just the system prompt and provided context',
10+
parameters: {
11+
type: 'object',
12+
properties: {
13+
respawnContext: {
14+
type: 'string',
15+
description: 'The context to keep after respawning',
16+
},
17+
},
18+
required: ['respawnContext'],
19+
additionalProperties: false,
20+
},
21+
returns: {
22+
type: 'string',
23+
description: 'A message indicating that the respawn has been initiated',
24+
},
25+
execute: async (params: Record<string, any>, context: ToolContext): Promise<string> => {
26+
// This is a special case tool - the actual respawn logic is handled in toolAgent
27+
return 'Respawn initiated';
28+
},
29+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { toolAgent } from '../../src/core/toolAgent.js';
3+
import { getTools } from '../../src/tools/getTools.js';
4+
import { Logger } from '../../src/utils/logger.js';
5+
6+
// Mock Anthropic SDK
7+
vi.mock('@anthropic-ai/sdk', () => {
8+
return {
9+
default: vi.fn().mockImplementation(() => ({
10+
messages: {
11+
create: vi.fn()
12+
.mockResolvedValueOnce({
13+
content: [
14+
{
15+
type: 'tool_use',
16+
name: 'respawn',
17+
id: 'test-id',
18+
input: { respawnContext: 'new context' },
19+
},
20+
],
21+
usage: { input_tokens: 10, output_tokens: 10 },
22+
})
23+
.mockResolvedValueOnce({
24+
content: [],
25+
usage: { input_tokens: 5, output_tokens: 5 },
26+
}),
27+
},
28+
})),
29+
};
30+
});
31+
32+
describe('toolAgent respawn functionality', () => {
33+
const mockLogger = new Logger({ name: 'test' });
34+
const tools = getTools();
35+
36+
beforeEach(() => {
37+
process.env.ANTHROPIC_API_KEY = 'test-key';
38+
vi.clearAllMocks();
39+
});
40+
41+
it('should handle respawn tool calls', async () => {
42+
const result = await toolAgent('initial prompt', tools, mockLogger, {
43+
maxIterations: 2, // Need at least 2 iterations for respawn + empty response
44+
model: 'test-model',
45+
maxTokens: 100,
46+
temperature: 0,
47+
getSystemPrompt: () => 'test system prompt',
48+
});
49+
50+
expect(result.result).toBe(
51+
'Agent returned empty message implying it is done its given task',
52+
);
53+
});
54+
});

tests/tools/respawn.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { respawnTool } from '../../src/tools/system/respawn.js';
3+
import { Logger } from '../../src/utils/logger.js';
4+
5+
describe('respawnTool', () => {
6+
const mockLogger = new Logger({ name: 'test' });
7+
8+
it('should have correct name and description', () => {
9+
expect(respawnTool.name).toBe('respawn');
10+
expect(respawnTool.description).toContain('Resets the agent context');
11+
});
12+
13+
it('should have correct parameter schema', () => {
14+
expect(respawnTool.parameters.type).toBe('object');
15+
expect(respawnTool.parameters.properties.respawnContext).toBeDefined();
16+
expect(respawnTool.parameters.required).toContain('respawnContext');
17+
});
18+
19+
it('should execute and return confirmation message', async () => {
20+
const result = await respawnTool.execute(
21+
{ respawnContext: 'new context' },
22+
{ logger: mockLogger },
23+
);
24+
expect(result).toBe('Respawn initiated');
25+
});
26+
});

0 commit comments

Comments
 (0)