@townco/agent 0.1.28 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-server/adapter.d.ts +21 -14
- package/dist/acp-server/adapter.js +15 -3
- package/dist/acp-server/cli.d.ts +1 -3
- package/dist/acp-server/cli.js +5 -9
- package/dist/acp-server/http.d.ts +1 -3
- package/dist/acp-server/http.js +3 -3
- package/dist/bin.js +0 -0
- package/dist/index.js +8 -0
- package/dist/runner/agent-runner.d.ts +1 -0
- package/dist/runner/index.d.ts +1 -3
- package/dist/runner/index.js +14 -18
- package/dist/runner/langchain/index.d.ts +2 -2
- package/dist/runner/langchain/index.js +66 -7
- package/dist/runner/langchain/tools/subagent.d.ts +43 -0
- package/dist/runner/langchain/tools/subagent.js +278 -0
- package/dist/runner/langchain/tools/todo.d.ts +33 -48
- package/dist/runner/langchain/tools/todo.js +2 -1
- package/dist/runner/langchain/tools/web_search.d.ts +3 -0
- package/dist/runner/langchain/tools/web_search.js +55 -13
- package/dist/scaffold/templates/dot-claude/CLAUDE-append.md +73 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/index.ts +8 -0
- package/package.json +8 -9
- package/dist/definition/mcp.d.ts +0 -0
- package/dist/definition/mcp.js +0 -0
- package/dist/definition/tools/todo.d.ts +0 -49
- package/dist/definition/tools/todo.js +0 -80
- package/dist/definition/tools/web_search.d.ts +0 -4
- package/dist/definition/tools/web_search.js +0 -26
- package/dist/dev-agent/index.d.ts +0 -2
- package/dist/dev-agent/index.js +0 -18
- package/dist/example.d.ts +0 -2
- package/dist/example.js +0 -19
- package/dist/utils/logger.d.ts +0 -39
- package/dist/utils/logger.js +0 -175
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { Readable, Writable } from "node:stream";
|
|
5
|
+
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { SUBAGENT_MODE_KEY } from "../../../acp-server/adapter.js";
|
|
8
|
+
/**
|
|
9
|
+
* Name of the Task tool created by makeSubagentsTool
|
|
10
|
+
*/
|
|
11
|
+
export const TASK_TOOL_NAME = "Task";
|
|
12
|
+
/**
|
|
13
|
+
* Creates a DirectTool that delegates work to one of multiple configured subagents.
|
|
14
|
+
*
|
|
15
|
+
* @param configs - Array of subagent configurations
|
|
16
|
+
* @returns A DirectTool named "Task" that can route to any configured subagent
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { makeSubagentsTool } from "@townco/agent/utils";
|
|
21
|
+
*
|
|
22
|
+
* const agent: AgentDefinition = {
|
|
23
|
+
* model: "claude-sonnet-4-5-20250929",
|
|
24
|
+
* systemPrompt: "You are a coordinator.",
|
|
25
|
+
* tools: [
|
|
26
|
+
* makeSubagentsTool([
|
|
27
|
+
* { agentName: "researcher", description: "Use this agent to research topics", cwd: "/path/to/agents" },
|
|
28
|
+
* { agentName: "writer", description: "Use this agent to write content", path: "/absolute/path/to/writer/index.ts" },
|
|
29
|
+
* ]),
|
|
30
|
+
* ]
|
|
31
|
+
* };
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function makeSubagentsTool(configs) {
|
|
35
|
+
if (configs.length === 0) {
|
|
36
|
+
throw new Error("makeSubagentsTool requires at least one subagent configuration");
|
|
37
|
+
}
|
|
38
|
+
// Build a map from agentName to resolved paths
|
|
39
|
+
const agentMap = new Map();
|
|
40
|
+
for (const config of configs) {
|
|
41
|
+
const { agentName } = config;
|
|
42
|
+
let agentPath;
|
|
43
|
+
let agentDir;
|
|
44
|
+
if ("path" in config) {
|
|
45
|
+
// Direct path variant
|
|
46
|
+
agentPath = config.path;
|
|
47
|
+
agentDir = path.dirname(agentPath);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Agent name + cwd variant
|
|
51
|
+
const { cwd: workingDirectory } = config;
|
|
52
|
+
const resolvedWorkingDirectory = workingDirectory ?? path.resolve("agents", agentName, "..", "..");
|
|
53
|
+
agentDir = path.join(resolvedWorkingDirectory, "agents", agentName);
|
|
54
|
+
agentPath = path.join(agentDir, "index.ts");
|
|
55
|
+
}
|
|
56
|
+
agentMap.set(agentName, { agentPath, agentDir });
|
|
57
|
+
}
|
|
58
|
+
// Build the tool description with all subagent descriptions
|
|
59
|
+
const agentDescriptions = configs
|
|
60
|
+
.map((config) => `"${config.agentName}": ${config.description}`)
|
|
61
|
+
.join("\n");
|
|
62
|
+
const agentNames = configs.map((c) => c.agentName);
|
|
63
|
+
return {
|
|
64
|
+
type: "direct",
|
|
65
|
+
name: TASK_TOOL_NAME,
|
|
66
|
+
description: `Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
67
|
+
|
|
68
|
+
The Task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
|
|
69
|
+
|
|
70
|
+
Available agent types and the tools they have access to:
|
|
71
|
+
${agentDescriptions}
|
|
72
|
+
|
|
73
|
+
When NOT to use the Task tool:
|
|
74
|
+
- If you want to read a specific file path, use the Read or Glob tool instead of the Task tool, to find the match more quickly
|
|
75
|
+
- If you are searching for a specific class definition like "class Foo", use the Glob tool instead, to find the match more quickly
|
|
76
|
+
- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Task tool, to find the match more quickly
|
|
77
|
+
- Other tasks that are not related to the agent descriptions above
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
Usage notes:
|
|
81
|
+
- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
|
82
|
+
- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
|
|
83
|
+
- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
|
84
|
+
- The agent's outputs should generally be trusted
|
|
85
|
+
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
|
|
86
|
+
- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
|
87
|
+
- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple Task tool use content blocks. For example, if you need to launch both a code-reviewer agent and a test-runner agent in parallel, send a single message with both tool calls.
|
|
88
|
+
|
|
89
|
+
Example usage:
|
|
90
|
+
|
|
91
|
+
<example_agent_descriptions>
|
|
92
|
+
"code-reviewer": use this agent after you are done writing a signficant piece of code
|
|
93
|
+
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
|
|
94
|
+
</example_agent_description>
|
|
95
|
+
|
|
96
|
+
<example>
|
|
97
|
+
user: "Please write a function that checks if a number is prime"
|
|
98
|
+
assistant: Sure let me write a function that checks if a number is prime
|
|
99
|
+
assistant: First let me use the Write tool to write a function that checks if a number is prime
|
|
100
|
+
assistant: I'm going to use the Write tool to write the following code:
|
|
101
|
+
<code>
|
|
102
|
+
function isPrime(n) {
|
|
103
|
+
if (n <= 1) return false
|
|
104
|
+
for (let i = 2; i * i <= n; i++) {
|
|
105
|
+
if (n % i === 0) return false
|
|
106
|
+
}
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
</code>
|
|
110
|
+
<commentary>
|
|
111
|
+
Since a signficant piece of code was written and the task was completed, now use the code-reviewer agent to review the code
|
|
112
|
+
</commentary>
|
|
113
|
+
assistant: Now let me use the code-reviewer agent to review the code
|
|
114
|
+
assistant: Uses the Task tool to launch the code-reviewer agent
|
|
115
|
+
</example>
|
|
116
|
+
|
|
117
|
+
<example>
|
|
118
|
+
user: "Hello"
|
|
119
|
+
<commentary>
|
|
120
|
+
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
|
|
121
|
+
</commentary>
|
|
122
|
+
assistant: "I'm going to use the Task tool to launch the greeting-responder agent"
|
|
123
|
+
</example>
|
|
124
|
+
`,
|
|
125
|
+
schema: z.object({
|
|
126
|
+
agentName: z
|
|
127
|
+
.enum(agentNames)
|
|
128
|
+
.describe("The name of the subagent to use"),
|
|
129
|
+
query: z.string().describe("The query or task to send to the subagent"),
|
|
130
|
+
}),
|
|
131
|
+
fn: async (input) => {
|
|
132
|
+
const { agentName, query } = input;
|
|
133
|
+
const agent = agentMap.get(agentName);
|
|
134
|
+
if (!agent) {
|
|
135
|
+
throw new Error(`Unknown agent: ${agentName}`);
|
|
136
|
+
}
|
|
137
|
+
return await querySubagent(agent.agentPath, agent.agentDir, query);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Internal function that spawns a subagent process and collects its response.
|
|
143
|
+
*/
|
|
144
|
+
async function querySubagent(agentPath, agentWorkingDirectory, query) {
|
|
145
|
+
// Validate that the agent exists
|
|
146
|
+
try {
|
|
147
|
+
await fs.access(agentPath);
|
|
148
|
+
}
|
|
149
|
+
catch (_error) {
|
|
150
|
+
throw new Error(`Agent not found at ${agentPath}. Make sure the agent exists and has an index.ts file.`);
|
|
151
|
+
}
|
|
152
|
+
let agentProcess = null;
|
|
153
|
+
let connection = null;
|
|
154
|
+
try {
|
|
155
|
+
// Spawn the agent process
|
|
156
|
+
agentProcess = spawn("bun", [agentPath], {
|
|
157
|
+
cwd: agentWorkingDirectory,
|
|
158
|
+
env: { ...process.env },
|
|
159
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
160
|
+
});
|
|
161
|
+
if (!agentProcess.stdin || !agentProcess.stdout || !agentProcess.stderr) {
|
|
162
|
+
throw new Error("Failed to create stdio pipes for agent process");
|
|
163
|
+
}
|
|
164
|
+
// Convert Node.js streams to Web streams
|
|
165
|
+
const outputStream = Writable.toWeb(agentProcess.stdin);
|
|
166
|
+
const inputStream = Readable.toWeb(agentProcess.stdout);
|
|
167
|
+
// Create the bidirectional stream using ndJsonStream
|
|
168
|
+
const stream = ndJsonStream(outputStream, inputStream);
|
|
169
|
+
// Track accumulated response text
|
|
170
|
+
let responseText = "";
|
|
171
|
+
// Create ACP client implementation factory
|
|
172
|
+
const clientFactory = (_agent) => {
|
|
173
|
+
return {
|
|
174
|
+
async requestPermission(_params) {
|
|
175
|
+
// Deny all permission requests from the subagent
|
|
176
|
+
return { outcome: { outcome: "cancelled" } };
|
|
177
|
+
},
|
|
178
|
+
async sessionUpdate(params) {
|
|
179
|
+
// Handle session updates from the agent
|
|
180
|
+
const paramsExtended = params;
|
|
181
|
+
const update = paramsExtended.update;
|
|
182
|
+
// Accumulate agent_message_chunk text content
|
|
183
|
+
if (update?.sessionUpdate === "agent_message_chunk") {
|
|
184
|
+
const content = update.content;
|
|
185
|
+
if (content &&
|
|
186
|
+
content.type === "text" &&
|
|
187
|
+
typeof content.text === "string") {
|
|
188
|
+
responseText += content.text;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
async writeTextFile() {
|
|
193
|
+
// Subagents should not write files outside their scope
|
|
194
|
+
throw new Error("Subagent attempted to write files, which is not allowed");
|
|
195
|
+
},
|
|
196
|
+
async readTextFile() {
|
|
197
|
+
// Subagents should not read files outside their scope
|
|
198
|
+
throw new Error("Subagent attempted to read files, which is not allowed");
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
// Create the client-side connection
|
|
203
|
+
connection = new ClientSideConnection(clientFactory, stream);
|
|
204
|
+
// Set up timeout for the entire operation
|
|
205
|
+
const timeoutMs = 5 * 60 * 1000; // 5 minutes
|
|
206
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
207
|
+
setTimeout(() => {
|
|
208
|
+
reject(new Error(`Subagent query timed out after ${timeoutMs / 1000} seconds`));
|
|
209
|
+
}, timeoutMs);
|
|
210
|
+
});
|
|
211
|
+
// Handle process errors and exit
|
|
212
|
+
const processExitPromise = new Promise((_resolve, reject) => {
|
|
213
|
+
agentProcess?.on("exit", (code, signal) => {
|
|
214
|
+
if (code !== 0 && code !== null) {
|
|
215
|
+
reject(new Error(`Agent process exited with code ${code} and signal ${signal}`));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
agentProcess?.on("error", (error) => {
|
|
219
|
+
reject(new Error(`Agent process error: ${error.message}`));
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
// Run the query with timeout and error handling
|
|
223
|
+
const queryPromise = (async () => {
|
|
224
|
+
// Initialize the connection
|
|
225
|
+
await connection?.initialize({
|
|
226
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
227
|
+
clientCapabilities: {
|
|
228
|
+
fs: {
|
|
229
|
+
readTextFile: false,
|
|
230
|
+
writeTextFile: false,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
// Create a new session with subagent mode flag
|
|
235
|
+
const sessionResponse = await connection?.newSession({
|
|
236
|
+
cwd: agentWorkingDirectory,
|
|
237
|
+
mcpServers: [],
|
|
238
|
+
_meta: {
|
|
239
|
+
[SUBAGENT_MODE_KEY]: true,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
// Send the prompt
|
|
243
|
+
await connection?.prompt({
|
|
244
|
+
sessionId: sessionResponse.sessionId,
|
|
245
|
+
prompt: [
|
|
246
|
+
{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: query,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
return responseText;
|
|
253
|
+
})();
|
|
254
|
+
// Race between query execution, timeout, and process exit
|
|
255
|
+
return await Promise.race([
|
|
256
|
+
queryPromise,
|
|
257
|
+
timeoutPromise,
|
|
258
|
+
processExitPromise,
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
// Cleanup: kill process and close connection
|
|
263
|
+
if (agentProcess) {
|
|
264
|
+
agentProcess.kill();
|
|
265
|
+
}
|
|
266
|
+
if (connection) {
|
|
267
|
+
try {
|
|
268
|
+
await Promise.race([
|
|
269
|
+
connection.closed,
|
|
270
|
+
new Promise((resolve) => setTimeout(resolve, 1000)),
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// Ignore cleanup errors
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -1,49 +1,34 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export declare const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
todos: {
|
|
36
|
-
content: string;
|
|
37
|
-
status: "pending" | "in_progress" | "completed";
|
|
38
|
-
activeForm: string;
|
|
39
|
-
}[];
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
todos: {
|
|
43
|
-
content: string;
|
|
44
|
-
status: "pending" | "in_progress" | "completed";
|
|
45
|
-
activeForm: string;
|
|
46
|
-
}[];
|
|
47
|
-
},
|
|
48
|
-
string
|
|
49
|
-
>;
|
|
2
|
+
export declare const TODO_WRITE_TOOL_NAME = "todo_write";
|
|
3
|
+
export declare const todoItemSchema: z.ZodObject<{
|
|
4
|
+
content: z.ZodString;
|
|
5
|
+
status: z.ZodEnum<{
|
|
6
|
+
pending: "pending";
|
|
7
|
+
in_progress: "in_progress";
|
|
8
|
+
completed: "completed";
|
|
9
|
+
}>;
|
|
10
|
+
activeForm: z.ZodString;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export declare const todoWrite: import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
13
|
+
todos: z.ZodArray<z.ZodObject<{
|
|
14
|
+
content: z.ZodString;
|
|
15
|
+
status: z.ZodEnum<{
|
|
16
|
+
pending: "pending";
|
|
17
|
+
in_progress: "in_progress";
|
|
18
|
+
completed: "completed";
|
|
19
|
+
}>;
|
|
20
|
+
activeForm: z.ZodString;
|
|
21
|
+
}, z.core.$strip>>;
|
|
22
|
+
}, z.core.$strip>, {
|
|
23
|
+
todos: {
|
|
24
|
+
content: string;
|
|
25
|
+
status: "pending" | "in_progress" | "completed";
|
|
26
|
+
activeForm: string;
|
|
27
|
+
}[];
|
|
28
|
+
}, {
|
|
29
|
+
todos: {
|
|
30
|
+
content: string;
|
|
31
|
+
status: "pending" | "in_progress" | "completed";
|
|
32
|
+
activeForm: string;
|
|
33
|
+
}[];
|
|
34
|
+
}, string>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tool } from "langchain";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
export const TODO_WRITE_TOOL_NAME = "todo_write";
|
|
3
4
|
export const todoItemSchema = z.object({
|
|
4
5
|
content: z.string().min(1),
|
|
5
6
|
status: z.enum(["pending", "in_progress", "completed"]),
|
|
@@ -9,7 +10,7 @@ export const todoWrite = tool(({ todos }) => {
|
|
|
9
10
|
// Simple implementation that confirms the todos were written
|
|
10
11
|
return `Successfully updated todo list with ${todos.length} items`;
|
|
11
12
|
}, {
|
|
12
|
-
name:
|
|
13
|
+
name: TODO_WRITE_TOOL_NAME,
|
|
13
14
|
description: `Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
|
|
14
15
|
It also helps the user understand the progress of the task and overall progress of their requests.
|
|
15
16
|
|
|
@@ -13,8 +13,11 @@ export declare function makeWebSearchTools(): readonly [import("langchain").Dyna
|
|
|
13
13
|
};
|
|
14
14
|
}>>, import("langchain").DynamicStructuredTool<z.ZodObject<{
|
|
15
15
|
url: z.ZodString;
|
|
16
|
+
prompt: z.ZodString;
|
|
16
17
|
}, z.core.$strip>, {
|
|
17
18
|
url: string;
|
|
19
|
+
prompt: string;
|
|
18
20
|
}, {
|
|
19
21
|
url: string;
|
|
22
|
+
prompt: string;
|
|
20
23
|
}, string>];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
1
2
|
import Exa from "exa-js";
|
|
2
3
|
import { tool } from "langchain";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -46,7 +47,7 @@ export function makeWebSearchTools() {
|
|
|
46
47
|
}),
|
|
47
48
|
});
|
|
48
49
|
// WebFetch tool - get contents of specific URLs
|
|
49
|
-
const webFetch = tool(async ({ url }) => {
|
|
50
|
+
const webFetch = tool(async ({ url, prompt }) => {
|
|
50
51
|
const client = getExaClient();
|
|
51
52
|
try {
|
|
52
53
|
const result = await client.getContents([url], {
|
|
@@ -60,16 +61,32 @@ export function makeWebSearchTools() {
|
|
|
60
61
|
if (!page) {
|
|
61
62
|
return `No content found for URL: ${url}`;
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const pageContent = page.text || "(No text content available)";
|
|
65
|
+
// Process the content with Anthropic API
|
|
66
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
67
|
+
if (!apiKey) {
|
|
68
|
+
throw new Error("ANTHROPIC_API_KEY environment variable is required to use the WebFetch tool. " +
|
|
69
|
+
"Please set it to your Anthropic API key.");
|
|
67
70
|
}
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
const anthropicClient = new Anthropic({ apiKey });
|
|
72
|
+
const userMessage = buildWebFetchUserMessage(pageContent, prompt);
|
|
73
|
+
const response = await anthropicClient.messages.create({
|
|
74
|
+
model: "claude-haiku-4-5-20251001",
|
|
75
|
+
max_tokens: 1024,
|
|
76
|
+
system: "You are a helpful assistant",
|
|
77
|
+
messages: [
|
|
78
|
+
{
|
|
79
|
+
role: "user",
|
|
80
|
+
content: userMessage,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
// Extract text from response
|
|
85
|
+
const textContent = response.content.find((block) => block.type === "text");
|
|
86
|
+
if (!textContent || textContent.type !== "text") {
|
|
87
|
+
return "Error: No text response from AI model";
|
|
70
88
|
}
|
|
71
|
-
|
|
72
|
-
return output;
|
|
89
|
+
return textContent.text;
|
|
73
90
|
}
|
|
74
91
|
catch (error) {
|
|
75
92
|
if (error instanceof Error) {
|
|
@@ -79,16 +96,41 @@ export function makeWebSearchTools() {
|
|
|
79
96
|
}
|
|
80
97
|
}, {
|
|
81
98
|
name: "WebFetch",
|
|
82
|
-
description: "Fetches
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
99
|
+
description: "- Fetches content from a specified URL and processes it using an AI model\n" +
|
|
100
|
+
"- Takes a URL and a prompt as input\n" +
|
|
101
|
+
"- Fetches the URL content, converts HTML to markdown\n" +
|
|
102
|
+
"- Processes the content with the prompt using a small, fast model\n" +
|
|
103
|
+
"- Returns the model's response about the content\n" +
|
|
104
|
+
"- Use this tool when you need to retrieve and analyze web content\n" +
|
|
105
|
+
"\n" +
|
|
106
|
+
"Usage notes:\n" +
|
|
107
|
+
" - The URL must be a fully-formed valid URL\n" +
|
|
108
|
+
" - HTTP URLs will be automatically upgraded to HTTPS\n" +
|
|
109
|
+
" - The prompt should describe what information you want to extract from the page\n" +
|
|
110
|
+
" - This tool is read-only and does not modify any files\n" +
|
|
111
|
+
" - Results may be summarized if the content is very large\n" +
|
|
112
|
+
" - When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.\n",
|
|
86
113
|
schema: z.object({
|
|
87
114
|
url: z
|
|
88
115
|
.string()
|
|
89
116
|
.url()
|
|
90
117
|
.describe("The URL of the webpage to fetch content from"),
|
|
118
|
+
prompt: z.string().describe("The prompt to run on the fetched content"),
|
|
91
119
|
}),
|
|
92
120
|
});
|
|
93
121
|
return [webSearch, webFetch];
|
|
94
122
|
}
|
|
123
|
+
function buildWebFetchUserMessage(pageContent, prompt) {
|
|
124
|
+
return `Web page content:
|
|
125
|
+
---
|
|
126
|
+
${pageContent}
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
${prompt}
|
|
130
|
+
|
|
131
|
+
Provide a concise response based only on the content above. In your response:
|
|
132
|
+
- Enforce a strict 125-character maximum for quotes from any source document. Open Source Software is ok as long as we respect the license.
|
|
133
|
+
- Use quotation marks for exact language from articles; any language outside of the quotation should never be word-for-word the same.
|
|
134
|
+
- You are not a lawyer and never comment on the legality of your own prompts and responses.
|
|
135
|
+
- Never produce or reproduce exact song lyrics.`;
|
|
136
|
+
}
|
|
@@ -97,3 +97,76 @@ const agent: AgentDefinition = {
|
|
|
97
97
|
],
|
|
98
98
|
};
|
|
99
99
|
```
|
|
100
|
+
|
|
101
|
+
## Adding Subagents
|
|
102
|
+
Subagents allow you to delegate complex, multi-step tasks to specialized agents. The `makeSubagentsTool` utility creates a "Task" tool that can route work to different subagents based on their capabilities.
|
|
103
|
+
|
|
104
|
+
### Configuration
|
|
105
|
+
Import `makeSubagentsTool` and add it to your agent's tools:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { makeSubagentsTool } from '@townco/agent/utils';
|
|
109
|
+
|
|
110
|
+
const agent: AgentDefinition = {
|
|
111
|
+
model: "claude-sonnet-4-5-20250929",
|
|
112
|
+
systemPrompt: "You are a coordinator agent.",
|
|
113
|
+
tools: [
|
|
114
|
+
"todo_write",
|
|
115
|
+
"filesystem",
|
|
116
|
+
makeSubagentsTool([
|
|
117
|
+
{
|
|
118
|
+
agentName: "researcher",
|
|
119
|
+
description: "Use this agent to research topics and gather information",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
agentName: "code-reviewer",
|
|
123
|
+
description: "Use this agent to review code for bugs and improvements",
|
|
124
|
+
path: "/absolute/path/to/code-reviewer/index.ts",
|
|
125
|
+
},
|
|
126
|
+
]),
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Configuration options
|
|
132
|
+
Each subagent config supports two variants:
|
|
133
|
+
|
|
134
|
+
**1. Adding an agent in the current workspace:**
|
|
135
|
+
This should be used for agents that are in the current Town workspace (in the
|
|
136
|
+
same `agents/` directory).
|
|
137
|
+
```typescript
|
|
138
|
+
{
|
|
139
|
+
agentName: "researcher",
|
|
140
|
+
description: "Agent description for the LLM",
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
This expects the agent at `{workspace_dir}/agents/{agentName}/index.ts`
|
|
144
|
+
|
|
145
|
+
**2. Using `path` (direct path):**
|
|
146
|
+
```typescript
|
|
147
|
+
{
|
|
148
|
+
agentName: "code-reviewer",
|
|
149
|
+
description: "Agent description for the LLM",
|
|
150
|
+
path: "/absolute/path/to/agent/index.ts",
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
This uses the exact path specified (this is useful for agents outside the
|
|
154
|
+
standard location -- this is rarely the case)
|
|
155
|
+
|
|
156
|
+
### Usage example
|
|
157
|
+
Once configured, the parent agent can delegate tasks:
|
|
158
|
+
```typescript
|
|
159
|
+
// The agent will see a "Task" tool with these parameters:
|
|
160
|
+
// - agentName: enum of ["researcher", "code-reviewer"]
|
|
161
|
+
// - query: string describing the task
|
|
162
|
+
|
|
163
|
+
// Example prompt that triggers the Task tool:
|
|
164
|
+
// "Please research the latest best practices for React hooks"
|
|
165
|
+
// -> Agent uses Task tool with agentName="researcher"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Best practices
|
|
169
|
+
- Give each subagent a clear, specific description of its purpose
|
|
170
|
+
- Keep subagent responsibilities focused and non-overlapping
|
|
171
|
+
- Use descriptive agent names that indicate their role
|
|
172
|
+
- Remember that subagents cannot use the Task tool (no nested subagents)
|