The builder is XSAF’s primary API. It records configuration without performing I/O, then creates one XsafAgent runtime when started.
Required agent configuration
interface AgentConfig {
name?: string;
description?: string;
model: string;
baseURL: string;
apiKey: string;
persona: string;
maxSteps?: number;
stream?: boolean;
reasoning?: "none" | "low" | "high";
modelAdapter?: XsafModelAdapter;
scheduler?: XsafSchedulerDriver;
}model, baseURL, apiKey, and persona must be nonblank. Optional agent names must be lowercase snake_case; descriptions must be nonblank when provided. maxSteps defaults to 3, stream to true, and reasoning to "none". Invalid required configuration throws during .agent() rather than during startup.
Configure before startup
const builder = xsaf
.agent(config)
.sandbox(sandbox)
.tool(tool)
.delegate(child)
.mcp(connection)
.memory(memory)
.channel(channel)
.serve({ transport: "http", path: "/mcp" })
.schedule(schedule)
.on("message.sent", (event) => {
console.log(event.sessionId);
})
.approve(approvalHandler);Builder methods return the same builder for chaining. Names must be unique within their category, and local tools and delegates share a model-visible name namespace. Only one memory driver can be registered.
Start and stop
const agent = await builder.start();
try {
const result = await agent.invoke("Summarize this", "session-1");
if ("text" in result) console.log(result.text);
} finally {
await builder.stop();
}- Concurrent
.start()calls coalesce. - Startup failure closes resources that already started.
.stop()is idempotent and waits for in-progress startup.- Shutdown runs sequentially in reverse registration order.
- All close callbacks are attempted. Multiple failures are reported as an
AggregateError.
A started builder exposes .invoke(), .run(), .ask(), .app, and .fetch(). Calling these through the builder before startup throws.
Seal a reusable agent
const researcher = xsaf
.agent({
...researchConfig,
name: "researcher",
description: "Research a focused question",
})
.asAgent();.asAgent() seals configuration without starting the agent and uses its configured name and description. A sealed agent can be delegated to another agent. For alpha compatibility, .asAgent(name, description) can override the configured identity. The name must be lowercase snake_case. Tools, drivers, delegates, and schedules cannot be changed after sealing. Event and privileged approval handlers may still be registered with .on() and .approve().
Unified request path
XSAF turns each inbound prompt into a ModelRequest. The request includes persona and session history, the configured model endpoint, model-visible tools, step limits, and reasoning effort.
The same request path handles:
agent.invoke(), orbuilder.invoke()and itsbuilder.run()alias- messages received by channel drivers
- scheduled prompts
- child-agent delegation
- Hono’s
POST /invoke
This keeps tool policy, memory ordering, events, and model behavior consistent across entry points.
Direct invocation
const result = await agent.invoke("Hello", "customer-42");On XsafBuilder, .run() is an alias for .invoke(); XsafAgent exposes .invoke() directly. Session ID defaults to "default" for direct invocation. Non-streaming calls resolve to:
interface AgentResult {
text: string;
usage?: Readonly<Record<string, number>>;
}Streaming calls resolve to an AgentStreamResult; see Sessions & Memory.
Model adapter contract
interface XsafModelAdapter {
generate(request: ModelRequest): Promise<ModelResponse>;
stream?(request: ModelRequest): ModelStreamResponse | Promise<ModelStreamResponse>;
ask?<Output>(request: ModelRequest, schema: StandardSchemaV1<unknown, Output>): Promise<Output>;
}Omitting modelAdapter uses XsaiModelAdapter, which calls xsAI’s text, streaming, and structured-output APIs. If streaming is enabled but a custom adapter has no stream() method, XSAF falls back to generate().
interface ModelRequest {
model: string;
baseURL: string;
apiKey: string;
messages: readonly Message[];
tools: readonly ModelTool[];
maxSteps: number;
reasoning: "none" | "low" | "high";
}The persona appears as a system message in each model request, but it is not stored in session memory. Local tools, delegated agents, and discovered MCP tools are normalized into the same ModelTool shape.
Structured output
const value = await agent.ask("Return a project summary", outputSchema, "customer-42");The configured model adapter must implement ask(). XSAF saves the user prompt and the JSON-stringified result to memory. The output schema here is a Standard Schema validator; model-visible tool schemas have an additional JSON Schema requirement.
Error boundaries
Model and memory failures reject the request. Tool errors follow the configured retry and onError policy. Event-handler failures are isolated from runtime state.
Read-only registries
agent.channels and builder.channels expose ReadonlyMap views. Drivers remain structurally typed, but callers cannot mutate runtime registries through those views.