> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frogbot.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agents

> Build tool-using loops for local and HTTP calls.

Agents wrap AI SDK v7 `ToolLoopAgent` with FrogBot model resolution, request context, access, and hooks. Register code-defined `AgentConfig` objects in the root configuration.

***

## Define one

Provide a URL-safe `slug` and instructions. Set `model` to a provider-prefixed model ID or router alias for a single choice, use the object form for selectable models, or omit it to use `ai.defaultModel`.

```ts theme={"theme":"tokyo-night"}
import { isStepCount } from 'frogbot'
import type { AgentConfig } from 'frogbot'

export const supportAgent: AgentConfig = {
  slug: 'support',
  model: 'fast',
  instructions: 'Answer support questions and use tools when needed.',
  stopWhen: isStepCount(8),
  access: ({ req }) => Boolean(req.user)
}
```

### Agent options

| Option | Type | Default | Description |
| - | - | - | - |
| `slug` | `string` | Required | Unique, URL-safe identifier. It names the agent in `frogbot.agents[slug]`, the HTTP routes, and generated types. |
| `model` | `ModelId \| AgentModelOptions` | `ai.defaultModel` | A model ID or router slug for a single choice, or [model options](#model-options) for a selectable list. Startup fails when neither `model` nor `ai.defaultModel` sets a default. |
| `instructions` | `string` | Required | System instructions for every run. Must be non-empty. Agents with [skills](#add-skills) get one discovery line per skill appended. |
| `profile` | `AgentProfile` | — | Client-facing identity. See [Profile options](#profile-options). |
| `channels` | `ChannelPieceInstance[]` | — | Channel-capable piece instances, such as [Slack](https://docs.frogbot.ai/pieces/slack.md), that the agent answers in. Each instance needs factory `auth` and can belong to only one agent. |
| `skills` | `SkillConfig[]` | — | Playbooks the agent loads on demand. See [Add skills](#add-skills). |
| `tools` | `(Tool \| ClientTool \| PieceAction \| PieceInstance \| MCPServer)[]` | — | Tools for this agent. A piece instance adds all of its actions, and an [MCP server](https://docs.frogbot.ai/mcp/overview.md) adds its tools on each run. See [Add tools](#add-tools). |
| `inheritTools` | `false` | Inherits root `tools` | Set `false` to skip the root `tools` list. |
| `stopWhen` | `StopCondition<ToolSet> \| StopCondition<ToolSet>[]` | `isStepCount(20)` | When to stop the tool loop. An array must be non-empty. |
| `access` | `({ req, agent }) => boolean \| Promise<boolean>` | `Boolean(req.user)` | Who may run the agent. See [Control access](#control-access). |
| `triggers` | `(AgentScheduleTrigger \| AgentPieceTrigger)[]` | — | [Schedule triggers](#schedule-runs) and [piece triggers](https://docs.frogbot.ai/pieces/triggers.md#mount-a-trigger). |

### Model options

| Option | Type | Default | Description |
| - | - | - | - |
| `default` | `ModelId` | `ai.defaultModel` | The model used when a request names none. Always listed first. |
| `options` | `'*' \| ModelId[]` | Required | Models users may pick. `'*'` includes every configured chat-capable model. An array lists models in addition to the default; duplicates are removed, and `[]` offers only the default. |

Use `model: { default, options }` to offer model choices. Omit `default` to use `ai.defaultModel`; if neither is set, startup fails. The default is always included first, and duplicate IDs are removed. An empty `options: []` offers only the default, as does a string `model`.

A provider's `models` list controls what the app can use; an agent's `model.options` controls what users can pick for that agent. `options: '*'` includes every configured chat-capable model:

* For a built-in provider with a `models` list, only chat models from that list.
* For a built-in provider without a `models` list, every chat model in that provider's catalog.
* For a custom provider, entries declared with `mode: 'chat'`.
* For configured routers, aliases whose target is chat-capable.

It excludes unconfigured providers and non-chat models such as embeddings, image generation, and dedicated transcription models. Chat models that also accept audio input remain chat-capable. The wildcard does not guarantee that your provider account, region, or credentials can serve every model.

For example, this agent offers every configured chat model, including the models from your Bedrock provider's `models` list, without copying that list:

```ts theme={"theme":"tokyo-night"}
import type { AgentConfig } from 'frogbot'

export const assistant: AgentConfig = {
  slug: 'assistant',
  model: { default: 'bedrock/zai.glm-5', options: '*' },
  instructions: 'You are a concise and friendly assistant.'
}
```

For a curated selector, replace `'*'` with `['bedrock/amazon.nova-lite-v1:0', 'bedrock/us.anthropic.claude-sonnet-4-6']`. You do not need to repeat the default in that array.

Put a `models` list on OpenRouter and Vercel AI Gateway provider entries to keep the picker focused. Without a list, `'*'` includes their entire chat catalogs, which can contain hundreds of models.

FrogBot validates the default and every explicit option. Unknown models or IDs outside a provider's `models` list fail startup with `is not configured`; configured non-chat models fail with `is not chat-capable`. `frogbot dev` and type generation warn for these model checks instead of throwing. [`ai.access.generate`](https://docs.frogbot.ai/ai/overview#control-access) further filters the choices each user sees and may use, for example through the [AI policy plugin](https://docs.frogbot.ai/plugins/ai-policy.md).

`allowModels` has been removed. Configs that still set it fail with a message naming the agent and showing the replacement. Migrate `model: 'provider/id', allowModels: [...]` to `model: { default: 'provider/id', options: [...] }`.

The chat composer shows the active model name and its reasoning level, for example `gpt-5 · High`. Clicking it opens a reasoning slider for that model, and the trigger reads `Select effort`; clicking the model name inside switches to the list of allowed models, and the trigger reads `Select model`. A model without reasoning options opens straight on the list. The slider starts at `Default`, which leaves the level to the provider, and offers only the options that model supports. The selector is hidden when the agent allows one model and that model has no reasoning options.

FrogBot saves the chosen model as a user preference and remembers one reasoning level per model. Switching models restores the level last used with the new model, or `Default` if it has none; levels never carry over from one model to another. A saved model or level that is no longer allowed or offered falls back to the agent's default model or to `Default`. Opening an existing chat starts from the model and level of its latest sent message instead of the saved preference, as long as that model is still allowed.

### Use the general preset

`general()` provides FrogBot's default general-purpose instructions and the `general` slug. It sets `model: { options: '*' }`, offering every configured chat model with `ai.defaultModel` as the default, subject to [`ai.access.generate`](https://docs.frogbot.ai/ai/overview#control-access). Configure that default and its provider. The chat composer shows a model picker whenever more than one model is available.

```ts theme={"theme":"tokyo-night"}
import { buildConfig } from 'frogbot'
import { general } from 'frogbot/agents'

export default buildConfig({
  // required options omitted
  ai: {
    defaultModel: 'openai/gpt-4o-mini',
    providers: {
      openai: true
    }
  },
  agents: [general()]
})
```

Pass agent fields to override the preset. The `general` slug is fixed, but options such as `tools`, `profile`, and `access` can be replaced.

```ts theme={"theme":"tokyo-night"}
agents: [general({ tools: [lookupTicket] })]
```

Override `model` to narrow the choices. `general({ model: 'openai/gpt-4o-mini' })` offers only that model; an object form can provide a curated `options` array instead.

***

## Add a profile

Use `profile` for client-facing agent identity. Each field is optional, but provided values must be non-empty strings.

```ts theme={"theme":"tokyo-night"}
export const supportAgent: AgentConfig = {
  slug: 'support',
  model: 'fast',
  instructions: 'Answer support questions.',
  profile: {
    name: 'Ada',
    avatar: '/agents/ada.png',
    description: 'Answers support questions.'
  }
}
```

`avatar` accepts a URL or app-relative path. It is a plain string rather than an admin component or media-document reference. The chat UI uses `name` for avatar alt text and renders initials when `avatar` is omitted. Without a profile, chat messages render as before.

### Profile options

| Option | Type | Default | Description |
| - | - | - | - |
| `name` | `string` | The agent slug | Display name in the chat UI, the agent selector, and the manifest `label`. |
| `avatar` | `string` | Initials of `name` | Image URL or app-relative path for the agent's avatar. |
| `description` | `string` | — | Short description returned in the [chat manifest](https://docs.frogbot.ai/chat/manifest.md). |

Accessible profiles appear in both `GET /api/agents` and the [chat manifest](https://docs.frogbot.ai/chat/manifest.md). An agent rejected by its access rule exposes neither its slug nor its profile.

***

## Add tools

Agents accept FrogBot `Tool` objects, not raw AI SDK tools. FrogBot converts them internally and supplies request, runtime, slug, and run ID context to every execution.

To add integration actions, pass individual piece actions or a whole piece instance. To add current web results, use the [web search pieces](https://docs.frogbot.ai/pieces/web-search.md). To give an agent the tools of an MCP server, such as Sidekick's browser, add an [`mcpServer()`](https://docs.frogbot.ai/mcp/overview).

```ts theme={"theme":"tokyo-night"}
import type { Tool } from 'frogbot'
import { z } from 'zod'

const lookupInput = z.object({
  ticket: z.string()
})

export const lookupTicket: Tool<
  typeof lookupInput,
  { ticket: string; status: string }
> = {
  slug: 'lookup-ticket',
  description: 'Look up the current status of a support ticket',
  inputSchema: lookupInput,
  execute: async ({ ticket }, ctx) => {
    ctx.frogbot.logger.info({ ticket }, 'Looking up ticket')

    return {
      ticket,
      status: 'open'
    }
  }
}
```

### Tool options

| Option | Type | Default | Description |
| - | - | - | - |
| `slug` | `string` | Required | Tool name the model sees. Must be unique in the agent's tool list. |
| `description` | `string` | Required | Tells the model when to use the tool. Must be non-empty. |
| `inputSchema` | `z.ZodType` | Required | Zod schema for the input. The model's arguments are validated against it before `execute` runs. |
| `execute` | `(input, ctx: ToolCtx) => TResult \| Promise<TResult>` | Required | Runs the tool and returns its result to the model. `ctx` is described in [Tool context](#tool-context). |
| `component` | `FrogBotComponent` | Generic tool render | Import-map component that renders the tool call in chat. See below. |
| `maxOutput` | `{ lines?: number; bytes?: number } \| false` | `{ lines: 2000, bytes: 51200 }` | How much of the result the model sees. A longer result is cut to its first whole lines, followed by `[showing lines 1-N of M; output truncated]`; an object result is cut from its indented JSON. `false` sends the whole result. The chat keeps the full result, and errors are never cut. |

Client tools replace `execute` with `outputSchema` and `client`; see [Build a client tool](https://docs.frogbot.ai/chat/questions.md#build-a-client-tool).

### Tool context

| Field | Type | Description |
| - | - | - |
| `req` | `FrogBotRequest` | The request for the run. |
| `frogbot` | `FrogBot` | The FrogBot instance. |
| `agent` | `{ slug: string; runId: string; chatId?: number \| string }` | The running agent, the run ID shared with [hooks](#follow-hooks), and the chat ID when the run belongs to a persisted chat. Direct `generateText` and `streamText` calls use the slug `'direct'`, and [MCP](https://docs.frogbot.ai/plugins/mcp.md#tools) calls `'mcp'`, with no `chatId`. |

Attach tools directly to the worker.

```ts theme={"theme":"tokyo-night"}
export const supportAgent: AgentConfig = {
  slug: 'support',
  model: 'fast',
  instructions: 'Use lookup-ticket before reporting ticket status.',
  tools: [lookupTicket]
}
```

Inference runs through the embedded gateway. Each model step can request tools, FrogBot executes them with `ToolCtx`, and the loop continues until a stop condition matches.

Set `component` to an import-map component reference when a tool needs purpose-built chat UI.

```ts theme={"theme":"tokyo-night"}
export const lookupTicket: Tool<typeof lookupInput, LookupResult> = {
  component: '@/components/LookupTicket#LookupTicket',
  slug: 'lookup-ticket',
  description: 'Look up the current status of a support ticket',
  inputSchema: lookupInput,
  execute: lookupTicketStatus
}
```

The component receives `{ part }`, including the tool input, output, state, and error details. Tools without `component` use the generic tool render. `frogbot dev` updates the import map automatically; build-only workflows must run `frogbot generate:importmap` before the app build.

Components resolve from the selected agent's effective tools. When an agent tool shadows an inherited root slug, the agent's component wins and FrogBot logs both definitions during initialization. Omitting `component` on the shadowing tool intentionally restores the generic render for that agent.

***

## Add skills

Skills keep playbooks and reference material out of the prompt until the agent needs them. Each skill adds a short discovery line to the agent instructions and makes three tools available: `list_skills`, `load_skill`, and `load_skill_resource`.

```ts theme={"theme":"tokyo-night"}
import type { SkillConfig } from 'frogbot'

const supportSkill: SkillConfig = {
  slug: 'support-playbook',
  description: 'Procedures for handling support requests',
  license: 'MIT',
  compatibility: 'Requires authenticated support access',
  metadata: { owner: 'support' },
  instructions: 'Identify the request category before choosing a procedure.',
  resources: [
    {
      path: 'refunds.md',
      description: 'Refund eligibility and workflow',
      content: 'Verify the purchase date and payment status before issuing a refund.'
    },
    {
      path: 'account.md',
      description: 'Account-specific guidance',
      content: async ({ req }) => loadAccountGuidance(req.user?.id)
    }
  ]
}

export const supportAgent: AgentConfig = {
  slug: 'support',
  model: 'fast',
  instructions: 'Use the relevant skill before answering policy questions.',
  skills: [supportSkill]
}
```

`instructions` and resource `content` accept a string or a function receiving `{ req, frogbot }`. Function content is evaluated only when the agent loads it. Unknown skill and resource names return the available names so the agent can correct its next call.

| Option | Type | Default | Description |
| - | - | - | - |
| `slug` | `string` | Required | Skill name, unique and URL-safe within the agent. Maps to the Agent Skills `name` field. |
| `description` | `string` | — | Shown in the discovery line added to the agent instructions and in `list_skills`. Must be non-empty when set. Maps to `description`. |
| `license` | `string` | — | Must be non-empty when set. Maps to `license`. |
| `compatibility` | `string` | — | Must be non-empty when set. Maps to `compatibility`. |
| `metadata` | `Record<string, string>` | — | Flat string map. Maps to `metadata`. |
| `instructions` | `string \| ({ req, frogbot }) => string \| Promise<string>` | Required | Returned by `load_skill`. A string must be non-empty. Maps to the `SKILL.md` body. |
| `resources` | `SkillResource[]` | — | Reference files returned by `load_skill_resource`. Maps to `references/` files. |

Each resource takes these options:

| Option | Type | Default | Description |
| - | - | - | - |
| `path` | `string` | Required | Name the agent passes to `load_skill_resource`. Unique within the skill. |
| `description` | `string` | — | Listed with the resource in `list_skills`. Must be non-empty when set. |
| `content` | `string \| ({ req, frogbot }) => string \| Promise<string>` | Required | Resource content. A string must be non-empty. |

`license` and `compatibility` are top-level fields. `metadata` is a flat string map. The three generated tool slugs are reserved when an agent has skills. `inheritTools: false` disables root tool inheritance but does not disable that agent's skill tools.

Folder-based `source` skills are not supported yet. Declare skill content in configuration; a future generated skills map will package folder content without runtime filesystem access.

***

## Share tools across agents

Add tools to the root configuration to attach them to every agent.

```ts theme={"theme":"tokyo-night"}
export default buildConfig({
  tools: [lookupTicket],
  agents: [supportAgent, reportingAgent]
})
```

Toolkits can be spread into the same array, such as `tools: [...todoTools]`. If an agent declares a tool with the same slug, its tool replaces the root tool and FrogBot logs a warning during initialization.

Set `inheritTools: false` when an agent should only use its own tools.

```ts theme={"theme":"tokyo-night"}
export const reportingAgent: AgentConfig = {
  slug: 'reporting',
  model: 'fast',
  instructions: 'Create reports from approved data sources.',
  inheritTools: false,
  tools: [queryWarehouse]
}
```

Piece actions and whole piece instances can be supplied directly in root and agent tool lists.

***

## Track chat todos

FrogBot provides `write_todos` and `read_todos` as an optional toolkit. `write_todos` replaces the entire list, and `read_todos` returns the current chat's list. Both render a task-list card with pending and completed counts by default.

```ts theme={"theme":"tokyo-night"}
import { todoTools } from 'frogbot/tools'

export default buildConfig({
  tools: [...todoTools],
  agents: [supportAgent]
})
```

Attach the toolkit to one agent instead when todos should not be shared across all agent configurations.

```ts theme={"theme":"tokyo-night"}
export const supportAgent: AgentConfig = {
  slug: 'support',
  model: 'fast',
  instructions: 'Keep the todo list current while completing multi-step work.',
  tools: [...todoTools]
}
```

Each item has `content` and a `status` of `pending`, `in_progress`, or `completed`. Keep one item `in_progress` at a time. Todos are stored on the current chat, survive continuations, and are deleted with that chat. Chat persistence must be enabled and the tool must run within a persisted agent chat.

## Ask the user a question

Add the optional `question` tool when an agent should pause for a decision instead of guessing. The reply stops at the question and continues in the same message once the user answers.

```ts theme={"theme":"tokyo-night"}
import { question } from 'frogbot/tools'

export const plannerAgent: AgentConfig = {
  slug: 'planner',
  model: 'fast',
  instructions: 'Ask before choosing between materially different plans.',
  tools: [question]
}
```

See [Ask the user questions](https://docs.frogbot.ai/chat/questions.md) for the answer shape, the headless API, and client tools of your own.

***

## Organize files

Use folders for organization, then import every config explicitly. FrogBot v0 does not scan a directory or load `AGENT.md` files.

```text theme={"theme":"tokyo-night"}
src/
├── agents/
│   ├── index.ts
│   └── support.ts
└── tools/
    └── lookup-ticket.ts
frogbot.config.ts
```

```ts theme={"theme":"tokyo-night"}
// src/agents/index.ts
import { supportAgent } from './support'

export const agents = [supportAgent]
```

```ts theme={"theme":"tokyo-night"}
// frogbot.config.ts
import { buildConfig } from 'frogbot'
import { agents } from './src/agents'

export default buildConfig({
  // secret, db, collections, and ai omitted
  agents
})
```

Slugs must be unique and URL-safe. Run `pnpm frogbot generate:types` to augment `GeneratedTypes` with configured agent slugs, matching collection typing; each configured model must resolve to a provider enabled under `ai.providers`.

***

## Invoke locally

Registered instances live at `frogbot.agents[slug]` and expose `generate` and `stream`.

```ts theme={"theme":"tokyo-night"}
const result = await frogbot.agents.support.generate({
  prompt: 'What is the status of ticket SUP-42?'
})

console.log(result.text)
```

Pass either a `prompt` or `messages`. Messages may be AI SDK `UIMessage[]` or `ModelMessage[]`; UI messages are converted before inference.

```ts theme={"theme":"tokyo-night"}
const result = await frogbot.agents.support.stream({
  messages: [
    {
      id: 'message-1',
      role: 'user',
      parts: [{ type: 'text', text: 'Help with ticket SUP-42' }]
    }
  ]
})

for await (const text of result.textStream) {
  process.stdout.write(text)
}
```

Local calls default to `overrideAccess: true`. To enforce the configured rule, pass a request and `overrideAccess: false`.

```ts theme={"theme":"tokyo-night"}
await frogbot.agents.support.generate({
  prompt: 'Review ticket SUP-42',
  req,
  overrideAccess: false
})
```

Local calls are stateless by default. Pass an existing `chatId` to `generate` to persist the new user message and assistant response against server-owned history.

```ts theme={"theme":"tokyo-night"}
await frogbot.agents.support.generate({
  prompt: 'Review ticket SUP-42 again',
  chatId,
  req,
  overrideAccess: false
})
```

Chat persistence accepts prompts or `UIMessage[]`; it does not accept `ModelMessage[]`. Local `stream()` calls remain stateless.

### Run options

`generate` and `stream` accept these options:

| Option | Type | Default | Description |
| - | - | - | - |
| `prompt` | `string` | Required unless `messages` is set | A single user message. Can't be combined with `messages`. |
| `messages` | `UIMessage[] \| ModelMessage[]` | Required unless `prompt` is set | The conversation to continue. UI messages are validated and converted before inference. |
| `req` | `FrogBotRequest` | A request without a user | Request for the run. Tools, hooks, and the access rule receive it. |
| `overrideAccess` | `boolean` | `true` | Set `false` to enforce the agent's `access` rule against `req`. |
| `abortSignal` | `AbortSignal` | — | Cancels the run. |
| `chatId` | `DocID` | — | `generate` only. Persists the new user message and the reply in this existing chat. |

***

## Schedule runs

Add schedule triggers next to the agent that owns the recurring work. Each trigger requires a URL-safe slug, exactly one schedule form, and exactly one of `prompt` or `handler`.

```ts theme={"theme":"tokyo-night"}
export const reportingAgent: AgentConfig = {
  slug: 'reporting',
  model: 'fast',
  instructions: 'Analyze account activity and report anomalies.',
  triggers: [
    {
      type: 'schedule',
      slug: 'frequent-check',
      schedule: { every: '30m' },
      prompt: 'Check account activity and report any anomalies.'
    },
    {
      type: 'schedule',
      slug: 'daily-rollup',
      schedule: { cron: '0 0 * * *' },
      handler: async ({ agent, req, job }) => {
        await agent.generate({
          prompt: `Prepare the rollup scheduled for ${job.scheduledFor.toISOString()}`,
          req
        })
      }
    }
  ]
}
```

`every` accepts positive whole durations in seconds, minutes, hours, or days when they divide evenly into the corresponding cron field. Use `cron` for other intervals. Cron expressions use UTC; timezone-specific schedules are not yet supported.

| Option | Type | Default | Description |
| - | - | - | - |
| `type` | `'schedule'` | Required | Marks a schedule trigger. |
| `slug` | `string` | Required | URL-safe name, unique within the agent. |
| `schedule` | `{ every } \| { cron, timezone? }` | Required | Exactly one of `every` or `cron`. See the next table. |
| `prompt` | `string` | Required unless `handler` is set | Non-empty prompt sent to the owning agent in a fresh chat on each occurrence. |
| `handler` | `(context: AgentScheduleContext) => void \| Promise<void>` | Required unless `prompt` is set | Runs on each occurrence instead of a prompt. |

| `schedule` option | Type | Default | Description |
| - | - | - | - |
| `every` | `` `${number}${'s' \| 'm' \| 'h' \| 'd'}` `` | Required unless `cron` is set | Interval such as `'30m'`, converted to a cron expression. |
| `cron` | `string` | Required unless `every` is set | Cron expression in UTC, with an optional leading seconds field. Invalid expressions fail startup. |
| `timezone` | `string` | — | Accepted by the type with `cron`, but any value fails startup because timezones are not supported yet. |

A handler receives:

| Field | Type | Description |
| - | - | - |
| `frogbot` | `FrogBot` | The FrogBot instance. |
| `agent` | `AgentInstance` | The owning agent, with `generate` and `stream`. |
| `req` | `FrogBotRequest` | A request without a user. `req.context` holds `source`, `agentSlug`, `triggerSlug`, and `jobId`. |
| `job` | `{ id: DocID; scheduledFor: Date }` | The job record ID and the time the occurrence was scheduled for. |

FrogBot runs schedules and skips an occurrence while the same trigger already has an active or runnable scheduled job. User-defined jobs, workflows, queues, and automatic runners remain configured alongside FrogBot schedules.

Each prompt trigger creates a fresh chat with `user: null` and persists its user and assistant messages. A handler may invoke the owning agent, another agent, or no agent. Every handler receives `{ frogbot, agent, req, job }`, and `req.context` contains `source`, `agentSlug`, `triggerSlug`, and `jobId`. Removing an agent or trigger makes already-queued work for it complete without running.

***

## Invoke over HTTP

Configuring at least one worker mounts two FrogBot endpoints at the root API path. They are served under the normal `/api` prefix.

| Method | Path | Result |
| - | - | - |
| `GET` | `/api/agents` | Permission-filtered [agent manifest](https://docs.frogbot.ai/chat/manifest.md#agent-manifest) with defaults, allowed models, and their reasoning options |
| `POST` | `/api/agents/:slug` | Generated JSON or an AI SDK UI message stream |

POST exactly one non-empty `prompt` or non-empty `messages` array. Add `model` to select one of the agent's allowed models; FrogBot rejects other models with a 403 response. Add `reasoning` to select one of the options the manifest lists under `reasoning` for that model, or for the default model when `model` is omitted; an option the model does not offer returns 400. A request without `model` uses the agent's default model. Each user message keeps the model and reasoning option it was sent with, and every turn it starts uses them. Authenticated calls create a chat when `chatId` is absent. Without an explicit event-stream accept header, the response is `{ text, usage, finishReason, chatId }`.

```bash theme={"theme":"tokyo-night"}
curl http://localhost:3000/api/agents/support \
  --request POST \
  --header "Authorization: Bearer $FROGBOT_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{ "prompt": "Help with ticket SUP-42" }'
```

Request an AI SDK UI stream with `Accept: text/event-stream`.

```bash theme={"theme":"tokyo-night"}
curl http://localhost:3000/api/agents/support \
  --request POST \
  --header "Authorization: Bearer $FROGBOT_TOKEN" \
  --header "Content-Type: application/json" \
  --header "Accept: text/event-stream" \
  --data '{ "prompt": "Help with ticket SUP-42" }'
```

Stream responses return the chat ID in the `X-FrogBot-Chat-Id` header. Continue the conversation by sending that ID with only the next user message.

```bash theme={"theme":"tokyo-night"}
curl http://localhost:3000/api/agents/support \
  --request POST \
  --header "Authorization: Bearer $FROGBOT_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{ "prompt": "What changed since then?", "chatId": "CHAT_ID" }'
```

FrogBot loads stored history, rejects chats owned by another authenticated user, and rejects client-supplied assistant or system messages. Custom access rules can allow anonymous callers to create and continue chats, but those chats are not private to the anonymous caller who created them because there is no anonymous identity to verify. Applications handling sensitive conversations should require authentication.

Malformed input returns `400`, an unknown slug returns `404`, and an aborted request returns a bodyless `499`.

See [Chat persistence](https://docs.frogbot.ai/chat/overview.md) for collection customization and durability behavior.

***

## Control access

`access` receives `{ req, agent }`, where `agent` is the agent instance being checked, and may return a boolean or promise. Its default is `Boolean(req.user)`.

```ts theme={"theme":"tokyo-night"}
access: ({ req }) => Boolean(req.user)
```

HTTP POST calls always set `overrideAccess: false`. The GET endpoint evaluates each worker's rule and omits denied entries or entries whose rule throws.

Custom rules may allow anonymous requests by returning `true`. Local calls bypass the rule unless you explicitly set `overrideAccess: false` with a request.

***

## Follow hooks

Worker runs use the five hooks configured under `ai.hooks`. Every hook receives `agent: { slug, runId, chatId? }`, plus `req` and `user` when available.

`beforeOperation` and `afterOperation` run once for the whole generate or stream call. Agent HTTP calls use that same lifecycle rather than adding another one.

`beforeUpstream`, `afterUpstream`, and `afterError` run around each model's `doGenerate` or `doStream` call, so a tool loop can produce multiple upstream phase pairs.

Access is checked before `beforeOperation`, so denied calls run no hooks. Stream completion, failure, and abort each finalize `afterOperation` at most once.

Tool execution has no separate lifecycle hook in v0. Match `ToolCtx.agent.runId` with the hook argument's `agent.runId` to correlate tool work.

See [AI hooks](https://docs.frogbot.ai/ai/overview.md#observe-calls) for phase arguments, mutation rules, and failure isolation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.