agent

js/ai/agent.ts

fino:ai/agent — reusable agent definitions and isolated conversation sessions.

This module is the primary entry point for agent applications. An Agent owns the model loop: it sends curated messages to a Model, executes tool calls, accumulates usage and cost, applies guardrails, and stops when the model finishes or a configured stop condition fires. Use this module when an application wants a complete LLM interaction loop instead of calling provider adapters directly. An Agent is safe to reuse across concurrent requests: mutable conversation state belongs to an AgentSession, never the definition.

Design

History policy is intentionally external. Each session appends incoming, assistant, and tool-result messages through a HistoryStrategy, then asks that strategy for the model-facing view before each request. The default is append-only in-memory history. Agent.generate() and Agent.stream() create an ephemeral session for one isolated run. Call createSession() when later turns should retain history. A history factory creates a fresh strategy for every session, preventing concurrent callers from sharing mutable policy state.

Tools are ordinary Tool values. They receive abort/run context and may throw SuspendSignal to pause a durable session. Agents can also be wrapped as tools with asTool() for simple composition; more structured branching or checkpointed orchestration belongs in fino:workflow.

Runs are resilient by configuration: retry adds backoff for provider failures, fallback tries alternate models in order, and guardrails can block a run or redact content on the way in and out. A run ends when the model stops without requesting tools, a stopWhen condition fires (the default is eight model steps), or a tool suspends. Every step and run emits OpenTelemetry gen_ai spans, metrics, and log records.

import { agent, streamText } from 'fino:ai/agent';
import { openai } from 'fino:ai/model';
import { tool } from 'fino:ai/tool';
import { v } from 'fino:validate';

const bot = agent({
  model: openai({ model: 'gpt-4o' }),
  instructions: 'Answer briefly.',
  tools: [tool({
    name: 'lookup',
    description: 'Look up a value by key.',
    parameters: v.object({ key: v.string().describe('Lookup key') }),
    execute: ({ key }: { key: string }) => `value for ${key}`,
  })],
});

const conversation = bot.createSession();
const stream = conversation.stream('lookup account status');
for await (const text of streamText(stream)) console.log(text);
const result = await stream.result;
conversation.close();

Functions

function streamText(stream: AgentStream): AsyncGenerator<string>

Iterate over only the text deltas from an agent stream.

Filters the stream's event reader down to model_event text deltas and yields each fragment as it arrives. Every other event — tool activity, retries, guardrails, step boundaries — is consumed and discarded, so use the raw stream.reader instead when those matter. The generator finishes when the run completes and rethrows the run's error if it fails. Because it consumes stream.reader, do not also iterate the reader yourself.

import { agent, streamText } from 'fino:ai/agent';
import { anthropic } from 'fino:ai/model';

const bot = agent({ model: anthropic({ model: 'claude-sonnet-4-6' }) });
const stream = bot.stream('Tell me a short story.');
for await (const text of streamText(stream)) console.log(text);
const result = await stream.result;

function agent(opts: AgentOptions): Agent

Create an Agent.

Convenience factory equivalent to new Agent(opts).

import { agent } from 'fino:ai/agent';
import { anthropic } from 'fino:ai/model';

const bot = agent({
  model: anthropic({ model: 'claude-sonnet-4-6' }),
  instructions: 'Answer briefly.',
});
const result = await bot.generate('hello');

Types

type HistoryStrategyFactory = (history?: MessageHistory) => HistoryStrategy

Factory that creates one fresh mutable history policy per agent session.

type AgentOptions = Omit<AgentRuntimeOptions, 'history'> & { history?: HistoryStrategyFactory }

Interfaces

