> ## 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.

# Self-managed state agents

> Build event-driven agents with explicit state persistence and parallel tool calls.

`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

```typescript theme={null}
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()
  },
})
```

<Note>
  Self-managed state agents do **not** use the `"use agent"` directive. That directive is only for auto-managed state agents.
</Note>

<Note>
  The `description` field is optional and deprecated as of `@guildai/agents-sdk` 0.4.0. Guild generates the agent's published description automatically from its code, so setting `description` no longer affects the published description.
</Note>

## State persistence

Use `task.save()` and `task.restore()` to persist state between tool calls. State must conform to your `stateSchema`.

```typescript theme={null}
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.

```typescript theme={null}
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.

```typescript theme={null}
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.

```typescript theme={null}
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.

```typescript theme={null}
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.

```typescript theme={null}
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.

```typescript theme={null}
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](/guide/coded-agents) |
| Task expressible as a prompt + tools      | No — use [LLM agents](/guide/llm-agents)           |

<Warning>
  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.
</Warning>
