Documentation Index
Fetch the complete documentation index at: https://docs.guild.ai/llms.txt
Use this file to discover all available pages before exploring further.
SelfManagedStateAgent is an event-driven state machine. Instead of a single run function, you implement two callbacks — start and onToolResults — and manage state explicitly with task.save() and task.restore().
It’s harder to implement than auto-managed state agents, but has no runtime constraints. Use it when you need parallel tool calls, custom state persistence, or full control over execution flow.
Lifecycle
Input
↓
start(input, task)
├─ return output(...) → Done
└─ return callTools([...]) → Runtime executes tools
↓
onToolResults(results, task)
├─ return output(...) → Done
└─ return callTools([...]) → Loop back ↑
start is called once with the agent’s input. Save any state you’ll need later, then return output() to finish or callTools() to request tool execution.
- The runtime executes the requested tools.
onToolResults is called with the results. Restore your state, process the results, and return output() to finish or callTools() to continue the loop.
Basic structure
import { agent, output, callTools } from "@guildai/agents-sdk"
import { z } from "zod"
export default agent({
description: "What the agent does",
inputSchema: z.object({ /* ... */ }),
outputSchema: z.object({ /* ... */ }),
stateSchema: z.object({ /* ... */ }),
tools: { /* ... */ },
start: async (input, task) => {
// Called once with the input
// Return output() or callTools()
},
onToolResults: async (results, task) => {
// Called after each round of tool execution
// Return output() or callTools()
},
})
Self-managed state agents do not use the "use agent" directive. That directive is only for auto-managed state agents.
State persistence
Use task.save() and task.restore() to persist state between tool calls. State must conform to your stateSchema.
const stateSchema = z.object({
step: z.enum(["fetching", "summarizing", "done"]),
issueBody: z.string().optional(),
})
// In start:
await task.save({ step: "fetching", issueBody: undefined })
return callTools([...])
// In onToolResults:
const state = await task.restore() // Returns State | undefined
task.restore() returns undefined if no state has been saved yet.
Return types
Every callback must return one of two results:
output(value)
Completes the agent and returns the output value.
import { output } from "@guildai/agents-sdk"
return output({ summary: "All done", count: 3 })
Requests one or more tool calls. The runtime executes them and calls onToolResults with the results.
import { callTools } from "@guildai/agents-sdk"
return callTools([
{ toolName: "github_issues_get", args: { owner: "myorg", repo: "myrepo", issue_number: 1 } },
{ toolName: "github_issues_get", args: { owner: "myorg", repo: "myrepo", issue_number: 2 } },
])
Returning multiple tool calls executes them in parallel — this is the primary advantage over auto-managed state agents.
ask(prompt)
A shorthand for prompting the user. Wraps callTools with the ui_prompt tool.
import { ask } from "@guildai/agents-sdk"
return ask("Which language should I use?")
Examples
Interactive agent: Marco Polo
A simple game that demonstrates the full save/restore loop with user interaction.
import {
agent,
ask,
assert,
output,
pick,
userInterfaceTools,
} from "@guildai/agents-sdk"
import { z } from "zod"
const tools = { ...pick(userInterfaceTools, ["ui_prompt"]) }
export default agent({
description: "Plays marco-polo until you get bored.",
inputSchema: z.object({
message: z.string().describe("Say 'marco'"),
}),
outputSchema: z.object({
count: z.number().describe("The number of rounds played"),
}),
stateSchema: z.object({
count: z.number().describe("Running count of marco-polo rounds"),
}),
tools,
start: async (input, task) => {
if (input.message !== "marco") {
return output({ count: 0 })
}
await task.save({ count: 1 })
return ask("polo!")
},
onToolResults: async (results, task) => {
assert(results.length === 1)
const result = results[0]
assert(result.toolName === "ui_prompt")
const { text } = result.output
const state = await task.restore()
assert(state !== undefined)
if (text === "marco") {
await task.save({ count: state.count + 1 })
return ask("polo!")
}
return output({ count: state.count })
},
})
Fetch multiple GitHub issues in parallel and summarize them — something auto-managed state agents can’t do in a single round.
import {
agent,
assert,
callTools,
output,
pick,
progressLogNotifyEvent,
userInterfaceTools,
} from "@guildai/agents-sdk"
import { gitHubTools } from "@guildai-services/guildai~github"
import { z } from "zod"
const tools = {
...userInterfaceTools,
...pick(gitHubTools, ["github_issues_get"]),
}
export default agent({
description: "Fetches multiple GitHub issues in parallel and summarizes them.",
inputSchema: z.object({
repo: z.string().describe("Repository in 'owner/repo' format"),
issues: z.array(z.number()).describe("Issue numbers to summarize"),
}),
outputSchema: z.object({
summaries: z.array(
z.object({
issue_number: z.number(),
title: z.string(),
summary: z.string(),
})
),
}),
stateSchema: z.object({
owner: z.string(),
repo: z.string(),
issueNumbers: z.array(z.number()),
}),
tools,
start: async (input, task) => {
const [owner, repo] = input.repo.split("/")
await task.ui?.notify(progressLogNotifyEvent("Fetching issues..."))
// Save state so onToolResults knows what we requested
await task.save({ owner, repo, issueNumbers: input.issues })
// Fetch all issues in parallel
return callTools(
input.issues.map((issue_number) => ({
toolName: "github_issues_get" as const,
args: { owner, repo, issue_number },
}))
)
},
onToolResults: async (results, task) => {
const state = await task.restore()
assert(state !== undefined)
await task.ui?.notify(progressLogNotifyEvent("Summarizing..."))
const summaries = await Promise.all(
results.map(async (result, i) => {
const issue = result.output
const llmResult = await task.llm.generateText({
prompt: `Summarize this GitHub issue in one sentence:\n\n${issue?.body}`,
})
return {
issue_number: state.issueNumbers[i],
title: issue?.title ?? "Unknown",
summary: llmResult.text,
}
})
)
return output({ summaries })
},
})
Error handling
Throw an error from start or onToolResults to fail the agent. Tool errors arrive in the results array as objects with an error property.
onToolResults: async (results, task) => {
for (const result of results) {
if ("error" in result) {
throw new Error(`Tool ${result.toolName} failed: ${result.error}`)
}
}
// Process successful results...
}
When to use self-managed state
| Situation | Use self-managed? |
|---|
| Parallel tool calls | Yes |
| Complex state across multiple tool rounds | Yes |
| Full control over execution flow | Yes |
| Simple sequential logic | No — use auto-managed state |
| Task expressible as a prompt + tools | No — use LLM agents |
The runtime only supports @guildai/agents-sdk and zod. You cannot import external npm packages or Node.js built-in modules — agents run in a sandboxed environment.