330 lines
19 KiB
Markdown
330 lines
19 KiB
Markdown
# CLAUDE.md — picogent
|
||
|
||
## Что это
|
||
|
||
Минимальный AI-агент с in-process agentic loop на базе Pi Agent Core. Два режима работы:
|
||
|
||
1. **WebSocket-режим (чат)** — сервер WebSocket, браузерный клиент (`client.html`) отправляет промпты, получает стриминг-ответы
|
||
2. **Agent-режим (Team Board Tracker)** — агент подключается к трекеру задач, получает задания (`task.assigned`), выполняет, отправляет результат
|
||
|
||
Режим определяется автоматически: если есть `agent.json` / директория с конфигом / env `TRACKER_URL` → agent, иначе → WebSocket.
|
||
|
||
## Команды
|
||
|
||
```bash
|
||
npm install # Установка
|
||
npm run build # tsc -> dist/
|
||
npm run dev # Dev (tsx + .env)
|
||
npm start # Production (node + .env)
|
||
npm test # Тесты (vitest)
|
||
```
|
||
|
||
## Запуск
|
||
|
||
```bash
|
||
npm run dev # WebSocket-режим на :3100
|
||
npm run dev -- ./agents/coder/ # Agent-режим (директория)
|
||
npm run dev -- ./agent.json # Agent-режим (файл)
|
||
AGENT_TRANSPORT=ws npm run dev -- ./agents/coder/ # Agent-режим через WebSocket
|
||
```
|
||
|
||
## Структура файлов
|
||
|
||
```
|
||
src/
|
||
index.ts # Точка входа, dual-mode wiring (WS-чат / Agent HTTP / Agent WS)
|
||
config.ts # Config + AgentConfig, loadConfig(), loadAgentConfig()
|
||
agent.ts # Ядро: Pi Agent Core session, runAgent() async generator
|
||
sandbox.ts # createSandboxedTools() — tools с проверкой allowed_paths
|
||
router.ts # TaskTracker interface + EventRouter: tracker events -> runAgent() -> tracker API
|
||
logger.ts # Pino logger
|
||
session.ts # (не используется, legacy)
|
||
transport/
|
||
websocket.ts # WebSocket-сервер (чат-режим, принимает подключения от клиентов)
|
||
http.ts # HTTP-сервер (agent-режим, transport=http, POST /events)
|
||
ws-client.ts # WS-клиент к трекеру (agent-режим, transport=ws)
|
||
types.ts # Transport interface (IncomingMessage, OutgoingMessage, MessageHandler)
|
||
tracker/
|
||
client.ts # HTTP-клиент к Tracker REST API (updateTask, addComment и т.д.)
|
||
registration.ts # Register + heartbeat с retry (используется только transport=http)
|
||
types.ts # TrackerTask, TrackerEvent, RegistrationPayload, HeartbeatPayload
|
||
```
|
||
|
||
## Архитектура Agent-режима — два транспорта
|
||
|
||
### Проблема
|
||
|
||
Изначально agent-режим работал только через HTTP: агент поднимает HTTP-сервер на `listen_port`, регистрируется в трекере с `callback_url`, трекер шлёт события POST-запросами. Это требует открытого порта на стороне агента и доступности агента по сети для трекера.
|
||
|
||
### Решение: WS-транспорт
|
||
|
||
Добавлен альтернативный WebSocket-транспорт (`transport: "ws"`). Агент сам подключается к трекеру — двунаправленный канал, не нужен ни открытый порт, ни callback_url. Удобно для NAT, firewall, облачных окружений.
|
||
|
||
### Как устроено
|
||
|
||
Выбор транспорта — поле `transport` в конфиге (`"http"` | `"ws"`, default `"http"` для обратной совместимости).
|
||
|
||
#### Общие компоненты (не зависят от транспорта)
|
||
|
||
| Компонент | Файл | Роль |
|
||
|-----------|------|------|
|
||
| `EventRouter` | `router.ts` | Получает `TrackerEvent`, вызывает `runAgent()`, отправляет результат через `TrackerClient` |
|
||
| `TrackerClient` | `tracker/client.ts` | HTTP-клиент к REST API трекера (PATCH task, POST comment). Используется в обоих транспортах |
|
||
| `TaskTracker` | `router.ts` (interface) | `addTask(id)` / `removeTask(id)` — нужен EventRouter для heartbeat-учёта |
|
||
|
||
#### transport=http
|
||
|
||
```
|
||
index.ts → startAgentHttp()
|
||
├── TrackerRegistration (implements TaskTracker) — register + heartbeat через REST
|
||
├── HttpTransport — HTTP-сервер POST /events
|
||
└── EventRouter(config, client, registration)
|
||
```
|
||
|
||
Поток: Tracker → HTTP POST → HttpTransport → EventRouter → runAgent() → TrackerClient → Tracker
|
||
|
||
#### transport=ws
|
||
|
||
```
|
||
index.ts → startAgentWs()
|
||
├── WsClientTransport (implements TaskTracker) — connect + auth + heartbeat + events через WS
|
||
└── EventRouter(config, client, wsTransport)
|
||
```
|
||
|
||
Поток: Agent → WS connect → Tracker ← events → WsClientTransport → EventRouter → runAgent() → TrackerClient (REST) → Tracker
|
||
|
||
**Ключевое отличие:** В WS-режиме нет `TrackerRegistration` и нет `HttpTransport`. Регистрация и heartbeat встроены в сам WebSocket-канал. А вот `TrackerClient` по-прежнему используется для REST-вызовов (updateTask, addComment) — эти операции идут через HTTP, не через WS.
|
||
|
||
### Интерфейс TaskTracker
|
||
|
||
```typescript
|
||
// router.ts
|
||
export interface TaskTracker {
|
||
addTask(taskId: string): void;
|
||
removeTask(taskId: string): void;
|
||
}
|
||
```
|
||
|
||
**Зачем:** EventRouter при обработке задачи вызывает `addTask()`/`removeTask()` чтобы heartbeat (неважно HTTP или WS) отправлял актуальный список задач и статус `busy`/`idle`. Раньше EventRouter зависел напрямую от `TrackerRegistration` — теперь зависит от интерфейса, что позволяет подставить WsClientTransport.
|
||
|
||
**Реализуют:**
|
||
- `TrackerRegistration` (для transport=http) — хранит Set, heartbeat через REST
|
||
- `WsClientTransport` (для transport=ws) — хранит Set, heartbeat через WS
|
||
|
||
### WS-клиент: детали реализации (`ws-client.ts`)
|
||
|
||
**URL:** Выводится из `tracker_url` автоматически — `http://` → `ws://`, `https://` → `wss://`, path `/ws?client_type=agent&client_id={slug}`. Совместим с существующим WS-эндпоинтом трекера (FastAPI, `ws/handler.py`).
|
||
|
||
**Протокол (JSON через WebSocket):**
|
||
|
||
```
|
||
Agent → Tracker: { type: "auth", token: "...", agent: { name, slug, capabilities, max_concurrent_tasks } }
|
||
Tracker → Agent: { type: "auth_ok" }
|
||
Tracker → Agent: { type: "auth_error", message: "..." }
|
||
Tracker → Agent: { type: "event", event: "task.assigned", data: {...}, ts: ..., id: "evt-xxx" }
|
||
Agent → Tracker: { type: "ack", id: "evt-xxx" }
|
||
Agent → Tracker: { type: "heartbeat", status: "idle"|"busy", current_tasks: ["task-1"] }
|
||
```
|
||
|
||
**Жизненный цикл:**
|
||
1. `start()` → `connect()` → WebSocket open → send `auth`
|
||
2. Получаем `auth_ok` → start heartbeat → resolve start() promise
|
||
3. Получаем `auth_error` → reject start() promise, close WS
|
||
4. При закрытии WS (если не `stop()`) → scheduleReconnect()
|
||
5. `stop()` → close WS, clear timers
|
||
|
||
**Reconnect:** Exponential backoff 1s → 2s → 4s → ... → 30s cap. Сбрасывается при успешной auth. При первом `start()` если auth_error — не реконнектится (promise reject). При последующих обрывах — реконнект автоматически.
|
||
|
||
**Dedup:** Set<string> с обрезкой при >10000 (оставляем последние 5000). Идентично HttpTransport.
|
||
|
||
**Heartbeat:** setInterval с `heartbeatIntervalSec` из конфига. Отправляет текущий статус (`idle`/`busy`) и список задач. Останавливается при закрытии WS, перезапускается при реконнекте.
|
||
|
||
### Что НЕ реализовано на стороне трекера
|
||
|
||
Серверная часть (tracker, Python/FastAPI) пока **не имеет** полной поддержки agent WS-протокола. Текущий `ws/handler.py`:
|
||
- Принимает WebSocket-подключения с `client_type=agent`
|
||
- Обрабатывает `agent.heartbeat` (отвечает `agent.heartbeat.ack`)
|
||
- **Не** обрабатывает: `auth`, `ack`, отправку `event` агенту
|
||
|
||
**Для полной работы transport=ws нужно доработать трекер:** добавить обработку `type: "auth"` (валидация токена, регистрация агента), отправку `type: "event"` при назначении задачи агенту, и обработку `type: "ack"`.
|
||
|
||
## Ключевые модули
|
||
|
||
### index.ts — Точка входа
|
||
|
||
- `main()` → `hasAgentConfig()` ? `startAgentMode()` : `startWebSocketMode()`
|
||
- `startAgentMode()` → `loadAgentConfig()` → `initAgent()` → `TrackerClient` → if ws: `startAgentWs()`, else: `startAgentHttp()`
|
||
- `startAgentHttp(config, client)` — TrackerRegistration + HttpTransport + EventRouter
|
||
- `startAgentWs(config, client)` — WsClientTransport + EventRouter (без HTTP-сервера)
|
||
- `startWebSocketMode()` — WebSocketTransport (чат-режим, не связан с трекером)
|
||
|
||
### agent.ts — Ядро
|
||
|
||
- `initAgent(provider, apiKey)` — вызвать при старте для injection API key (через `AuthStorage.setRuntimeApiKey()`)
|
||
- `runAgent(prompt, options)` — async generator, yield-ит AgentMessage (`type: 'text'|'tool_use'|'tool_result'|'error'|'done'|'session'`)
|
||
- MODEL_ALIASES: sonnet → claude-sonnet-4-6, opus → claude-opus-4-6, haiku → claude-haiku-4-5 и т.д.
|
||
- DEFAULT_MODELS: автоматически создаёт `~/.picogent/models.json`
|
||
- Skills: `loadSkills()` + `formatSkillsForPrompt()` из pi-coding-agent (progressive loading)
|
||
- Sandbox: `createSandboxedTools(workDir, allowedPaths)` из sandbox.ts
|
||
|
||
### config.ts — Конфигурация
|
||
|
||
- `Config` — для WebSocket-режима, из env vars
|
||
- `AgentConfig` — для Agent-режима, из agent.json + env
|
||
- `loadConfig()` / `loadAgentConfig()` — загрузчики
|
||
- `resolveConfig()` — CLI arg (dir/file) → env → default
|
||
- `hasAgentConfig()` — определяет режим (есть конфиг/dir/TRACKER_URL?)
|
||
|
||
Приоритет: env > JSON file > defaults.
|
||
|
||
### sandbox.ts
|
||
|
||
- `createSandboxedTools(cwd, allowedPaths?)` — создаёт tools с проверкой путей
|
||
- allowedPaths пустой/undefined = без ограничений
|
||
- Оборачивает Read/Write/Edit operations с `guard()`, Bash с `spawnHook`
|
||
|
||
### router.ts — Роутер событий
|
||
|
||
- `TaskTracker` interface — абстракция для учёта активных задач
|
||
- `EventRouter` — принимает `TrackerEvent`, диспетчеризует:
|
||
- `task.assigned` → updateTask(in_progress) → runAgent() → addComment → updateTask(review)
|
||
- `chat.message` → runAgent() → addComment (если есть task_id)
|
||
- `buildPromptFromTask(task)` — формирует промпт из данных задачи (key, title, description, priority, files)
|
||
- Контроль конкурентности: `activeTasks` counter + `maxConcurrentTasks` check
|
||
|
||
### tracker/client.ts — REST-клиент
|
||
|
||
HTTP-клиент к API трекера. Используется **в обоих** транспортах (http и ws) для мутаций:
|
||
- `register(payload)` → POST `/api/v1/agents/register`
|
||
- `heartbeat(payload)` → POST `/api/v1/agents/heartbeat`
|
||
- `updateTask(id, fields)` → PATCH `/api/v1/tasks/{id}`
|
||
- `addComment(id, content)` → POST `/api/v1/tasks/{id}/comments`
|
||
- `uploadFile()`, `getTask()`, `listTaskFiles()` — дополнительные
|
||
|
||
Авторизация: Bearer token в заголовке.
|
||
|
||
### tracker/registration.ts — HTTP-регистрация
|
||
|
||
Используется **только** в transport=http:
|
||
- `register(callbackUrl)` — отправляет POST register, retry каждые 5с при ошибке
|
||
- `startHeartbeat()` / `stopHeartbeat()` — интервальные POST heartbeat
|
||
- Implements `TaskTracker` — addTask/removeTask для учёта в heartbeat
|
||
|
||
### tracker/types.ts — Типы
|
||
|
||
```typescript
|
||
TrackerEvent { event: string, data: Record<string,unknown>, ts: number, id: string }
|
||
TrackerTask { id, key, title, description, status, priority, project_id, parent_id?, files? }
|
||
TrackerFile { id, name, url, size }
|
||
RegistrationPayload { name, slug, capabilities, max_concurrent_tasks, callback_url }
|
||
HeartbeatPayload { status: 'idle'|'busy', current_tasks: string[] }
|
||
```
|
||
|
||
### transport/http.ts — HTTP-транспорт
|
||
|
||
HTTP-сервер (agent-режим, transport=http):
|
||
- POST `/events` — принимает TrackerEvent, dedup по id, вызывает handler async
|
||
- GET `/health` — `{ status: 'ok' }`
|
||
- Dedup: Set<string>, обрезка при >10000 (последние 5000)
|
||
|
||
### transport/ws-client.ts — WS-транспорт
|
||
|
||
WebSocket-клиент к трекеру (agent-режим, transport=ws). Описание выше в разделе архитектуры.
|
||
|
||
### transport/websocket.ts — WS-сервер (чат)
|
||
|
||
WebSocket-сервер для чат-режима (не agent). Принимает `IncomingMessage`, стримит `OutgoingMessage`.
|
||
|
||
### transport/types.ts — Общие типы
|
||
|
||
```typescript
|
||
IncomingMessage { id, content, sessionId?, workDir?, model?, systemPrompt? }
|
||
OutgoingMessage { id, type: 'text'|'tool_use'|'tool_result'|'error'|'done', content, sessionId? }
|
||
MessageHandler (msg: IncomingMessage) => Promise<void>
|
||
Transport { start(), stop(), onMessage(handler), send(clientId, msg) }
|
||
```
|
||
|
||
## Зависимости (Pi SDK)
|
||
|
||
```
|
||
@mariozechner/pi-agent-core — AgentTool, createAgentSession, SessionManager
|
||
@mariozechner/pi-coding-agent — createReadTool, createWriteTool и т.д., loadSkills
|
||
@mariozechner/pi-ai — streamSimple (LLM streaming)
|
||
```
|
||
|
||
Важно:
|
||
- `codingTools` — дефолтные tools привязанные к process.cwd(), **НЕ** используем напрямую
|
||
- `createCodingTools(cwd)` — tools привязанные к cwd, но без path restriction
|
||
- Вместо них используем свой `createSandboxedTools()` из sandbox.ts
|
||
- `ModelRegistry.find(provider, modelId)` требует ПОЛНЫЙ model ID, не алиас
|
||
- `AuthStorage.setRuntimeApiKey()` — runtime injection, вызывать до первого prompt
|
||
|
||
## Конфигурация агента
|
||
|
||
Файл `agent.json` (или через env). Полный пример с описаниями: `agent.example.json`.
|
||
|
||
Обязательные поля: `tracker_url`, `token`.
|
||
Env-переменные имеют приоритет над JSON.
|
||
|
||
| Поле | Env | Default | Описание |
|
||
|---|---|---|---|
|
||
| `name` | `AGENT_NAME` | `Agent` | Имя агента в трекере |
|
||
| `slug` | `AGENT_SLUG` | `agent` | Уникальный ID (латиница) |
|
||
| `prompt` | `AGENT_PROMPT` | — | Системный промпт (роль агента) |
|
||
| `tracker_url` | `TRACKER_URL` | — | URL трекера (**обязательно**) |
|
||
| `token` | `AGENT_TOKEN` | — | Bearer-токен (**обязательно**) |
|
||
| `transport` | `AGENT_TRANSPORT` | `http` | `http` или `ws` |
|
||
| `listen_port` | `AGENT_PORT` | `3200` | Порт HTTP (только transport=http) |
|
||
| `work_dir` | `PICOGENT_WORK_DIR` | agentHome/cwd | Рабочая директория |
|
||
| `model` | `PICOGENT_MODEL` | `sonnet` | Модель LLM |
|
||
| `provider` | `PICOGENT_PROVIDER` | `anthropic` | Провайдер LLM |
|
||
| `api_key` | `PICOGENT_API_KEY` | — | API ключ |
|
||
| `capabilities` | — | `["coding"]` | Возможности для трекера |
|
||
| `max_concurrent_tasks` | — | `2` | Макс. параллельных задач |
|
||
| `heartbeat_interval_sec` | — | `30` | Интервал heartbeat (сек.) |
|
||
| `allowed_paths` | — | `[]` (без ограничений) | Ограничение доступа к FS |
|
||
|
||
## Режим директории
|
||
|
||
```
|
||
agents/coder/
|
||
agent.json # конфиг
|
||
skills/ # скилы (SKILL.md)
|
||
sessions/ # сессии (auto)
|
||
```
|
||
|
||
`agentHome` = путь к директории. Skills из `agentHome/skills/`, сессии в `agentHome/sessions/`.
|
||
|
||
## Связь с трекером (Team Board Tracker)
|
||
|
||
Трекер — отдельный проект (`tracker/`), Python/FastAPI + SQLAlchemy. API:
|
||
|
||
- REST: `/api/v1/agents/`, `/api/v1/tasks/`, `/api/v1/projects/`, `/api/v1/labels/`, `/api/v1/chats/`
|
||
- WebSocket: `/ws?client_type=...&client_id=...`
|
||
|
||
Текущий WS-handler трекера (`tracker/src/tracker/ws/handler.py`) поддерживает:
|
||
- `agent.heartbeat` → `agent.heartbeat.ack`
|
||
- `chat.send` / `chat.subscribe` / `chat.unsubscribe` — чат-функционал
|
||
|
||
**Не поддерживает пока:** protocol `type: "auth"/"auth_ok"/"event"/"ack"` — нужно для transport=ws.
|
||
|
||
Агенты в трекере: CRUD `/api/v1/agents/`, модель Agent (id, name, slug, token, capabilities, status, max_concurrent, timeout_seconds).
|
||
|
||
## Известные ограничения
|
||
|
||
- Bash sandbox проверяет только cwd, не содержимое команд
|
||
- Grep/Find/Ls не ограничиваются allowed_paths (read-only)
|
||
- session.ts не используется (legacy от первой версии)
|
||
- **transport=ws** требует доработки серверной части трекера (auth, event dispatch, ack handling)
|
||
- TrackerClient (REST) endpoints — могут потребовать адаптации под конкретный трекер
|
||
- В WS-режиме REST-вызовы (updateTask, addComment) идут отдельным HTTP-каналом, не через WS
|
||
|
||
## Происхождение
|
||
|
||
Проект извлечён из двух соседних проектов:
|
||
|
||
- **OpenClaw** (`openclaw/`) — паттерн in-process agent loop через Pi Agent Core
|
||
- **NanoClaw** (`nanoclaw/`) — паттерн pino-логгера, структура "сообщение → агент → результат"
|
||
|
||
Сознательно выброшено: каналы, контейнеры, плагины, auth profiles, MCP, subagents и прочая сложность. Picogent — это голый agent loop + транспорт.
|