AutomaticallyManagedStateAgent lets the runtime serialize and restore your function’s state transparently — you write a normal async function and don’t think about it. SelfManagedStateAgent gives you explicit control: you decide what to save, when to save it, and how to resume.
Use coded agents when you need precise control, predictable costs, or algorithmic logic.
AutomaticallyManagedStateAgent
The recommended starting point. Write a normalasync run function with the "use agent" directive, and the runtime serializes and restores state for you automatically.
Input and output schemas
Define your schemas using Zod. The runtime uses them to validate input and expose the agent as a typed tool for orchestrating agents.Error handling
Any exception thrown from yourrun function is returned to the calling agent or user. Use standard TypeScript error handling:
SelfManagedStateAgent
An event-driven state machine where you control what gets saved and when. Harder to implement thanAutomaticallyManagedStateAgent, but has no runtime constraints and works without the "use agent" directive.
Use it when:
- You need to make parallel tool calls (the babel plugin forbids
Promise.allacrossawaitpoints) - You want explicit control over what gets saved and when
- You’re building a state machine that doesn’t map cleanly to a procedural
runfunction
Anatomy
A self-managed agent declares callbacks instead of arun function:
| Field | Required | Description |
|---|---|---|
stateSchema | yes | Zod schema for the state the agent persists via task.save() |
init | no | Runs immediately before start and before every onToolResults call. Use it to mutate the tool set (e.g., add workspace agents) |
start(input, task) | yes | Initial entry-point. Returns an AgentResult — either output(...) to finish, or callTools(...) / ask(...) to request tool execution |
onToolResults(results, task) | yes if start may call tools | Resumes the agent after tool results arrive. Returns another AgentResult |
AgentResult, built with one of three helpers:
| Helper | Returns | Use for |
|---|---|---|
output(value) | OutputResult | Agent is done — value is the final output |
callTools(calls) | ToolCallsResult | Request the runtime to execute one or more tool calls in parallel, then invoke onToolResults with the results |
ask(prompt) | ToolCallsResult (via ui_prompt) | Shortcut for a single callTools([...]) that asks the user for text input |
start or onToolResults invocation run in parallel; the runtime resolves all of them before calling onToolResults.
Example: marco-polo
A classic state-machine agent that pings back and forth with the user until they stop saying “marco”:startsaves{ count: 1 }and returnsask("polo!"). The runtime asks the user “polo!” and suspends the agent.- When the user replies, the runtime calls
onToolResultswith theui_promptresult. - If the user said “marco” again,
onToolResultsrestores the state, incrementscount, saves, and asks again viaask("polo!"). Otherwise it returnsoutput({ count })and the agent finishes.
The init callback
init runs immediately before start and before every resumption via onToolResults. It’s the right place to mutate the tool set — for example, to add workspace agents as tools, the way llmAgent does internally:
When to use each type
| Situation | Recommended type |
|---|---|
| Task can be described as a prompt + tools | llmAgent |
| Algorithmic, deterministic logic | AutomaticallyManagedStateAgent |
| Need parallel tool calls or complex state | SelfManagedStateAgent |
| Want to minimize LLM costs | AutomaticallyManagedStateAgent or SelfManagedStateAgent |
Performance tips
- LLM calls are expensive. Store the result of
task.llm.generateText()in a variable if you need it more than once. - Batch operations. Group related API calls to reduce round-trips.
- Use progress logs. Keep users informed during long-running operations with
task.ui?.notify(progressLogNotifyEvent(...)).