# AI agents

> Agent members, trigger types, OpenHands conversation lifecycle, Docker isolation, Valkey streams, and how agents participate as project teammates.

- 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/default-skills.md`
- `services/ai-agent/src/main.py`
- `services/ai-agent/src/worker.py`
- `services/api/internal/transport/http/handler/agent_handler.go`

---

---
title: "AI agents"
description: "Agent members, trigger types, OpenHands conversation lifecycle, Docker isolation, Valkey streams, and how agents participate as project teammates."
---

Paca AI agents are project-scoped entities backed by the OpenHands Software Agent SDK. Each agent is created through `services/api`, registered as a `project_members` row with `member_type = 'agent'`, and executed by `services/ai-agent`, which consumes trigger events from the Valkey stream `paca:agent:triggers`, runs one isolated Docker container per conversation, and publishes events to `paca:agent:events` and the `paca.events` pub/sub channel for realtime fan-out.

## Agent members

Creating an agent atomically inserts both an `agents` row and a matching `project_members` entry. Agent members share the same assignment, mention, and chat surfaces as human members.

| Concept | Storage | Behavior |
|---|---|---|
| Agent | `agents` | Holds LLM config, prompts, skills, MCP servers, repo permissions |
| Agent member | `project_members` (`member_type = 'agent'`, `agent_id` set, `user_id` null) | Appears in member lists, can be task assignees, has a project role |
| Handle | `agents.handle` (unique per project) | Used for `@mention` detection in comments |

<ParamField body="member_type" type="string">
Discriminator on `project_members`. Values: `human`, `agent`.
</ParamField>

<ParamField body="agent_id" type="uuid">
Foreign key to `agents.id`. Required when `member_type = 'agent'`.
</ParamField>

<Check>
Constraint `ck_pm_member_type_ref` enforces: human members have `user_id`; agent members have `agent_id`.
</Check>

On `POST /api/v1/projects/:projectId/agents`, the API encrypts the LLM API key at rest, creates the agent record, and links it to a new `project_members` row with the supplied `project_role_id`. The returned agent includes `member_id` for use in task assignment and activity recording.

## Architecture

```mermaid
flowchart TB
  subgraph web["apps/web"]
    UI["Agent settings, chat, monitoring"]
  end

  subgraph api["services/api"]
    CRUD["Agent CRUD + triggers"]
    ConvAPI["Conversation REST"]
  end

  subgraph valkey["Valkey"]
    Triggers["paca:agent:triggers"]
    Events["paca:agent:events"]
    PubSub["paca.events pub/sub"]
  end

  subgraph aiagent["services/ai-agent"]
    Worker["worker.py consumer group"]
    Executor["executor.py OpenHands run"]
    DockerMgr["docker_workspace.py"]
  end

  subgraph realtime["services/realtime"]
    SIO["Socket.IO fan-out"]
  end

  subgraph sandbox["Agent Docker containers"]
    OH["openhands agent-server"]
  end

  UI -->|HTTP / Socket.IO| api
  CRUD -->|XADD| Triggers
  Worker -->|XREADGROUP ai-agent-workers| Triggers
  Worker --> Executor
  Executor --> DockerMgr
  DockerMgr -->|spawn per conversation| OH
  Executor -->|XADD| Events
  Executor -->|PUBLISH| PubSub
  PubSub --> SIO
  Events --> SIO
  ConvAPI -->|internal proxy| aiagent
