@inngest/x-agents
v0.0.2-rc.0
Published
Thin, composable, forkable primitives for building agents on Inngest's durable execution model.
Readme
Inngest Agents SDK
A durable agent loop for TypeScript. Not a framework.
runAgentLoop() runs your agent inside an Inngest function. Every LLM call,
every tool call, every retry is a durable step: your agent survives crashes,
cold starts, and three-hour runs without losing state. Hit the context limit?
The loop compacts and retries. Model down? It falls back. Rate limited? It
waits. Invalid tool call? The error goes back to the model. You configure the
behavior — the loop handles the failure. Read the whole core in 30 minutes.
Install
pnpm add @inngest/x-agents inngestPublished for internal Inngest usage and testing — see
docs/publishing.md for how releases are built and shipped.
Consumers should set "skipLibCheck": true in their tsconfig.json (transitive
model-provider SDKs don't typecheck cleanly without it).
Quick start — runAgentLoop()
An agent is a loop inside an Inngest function:
import { Inngest } from "inngest";
import { createPiAdapter, runAgentLoop, type StepLike } from "@inngest/x-agents";
const inngest = new Inngest({ id: "my-app" });
const adapter = createPiAdapter("anthropic/claude-sonnet-4-6");
export const agent = inngest.createFunction(
{ id: "assistant", triggers: [{ event: "agent/run" }] },
async ({ event, step }) => {
const result = await runAgentLoop({
step: step as unknown as StepLike,
adapter,
instructions: "You are a concise, helpful assistant.",
messages: [{ role: "user", content: event.data.query, timestamp: Date.now() }],
});
return { text: result.text };
},
);That's the harness. Serve it like any Inngest function; every iteration is a memoized step you can watch, retry, and replay in the dashboard.
Adding tools
import { createDurableTool, createTool, Type } from "@inngest/x-agents";
// The default: the WHOLE handler runs in one memoized step — a side effect
// fires exactly once across replays.
const getWeather = createDurableTool({
name: "get_weather",
description: "Get the current weather for a city.",
parameters: Type.Object({ city: Type.String() }),
handler: async ({ city }) => fetchWeather(city),
});
// The escape hatch: the handler runs at loop level with a full `ctx.step`,
// so a tool can be a mini-workflow that owns its own durable steps.
const publish = createTool({
name: "publish",
description: "Render and upload a report.",
parameters: Type.Object({ title: Type.String() }),
handler: async ({ title }, ctx) => {
const html = await ctx.step.run("render", () => render(title));
return ctx.step.run("upload", () => upload(html));
},
});Pass tools: [getWeather, publish] to the loop. Gate anything risky with
approval: { reason, timeout } — the run pauses durably for a human before
the tool executes.
Any Inngest function is a tool — asAgentTool()
Sub-agents don't need a framework; they need step.invoke:
import { asAgentTool } from "@inngest/x-agents";
tools: [
asAgentTool(researchAgent), // another agent
asAgentTool(sendEmailFn), // a plain Inngest function
asAgentTool(pipelineFn, { name: "run_analysis" }),
asAgentTool(workerFn, { async: true, event: "work/queued" }), // fire-and-forget
]The call becomes a step.invoke (or step.sendEvent) — retries, replay, and
observability come from the platform, not from the Inngest Agents SDK.
createAgent() — the convenience wrapper
When you want the boring stuff handled — trigger events, sessions, delivery, flow control — wrap the same loop in config:
import { createAgent } from "@inngest/x-agents";
import { createFileSessionStore } from "@inngest/x-agents/session";
const assistant = createAgent({
id: "assistant",
model: "anthropic/claude-sonnet-4-6",
instructions: "You are a concise, helpful assistant.",
tools: [getWeather],
session: createFileSessionStore({ dir: ".sessions", primary: "conversation_id" }),
flowControl: { singleton: { key: "event.data.channelKey", mode: "cancel" } },
});assistant.asTool() / assistant.asAsyncTool() wire it into another agent.
Every run emits agent/run.completed — score runs with standalone Inngest
scorer functions; the agent never knows it's being evaluated.
Capabilities — opt-in, composable
Each capability is a plain function returning tools, instructions, or hooks. No plugins, no registration — compose them yourself:
import { memoryTools } from "@inngest/x-agents/memory";
import { skillsInstructions, readSkillTool } from "@inngest/x-agents/skills";
import { realtimeHooks, slackHooks } from "@inngest/x-agents/delivery";
import { composeInstructions, mergeHooks } from "@inngest/x-agents";
createAgent({
instructions: composeInstructions(base, skillsInstructions({ store })),
tools: [...myTools, ...memoryTools({ store }), readSkillTool({ store })],
hooks: (ctx) => mergeHooks(realtimeHooks(ctx), slackHooks({ token, channelKey: ctx.channelKey })),
});More lines than plugins: [...]. More clarity. No magic.
Context — dependency injection for tools
The loop takes a generic ctx and hands it to every tool handler — a sandbox,
a DB client, the current user. Tools declare what they need and TypeScript
checks it against the loop's ctx. Generic onPause/onResume hooks fire
around human waits, so live resources hibernate while a run sleeps on approval:
import { createSandboxContext, localSandbox } from "@inngest/x-agents/sandbox";
import { sandboxTools } from "@inngest/x-agents/tools";
const sb = await createSandboxContext(step, { provider: localSandbox({ root: ".ws" }), sessions });
await runAgentLoop({
step, adapter, messages,
ctx: { sandbox: sb.sandbox, db: prisma }, // any resources you like
onPause: sb.onPause, // hibernate during HITL waits
onResume: sb.onResume,
tools: [...sandboxTools()], // typed: they require ctx.sandbox
});There's no sandbox config field — a sandbox is just one thing you might put
on ctx. See docs/context.md.
Patterns
Complete, forkable files in /patterns — basic loop, tools, multi-turn sessions, sub-agents, functions-as-tools, human approval, user-defined context, skills, memory, background agents. Copy one, modify it, ship it.
Repo
lib/— the library (see ARCHITECTURE.md for contributor docs)patterns/— copy-paste starting pointsexample/— a full end-to-end app (streaming TUI chat, sandbox, skills, memory)examples/— six applied apps (support bot, coding agent, Slack, code review, HITL, personal agent)
pnpm typecheck # tsc --noEmit
pnpm smoke # dependency-free smoke tests
pnpm example:serve && pnpm example:chat # run the example app