stratus

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.

run.ts
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

PropertyTypeDescription
outputstringRaw text output from the last model response
finalOutputTOutputParsed structured output (if outputType is set on the agent)
messagesChatMessage[]Full message history for the run, including system, user, assistant, and tool messages
usageUsageInfoAccumulated token usage across all model calls in this run
lastAgentAgentThe agent that produced the final response (differs from the entry agent after a handoff)
finishReasonFinishReason?The model's finish reason from the last call ("stop", "tool_calls", "length", "content_filter")
numTurnsnumberNumber of model calls made during the run
totalCostUsdnumberEstimated cost in USD (requires costEstimator in options, otherwise 0)
responseIdstring?The response ID from the last model call (when using store: true)
inputGuardrailResultsGuardrailRunResult[]Results from input guardrails that ran during this execution
outputGuardrailResultsGuardrailRunResult[]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:

chaining.ts
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.

stream.ts
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

EventFieldsDescription
content_deltacontent: stringA chunk of text content from the model
tool_call_starttoolCall: { id, name }A tool call has started
tool_call_deltatoolCallId, argumentsIncremental tool call argument data
tool_call_donetoolCallIdTool call arguments are complete
hosted_tool_calltoolType, statusA built-in tool is executing server-side
doneresponse: ModelResponseThe 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.

multi-round-events.ts
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.

prompt.ts
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:

OptionTypeDefaultDescription
contextTContextundefinedShared context object passed to instructions, tools, guardrails, and hooks
maxTurnsnumber10Maximum number of model calls before throwing MaxTurnsExceededError
signalAbortSignalundefinedAbort signal for cancellation. Throws RunAbortedError when aborted
modelModelAgent's modelOverride the agent's model for this run
costEstimatorCostEstimatorundefinedFunction that converts UsageInfo to a dollar cost. Enables totalCostUsd on the result
maxBudgetUsdnumberundefinedMaximum dollar budget. Throws MaxBudgetExceededError when exceeded. Requires costEstimator
runHooksRunHooksundefinedRun-level hooks that fire across all agents
toolErrorFormatterToolErrorFormatterundefinedCustom formatter for tool error messages sent to the LLM
callModelInputFilterCallModelInputFilterundefinedTransform model requests before they're sent to the API
errorHandlers{ maxTurns? }undefinedGraceful error handlers. maxTurns returns a RunResult instead of throwing
toolInputGuardrailsToolInputGuardrail[]undefinedTool guardrails that run before tool execution
toolOutputGuardrailsToolOutputGuardrail[]undefinedTool guardrails that run after tool execution
resetToolChoicebooleanundefinedReset toolChoice to "auto" after the first LLM call to prevent infinite loops
allowedToolsstring[]undefinedRestrict which tools are available. Supports glob wildcards (e.g. "mcp__github__*"). See Allowed tools.
canUseToolCanUseToolundefinedPermission callback invoked before any tool executes. See Tool permissions.
dynamicSubagentsSubAgent[]undefinedAdditional subagents available at runtime beyond those defined on the agent
debugbooleanfalseLog model calls, tool executions, and handoffs to stderr. See Testing.
options.ts
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:

error-formatter.ts
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:

input-filter.ts
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:

max-turns-handler.ts
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:

reset-tool-choice.ts
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 *:

allowed-tools.ts
// 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.

can-use-tool.ts
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:

modify-input.ts
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.

interrupt.ts
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 interrupt

interrupt() 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:

multi-turn.ts
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

Edit on GitHub

Last updated on

On this page