```

| Service | Role |
|---|---|
| `services/api` | Owns agent configuration, publishes triggers, stores conversations and events, exposes public REST |
| `services/ai-agent` | Consumes triggers, runs OpenHands conversations in Docker, persists events, handles stop control |
| `services/realtime` | Routes `agent.*` events to project task rooms via Socket.IO |
| Docker host | Provides per-conversation sandbox containers |

## Trigger types

A trigger creates one `agent_conversations` row (status `queued`) and appends a flat-field message to `paca:agent:triggers`. The stream entry carries both a routing `type` (prefixed `agent.*`) and a `trigger_type` value stored on the conversation record.

| Trigger | Stream `type` | `trigger_type` | When fired |
|---|---|---|---|
| Task assignment | `agent.task_assigned` | `task_assigned` | Task `assignee_id` resolves to an agent member (via notification consumer) |
| Comment @mention | `agent.comment_mention` | `comment_mention` | Comment body mentions an agent handle (task or doc comments) |
| Direct chat | `agent.chat_message` | `chat_message` | `POST .../chat-sessions` or `POST .../chat-sessions/:sessionId/messages` |
| Description write | `agent.description_write` | `description_write` | `POST .../tasks/:taskId/write-with-ai` |

<RequestExample>
```json
{
  "conversation_id": "uuid",
  "project_id": "uuid",
  "agent_id": "uuid",
  "task_id": "uuid",
  "actor_member_id": "uuid",
  "trigger_type": "task_assigned",
  "type": "agent.task_assigned",
  "repo_plugin_ids": "com.paca.github"
}
```
</RequestExample>

<ParamField body="repo_plugin_ids" type="string">
Comma-separated plugin names (e.g. `com.paca.github`) for installed repository-capability plugins. Gathered automatically at trigger time.
</ParamField>

<ParamField body="message" type="string">
User message text. Required for `comment_mention`, `chat_message`, and `description_write`.
</ParamField>

<Note>
Task assignment does not send a human notification to agent members. The notification consumer detects `member.IsAgent()` and publishes a trigger instead.
</Note>

## Valkey streams

### Inbound: `paca:agent:triggers`

Consumer group `ai-agent-workers` with per-replica consumer name `worker-<hostname>`. The worker in `services/ai-agent/src/worker.py` reads batches up to `WORKER_CONCURRENCY` (default `10`), dispatches each message, then `XACK`s on completion.

| Field | Description |
|---|---|
| `type` | Routing topic: `agent.task_assigned`, `agent.comment_mention`, `agent.chat_message`, `agent.description_write`, or `agent.stop` |
| `trigger_type` | Conversation trigger stored in DB |
| `conversation_id` | Pre-allocated UUID used as the application conversation ID |
| `agent_id`, `project_id` | Agent and project scope |
| `task_id`, `comment_id`, `chat_session_id` | Context fields depending on trigger |
| `message` | Initial user message |
| `actor_member_id` | Human member who initiated the trigger |
| `repo_plugin_ids` | Comma-separated repository plugin names |

### Control: `agent.stop`

Stop requests publish a control message (`type = agent.stop`) rather than a new conversation. The worker sets a per-conversation `threading.Event`, the executor calls `conversation.pause()`, and status moves to `stopped`.

### Outbound: `paca:agent:events`

Each OpenHands SDK event is serialized and appended with fields `conversation_id`, `project_id`, `event_type`, `event_source`, `event_index`, `payload`, and `status`. Events are also inserted into `agent_conversation_events`.

### Realtime: `paca.events` pub/sub

The executor publishes lightweight notifications (e.g. `agent.messageevent`, `agent.conversation.finished`) to the `paca.events` channel. `services/realtime` routes any `agent.*` type to the project tasks room.

## OpenHands conversation lifecycle

```mermaid
stateDiagram-v2
  [*] --> queued: API creates conversation + publishes trigger
  queued --> running: ai-agent worker picks up trigger
  running --> finished: SDK status FINISHED
  running --> failed: SDK status ERROR or STUCK
  running --> stopped: agent.stop control or POST .../stop
  finished --> [*]
  failed --> [*]
  stopped --> [*]
