# Configure AI agents

> Create agent types and project agents, set LLM and MCP config, wire repository access via plugin adapters, and control conversation lifecycle.

- Repository: Paca-AI/paca
- GitHub: https://github.com/Paca-AI/paca
- Human docs: https://www.grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25
- Complete Markdown: https://www.grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25/llms-full.txt

## Source Files

- `docs/ai-agent/overview.md`
- `docs/ai-agent/ai-agent-service.md`
- `docs/ai-agent/repository-plugin-adapter.md`
- `docs/ai-agent/default-skills.md`
- `services/ai-agent/src/config.py`
- `services/ai-agent/src/agent/builder.py`
- `services/api/internal/transport/http/handler/conversation_handler.go`

---

---
title: "Configure AI agents"
description: "Create agent types and project agents, set LLM and MCP config, wire repository access via plugin adapters, and control conversation lifecycle."
---

Project agents are configured through `services/api` REST endpoints and the project Agents UI. Each agent is a project member (`member_type = 'agent'`) with its own LLM credentials, system prompt, per-trigger prompts, skills, MCP servers, and repository capability flags. When triggered, `services/ai-agent` loads that configuration from PostgreSQL, builds an OpenHands SDK conversation in an isolated Docker sandbox, and streams events back through Valkey.

## Configuration model

| Layer | Storage | Purpose |
|---|---|---|
| Agent preset | Frontend catalog (`AGENT_PRESETS`) | Seeds name, default LLM, and system prompt at creation time |
| Skill template | `GET /api/v1/agents/skill-templates` | Reusable `SKILL.md` bodies (developer, ba, manual-tester, po-assistant) |
| Project agent | `agents` table | Per-project runtime config: LLM, prompts, limits, git identity |
| MCP servers | `agent_mcp_servers` table | Per-agent `mcpServers` entries (stdio, sse, http) |
| Skills | `agent_skills` table | Per-agent AgentSkills content and keyword triggers |
| Conversations | `agent_conversations` table | One row per trigger invocation; tracks status and outputs |

<Note>
Agent presets and skill templates replace a separate `agent_types` database table. There is no `agent_types` migration or REST route in the current codebase — configure personas by choosing a preset at creation and attaching skill templates afterward.
</Note>

```mermaid
stateDiagram-v2
    [*] --> queued: API creates conversation + publishes trigger
    queued --> running: ai-agent worker starts OpenHands run
    running --> finished: conversation completes normally
    running --> failed: ERROR or STUCK status / unhandled exception
    running --> stopped: user calls stop endpoint
    finished --> [*]
    failed --> [*]
    stopped --> [*]
```

## Prerequisites

Before creating agents, ensure the stack includes the ai-agent service and shared secrets:

| Variable | Service | Purpose |
|---|---|---|
| `AGENT_API_KEY` | `api` | Pre-shared key for agent-authenticated plugin API calls |
| `PACA_API_KEY` | `ai-agent` | Must match `AGENT_API_KEY`; enables built-in `@paca-ai/paca-mcp` injection |
| `ENCRYPTION_KEY` | `api`, `ai-agent` | AES-256-GCM key (64-char hex) for LLM API keys at rest |
| `INTERNAL_API_KEY` | `ai-agent` | Authenticates internal conversation control routes |
| `GATEWAY_BASE_URL` | `ai-agent` | Gateway address for plugin MCP bundle URLs (`/plugins-mcp/`) |

Generate secrets with `openssl rand -hex 32` for `AGENT_API_KEY` and `ENCRYPTION_KEY`.

<Warning>
If `ENCRYPTION_KEY` is set on `api` but missing on `ai-agent`, the worker cannot decrypt LLM API keys and conversations fail at the provider call.
</Warning>

## Create a project agent

<Steps>
<Step title="Choose a preset and identity">

In the web UI (**Project → Agents → Create agent**), pick a preset (`software-engineer`, `code-reviewer`, `qa-engineer`, `planner`, `business-analyst`, or `custom`). Set display `name`, `@mention` `handle`, and `project_role_id` so the agent appears in the member list with the correct permissions.

</Step>
<Step title="Configure the LLM">

Select `llm_provider` and `llm_model`. The verified model list comes from `GET /api/v1/agents/llm-models`, which proxies the OpenHands SDK `VERIFIED_MODELS` catalog from `services/ai-agent`.