interface AgentSessionOptions {

Options for one isolated in-memory conversation.

Properties

history?: MessageHistory

Initial immutable history, usually restored by a durable session store.

tools?: Tool[]

Additional tools available only within this session.

requestToolApproval?: ToolApprovalHandler

Inline approval host used only by this session.

Classes

class AgentSessionBusyError extends Error {

Raised when two turns try to use the same AgentSession concurrently.

Constructors

constructor()

Create the deterministic overlapping-turn error.

class AgentSessionClosedError extends Error {

Raised when work is started after an AgentSession has been closed.

Constructors

constructor()

Create the closed-session lifecycle error.

class AgentSession {

Mutable execution state for one conversation.

Sessions retain their own history strategy and may therefore continue across sequential turns. Different sessions created from the same Agent are fully isolated and may run concurrently. A session permits one active turn at a time; overlapping generate(), stream(), step(), or approveTool() calls throw AgentSessionBusyError instead of interleaving state.

Getters

get history(): MessageHistory

Current immutable conversation history for persistence or inspection.

Methods

async generate(input: string | RunInput): Promise<AgentResult>

Run one isolated turn while retaining this session's prior history.

stream(input: string | RunInput): AgentStream

Stream one isolated turn while retaining this session's prior history.

async step(state: AgentState): Promise<StepResult>

Advance one explicit state for durable or custom orchestration.

async approveTool( state: AgentState, request: ToolApprovalRequest, approval: unknown, ): Promise<StepResult>

Resolve one pending approval in this session.

cancel(reason: unknown = abortError('Agent session cancelled')): void

Abort the active turn, if any. The session remains reusable afterwards.

close(reason: unknown = abortError('Agent session closed')): void

Abort active work and permanently close this session. Repeated calls are harmless.

class Agent {

Reusable, concurrency-safe definition of an agent loop.

Each step curates history through the configured strategy, sends the model-facing view to the model, executes any requested tool calls, and appends the results. The loop repeats until the model stops without requesting tools, a stopWhen condition fires, or a tool throws SuspendSignal. Usage and cost accumulate across steps and are reported on the final AgentResult.

generate() and stream() create isolated ephemeral sessions. Use createSession() for a multi-turn conversation. step() and approveTool() are stateless lower-level hooks for custom orchestration; durable sessions create their own isolated execution session before driving multiple steps.

import { Agent } from 'fino:ai/agent';
import { anthropic } from 'fino:ai/model';

const bot = new Agent({
  model: anthropic({ model: 'claude-sonnet-4-6' }),
  instructions: 'You are a terse assistant.',
});
const result = await bot.generate('What is the capital of France?');
console.log(result.text, result.usage.outputTokens);

Properties

name?: string

Optional display name.

Recorded as the gen_ai.agent.name telemetry attribute on run spans and used as the default tool name by asTool().

Constructors

constructor(opts: AgentOptions)

Create an agent definition from AgentOptions, installing a factory for append-only in-memory history strategies when none is supplied.

Methods

createSession(opts: AgentSessionOptions = {}): AgentSession

Create an isolated conversation session from this reusable definition.

opts.history seeds a restored or forked conversation. opts.tools are appended to the definition's base tools for this session only. The history factory configured on the agent must return a fresh strategy each time; returning the same object twice throws to prevent accidental state sharing.

step(state: AgentState): Promise<StepResult>

Run one model/tool step from an existing state.

Sends the state's messages to the model, executes any tool calls the model requests, and returns the advanced state. done is true when the model stopped without requesting tools; suspend carries the SuspendSignal when a tool paused the run. Messages the history strategy has not yet seen are appended before the request. The caller owns the loop: feed result.state back into the next call and apply stop conditions itself — stopWhen is only consulted by generate() and stream().

Most applications should use generate() or stream(); step() exists for custom loop control, such as sessions checkpointing between steps.

import type { AgentState } from 'fino:ai/agent';

let state: AgentState = {
  messages: [{ role: 'user', content: 'hi' }],
  stepIndex: 0,
  usage: { inputTokens: 0, outputTokens: 0 },
};
for (let i = 0; i < 8; i++) {
  const r = await bot.step(state);
  state = r.state;
  if (r.done || r.suspend) break;
}
approveTool( state: AgentState, request: ToolApprovalRequest, approval: unknown, ): Promise<StepResult>

Execute or reject a pending approval-required tool call.

Resumes a run that suspended because a tool required approval. When approval is true or { approved: true } the tool executes with the original arguments; any other value records an error tool result of the form Tool call rejected: <reason>. Either way the outcome is appended to history as a tool-result message and the returned StepResult carries the advanced state, ready for the next step().

Throws if request.toolCallId is not the pending, unresolved tool call at the end of history — for example when it was already resolved or the conversation has moved on.

Sessions call this after validating a resume token. Application code should usually use Session.approveTool() or Session.rejectTool() instead so the decision and resulting tool output are checkpointed durably.

generate(input: string | RunInput): Promise<AgentResult>

Run until the agent reaches a stop condition, suspension, or error.

A plain string is wrapped as a single user message; pass a RunInput to supply multiple messages or an abort signal. The resolved AgentResult carries the final assistant text, the full message transcript, per-step states, accumulated usage and cost, and the final stop reason. When an output schema is configured the validated structured value is on result.object.

Throws GuardrailError when a guardrail blocks input or output, and rethrows the last provider error once retries and fallback models are exhausted.

const result = await bot.generate('Summarize the release notes.');
console.log(result.text);
console.log(result.usage.inputTokens, result.usage.outputTokens);
stream(input: string | RunInput): AgentStream

Start a streamed run.

The run begins immediately. The returned AgentStream exposes three views: reader yields every AgentEvent (model deltas, tool activity, retries, guardrails, suspension, and the final result); result resolves to the same AgentResult that generate() would return; and state is a signal holding a coarse AgentRunView for progress display without consuming the reader. Awaiting result without ever reading events is fine. A failed run rejects result and fails the reader with the same error.

const stream = bot.stream('Plan my week.');
for await (const ev of stream.reader) {
  if (ev.type === 'tool_start') console.log('calling', ev.name);
  if (ev.type === 'model_event' && ev.event.type === 'text_delta') {
    console.log(ev.event.text);
  }
}
const result = await stream.result;
asTool(opts: { name?: string; description: string; input?: SchemaBuilder | Record<string, unknown>; }): ReturnType<typeof tool>

Expose this agent as a tool for composition with another agent.

The wrapper tool runs generate() on this agent and returns the run's final text. Its name defaults to opts.name, then the agent's name, then 'agent'. input may be a fino:validate builder or plain JSON Schema describing the arguments the calling model should supply; when omitted the tool accepts an empty object. Non-empty arguments are JSON-stringified into the sub-agent's user message, and empty arguments become the prompt 'Please respond.'. The caller's abort signal is forwarded to the sub-agent run.

This is the simple composition primitive — for branching, checkpointing, or parallel orchestration, use fino:workflow instead.

import { agent } from 'fino:ai/agent';
import { openai } from 'fino:ai/model';
import { v } from 'fino:validate';

const researcher = agent({
  model: openai({ model: 'gpt-4o' }),
  name: 'researcher',
  instructions: 'Research the topic and report findings.',
});

const writer = agent({
  model: openai({ model: 'gpt-4o' }),
  instructions: 'Write articles. Use the researcher for facts.',
  tools: [researcher.asTool({
    description: 'Research a topic and return findings.',
    input: v.object({ topic: v.string().describe('Topic to research') }),
  })],
});

const article = await writer.generate('Write about deep-sea vents.');