# 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 с обрезкой при >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, 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, обрезка при >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 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 + транспорт.