Supply a provider API key in `llm_api_key` (required at creation). The API encrypts it when `ENCRYPTION_KEY` is configured. Set `llm_base_url` for Azure, local OpenAI-compatible endpoints, or providers such as `glm`, `nvidia`, and `qwen` (the ai-agent builder injects known compatible base URLs automatically).

</Step>
<Step title="Attach default trigger prompts">

Creation seeds four per-trigger prompt fields from `TRIGGER_PROMPTS` in the web client. These append to the system prompt at invocation time based on how the agent was triggered.

</Step>
<Step title="Verify the agent member">

A successful `POST /api/v1/projects/:projectId/agents` returns `201` with the agent object including `member_id`. The agent is assignable to tasks and @mentionable by `handle`.

</Step>
</Steps>

:::endpoint POST /api/v1/projects/:projectId/agents
Create a project agent and its `project_members` row.

<ParamField body="name" type="string" required>
Display name shown in member lists.
</ParamField>

<ParamField body="handle" type="string" required>
Unique @mention handle per project (e.g. `dev-bot`).
</ParamField>

<ParamField body="llm_provider" type="string" required>
LiteLLM provider prefix (e.g. `anthropic`, `openai`, `azure`).
</ParamField>

<ParamField body="llm_model" type="string" required>
Model name within the provider.
</ParamField>

<ParamField body="llm_api_key" type="string" required>
Provider API key; stored encrypted when `ENCRYPTION_KEY` is set.
</ParamField>

<ParamField body="llm_base_url" type="string">
Optional custom endpoint for OpenAI-compatible providers.
</ParamField>

<ParamField body="system_prompt" type="string">
Base role instructions appended as `system_message_suffix`.
</ParamField>

<ParamField body="task_trigger_prompt" type="string">
Appended when triggered by task assignment or task-comment @mention.
</ParamField>

<ParamField body="doc_comment_trigger_prompt" type="string">
Appended when triggered by documentation-comment @mention (no `task_id`).
</ParamField>

<ParamField body="chat_trigger_prompt" type="string">
Appended for direct chat sessions.
</ParamField>

<ParamField body="description_write_trigger_prompt" type="string">
Appended for **Write with AI** task description generation.
</ParamField>

<ParamField body="can_clone_repos" type="boolean">
Default `true`. When `true` and repository plugins are installed, the executor injects repository tools.
</ParamField>

<ParamField body="can_create_prs" type="boolean">
Default `true`. Stored on the agent; PR creation is enforced by repository plugin tools.
</ParamField>

<ParamField body="max_iterations" type="integer">
OpenHands step cap per conversation. Default `500`, hard cap `500`.
</ParamField>

<ParamField body="timeout_minutes" type="integer">
Wall-clock timeout hint. Default `30`, max `480`.
</ParamField>

<ParamField body="git_committer_name" type="string">
Git commit author name in the sandbox. Default `paca-agent`.
</ParamField>

<ParamField body="git_committer_email" type="string">
Git commit author email. Default `280579135+paca-agent@users.noreply.github.com`.
</ParamField>

<ParamField body="project_role_id" type="uuid" required>
Project role assigned to the agent member.
</ParamField>

<RequestExample>

```bash title="Create a developer agent"
curl -X POST "$PACA_URL/api/v1/projects/$PROJECT_ID/agents" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Dev Bot",
    "handle": "dev-bot",
    "llm_provider": "anthropic",
    "llm_model": "claude-sonnet-4-6",
    "llm_api_key": "sk-ant-...",
    "system_prompt": "You are a senior software engineer.",
    "can_clone_repos": true,
    "can_create_prs": true,
    "project_role_id": "'"$ROLE_ID"'"
  }'
```

</RequestExample>

<ResponseExample>

```json title="201 Created"
{
  "data": {
    "id": "uuid",
    "project_id": "uuid",
    "member_id": "uuid",
    "name": "Dev Bot",
    "handle": "dev-bot",
    "llm_provider": "anthropic",
    "llm_model": "claude-sonnet-4-6",
    "can_clone_repos": true,
    "max_iterations": 500,
    "timeout_minutes": 30,
    "git_committer_name": "paca-agent",
    "git_committer_email": "280579135+paca-agent@users.noreply.github.com"
  }
}
```

