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

19 KiB
Raw Blame History

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.

Команды

npm install          # Установка
npm run build        # tsc -> dist/
npm run dev          # Dev (tsx + .env)
npm start            # Production (node + .env)
npm test             # Тесты (vitest)

Запуск

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

// 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 — Типы

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, обрезка при >10000 (последние 5000)

transport/ws-client.ts — WS-транспорт

WebSocket-клиент к трекеру (agent-режим, transport=ws). Описание выше в разделе архитектуры.

transport/websocket.ts — WS-сервер (чат)

WebSocket-сервер для чат-режима (не agent). Принимает IncomingMessage, стримит OutgoingMessage.

transport/types.ts — Общие типы

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