Running Agents
Execute agents with run(), stream(), or prompt()
Three ways to execute an agent. All handle the full tool loop, guardrails, hooks, and tracing automatically.
run()-- Returns the final result. Best when you don't need real-time output.stream()-- Yields events as they arrive. Best for real-time UIs and CLIs.prompt()-- One-shot convenience. Single turn in, result out.
run()
run() takes an agent and input, executes the full tool loop, and returns a RunResult when done. You get no intermediate output -- just the final result.
import { Agent, run } from "@usestratus/sdk/core";
import { AzureResponsesModel } from "@usestratus/sdk";
const model = new AzureResponsesModel({
endpoint: process.env.AZURE_ENDPOINT!,
apiKey: process.env.AZURE_API_KEY!,
deployment: "gpt-5.2",
});
const agent = new Agent({
name: "assistant",
model,
instructions: "You are a helpful assistant.",
});
const result = await run(agent, "What is the capital of France?");
console.log(result.output); // "The capital of France is Paris."
console.log(result.messages); // Full message history (system, user, assistant, tool)
console.log(result.lastAgent); // The agent that produced the final response
console.log(result.usage); // { promptTokens, completionTokens, totalTokens }
console.log(result.finishReason); // "stop"
console.log(result.finalOutput); // undefined (no outputType set)If the agent has an outputType, finalOutput contains the parsed and validated object. See Structured Output for details.
RunResult
| Property | Type | Description |
|---|---|---|
output | string | Raw text output from the last model response |
finalOutput | TOutput | Parsed structured output (if outputType is set on the agent) |
messages | ChatMessage[] | Full message history for the run, including system, user, assistant, and tool messages |
usage | UsageInfo | Accumulated token usage across all model calls in this run |
lastAgent | Agent | The agent that produced the final response (differs from the entry agent after a handoff) |
finishReason | FinishReason? | The model's finish reason from the last call ("stop", "tool_calls", "length", "content_filter") |
numTurns | number | Number of model calls made during the run |
totalCostUsd | number | Estimated cost in USD (requires costEstimator in options, otherwise 0) |
responseId | string? | The response ID from the last model call (when using store: true) |
inputGuardrailResults | GuardrailRunResult[] | Results from input guardrails that ran during this execution |
outputGuardrailResults | GuardrailRunResult[] | Results from output guardrails that ran during this execution |
UsageInfo includes promptTokens, completionTokens, totalTokens, optional cacheReadTokens, cacheCreationTokens, and reasoningTokens fields.
toInputList()
RunResult has a toInputList() method that returns the message history without system messages. Use it to chain one run's output as input to another:
const result1 = await run(agent1, "Research this topic");
const result2 = await run(agent2, result1.toInputList()); stream()
stream() returns two things: an async generator of StreamEvent objects and a Promise<RunResult>. You must drain the stream before awaiting the result.
import { Agent, stream } from "@usestratus/sdk/core";
const agent = new Agent({
name: "writer",
model,
instructions: "You are a creative writer.",
});
const { stream: s, result } = stream(agent, "Write a haiku about TypeScript");
for await (const event of s) {
if (event.type === "content_delta") {
process.stdout.write(event.content);
}
}
const finalResult = await result;
console.log(finalResult.output);
console.log(finalResult.usage);You must fully consume the stream before awaiting result. If you skip the stream, the result promise never resolves.
Stream Events
| Event | Fields | Description |
|---|---|---|
content_delta | content: string | A chunk of text content from the model |
tool_call_start | toolCall: { id, name } | A tool call has started |
tool_call_delta | toolCallId, arguments | Incremental tool call argument data |
tool_call_done | toolCallId | Tool call arguments are complete |
hosted_tool_call | toolType, status | A built-in tool is executing server-side |
done | response: ModelResponse | The model finished a response |
When the model makes tool calls, you see multiple rounds of events. Each round starts with tool call events, followed by content events after the tools execute and the model responds again.
for await (const event of s) {
switch (event.type) {
case "tool_call_start":
console.log(`Calling: ${event.toolCall.name}`);
break;
case "content_delta":
process.stdout.write(event.content);
break;
case "done":
// One 'done' per model call - multiple if tools are used
console.log(`Tokens: ${event.response.usage?.totalTokens}`);
break;
}
}prompt()
prompt() is the simplest way to get a response. It creates a temporary session, sends your message, drains the stream, and returns the result.
import { prompt } from "@usestratus/sdk/core";
const result = await prompt("What is 2 + 2?", {
model,
instructions: "You are a math tutor.",
tools: [calculator],
});
console.log(result.output); // "4"prompt() creates a temporary session under the hood. For multi-turn conversations, use createSession() instead.
prompt() accepts the same configuration options as createSession() -- including tools, instructions, outputType, guardrails, and hooks.
Options
run() and stream() accept an optional RunOptions object as the third argument:
| Option | Type | Default | Description |
|---|---|---|---|
context | TContext | undefined | Shared context object passed to instructions, tools, guardrails, and hooks |
maxTurns | number | 10 | Maximum number of model calls before throwing MaxTurnsExceededError |
signal | AbortSignal | undefined | Abort signal for cancellation. Throws RunAbortedError when aborted |
model | Model | Agent's model | Override the agent's model for this run |
costEstimator | CostEstimator | undefined | Function that converts UsageInfo to a dollar cost. Enables totalCostUsd on the result |
maxBudgetUsd | number | undefined | Maximum dollar budget. Throws MaxBudgetExceededError when exceeded. Requires costEstimator |
runHooks | RunHooks | undefined | Run-level hooks that fire across all agents |
toolErrorFormatter | ToolErrorFormatter | undefined | Custom formatter for tool error messages sent to the LLM |
callModelInputFilter | CallModelInputFilter | undefined | Transform model requests before they're sent to the API |
errorHandlers | { maxTurns? } | undefined | Graceful error handlers. maxTurns returns a RunResult instead of throwing |
toolInputGuardrails | ToolInputGuardrail[] | undefined | Tool guardrails that run before tool execution |
toolOutputGuardrails | ToolOutputGuardrail[] | undefined | Tool guardrails that run after tool execution |
resetToolChoice | boolean | undefined | Reset toolChoice to "auto" after the first LLM call to prevent infinite loops |
allowedTools | string[] | undefined | Restrict which tools are available. Supports glob wildcards (e.g. "mcp__github__*"). See Allowed tools. |
canUseTool | CanUseTool | undefined | Permission callback invoked before any tool executes. See Tool permissions. |
dynamicSubagents | SubAgent[] | undefined | Additional subagents available at runtime beyond those defined on the agent |
debug | boolean | false | Log model calls, tool executions, and handoffs to stderr. See Testing. |
const ac = new AbortController();
setTimeout(() => ac.abort(), 10_000);
const result = await run(agent, "Summarize this document", {
context: { userId: "user_123", db: myDatabase },
maxTurns: 5,
signal: ac.signal,
});toolErrorFormatter
Customize the error message sent to the model when a tool throws:
await run(agent, input, {
toolErrorFormatter: (toolName, error) => {
return `Tool "${toolName}" failed: ${error instanceof Error ? error.message : String(error)}`;
},
});callModelInputFilter
Transform model requests before they're sent to the API. Useful for logging, redacting, or modifying messages:
await run(agent, input, {
callModelInputFilter: ({ agent, request, context }) => {
console.log(`Sending ${request.messages.length} messages to ${agent.name}`);
return request; // Return modified or original request
},
});errorHandlers.maxTurns
Handle max turns gracefully instead of throwing MaxTurnsExceededError:
await run(agent, input, {
maxTurns: 3,
errorHandlers: {
maxTurns: ({ agent, messages, context, maxTurns }) => {
return new RunResult({
output: "I need more time to complete this task.",
messages,
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
lastAgent: agent,
});
},
},
});resetToolChoice
When using toolChoice: "required" or a specific function, the model is forced to call a tool on every turn — which can cause infinite loops. Set resetToolChoice: true to reset to "auto" after the first model call:
const agent = new Agent({
name: "assistant",
model,
tools: [getWeather],
modelSettings: { toolChoice: "required" },
});
await run(agent, "What's the weather?", {
resetToolChoice: true,
});allowedTools
Restrict which tools the agent can use for a specific run. Supports exact names and glob-style wildcards with a trailing *:
// Only allow MCP GitHub tools
await run(agent, "Search for issues", {
allowedTools: ["mcp__github__*"],
});
// Allow specific tools by name
await run(agent, "Get weather and calculate", {
allowedTools: ["get_weather", "calculate"],
});
// Empty array = no tools (agent responds with text only)
await run(agent, "Hello", {
allowedTools: [],
});allowedTools filters tools, handoffs, and subagents. Only items whose names match at least one pattern are included in the request sent to the model.
canUseTool
A centralized permission callback invoked before any tool executes. Return { behavior: "allow" } to proceed or { behavior: "deny", message } to block. The deny message is sent to the model so it can adjust its approach.
import type { CanUseTool } from "@usestratus/sdk/core";
const canUseTool: CanUseTool = async (toolName, input, context) => {
// Ask the user for permission
const approved = await promptUser(`Allow ${toolName}?`);
if (!approved) {
return { behavior: "deny", message: "User rejected this action" };
}
return { behavior: "allow" };
};
await run(agent, "Delete the file", { canUseTool });You can also modify the tool's input before it executes:
const canUseTool: CanUseTool = async (toolName, input) => ({
behavior: "allow",
updatedInput: { ...input, safe_mode: true },
});canUseTool takes precedence over per-tool needsApproval. If canUseTool denies a tool that has needsApproval: true, the run will not be interrupted — it will be denied immediately.
interrupt()
stream() returns an interrupt() function for gracefully stopping a run. Unlike AbortSignal (which throws), interrupt() lets the current model call or tool execution finish, then returns a partial RunResult.
const { stream: s, result, interrupt } = stream(agent, "Do a complex task", {
maxTurns: 20,
});
for await (const event of s) {
if (event.type === "done") {
if (shouldStop()) {
interrupt();
}
}
}
const r = await result; // Resolves normally with partial result
console.log(r.numTurns); // Number of turns completed before interruptinterrupt() is checked between turns. If called during a model call, the current call finishes before the run stops. Calling interrupt() multiple times is safe and idempotent.
Passing input
You can pass input as a plain string, an array of ChatMessage objects, or a ContentPart[] array for multimodal content.
The most common form. Stratus wraps it in a user message automatically.
const result = await run(agent, "Hello, world!");Pass a full message array when you need to prefill conversation history or include system messages:
import type { ChatMessage } from "@usestratus/sdk/core";
const messages: ChatMessage[] = [
{ role: "user", content: "My name is Alice." },
{ role: "assistant", content: "Hello Alice! How can I help?" },
{ role: "user", content: "What is my name?" },
];
const result = await run(agent, messages);Use ContentPart[] inside a message to send images alongside text:
import type { ChatMessage, ContentPart } from "@usestratus/sdk/core";
const messages: ChatMessage[] = [
{
role: "user",
content: [
{ type: "text", text: "What is in this image?" },
{ type: "image_url", image_url: { url: "https://example.com/photo.png" } },
],
},
];
const result = await run(agent, messages);Multi-turn with sessions
run() and stream() are stateless -- they don't preserve messages between calls. For multi-turn conversations, use sessions:
import { createSession } from "@usestratus/sdk/core";
await using session = createSession({
model,
instructions: "You are a helpful assistant.",
tools: [getWeather],
});
session.send("What's the weather in NYC?");
for await (const event of session.stream()) {
if (event.type === "content_delta") process.stdout.write(event.content);
}
session.send("What about London?");
for await (const event of session.stream()) {
if (event.type === "content_delta") process.stdout.write(event.content);
}See Sessions for the full API, including save/resume/fork and Symbol.asyncDispose.
Next steps
Last updated on