agent
js/ai/agent.ts
fino:ai/agent — high-level model loop with tools and strategy-owned history.
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.
Design
History policy is intentionally external. The agent 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. Durable sessions, summarization, selective
retrieval, and memory emission are supplied by strategies and session stores,
not by hidden agent 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 stream = bot.stream('lookup account status');
for await (const text of streamText(stream)) console.log(text);
const result = await stream.result;
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 AgentOptions = Omit<AgentRuntimeOptions, 'history'> & {
history?: HistoryStrategy;
}
Options for Agent and agent().
model is the only required field. instructions becomes the system
prompt. tools and skills define what the model may call; a skill
registry adds a manifest of available skills to the instructions plus a
load_skill tool that pulls in a skill's instructions and tools on demand.
stopWhen takes one or more StopConditions and defaults to stopping
after eight model steps. output requests structured output — a
fino:validate builder or plain JSON Schema — surfaced as
AgentResult.object. retry, fallback, and guardrails control
resilience and content policy.
history defaults to an append-only in-memory strategy. Supplying a strategy
gives that strategy complete ownership of append and read-time curation —
summarization, selective retrieval, and durable persistence all live there.
import { agent, type AgentOptions } from 'fino:ai/agent';
import { anthropic, openai } from 'fino:ai/model';
import { maxSteps } from 'fino:ai/runtime';
const opts: AgentOptions = {
model: anthropic({ model: 'claude-sonnet-4-6' }),
instructions: 'You are a careful analyst.',
stopWhen: maxSteps(4),
fallback: [openai({ model: 'gpt-4o' })],
retry: { maxRetries: 2 },
};
const bot = agent(opts);
Classes
class Agent {
Runs an agent loop against a model.
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() are the usual entry points. step() and
approveTool() are lower-level hooks used by fino:ai/session for
durable, resumable runs that checkpoint between 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 from AgentOptions, installing the default append-only
in-memory history strategy when none is supplied.
Methods
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.');