</ResponseExample>
:::

## Agent presets and skill templates

### Presets (creation-time templates)

The web UI ships six presets that pre-fill LLM defaults and `system_prompt`:

| Preset ID | Label | Default model |
|---|---|---|
| `software-engineer` | Software Engineer | `anthropic` / `claude-sonnet-4-6` |
| `code-reviewer` | Code Reviewer | `anthropic` / `claude-sonnet-4-6` |
| `qa-engineer` | QA Engineer | `anthropic` / `claude-sonnet-4-6` |
| `planner` | Planner | `anthropic` / `claude-sonnet-4-6` |
| `business-analyst` | Business Analyst | `anthropic` / `claude-sonnet-4-6` |
| `custom` | Custom | Empty — fill all fields manually |

Presets are frontend-only; they do not create a separate database record.

### Skill templates (reusable SKILL.md catalog)

`GET /api/v1/agents/skill-templates` returns four built-in templates with full `SKILL.md` content and keyword triggers:

| Slug | Name | Example triggers |
|---|---|---|
| `developer` | Developer | `implement`, `fix`, `refactor`, `pr` |
| `ba` | Business Analyst | `requirements`, `gap analysis`, `use case` |
| `manual-tester` | Manual Tester | `test case`, `test plan`, `QA` |
| `po-assistant` | Product Owner Assistant | `acceptance criteria`, `backlog`, `roadmap` |

Attach a template to an agent with `POST /api/v1/projects/:projectId/agents/:agentId/skills` using `skill_source: "inline"` and the template `content` as `skill_content`.

## LLM configuration

### Provider selection

`services/ai-agent/src/agent/builder.py` constructs the OpenHands `LLM` object:

- Without `llm_base_url`: model string is `{provider}/{model}` (LiteLLM routing).
- With `llm_base_url`: model string becomes `openai/{model}` for OpenAI-compatible endpoints.
- Auto-injected base URLs exist for `glm`, `nvidia`, and `qwen`.

Any LiteLLM-supported provider works — the stack does not require a specific vendor.

### Update LLM settings

`PATCH /api/v1/projects/:projectId/agents/:agentId` accepts partial updates. Omit `llm_api_key` to keep the existing encrypted secret; include it to rotate.

### Discover models

```bash
curl "$PACA_URL/api/v1/agents/llm-models" \
  -H "Authorization: Bearer $TOKEN"
```

Returns a map of provider → verified model IDs from the deployed OpenHands SDK.

## MCP server configuration

Each agent stores MCP servers in `agent_mcp_servers`. At conversation start, `build_mcp_config` assembles the `mcpServers` map for the OpenHands SDK.

### User-configured servers

:::endpoint POST /api/v1/projects/:projectId/agents/:agentId/mcp-servers
Add an MCP server to an agent.

<ParamField body="server_name" type="string" required>
Key in the `mcpServers` map.
</ParamField>

<ParamField body="transport" type="string" required>
One of `stdio`, `sse`, or `http`.
</ParamField>

<ParamField body="command" type="string">
Required for `stdio` transport.
</ParamField>

<ParamField body="args" type="string[]">
Arguments for `stdio` transport.
</ParamField>

<ParamField body="url" type="string">
Server URL for `sse` or `http` transport.
</ParamField>

<ParamField body="env" type="object">
Environment variables injected into the MCP process. Secret-like keys are redacted (`***`) in API responses.
</ParamField>
:::

<RequestExample>

```json title="stdio MCP server"
{
  "server_name": "fetch",
  "transport": "stdio",
  "command": "uvx",
  "args": ["mcp-server-fetch"],
  "env": {}
}
```

</RequestExample>

Manage servers with `GET`, `PATCH` (enable/disable, update command/args/url/env), and `DELETE` on `/mcp-servers/:serverId`.

### Built-in Paca MCP server

When `PACA_API_KEY` is set on `ai-agent`, the executor always appends a `paca` stdio server **after** user entries (so it cannot be overridden):

```json
{
  "command": "npx",
  "args": ["-y", "@paca-ai/paca-mcp"],
  "env": {
    "PACA_API_KEY": "<AGENT_API_KEY>",
    "PACA_API_URL": "<api internal URL>",
    "PACA_GATEWAY_URL": "<gateway URL>",
    "PACA_AGENT_ID": "<agent uuid>",
    "PACA_PROJECT_ID": "<project uuid>"
  }
}
```

