Skip to main content

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 ↑
  1. 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.
  2. The runtime executes the requested tools.
  3. 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 })

callTools(calls)

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 })
  },
})

Parallel tool calls: Multi-issue summary

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

SituationUse self-managed?
Parallel tool callsYes
Complex state across multiple tool roundsYes
Full control over execution flowYes
Simple sequential logicNo — use auto-managed state
Task expressible as a prompt + toolsNo — 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.