Agents and Agent Tools
You can think of an Agent as a wrapper over a single model, with instructions and tools. Calling .run
on an Agent
passes the system prompt, tools, and input to the Agent's model.
Agents are the core of AgentKit. An Agent is used to call a single model with a system prompt and, optionally, set of tools. The an agent’s additional properties like a name, description, and lifecycle hooks make calls more powerful and composable. An Agent has the following structure:
name
: the name of the agent shown in tracingdescription
: an optional description for the agent, used for LLM-based routing to help the network pick which agent to run nextsystem
: the system prompt, as a string or function. Functions let you change prompts based off of state and memorytools
: a set of tools that this agent has access to
Understanding how an agent makes calls
Here’s a simple agent, which makes a single model call using system prompts and user input:
import { Agent, agenticOpenai as openai } from "@inngest/agent-kit";
const agent = createAgent({
name: "Code writer",
system: "You are an expert TypeScript programmer. Given a set of asks, you think step-by-step to plan clean, " +
"idiomatic TypeScript code, with comments and tests as necessary." +
"Do not respond with anything else other than the following XML tags:" +
"- If you would like to write code, add all code within the following tags (replace $filename and $contents appropriately):" +
" <file name='$filename.ts'>$contents</file>",
model: openai("gpt-4o-mini"),
});
You can run an agent individually. This creates a new inference request with its system prompt as the first message, and the input as the user message:
// Run the agent:
const { output } = await agent.run("Write a function that trims a string");
// This is similar to:
// const chatCompletion = await step.ai.infer("Code writer", {
// model: openai("gpt-4o-mini"),
// body: {
// messages: [
// { role: "system", content: "You are an expert..." },
// { role: "user", content: "Write a function that trims a string" }
// ],
// },
// });
Under the hood, the agent will call your model provider using an Inngest step. This gives you all of the benefits of Inngest: reliability, durability, automatic retries, and observability.
How agents work
Agents themselves are relatively simple. When you call run
on an agent, there are several steps that happen:
- The prompt is created from the system, input, and any network state (if the agent is part of a network)
- The agent’s
onStart
lifecycle is called, if defined. This lets you modify the agent’s prompts before inference
- The agent’s
- An inference is made as an Inngest step — with retries and durability built in. The inference result is parsed into an
InferenceResult
class, containing standardized messages, any tool call responses, and the raw API response in the format of your provider- The agent’s
onResponse
lifecycle is called with the result. This lets you modify and manage the result prior to calling tools
- The agent’s
- If any tools were specified in the response, those tools are automatically called. The outputs are added to the result
- The agent’s
onFinish
lifecycle is called with the new result. This lets you inspect the output of tool use
- The agent’s
- The result is returned to the caller
Agent tools
When you create an agent you can specify any number of tools that the agent can use. Tools follow the standard formats that OpenAI and Anthropic provide: a name and a description, plus typed parameters.
In AgentKit, you also define a handler
function which is called when the tool is invoked. Because AgentKit runs in the backend (on your own infrastructure) these handlers can run almost any code you define.
Complex agents with tools
A more complex agent used in a network defines a description, lifecycle hooks, tools, and a dynamic set of instructions based off of network state:
import { Agent, Network, agenticOpenai as openai } from "@inngest/agent-kit";
const systemPrompt =
"You are an expert TypeScript programmer. Given a set of asks, think step-by-step to plan clean, " +
"idiomatic TypeScript code, with comments and tests as necessary."
const agent = createAgent({
name: "Code writer",
// description helps LLM routers choose the right agents to run.
description: "An expert TypeScript programmer which can write and debug code",
// system defines a system prompt. This function is called by the network each time
// the agent runs, and allows you to customize the instructions based off of past state.
system: async ({ network }) => {
// Each time this agent runs, it may produce "file" content. Check if any
// content has already been produced in an agentic workflow.
const files: Record<string, string> | undefined = network?.state.kv.get("files")
if (files === undefined) {
return systemPrompt;
}
// There are files present in the network's state, so add them to the promp to help
// provide previous context automatically.
let prompt = systemPrompt + "The following code already exists:"
for (const [name, contents] of Object.entries(record)) {
prompt += `<file name='${name}'>$contents</file>`
}
return prompt;
},
// tools are provided to the model and are automatically called.
tools: [
// This tool forces the model to generate file content as structured data.
// createTypedTool is a utility that forces typescript to strictly type the handler.
createTypedTool({
name: "write_files",
description: "Write code with the given filenames",
parameters: z.object({
files: z.array(z.object({
filename: z.string(),
content: z.string(),
})),
}),
handler: (output, { network }) => {
// files is the output from the model's response in the format above.
// Here, we store OpenAI's generated files in the response.
const files = network?.state.kv.get("files") || {};
for (const file of output.files) {
files[file.filename] = file.content;
}
network?.state.kv.set("files", files);
},
}),
],
});
// Use this agent in a network of other agents, or with long-term state.
const network = createNetwork({
agents: [agent]
});
Calling .run
on this agent will pass the tools into your provider, allowing the model to select whether to run the write_files
tool as a result. Tools are automatically called on your behalf.
If the agent is part of a network, the agent’s inference calls are automatically added to the network’s state as memory, and the network’s state is used to adjust the prompt at any call. This is one way of building a complex network of agents, which learns as the network solves the problem.
Networks manage shared state between a sequence of Agent calls, and allow you to manage Agent calls over time as state changes.
Step functions in tools
AgentKit also exposes Inngest’s step
tooling directly within tools. This lets you build complex step functions as tools, including human-in-the-loop tasks via step.waitForEvent
or invoking other Inngest functions with step.invoke
:
createTypedTool({
name: "request_refund_approval",
description: "Request refund approval",
parameters: z.array(z.object({
refund_id: z.string(),
}).required(),
handler: async (output, { network, agent, step }) => {
await step.run("request approval in slack", async () => {
// XXX: Send a message in slack which has an "approve/reject" button.
});
// wait 1 hour for the approval
const approval = await step.waitForEvent("wait for approval event", {
event: "api/refund.approved",
if: `async.data.refund_id == "${output.refund_id}"`,
timeout: "1h",
});
if (approval === null) {
// This was _not_ approved.
return { approved: false };
}
return { approved: true };
}
})
This example shows how Inngest’s orchestration allows for long running, stateful agentic workflows. For more information on Inngest’s step tooling, read the documentation here.
Handler typing and reference
AgentKit exposes a createTypedTool
utility that forces the output
parameter in a handler to be typed according to your parameter’s Zod schema:
createTypedTool({
name: "list_charges",
description: "Returns all of a user's charges",
parameters: z.array(z.object({
id: z.string(),
amount: z.number(),
})),
handler: async (output, { network, agent, step }) => {
// output is strongly typed to match the parameter type.
}
})