Run an agent
In this tutorial you will drive one turn of the agent runtime from scratch. You will create
an OpenAI client, define a couple of tools the model may call, build the conversation as a
Prompt, construct an AgentRun, stream the reply as it arrives, and read the finished
transcript.
By the end you will have a working script that asks the model a question, watches it call a tool, and prints its answer.
Before you start
Section titled “Before you start”You need an OpenAI API key and the two runtime packages installed:
npm install @grundlag/agent @grundlag/core openaiExport your key so the OpenAI client can pick it up:
export OPENAI_API_KEY=sk-...1. Create the OpenAI client
Section titled “1. Create the OpenAI client”The agent runtime does not wrap OpenAI — it uses a client you hand it. Create one directly:
import OpenAI from 'openai';
const client = new OpenAI();2. Define the tools
Section titled “2. Define the tools”The model calls tools — transport-neutral callables with typed input and output. Define
two small ones with defineTool. A tool’s execute takes the input directly (its second
argument is a context object, unused here):
import { defineTool, z } from '@grundlag/core';
const getTime = defineTool({ id: 'getTime', name: 'Get the current time', description: 'Returns the current time as an ISO 8601 string.', input: z.object({}), output: z.object({ now: z.string() }), execute: async () => ({ now: new Date().toISOString() }),});
const daysUntil = defineTool({ id: 'daysUntil', name: 'Days until a date', description: 'Returns the whole number of days between now and a target date.', input: z.object({ target: z.string().describe('The target date as an ISO 8601 string.'), }), output: z.object({ days: z.number().int() }), execute: async (input) => { const ms = new Date(input.target).getTime() - Date.now(); return { days: Math.floor(ms / 86_400_000) }; },});Each tool’s input schema becomes the model’s parameter schema, and its description
becomes the tool description the model reads. We define tools directly here; in a real app
they usually come from a host’s providers via providers.toTools(). See
Tools and transports.
3. Build the conversation
Section titled “3. Build the conversation”A run operates over a list of Prompt objects — the conversation history. The last
prompt in the list carries the new user message; everything before it is prior context.
For a first turn there is just one prompt:
import type { Prompt } from '@grundlag/core';
const prompt: Prompt = { id: crypto.randomUUID(), timestamp: new Date().toISOString(), content: [{ type: 'text', text: 'How many days until 2027-01-01?' }], output: [],};The output array starts empty. The run fills it in as the model produces text, calls
tools, and records their results.
4. Construct the run
Section titled “4. Construct the run”AgentRun ties the pieces together. The actions map keys are the tool names the model
sees, so give them names the model can reason about:
import { AgentRun } from '@grundlag/agent';
const run = new AgentRun({ systemPrompt: 'You are a concise assistant. Use the tools when they help.', client, model: 'gpt-4o', prompts: [prompt], actions: { getTime, daysUntil, },});The actions map is a Record<string, Tool>; its keys are the tool names the model sees.
Each tool already carries whatever it depends on, so the run needs no service container.
5. Stream the reply
Section titled “5. Stream the reply”The run is an event emitter. Subscribe to output:delta to receive text as the model
produces it, chunk by chunk:
run.on('output:delta', ({ delta }) => { process.stdout.write(delta);});6. Run it and read the transcript
Section titled “6. Run it and read the transcript”Call run(). It resolves once the model stops requesting tools:
const result = await run.run();
console.log('\n\nstatus:', result.status);console.log('outputs:', result.prompt.output.length);Run the script. You will see the answer stream in, then a summary like:
It's 164 days until 2027-01-01.
status: completedoutputs: 2What happened
Section titled “What happened”Under the hood the run did this:
- It converted
getTimeanddaysUntilinto OpenAI function tools from theirinputschemas. - It sent your system prompt and the conversation to the model as a streaming completion.
- The model chose to call
daysUntilwith atarget. The run executed the matching action, recorded the result on the prompt, and looped. - With the tool result in hand, the model produced its final text answer — which streamed
to your listener as
output:deltaevents. - With no more tool calls requested, the run emitted
completedand resolved withstatus: 'completed'.
Everything that happened is captured in result.prompt.output: the tool call (with its
input and output) and the final text. Because the whole conversation lives in the Prompt,
you can persist it and reconstruct the run later.
Next steps
Section titled “Next steps”- Configure and run an agent — every option explained, and how to source actions from your providers.
- Stream output and react to events — the full
event model beyond
output:delta. - Handle interrupts — pause a run for human approval, then resume it.
- The agent run loop — the model behind the mechanics you just saw.