19 KiB
CLAUDE.md — picogent
Что это
Минимальный AI-агент с in-process agentic loop на базе Pi Agent Core. Два режима работы:
- WebSocket-режим (чат) — сервер WebSocket, браузерный клиент (
client.html) отправляет промпты, получает стриминг-ответы - 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 через RESTWsClientTransport(для 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"] }
Жизненный цикл:
start()→connect()→ WebSocket open → sendauth- Получаем
auth_ok→ start heartbeat → resolve start() promise - Получаем
auth_error→ reject start() promise, close WS - При закрытии WS (если не
stop()) → scheduleReconnect() 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 + EventRouterstartAgentWs(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 varsAgentConfig— для Agent-режима, из agent.json + envloadConfig()/loadAgentConfig()— загрузчикиresolveConfig()— CLI arg (dir/file) → env → defaulthasAgentConfig()— определяет режим (есть конфиг/dir/TRACKER_URL?)
Приоритет: env > JSON file > defaults.
sandbox.ts
createSandboxedTools(cwd, allowedPaths?)— создаёт tools с проверкой путей- allowedPaths пустой/undefined = без ограничений
- Оборачивает Read/Write/Edit operations с
guard(), Bash сspawnHook
router.ts — Роутер событий
TaskTrackerinterface — абстракция для учёта активных задачEventRouter— принимаетTrackerEvent, диспетчеризует:task.assigned→ updateTask(in_progress) → runAgent() → addComment → updateTask(review)chat.message→ runAgent() → addComment (если есть task_id)
buildPromptFromTask(task)— формирует промпт из данных задачи (key, title, description, priority, files)- Контроль конкурентности:
activeTaskscounter +maxConcurrentTaskscheck
tracker/client.ts — REST-клиент
HTTP-клиент к API трекера. Используется в обоих транспортах (http и ws) для мутаций:
register(payload)→ POST/api/v1/agents/registerheartbeat(payload)→ POST/api/v1/agents/heartbeatupdateTask(id, fields)→ PATCH/api/v1/tasks/{id}addComment(id, content)→ POST/api/v1/tasks/{id}/commentsuploadFile(),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.heartbeat→agent.heartbeat.ackchat.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 + транспорт.