js/ai/runtime
js/ai/runtime.ts
fino:ai/runtime — shared agent integration helpers and result types.
This module exposes the small runtime surface that application adapters need
without exposing the internal agent loop implementation. It carries the
cross-cutting helpers — runContext, maxSteps(), streamText(), and the
guardrail and suspension error types — together with the public state and
result types shared by fino:ai/agent and fino:ai/session: run inputs and
results, per-step state, streaming events, retry policy, and tool-approval
requests. Import from here when writing integration code (UI adapters,
observability hooks, session stores) that consumes agent runs but does not
construct agents itself.
Boundary
Application code should create agents through fino:ai/agent. The runtime
module is intentionally not a harness API and does not export the internal
runtime class. This keeps the public integration layer stable while allowing
the agent loop internals to evolve.
runContext is an async context populated while an agent step or session is
driving work. Tools can read it for run metadata, but should use the explicit
ToolRunContext when possible.
import { agent } from 'fino:ai/agent';
import { openai } from 'fino:ai/model';
import { maxSteps, streamText } from 'fino:ai/runtime';
const bot = agent({
model: openai({ model: 'gpt-4o' }),
stopWhen: maxSteps(4),
});
for await (const text of streamText(bot.stream('Draft a title.'))) {
console.log(text);
}
Constants
const GuardrailError
Error thrown when an input or output guardrail blocks execution.
const runContext
Async context for the active agent or workflow run.
Functions
function maxSteps(n: number): StopCondition
Stop after n model steps.
function streamText(stream: AgentStream): AsyncGenerator<string>
Iterate over only text deltas from an agent stream.
Types
type AgentEvent = {
type: 'model_event';
event: StreamEvent;
} | {
type: 'step_start';
stepIndex: number;
model: string;
provider: string;
} | {
type: 'step_end';
stepIndex: number;
stopReason: StopReason;
model: string;
provider: string;
} | {
type: 'tool_start';
stepIndex: number;
id: string;
name: string;
} | {
type: 'tool_result';
stepIndex: number;
id: string;
name: string;
isError?: boolean;
} | {
type: 'tool_error';
stepIndex: number;
id: string;
name: string;
message: string;
} | {
type: 'retry';
attempt: number;
model: string;
provider: string;
delayMs: number;
} | {
type: 'fallback';
model: string;
provider: string;
} | {
type: 'guardrail';
stage: 'input' | 'output';
action: GuardrailResult['action'];
reason?: string;
} | {
type: 'suspend';
stepIndex: number;
reason?: string;
payload?: unknown;
} | {
type: 'final';
result: AgentResult;
}
Agent-level streaming event.
Provider StreamEvent values are wrapped in model_event. Lifecycle events
add run-loop context around model streaming, retries, fallback, tool
execution, guardrails, suspension, and final completion. Every run emits
step_start/step_end pairs per model turn and exactly one final event
(carrying the same AgentResult that AgentStream.result resolves with)
when it completes successfully.
for await (const ev of stream.reader) {
switch (ev.type) {
case 'model_event':
if (ev.event.type === 'text_delta') render(ev.event.text);
break;
case 'tool_start':
console.log(`running ${ev.name}`);
break;
case 'final':
console.log('cost:', ev.result.cost);
}
}
type StopCondition = (state: AgentState, info: {
stopReason: StopReason;
}) => boolean
Predicate that decides whether an agent run should stop.
Conditions are checked after every completed step, including tool-execution
steps; the run ends as soon as any configured condition returns true. When
no stopWhen is configured, the runtime defaults to maxSteps(8).
import type { StopCondition } from 'fino:ai/runtime';
const budgetCap: StopCondition = (state) => (state.cost ?? 0) > 0.5;
Interfaces
interface AgentResult {
Final result of an agent run.
text is the assistant text from the last completed step, messages is
the full conversation as rendered by the history strategy, and steps
records the state after each step for inspection. object is set only on
structured-output runs.
const result = await bot.generate('Name three sorting algorithms.');
console.log(result.text);
console.log(result.usage.inputTokens, result.usage.outputTokens);
console.log(result.stopReason); // 'end_turn'
Properties
text: string
Assistant text from the final step.
object?: unknown
Parsed structured output; present only when an output schema was set.
messages: ModelMessage[]
Full conversation, including tool calls and results.
steps: AgentState[]
Per-step state snapshots, in order.
usage: Usage
Token usage summed across all steps.
cost?: number
Estimated USD cost when pricing for the models used is known.
stopReason: StopReason
Stop reason from the final model turn.
interface AgentState {
Mutable state passed through one agent run.
Each step consumes a state and produces the next one: messages is the
model-facing conversation rendered by the history strategy, usage and
cost accumulate across steps, and stepIndex counts completed model
turns. Harnesses that drive the loop manually with step() keep threading
the returned state back in.
import type { AgentState } from 'fino:ai/runtime';
let state: AgentState = {
messages: [{ role: 'user', content: 'Summarize the report.' }],
stepIndex: 0,
usage: { inputTokens: 0, outputTokens: 0 },
};
Properties
messages: ModelMessage[]
Model-facing conversation, as rendered by the history strategy.
stepIndex: number
Number of completed model steps in this run.
usage: Usage
Token usage accumulated across all steps so far.
cost?: number
Running USD cost derived from usage and the pricing table, when known.
history?: MessageHistory
History handle backing messages; present once a step has run.
signal?: AbortSignal
Abort signal observed by model requests and tool executions.
interface AgentStream {
Stream handle returned by Agent.stream().
Offers three views of one run: reader yields every AgentEvent in order,
state is a retained signal holding a coarse AgentRunView snapshot, and
result settles with the final AgentResult. The run starts immediately;
if it throws, the reader fails with the error and result rejects.
const stream = bot.stream('Draft the changelog.');
for await (const ev of stream.reader) {
if (ev.type === 'model_event' && ev.event.type === 'text_delta') {
render(ev.event.text);
}
}
const result = await stream.result;
Properties
reader: Reader<AgentEvent>
Ordered stream of every agent event; closes when the run finishes.
result: Promise<AgentResult>
Settles with the final result; rejects if the run throws.
state: ReadonlySignal<AgentRunView>
Retained coarse view of the run, updated as events arrive.
interface GuardrailResult {
Result returned by an input or output guardrail.
allow passes content through unchanged. block aborts the step with a
GuardrailError. redact substitutes content: input guardrails supply
replacement messages, output guardrails supply replacement text. If the
relevant replacement field is omitted on a redact, the original content is
kept.
import type { GuardrailResult } from 'fino:ai/runtime';
const redacted: GuardrailResult = {
action: 'redact',
text: '[removed]',
reason: 'pii',
};
Properties
action: 'allow' | 'block' | 'redact'
Disposition: pass through, abort the step, or substitute content.
messages?: ModelMessage[]
Replacement conversation used when an input guardrail redacts.
text?: string
Replacement assistant text used when an output guardrail redacts.
reason?: string
Explanation surfaced on guardrail events and GuardrailError.reason.
interface Guardrails {
Optional input and output guardrails for an agent.
The input guardrail runs before every model request, after the history
strategy has produced the model-facing view. The output guardrail runs on
each step's assistant text and is skipped when the turn produced no text.
Both may be async, and each invocation emits a guardrail agent event with
the action taken.
import type { Guardrails } from 'fino:ai/runtime';
const guardrails: Guardrails = {
input: (messages) =>
messages.length > 200
? { action: 'block', reason: 'conversation too long' }
: { action: 'allow' },
output: (text) =>
/ssn:\S+/.test(text)
? { action: 'redact', text: text.replace(/ssn:\S+/g, '[redacted]') }
: { action: 'allow' },
};
Properties
input?: (messages: ModelMessage[]) => GuardrailResult | Promise<GuardrailResult>
Inspects the outgoing conversation before each model request.
output?: (text: string) => GuardrailResult | Promise<GuardrailResult>
Inspects each step's assistant text before it is recorded.
interface RetryOptions {
Retry policy for provider failures.
Retries apply per model before fallback advances: each model in
[model, ...fallback] gets the full retry budget. Delays use exponential
backoff with full jitter, capped at maxDelayMs, except when the provider
supplied an explicit retry-after delay, which is honored verbatim. Errors
thrown after streaming has begun are never retried — partial output cannot
be safely replayed.
const bot = agent({
model,
retry: { maxRetries: 3, baseDelayMs: 250 },
fallback: [backupModel],
});
Properties
maxRetries?: number
Additional attempts per model after the first failure. Default 0.
baseDelayMs?: number
Base backoff delay in milliseconds. Default 500.
maxDelayMs?: number
Upper bound on any backoff delay, in milliseconds. Default 30000.
retryOn?: (err: unknown) => boolean
Overrides which errors are retryable. By default only ModelErrors with
status 429 or 5xx are retried.
interface RunInput {
Input accepted by Agent.generate() and Agent.stream().
Messages are appended to the run's history before the first step. The signal, when provided, aborts in-flight model requests and is passed to tool executions.
import type { RunInput } from 'fino:ai/runtime';
const controller = new AbortController();
const input: RunInput = {
messages: [{ role: 'user', content: 'Plan the release.' }],
signal: controller.signal,
};
Properties
messages: ModelMessage[]
Conversation to append before the first model request.
signal?: AbortSignal
Cancels the run: model streaming and tool calls observe this signal.
interface StepResult {
Result of one agent step.
done is true when the model finished without requesting tool calls, so
the loop has nothing further to execute. When a tool suspended the step,
suspend carries the SuspendSignal (with its payload, such as a
ToolApprovalRequest) and the run should be checkpointed for later
resumption.
import type { StepResult } from 'fino:ai/runtime';
let r: StepResult = await agent.step(state);
while (!r.done && !r.suspend) r = await agent.step(r.state);
Properties
state: AgentState
State after the step, including appended messages and updated usage.
done: boolean
True when the model stopped without requesting tool calls.
suspend?: SuspendSignal
Present when a tool paused the run; resume via a session or approveTool().
stopReason: StopReason
Why the model stopped this turn.
interface StrategyMemorySink {
Minimal sink a history strategy can use to emit durable memory.
Matches the ingest surface of fino:ai/memory stores so strategies can
archive summarized or evicted conversation content without depending on a
concrete store type. Hosts such as sessions hand a sink to their strategy,
which calls it as part of compaction.
import type { StrategyMemorySink } from 'fino:ai/runtime';
async function archive(sink: StrategyMemorySink, summary: string) {
await sink.ingest([{ text: summary, metadata: { kind: 'summary' } }]);
}
Methods
ingest(docs: {
text: string;
metadata?: Record<string, unknown>;
}[]): Promise<void>
Stores the given documents durably.
interface ToolApprovalRequest {
Persisted approval request for a tool call that suspended before execution.
When a tool declares requiresApproval, the runtime does not execute it;
instead the step suspends with a SuspendSignal whose payload is this
shape. Sessions persist the payload and later pass it back to
approveTool() along with the human decision. risk and sideEffects
mirror the tool's own annotations so approval UIs can render them without
loading the tool.
import type { ToolApprovalRequest } from 'fino:ai/runtime';
const request = r.suspend?.payload as ToolApprovalRequest;
if (request?.type === 'tool_approval') {
console.log(`Approve ${request.toolName}?`, request.args);
}
Properties
type: 'tool_approval'
Discriminant identifying the suspend payload as a tool approval.
toolCallId: string
Id of the pending tool_use part awaiting a result.
toolName: string
Name of the tool the model asked to run.
args: unknown
Arguments the model supplied for the call.
risk?: string
Tool-declared risk annotation, for approval UIs.
sideEffects?: boolean
Whether the tool declares side effects.