Overview

Our Code Assistant v1, relying on a RAG workflow, had limited capabilities linked to its lack of reasoning. The second version of our Code Assistant will introduce reasoning capabilities to adapt analysis based on the user’s input:

const {
    state: { kv },
} = await network.run(
`Analyze the files/example.ts file by suggesting improvements and documentation.`
);
console.log("Analysis:", kv.get("summary"));

// Analysis: The code analysis suggests several key areas for improvement:

// 1. Type Safety and Structure:
// - Implement strict TypeScript configurations
// - Add explicit return types and interfaces
// - Break down complex functions
// - Follow Single Responsibility Principle
// - Implement proper error handling

// 2. Performance Optimization:
// - Review and optimize critical operations
// ...

These agentic (reasoning) capabilities are introduced by the following AgentKit concepts:

  • Tools: Enables Agents to interact with their environment (ex: file system or shared State).
  • Router: Powers the flow of the conversation between Agents.
  • Network: Add a shared State to share information between Agents.

Let’s learn these concepts in practice.

Setup

Similarly to the Code Assistant v1, perform the following steps to setup your project:

npm init

You are now set up, let’s implement the v2 of our Code Assistant.

Implementing our Code Assistant v2

Overview of the agentic workflow

Our Code Assistant v2 introduces reasoning to perform tailored recommendations based on a given code file: refactoring, documentation, etc.

To achieve this behavior, we will need to:

  • Create a code_assistant_agent Agent that will load a given filename from disk and plan a workflow using the following available Agents:
    • analysis_agent that will analyze the code file and suggest improvements
    • documentation_agent that will generate documentation for the code file
  • Finally, create a summarization_agent Agent that will generate a summary of the suggestions made by other agents

Compared to our Code Assistant v1, this new version does not consist of simple retrieval and generations steps. Instead, it introduces more flexibility by enabling LLM models to plan actions and select tools to use.

Let’s see how to implement the Agents.

A Network of Agents

Our Code Assistant v2 is composed of 4 Agents collaborating together to analyze a given code file. Such collaboration is made possible by using a Network to orchestrate the Agents and share State between them.

Unlike the Code Assistant v1, the user prompt will be passed to the network instead of an individual Agent:

await network.run(
`Analyze the files/example.ts file by suggesting improvements and documentation.`
);

To successfully run, a Network relies on:

  • A Router to indicate which Agent should be run next
  • A shared State, updated by the Agents’ LLM responses and tool calls

Let’s start by implementing our Agents and registering them into the Network.

Creating Agents with Tools

Attaching Tools to an Agent helps to:

  • Enrich dynamically the Agent context with dynamic data
  • Store the Agent results in the shared State

Learn more about Tools.

The Analysis and Documentation Agents

Our first two analysis Agents are straightforward:

import { createAgent } from "@inngest/agent-kit";

const documentationAgent = createAgent({
  name: "documentation_agent",
  system: "You are an expert at generating documentation for code",
});

const analysisAgent = createAgent({
  name: "analysis_agent",
  system: "You are an expert at analyzing code and suggesting improvements",
});

Defining task specific LLM calls (Agents) is a great way to make the LLM reasoning more efficient and avoid unnecessary generations.

Our documentation_agent and analysis_agent are currently stateless and need to be connected to the Network by saving their suggestions into the shared State.

For this, we will create our first Tool using createTool:

const saveSuggestions = createTool({
  name: "save_suggestions",
  description: "Save the suggestions made by other agents into the state",
  parameters: z.object({
    suggestions: z.array(z.string()),
  }),
  handler: async (input, { network }) => {
    const suggestions = network?.state.kv.get("suggestions") || [];
    network?.state.kv.set("suggestions", [
      ...suggestions,
      ...input.suggestions,
    ]);
    return "Suggestions saved!";
  },
});

A Tool is a function that can be called by an Agent.

The name, description and parameters are used by the Agent to understand what the Tool does and what it expects as input.

The handler is the function that will be called when the Tool is used. save_suggestions’s handler relies on the Network’s State kv (key-value store) API to share information with other Agents.

Learn more about the createTool() API.

The save_suggestions Tool is used by both documentation_agent and analysis_agent to save their suggestions into the shared State:

