picogent/CLAUDE.md
2026-02-21 02:41:39 +03:00

330 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 + транспорт.