```

<Steps>
<Step title="Dequeue trigger">
The worker loads agent config (LLM key decrypted with shared `ENCRYPTION_KEY`), skills, and MCP servers from PostgreSQL.
</Step>
<Step title="Build agent context">
`executor.py` assembles the OpenHands `Agent` with LLM, skills, MCP config, system prompt suffix, documentation workflow instructions, and trigger-specific prompt fields (`task_trigger_prompt`, `doc_comment_trigger_prompt`, `chat_trigger_prompt`, `description_write_trigger_prompt`).
</Step>
<Step title="Spawn Docker sandbox">
`docker_sandbox()` starts `ghcr.io/openhands/agent-server:latest-python` (overridable via `AGENT_SERVER_IMAGE`), yields a `RemoteWorkspace`, and tears down the container on exit.
</Step>
<Step title="Run conversation">
`Conversation.run(blocking=False)` starts the SDK loop. Event and token callbacks persist `MessageEvent` and action/observation events to the database and Valkey streams.
</Step>
<Step title="Finalize">
On natural completion the status becomes `finished` or `failed`. A realtime `agent.conversation.finished` or `agent.conversation.failed` event is published.
</Step>
</Steps>

### Conversation statuses

| Status | Meaning |
|---|---|
| `queued` | Created by API, waiting for worker |
| `running` | Executor active |
| `paused` | Reserved in schema; not exposed via current REST control endpoints |
| `finished` | Completed successfully |
| `failed` | SDK error, stuck state, or unhandled exception |
| `stopped` | User-initiated stop |

### Event persistence

OpenHands events are stored in `agent_conversation_events` with monotonic `event_index`. Agent `MessageEvent` text is captured via streaming token callbacks to avoid duplicate rows. Internal SDK events (`StreamingDeltaEvent`, `ConversationStateUpdateEvent`, `SystemPromptEvent`, `ConversationErrorEvent`) are filtered out of persistence.

## Docker isolation

Each active conversation gets one dedicated container managed by `docker_workspace.py`.

| Property | Value |
|---|---|
| Image | `ghcr.io/openhands/agent-server:latest-python` (configurable) |
| Lifecycle | `remove=True` — container deleted on stop |
| Labels | `paca.conversation_id`, `paca.managed=true` |
| Network (in Compose) | Joins the ai-agent service network so MCP can reach `api` and `gateway` hostnames |
| Network (local dev) | Host port from pool `PORT_POOL_START`–`PORT_POOL_START + PORT_POOL_SIZE - 1` (default `10000`–`10099`) |
| Git identity | `GIT_AUTHOR_NAME`, `GIT_COMMITTER_NAME` from agent config (defaults `paca-agent`) |

Repository tools (`list_repositories`, `clone_repository`, `push_branch`, `create_pull_request`) are injected into the sandbox via `repo_tools.py` when `can_clone_repos` is true and repository plugins are linked.

<Warning>
The ai-agent service requires access to the Docker socket (`DOCKER_SOCKET`, default `/var/run/docker.sock`). Without it, conversations cannot start.
</Warning>

## Skills and MCP

### Built-in skill templates

Four hardcoded templates ship in `services/api` and are listed at `GET /api/v1/agents/skill-templates`:

| Slug | Persona | Keyword triggers |
|---|---|---|
| `po-assistant` | Product Owner | acceptance criteria, backlog, roadmap, groom |
| `ba` | Business Analyst | requirements, functional spec, gap analysis |
| `developer` | Developer | implement, fix, bug, pr, test |
| `manual-tester` | Manual QA | test case, test plan, defect, regression |

Skills are stored per agent in `agent_skills` (`skill_source`: `inline`, `marketplace`, `github_url`) and converted to OpenHands `Skill` objects with optional `KeywordTrigger`.

### MCP servers

Per-agent MCP config lives in `agent_mcp_servers` (`transport`: `stdio`, `sse`, `http`, `oauth`). When `PACA_API_KEY` is set on the ai-agent service, a built-in `paca` MCP server is always appended:

```json
{
  "command": "npx",
  "args": ["-y", "@paca-ai/paca-mcp"],
  "env": {
    "PACA_API_KEY": "...",
    "PACA_API_URL": "http://api:8080",
    "PACA_GATEWAY_URL": "http://gateway",
    "PACA_AGENT_ID": "...",
    "PACA_PROJECT_ID": "..."
  }
}
```

LLM providers are BYOC/BYOK: any LiteLLM-compatible provider and model string (`provider/model`) with an encrypted API key reference on the agent record.

## Public REST API

All paths are under `/api/v1/projects/:projectId`.

### Agents

| Method | Path | Permission |
|---|---|---|
| `GET` | `/agents` | `agents:read` |
| `POST` | `/agents` | `agents:write` |
| `GET` | `/agents/:agentId` | `agents:read` |
| `PATCH` | `/agents/:agentId` | `agents:write` |
| `DELETE` | `/agents/:agentId` | `agents:write` |
| `GET/POST/PATCH/DELETE` | `/agents/:agentId/mcp-servers[...]` | read/write |
| `GET/POST/PATCH/DELETE` | `/agents/:agentId/skills[...]` | read/write |
| `GET` | `/agents/:agentId/chat-sessions` | `agents:read` |
| `POST` | `/agents/:agentId/chat-sessions` | `agents:read` |
| `POST` | `/agents/:agentId/chat-sessions/:sessionId/messages` | `agents:read` |

:::endpoint POST /projects/:projectId/tasks/:taskId/write-with-ai
Trigger an agent to write a task description. Records `agent.session.started` activity on success.

<ParamField body="agent_id" type="uuid" required>
Agent to run the description-write prompt.
</ParamField>

<ResponseField name="conversation" type="object">
New `agent_conversations` record with `trigger_type = description_write`.
</ResponseField>
:::

### Conversations

| Method | Path | Description |
|---|---|---|
| `GET` | `/conversations` | List with `agent_id`, `status` filters |
| `GET` | `/conversations/:conversationId` | Metadata and status |
| `GET` | `/conversations/:conversationId/events` | Paginated event history |
| `POST` | `/conversations/:conversationId/stop` | Stop running conversation |
| `POST` | `/conversations/:conversationId/messages` | Send follow-up message (requires `status = running`) |

### Global agent utilities

| Method | Path | Description |
|---|---|---|
| `GET` | `/agents/skill-templates` | Built-in skill catalog |
| `GET` | `/agents/llm-models` | Proxied from ai-agent `/llm/models` |

Internal ai-agent endpoints (`/conversations/:id`, `/conversations/:id/stop`, etc.) require header `X-Internal-Token` matching `INTERNAL_API_KEY` and are not exposed through the public gateway.

## Configuration

Key environment variables for `services/ai-agent`:

| Variable | Required | Default | Purpose |
|---|---|---|---|
| `VALKEY_URL` | yes | `redis://valkey:6379/0` | Stream and pub/sub client |
| `DATABASE_URL` | yes | — | Conversation and event persistence |
| `INTERNAL_API_KEY` | yes | — | Internal REST auth |
| `API_BASE_URL` | yes | `http://api:8080` | Plugin and repository API calls |
| `GATEWAY_BASE_URL` | — | `http://gateway` | Plugin MCP bundle URLs |
| `PACA_API_KEY` | — | empty | Enables built-in Paca MCP injection |
| `ENCRYPTION_KEY` | — | empty | Decrypts `llm_api_key_secret` from DB |
| `DOCKER_SOCKET` | yes | `/var/run/docker.sock` | Container orchestration |
| `AGENT_SERVER_IMAGE` | — | `ghcr.io/openhands/agent-server:latest-python` | Sandbox image |
| `PORT_POOL_START` / `PORT_POOL_SIZE` | — | `10000` / `100` | Local dev port pool |
| `WORKER_CONCURRENCY` | — | `10` | Parallel trigger processing |

