Agents, handoffs & sessions
defineAgent() bundles instructions, model, tools, and optional handoffs into one reusable unit. The run loop owns tool execution and multi-turn orchestration itself, so behavior is identical no matter which provider backs each agent.
Defining and delegating between agents
Any agent listed in another agent's handoffs becomes callable as a synthetic tool the model can invoke like any other tool call. The run loop intercepts these before normal tool execution, switches the active agent, and carries the full message history forward — the new agent sees everything that happened before the handoff.
import { z } from "zod";
import { createClient, anthropic, defineAgent, runAgent } from "samai-sdk";
const client = createClient({ provider: anthropic({ apiKey: "..." }) });
const getWeather = {
name: "get_weather",
description: "Get the current weather for a city",
parameters: z.object({ city: z.string() }),
execute: async ({ city }: { city: string }) => `18C and cloudy in ${city}`,
};
const packingAgent = defineAgent({
name: "packing_specialist",
instructions: "Give concise packing advice based on the weather already in the conversation.",
model: "claude-sonnet-4-6",
});
const routerAgent = defineAgent({
name: "trip_router",
instructions: "Look up weather with get_weather, then hand off to packing_specialist.",
model: "claude-sonnet-4-6",
tools: [getWeather],
handoffs: [packingAgent], // <- agents this one is allowed to delegate to
});
const result = await runAgent(client, routerAgent, "What should I pack for Tokyo?");
console.log(result.output); // packing_specialist's final answer
console.log(result.finalAgent); // "packing_specialist" — may differ from the starting agent
console.log(result.trace.agentPath); // ["trip_router", "packing_specialist"]Streaming events
runAgent() drains the run and returns the final result. For live updates — driving a chat UI, say — use runAgentStream() directly and consume its events:
import { runAgentStream } from "samai-sdk";
for await (const event of runAgentStream(client, routerAgent, "What should I pack for Tokyo?")) {
switch (event.type) {
case "text-delta": process.stdout.write(event.textDelta); break;
case "tool-started": console.log("calling", event.toolName, event.args); break;
case "tool-completed": console.log("tool result", event.result); break;
case "handoff-started": console.log(`${event.fromAgent} -> ${event.toAgent}: ${event.reason}`); break;
case "guardrail-triggered": console.warn(`${event.stage} guardrail blocked: ${event.reason}`); break;
case "run-completed": console.log("done", event.usage); break;
case "run-failed": console.error(event.error); break;
}
}Loop prevention
To stop infinite delegation (A → B → A → B → ...), the run loop tracks every agent visited during a run: handing off to an already-visited agent throws HandoffLoopError, and a hard maxHandoffs cap (default 5, override via runAgent(client, agent, input, { maxHandoffs: 10 })) catches runaway delegation even across distinct agents. Both are wrapped in an AgentRunError that also carries the trace collected up to the point of failure:
try {
await runAgent(client, routerAgent, input);
} catch (err) {
if (err instanceof AgentRunError) {
console.error(err.cause); // HandoffLoopError, MaxTurnsExceededError, etc.
console.error(err.trace.events); // full trace up to the failure point
}
}Sessions (memory)
A Session persists conversation history across separate runAgent() calls — kept deliberately distinct from defineAgent()'s static config and the transient message list a single run builds internally.
import {
createSession,
InMemorySessionStore,
FileSessionStore,
RedisSessionStore,
SqliteSessionStore,
} from "samai-sdk";
// In-memory — lives for the process lifetime, good for scripts/tests
const session = createSession("user-123", new InMemorySessionStore());
// Or persist to disk as JSON — survives process restarts, no extra infra
const fileSession = createSession("user-123", new FileSessionStore("./sessions"));
// Or persist to Redis — shared across processes/instances, with optional TTL
// Requires the optional `ioredis` peer dependency
const redisSession = createSession(
"user-123",
new RedisSessionStore({ url: process.env.REDIS_URL, ttlSeconds: 60 * 60 * 24 })
);Tracing is always on
runAgent() call produces a RunTrace for free — see Reliability & tracing for exporting it to OpenTelemetry or rendering it as an HTML timeline.