import { createAgent } from "@inngest/agent-kit";

// `save_suggestions` definition...

const documentationAgent = createAgent({
  name: "documentation_agent",
  system: "You are an expert at generating documentation for code",
  tools: [saveSuggestions],
});

const analysisAgent = createAgent({
  name: "analysis_agent",
  system: "You are an expert at analyzing code and suggesting improvements",
  tools: [saveSuggestions],
});

Our documentation_agent and analysis_agent are now connected to the Network and will save their suggestions into the shared State.

Let’s now create our code_assistant_agent that will read the code file from disk and plan the workflow to run.

The Code Assistant Agent

Let’s jump into the action by looking at the full implementation of our code_assistant_agent:

const codeAssistantAgent = createAgent({
  name: "code_assistant_agent",
  system: ({ network }) => {
    const agents = Array.from(network?.agents.values() || [])
      .filter(
        (agent) =>
          !["code_assistant_agent", "summarization_agent"].includes(agent.name)
      )
      .map((agent) => `${agent.name} (${agent.system})`);
    return `From a given user request, ONLY perform the following tool calls:
- read the file content
- generate a plan of agents to run from the following list: ${agents.join(", ")}

Answer with "done" when you are finished.`;
  },
  tools: [
    createTool({
      name: "read_file",
      description: "Read a file from the current directory",
      parameters: z.object({
        filename: z.string(),
      }),
      handler: async (input, { network }) => {
        const filePath = join(process.cwd(), `files/${input.filename}`);
        const code = readFileSync(filePath, "utf-8");
        network?.state.kv.set("code", code);
        return "File read!";
      },
    }),
    createTool({
      name: "generate_plan",
      description: "Generate a plan of agents to run",
      parameters: z.object({
        plan: z.array(z.string()),
      }),
      handler: async (input, { network }) => {
        network?.state.kv.set("plan", input.plan);
        return "Plan generated!";
      },
    }),
  ],
});

The highlighted lines emphasize three important parts of the code_assistant_agent:

  • The system property can take a function receiving the current Network state as argument, enabling more flexibility in the Agent’s behavior

    • Here, the system function is used to generate a prompt for the LLM based on the available Agents in the Network, enabling the LLM to plan the workflow to run
  • The code_assistant_agent relies on two Tools to achieve its goal:

    • read_file to read the code file from disk and save it into the shared State
    • generate_plan to generate a plan of agents to run and save it into the shared State

The pattern of dynamic system prompt and tools are also used by the summarization_agent to generate a summary of the suggestions made by other agents.

The Summarization Agent

const summarizationAgent = createAgent({
  name: "summarization_agent",
  system: ({ network }) => {
    const suggestions = network?.state.kv.get("suggestions") || [];
    return `Save a summary of the following suggestions:
    ${suggestions.join("\n")}`;
  },
  tools: [
    createTool({
      name: "save_summary",
      description:
        "Save a summary of the suggestions made by other agents into the state",
      parameters: z.object({
        summary: z.string(),
      }),
      handler: async (input, { network }) => {
        network?.state.kv.set("summary", input.summary);
        return "Saved!";
      },
    }),
  ],
});

The summarization_agent is a good example on how the State can be used to store intermediate results and pass them to the next Agent:

  • the suggestions are stored in the State by the documentation_agent and analysis_agent
  • the summarization_agent will read the suggestions from the State and generate a summary
  • the summary is then stored in the State as the summary key

Our four Agents are now propely defined and connected to the Network’s State.

Let’s now configure our Network to run the Agents with a Router.

Assembling the Network

An AgentKit Network is defined by a set of Agents and an optional defaultModel:

import { createNetwork, anthropic } from "@inngest/agent-kit";

// Agent and Tools definitions...

const network = createNetwork({
  name: "code-assistant-v2",
  agents: [
    codeAssistantAgent,
    documentationAgent,
    analysisAgent,
    summarizationAgent,
  ],
  defaultModel: anthropic({
    model: "claude-3-5-sonnet-latest",
    max_tokens: 4096,
  }),
});

The defaultModel will be applied to all Agents part of the Network. A model can also be set on an individual Agent by setting the model property.

Learn more about the Network Model configuration.