On the API service, set `AGENT_API_KEY` to the same value as ai-agent `PACA_API_KEY` so the built-in MCP can authenticate.

## Participating as project teammates

Agents integrate into standard project workflows:

- **Assignment**: Assign a task to an agent member; the notification consumer triggers `agent.task_assigned` instead of sending an in-app notification.
- **Comments**: Mention `@agent-handle` in a task or doc comment; `activity_service` calls `TriggerCommentMention` with the comment body.
- **Chat**: Start a persistent `agent_chat_sessions` thread per user–agent pair; each message spawns a new conversation linked to the session.
- **Monitoring**: The web UI subscribes to `agent.*` realtime events and polls conversation events for live action/observation display.
- **Activity feed**: Task assignment and description-write triggers record `agent.session.started` on the task activity timeline.

<Info>
Repository credentials never enter agent prompts. Clone and PR operations use plugin HTTP APIs with short-lived tokens; `repo_tools.py` scrubs tokens from command output.
</Info>

## Related pages

<CardGroup>
<Card title="Configure AI agents" href="/configure-ai-agents">
Create agents, set LLM and MCP config, attach skills, and wire repository access.
</Card>
<Card title="Platform architecture" href="/platform-architecture">
Service boundaries, Valkey decoupling, and nginx gateway routing.
</Card>
<Card title="Realtime events" href="/realtime-events">
Socket.IO auth, Valkey fan-out, and `agent.*` event payloads.
</Card>
<Card title="Connect MCP server" href="/connect-mcp">
Configure `@paca-ai/paca-mcp` for external clients and understand agent-mode constraints.
</Card>
<Card title="REST API" href="/rest-api">
Versioned `/api/v1` paths, auth, and response envelopes.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Full environment variable catalog across API, web, realtime, and ai-agent.
</Card>
</CardGroup>
