Skip to content

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.

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.

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.

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.

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.

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. prompt is the active prompt with the full output transcript for the turn.
  • status: 'interrupted' — a tool suspended the run by throwing ToolInterrupt. The prompt holds 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.