Our Code Assistant v2 is missing a final piece: the Router. Without a Router, the Network will not know which Agent to run next.

Implementing the Router

As stated in the workflow overview, our Code Assistant v2 is an agentic worflow composed of the following steps:

  1. The code_assistant_agent will read the code file from disk and generate a plan of agents to run
  2. Depending on the plan, the Network will run the next Agent in the plan (ex: analysis_agent and documentation_agent)
  3. Finally, the summarization_agent will generate a summary of the suggestions made by other agents

AgentKit’s Router enables us to implement such dynamic workflow with code by providing a defaultRouter function:

const network = createNetwork({
  name: "code-assistant-v2",
  agents: [
    codeAssistantAgent,
    documentationAgent,
    analysisAgent,
    summarizationAgent,
  ],
  defaultRouter: ({ network }) => {
    if (!network?.state.kv.has("code") || !network?.state.kv.has("plan")) {
      return codeAssistantAgent;
    } else {
      const plan = (network?.state.kv.get("plan") || []) as string[];
      const nextAgent = plan.pop();
      if (nextAgent) {
        network?.state.kv.set("plan", plan);
        return network?.agents.get(nextAgent);
      } else if (!network?.state.kv.has("summary")) {
        return summarizationAgent;
      } else {
        return undefined;
      }
    }
  },
  defaultModel: anthropic({
    model: "claude-3-5-sonnet-latest",
    max_tokens: 4096,
  }),
});

How does a Router work?

The Router is a function called by the Network when starting a new run and between each Agent call.

The provided Router function (defaultRouter) receives a network argument granting access to the Network’s state and Agents.

Learn more about the Router.

Let’s have a closer look at the Router implementation:

const router = ({ network }) => {
  // the first iteration of the network will have an empty state
  //  also, the first run of `code_assistant_agent` will store the `code`,
  //  requiring a second run to generate the plan
  if (!network?.state.kv.has("code") || !network?.state.kv.has("plan")) {
    return codeAssistantAgent;
  } else {
    // once the `plan` available in the state, we iterate over the agents to execute
    const plan = (network?.state.kv.get("plan") || []) as string[];
    const nextAgent = plan.pop();
    if (nextAgent) {
      network?.state.kv.set("plan", plan);
      return network?.agents.get(nextAgent);
      // we no agents are left to run, we generate a summary  
    } else if (!network?.state.kv.has("summary")) {
      return summarizationAgent;
      // if no agent are left to run and a summary is available, we are done
    } else {
      return undefined;
    }
  }
}

Our Code Assistant v2 iteration is now complete. Let’s run it!

Running the Code Assistant v2

First, go to your Anthropic dashboard and create a new API key.

Then, run the following command to execute our Code Assistant:

ANTHROPIC_API_KEY=<your-api-key> npm run start

The following output should be displayed in your terminal:

Analysis: The code analysis suggests several key areas for improvement:

1. Type Safety and Structure:
- Implement strict TypeScript configurations
- Add explicit return types and interfaces
- Break down complex functions
- Follow Single Responsibility Principle
- Implement proper error handling

2. Performance Optimization:
- Review and optimize critical operations
- Consider caching mechanisms
- Improve data processing efficiency

3. Documentation:
- Add comprehensive JSDoc comments
- Document complex logic and assumptions
- Create detailed README
- Include setup and usage instructions
- Add code examples

Updating the files/example.ts by applying the suggestions and running the Code Assistant again will yield a different planning with a different summary.

Try it out!

What we’ve learned so far

Let’s recap what we’ve learned so far:

  • Agentic workflows, compared to RAG workflows, are more flexible and can be used to perform more complex tasks
  • Combining multiple Agents improves the accuracy of the LLM reasoning and can save tokens
  • AgentKit enables to combine multiple Agents into a Network, connected by a common State
  • AgentKit’s Router enables to implement our workflow with code, keeping control over our reasoning planning

Next steps

This Code Assistant v2 shines by its analysis capabilities, but cannot be qualified as an AI Agent.

In the next version of our Code Assistant, we will transform it into a semi-autonomous AI Agent that can solve bugs and improve code of a small project.

Code Assistant v3: Autonomous Code Assistant

The final version update of our Code Assistant will transform it into a semi-autonomous AI Agent.