Configure and run an agent
An AgentRun is one turn of the tool-calling loop over a conversation. This guide covers
each option you pass it, where the actions come from, how the prompts array is
interpreted, and what run() returns.
The options
Section titled “The options”import { AgentRun } from '@grundlag/agent';
const run = new AgentRun({ systemPrompt, client, model, prompts, actions, itemTypes, userId, logger,});| Option | Type | Required | Description |
|---|---|---|---|
systemPrompt |
string |
yes | Prepended as the system message on every model turn in the run. |
client |
OpenAI |
yes | An OpenAI SDK client. The run calls client.chat.completions.create — see custom endpoints. |
model |
string |
yes | The model id, e.g. 'gpt-4o'. |
prompts |
Prompt[] |
yes | The conversation history. The last entry carries the new user turn — see The conversation array. |
actions |
Record<string, Tool> |
yes | The tools the model may call. The map keys are the tool names the model sees. Each tool arrives with its dependencies bound in, so the run needs no service container. |
itemTypes |
ItemTypeRegistry |
no | Registry used to validate and render typed conversation items a tool emits. |
userId |
string |
no | The acting user’s id, forwarded to each tool’s execute context. |
logger |
Logger |
no | Logger forwarded to tools. Defaults to a console logger when omitted. |
Sourcing the tools
Section titled “Sourcing the tools”The actions map is just Record<string, Tool>, so you can assemble it however you like.
The keys become the tool names, so keep them stable and legible to the model.
The usual source is a ProviderRegistry. Its toTools() flattens every registered provider
into one map — each action bound into a tool with the
registry’s Services closed over — which is how a composed host exposes its whole surface:
const actions = await providers.toTools();toTools() is async and derives each key from the provider and action id — normalised to a
safe camelCase function name and deduped so names never collide across providers. Whatever
map you build, the agent treats each entry the same way — it consumes the identical tools the
sandbox and the HTTP API do. See Define actions for the
action contract, and Tools and transports for how an
action becomes a tool.
To bind a single action into a tool yourself, use toTool from the core package, closing
over a Services container:
import { toTool } from '@grundlag/core';
const actions = { search: toTool(provider.actions.get('search')!, { services }), summarize: toTool(provider.actions.get('summarize')!, { services }),};Narrowing the set this way — passing only the tools relevant to the task — keeps the tool list short and the model focused.
The conversation array
Section titled “The conversation array”prompts is the full conversation the model should see, oldest first. The runtime turns it
into chat messages: each prompt’s content becomes a user message, and each entry in its
output array becomes an assistant message, a tool call plus its result, or a rendered
item.
The last prompt is the active turn. Its content is the new user message, and its
output is the array the run appends to as the model responds. For a fresh turn, start it
empty:
import type { Prompt } from '@grundlag/core';
const prompt: Prompt = { id: crypto.randomUUID(), timestamp: new Date().toISOString(), content: [{ type: 'text', text: 'Summarise the latest report.' }], output: [],};To continue a conversation, keep the earlier prompts (with their filled-in output) and
append a new one for the next user turn:
const run = new AgentRun({ // ... prompts: [...history, nextPrompt],});Because the entire state of a run lives in these prompts, you can persist them and reconstruct an equivalent run later — including one that is mid-interrupt. See The agent run loop.
Pointing at a custom endpoint
Section titled “Pointing at a custom endpoint”The runtime never assumes api.openai.com. Because you construct the client, you can point
it at any OpenAI-compatible endpoint by setting baseURL:
import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'https://llm.internal.example.com/v1', apiKey: process.env.LLM_API_KEY,});Set model to whatever id that endpoint expects. The run’s behaviour is unchanged — it
still streams completions and executes tool calls the same way.
Running and the result
Section titled “Running and the result”run() drives the loop until the model stops requesting tools, then resolves:
const result = await run.run();The result is an AgentRunResult:
type AgentRunResult = { status: 'completed' | 'interrupted'; prompt: Prompt;};status: 'completed'— the model finished.promptis the active prompt with the fulloutputtranscript for the turn.status: 'interrupted'— a tool suspended the run by throwingToolInterrupt. Thepromptholds the state to resume from. See Handle interrupts.
Calling run() on a run that is already suspended throws — inspect run.state and call
resume() or reject() instead.