This gives every agent project-scoped Paca tools (tasks, docs, comments) without per-agent MCP configuration. Set `AGENT_API_KEY` on `api` and the matching `PACA_API_KEY` on `ai-agent`.

## Skills configuration

Skills follow the [Agent Skills](https://agentskills.io/specification) format. Content is stored in PostgreSQL and loaded into the OpenHands `Skill` list at conversation start. Disabled skills (`is_enabled: false`) are skipped.

:::endpoint POST /api/v1/projects/:projectId/agents/:agentId/skills
Add a skill to an agent.

<ParamField body="skill_name" type="string" required>
Unique identifier for this agent.
</ParamField>

<ParamField body="skill_source" type="string" required>
`inline`, `marketplace`, or `github_url`.
</ParamField>

<ParamField body="skill_content" type="string">
Full `SKILL.md` body for `inline` skills.
</ParamField>

<ParamField body="source_url" type="string">
URL or marketplace ID for non-inline sources.
</ParamField>

<ParamField body="triggers" type="string[]">
Keyword triggers mapped to OpenHands `KeywordTrigger`.
</ParamField>
:::

## Wire repository access

Repository access does not require per-agent credential configuration. Install a plugin with the `repository` capability (for example GitHub or GitLab from the marketplace), link repositories to the project, and enable `can_clone_repos` on the agent.

### How plugins are discovered

When publishing triggers, `services/api` calls `FindByCapability(ctx, "repository")` and passes all matching plugin names as `repo_plugin_ids` on the Valkey stream entry `paca:agent:triggers`.

### Runtime tools injected in the sandbox

When `can_clone_repos` is `true` and at least one repository plugin is installed, `services/ai-agent` adds custom tools alongside the OpenHands defaults:

| Tool | Purpose |
|---|---|
| `list_repositories` | Lists linked repos across all repository plugins |
| `clone_repository` | Fetches short-lived clone credentials from the plugin API |
| `push_branch` | Pushes a feature branch with a fresh token |
| `create_pull_request` | Opens a PR and links it to the current task |

Clone credentials are fetched at runtime from plugin-scoped API routes — agents never store VCS tokens:

```
GET /api/v1/plugins/:pluginId/projects/:projectId/repositories
GET /api/v1/plugins/:pluginId/projects/:projectId/repositories/:repoId/clone-info
```

The clone-info response includes a short-lived `token` and `clone_url`. The executor embeds the token in the git URL and scrubs it from command output.

<Info>
The executor also appends repository workflow instructions to the system prompt (clone → branch → commit → push → PR) when repositories are available.
</Info>

### Git identity

Set `git_committer_name` and `git_committer_email` on the agent so commits in the sandbox use a consistent author. Defaults apply when omitted.

## Trigger types and per-invocation prompts

| Trigger | How it fires | `trigger_type` value |
|---|---|---|
| Task assignment | Task `assignee_id` points to agent member | `task_assigned` |
| Task comment @mention | Comment body contains `@handle` on a task | `comment_mention` |
| Doc comment @mention | Comment on a doc page contains `@handle` | `comment_mention` |
| Direct chat | `POST .../chat-sessions` or follow-up message | `chat_message` |
| Write with AI | `POST .../tasks/:taskId/write-with-ai` | `description_write` |

At conversation start the executor builds `system_message_suffix` from:

1. Agent `system_prompt`
2. Hardcoded documentation workflow instructions (always use Paca MCP for docs)
3. Repository workflow instructions (when repos are linked and `can_clone_repos` is true)
4. The matching per-trigger prompt field (`task_trigger_prompt`, `doc_comment_trigger_prompt`, `chat_trigger_prompt`, or `description_write_trigger_prompt`)

Edit trigger prompts on the agent **Overview** tab in the web UI or via `PATCH /agents/:agentId`.

## Conversation lifecycle control

Conversations are created by `services/api` (status `queued`), executed by `services/ai-agent` (status `running`), and settle to `finished`, `failed`, or `stopped`.

### Public control endpoints

| Method | Path | Behavior |
|---|---|---|
| `GET` | `/api/v1/projects/:projectId/conversations` | List conversations; filter with `?agent_id=&status=` |
| `GET` | `/api/v1/projects/:projectId/conversations/:conversationId` | Conversation metadata |
| `GET` | `/api/v1/projects/:projectId/conversations/:conversationId/events` | Paginated OpenHands event history |
| `POST` | `/api/v1/projects/:projectId/conversations/:conversationId/stop` | Stop run; publishes `agent.stop` to Valkey; destroys sandbox |
| `POST` | `/api/v1/projects/:projectId/conversations/:conversationId/messages` | Send follow-up message to a **running** conversation |

:::endpoint POST /api/v1/projects/:projectId/conversations/:conversationId/stop
Stop a running or queued conversation. Sets status to `stopped` and signals the ai-agent worker via the `agent.stop` control message on `paca:agent:triggers`.

<RequestExample>

```bash
curl -X POST "$PACA_URL/api/v1/projects/$PROJECT_ID/conversations/$CONV_ID/stop" \
  -H "Authorization: Bearer $TOKEN"
```

</RequestExample>

<ResponseExample>

```json
{ "data": { "message": "conversation stopped" } }
```

</ResponseExample>
:::

:::endpoint POST /api/v1/projects/:projectId/conversations/:conversationId/messages
Send an additional message to a running conversation.

<ParamField body="message" type="string" required>
Follow-up instruction for the agent.
</ParamField>
:::

### Realtime monitoring

`services/ai-agent` publishes conversation events to `paca:agent:events` and pushes realtime notifications on `paca.events`. Connected clients receive `agent.conversation.finished`, `agent.conversation.failed`, and per-event updates through Socket.IO project rooms.

### Chat sessions

Direct chat uses persistent `agent_chat_sessions`. Each message creates a new `agent_conversations` row:

- `POST /api/v1/projects/:projectId/agents/:agentId/chat-sessions` — start session with initial message
- `POST /api/v1/projects/:projectId/agents/:agentId/chat-sessions/:sessionId/messages` — follow-up in existing session

## Agent management endpoints

| Method | Path | Description |
|---|---|---|
| `GET` | `/api/v1/projects/:projectId/agents` | List project agents |
| `GET` | `/api/v1/projects/:projectId/agents/:agentId` | Get agent with MCP servers and skills |
| `PATCH` | `/api/v1/projects/:projectId/agents/:agentId` | Update configuration |
| `DELETE` | `/api/v1/projects/:projectId/agents/:agentId` | Soft-delete agent and member |
| `GET` | `/api/v1/agents/llm-models` | Verified LLM catalog |
| `GET` | `/api/v1/agents/skill-templates` | Built-in skill template catalog |

All project-scoped routes require JWT or API-key authentication and project authorization.

## Verification checklist

<Check>
After configuration, confirm these signals before assigning production work:
</Check>

1. `GET /api/v1/agents/llm-models` returns providers and the agent's chosen model appears in the list.
2. `GET /api/v1/projects/:projectId/agents` shows the agent with a populated `member_id`.
3. `PACA_API_KEY` on `ai-agent` matches `AGENT_API_KEY` on `api` (built-in Paca MCP appears in conversation tool calls).
4. Repository plugins with the `repository` capability are installed and repositories are linked to the project.
5. A test trigger (assign a simple task or send a chat message) produces a conversation that moves from `queued` → `running` → `finished`.
6. `GET .../conversations/:id/events` returns `MessageEvent`, `CmdRunAction`, or other OpenHands event types.

## Related pages

<CardGroup>
<Card title="AI agents" href="/ai-agents">
Agent members, trigger types, OpenHands lifecycle, Docker isolation, and Valkey streams.
</Card>
<Card title="Connect MCP server" href="/connect-mcp">
Configure `@paca-ai/paca-mcp` for external MCP clients and understand agent-mode constraints.
</Card>
<Card title="Install marketplace plugins" href="/install-marketplace-plugins">
Install repository plugins that supply clone credentials and PR creation.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Full environment variable catalog across api, ai-agent, gateway, and realtime.
</Card>
<Card title="Realtime events" href="/realtime-events">
Socket.IO rooms and agent conversation event payloads.
</Card>
<Card title="REST API reference" href="/rest-api">
Auth envelopes, pagination, and the complete endpoint catalog.
</Card>
</CardGroup>
