Our Code Assistant v2 introduced some limited reasoning capabilities through Tools and a Network of Agents.
This third version will transform our Code Assistant into a semi-autonomous AI Agent that can solve bugs and improve code.
Our AI Agent will operate over an Express API project containing bugs:
Can you help me fix the following error?1. TypeError: Cannot read properties of undefined (reading 'body') at app.post (/project/src/routes/users.ts:10:23)
Our Code Assistant v3 will autonomously navigate through the codebase and fix the bug by updating the impacted files.
This new version relies on previously covered concepts such as Tools, Agents, and Networks but introduces
the creation of a custom Router Agent bringing routing autonomy to the AI Agent.
Our Code Assistant v3 needs to interact with the file system and search through code. Let’s implement these capabilities as Tools:
Copy
Ask AI
import { createTool } from "@inngest/agent-kit";const writeFile = createTool({ name: "writeFile", description: "Write a file to the filesystem", parameters: z.object({ path: z.string().describe("The path to the file to write"), content: z.string().describe("The content to write to the file"), }), handler: async ({ path, content }) => { try { let relativePath = path.startsWith("/") ? path.slice(1) : path; writeFileSync(relativePath, content); return "File written"; } catch (err) { console.error(`Error writing file ${path}:`, err); throw new Error(`Failed to write file ${path}`); } },});const readFile = createTool({ name: "readFile", description: "Read a file from the filesystem", parameters: z.object({ path: z.string().describe("The path to the file to read"), }), handler: async ({ path }) => { try { let relativePath = path.startsWith("/") ? path.slice(1) : path; const content = readFileSync(relativePath, "utf-8"); return content; } catch (err) { console.error(`Error reading file ${path}:`, err); throw new Error(`Failed to read file ${path}`); } },});const searchCode = createTool({ name: "searchCode", description: "Search for a given pattern in a project files", parameters: z.object({ query: z.string().describe("The query to search for"), }), handler: async ({ query }) => { const searchFiles = (dir: string, searchQuery: string): string[] => { const results: string[] = []; const walk = (currentPath: string) => { const files = readdirSync(currentPath); for (const file of files) { const filePath = join(currentPath, file); const stat = statSync(filePath); if (stat.isDirectory()) { walk(filePath); } else { try { const content = readFileSync(filePath, "utf-8"); if (content.includes(searchQuery)) { results.push(filePath); } } catch (err) { console.error(`Error reading file ${filePath}:`, err); } } } }; walk(dir); return results; }; const matches = searchFiles(process.cwd(), query); return matches.length === 0 ? "No matches found" : `Found matches in following files:\n${matches.join("\n")}`; },});
Some notes on the highlighted lines:
As noted in the “Building Effective Agents” article from Anthropic, Tools based on file system operations are most effective when provided with absolute paths.
Tools performing action such as writeFile should always return a value to inform the Agent that the action has been completed.
These Tools provide our Agents with the following capabilities:
The Router Agent is the “brain” of our Code Assistant, deciding which Agent to use based on the context.
The router developed in the Code Assistant v2 was a function that decided which Agent to call next
based on the progress of the workflow. Such router made a Agent deterministic, but lacked the reasoning capabilities to autonomously orchestrate the Agents.
In this version, we will provide an Agent as a router, called a Router Agent.
By doing so, we can leverage the reasoning capabilities of the LLM to autonomously orchestrate the Agents around a given goal (here, fixing the bug).
Creating a Router Agent is done by using the createRoutingAgent helper function:
Copy
Ask AI
import { createRoutingAgent } from "@inngest/agent-kit";const router = createRoutingAgent({ name: "Code Assistant routing agent", system: async ({ network }): Promise<string> => { if (!network) { throw new Error( "The routing agent can only be used within a network of agents" ); } const agents = await network?.availableAgents(); return `You are the orchestrator between a group of agents. Each agent is suited for a set of specific tasks, and has a name, instructions, and a set of tools. The following agents are available: <agents> ${agents .map((a) => { return ` <agent> <name>${a.name}</name> <description>${a.description}</description> <tools>${JSON.stringify(Array.from(a.tools.values()))}</tools> </agent>`; }) .join("\n")} </agents> Follow the set of instructions: <instructions> Think about the current history and status. If the user issue has been fixed, call select_agent with "finished" Otherwise, determine which agent to use to handle the user's request, based off of the current agents and their tools. Your aim is to thoroughly complete the request, thinking step by step, choosing the right agent based off of the context. </instructions>`; }, tools: [ createTool({ name: "select_agent", description: "select an agent to handle the input, based off of the current conversation", parameters: z .object({ name: z .string() .describe("The name of the agent that should handle the request"), }) .strict(), handler: ({ name }, { network }) => { if (!network) { throw new Error( "The routing agent can only be used within a network of agents" ); } if (name === "finished") { return undefined; } const agent = network.agents.get(name); if (agent === undefined) { throw new Error( `The routing agent requested an agent that doesn't exist: ${name}` ); } return agent.name; }, }), ], tool_choice: "select_agent", lifecycle: { onRoute: ({ result }) => { const tool = result.toolCalls[0]; if (!tool) { return; } const agentName = (tool.content as any).data || (tool.content as string); if (agentName === "finished") { return; } else { return [agentName]; } }, },});
Looking at the highlighted lines, we can see that a Router Agent mixes features from regular Agents and a function Router:
A Router Agent is a regular Agent with a system function that returns a prompt
A Router Agent can use Tools to interact with the environment
Finally, a Router Agent can also define lifecycle callbacks, like Agents do
Let’s now dissect how this Router Agent works:
The system function is used to define the prompt dynamically based on the Agents available in the Network
You will notice that the prompt explicitly ask to call a “finished” tool when the user issue has been fixed
The select_agent Tool is used to validate that the Agent selected is available in the Network
The tool ensures that the “finished” edge case is handled
The onRoute lifecycle callback is used to determine which Agent to call next
This callback stops the conversation when the user issue has been fixed (when the “finished” Agent is called)
This is it! Using this prompt, our Router Agent will orchestrate the Agents until the given bug is fixed.