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.');