# agents

**Historical proof boundary (T-111):** 2026-06-17 evidence used the former
replacement-session `/agent run --yes` / `--approve` implementation and showed
session switch-back. It remains historical evidence only. Current `/agent run`
does not replace the parent session: it uses the shared `createAgentSession` SDK
child, live row, progress panel and run boundary described below.

## Назначение

`agents` — default-loaded operator/catalog extension. Он регистрирует команду `/agent` через `package.json#pi.extensions`, импортирует agent definitions из `.agents/agents/*.md` и показывает diagnostics для invalid files.

`/agent run <name> <task>` запускает один command-side SDK child через `createAgentSession` и тот же `executeAgentRunBoundary`, что используется tool path. Флаги `/agent run --yes <name> <task>` и `/agent run --approve <name> <task>` сохранены как legacy trusted-operator syntax, но локальный Locus approval prompt больше не выполняется. Команда создаёт shared live row, показывает progress, собирает structured result и записывает result artifact без parent-session switch. Tool `task` спавнит реального child-агента из programmatic tool-call контекста через тот же host SDK и возвращает его structured result вызывающему; этот путь покрыт retained SDK-host tests и повторным installed-package Pi smoke. Оба пути fail-closed, когда host не экспортирует `createAgentSession` или child run реально завершился ошибкой. `task` также принимает старую форму `{ agent, tasks }`, optional `agent`, aliases `default -> task` и `general -> task`, и top-level `parentContext`. `general` является built-in alias только когда project/user catalog не содержит реального `.agents/agents/general.md`.

## Почему он есть в проекте

`locus-pi` владеет catalog layer и общим SDK child-session adapter для slash/tool triggers. Это осознанная граница: оператор может проверить agent definitions и live command path, а model-callable `task` запускает агентов через тот же host contract с safety, timeouts, cancellation and durable evidence.

Старый deterministic accepted-task summary был опасен тем, что выглядел как успешный запуск агента, хотя модель и subprocess реально не создавались. Текущий runner принимает `completed` только после реального child turn и непустого ответа. Tool calls, JSONL transcript, `LOCUS_AGENT_RESULT_V1` и `locus_workload_proof` остаются evidence, но reasoning-only ответ без tool call теперь валиден.

## Контракт поверхностей

- Оператор вызывает `/agent list`, чтобы увидеть загруженные markdown definitions. В TUI полный typed `VIEW` открывается в scroll overlay. RPC и хосты без custom UI получают компактный passive preview с source counts, `+N definition(s) hidden` и командами inspect/run, поэтому host limit не скрывает recovery action.
- Оператор вызывает `/agent inspect <name>`, чтобы увидеть typed `VIEW` с source, file path, tools, read-only status, risk, spawn policy and model hints для одной definition. Известная definition использует тот же TUI overlay; unknown target остаётся bounded `ERROR`, показывает source-priority candidates, built-in aliases, `/agent list` recovery и путь diagnostic artifact.
- Оператор вызывает `/agent run <name> <task>`, чтобы запустить один SDK child в текущей parent-side live surface без session switch. Pi native approval handles child tool prompt/deny decisions; `/agent run --yes` and `/agent run --approve` are retained legacy trusted-operator forms. Headless/fallback completion uses typed `RESULT` or `ERROR` chrome with child/result evidence and drill command. Рабочий TUI progress panel остаётся отдельным domain-owned `LIVE` surface и не заменён static renderer. Команда показывает `completed`, `failed`, `cancelled` или `blocked`. Unknown-agent ошибка рендерится в widget `agents`, показывает доступных агентов, built-in aliases, подсказку `/agent list` и пишет diagnostic artifact в `.locus/runtime/artifacts/*.json`.
- Unknown or incomplete `/agent` syntax returns typed `WARN` with a no-action statement and the exact relevant usage. It does not leave recovery as an equal-weight notification.
- Оператор вызывает `/agent observe` или `/agent summary`, чтобы увидеть bounded snapshot текущих и recent live rows без фонового daemon. Rows show source-backed status, elapsed time, activity state, concrete runtime model plus `/effort` thinking level when available, current path, current tools, bounded tool args, turn/token counts, child session, result artifact, final answer, flags, and errors. `steps` is rendered as event count, `turns` as model turns, and `flags` include inline meaning such as `no-mcp(no MCP tools)` so the row is readable without source code. `done`, `cancelled` и `error` rows остаются terminal, not running. Любой terminal transition атомарно очищает `currentTools`, `currentToolArgs` и `currentToolStartMs`, а также фиксирует `elapsedMs`, поэтому drill не показывает старую команду и длительность больше не растёт после завершения.
- `/agent list`, `/agent inspect`, `/agent observe`, and `/agent summary` render transient `agents` widgets. Shared command UI lifecycle clears that widget and `agents` status on the next unrelated user input or slash command; related `/agent ...` input is left alone so the command can replace the widget. `/devext doctor` now gets the same cleanup through the shared lifecycle instead of owning an `agents` special case.
- Пока есть live rows, под редактором остаётся развёрнутый fleet. Bare `Up`/`Down` (`↑/↓`) всегда принадлежат Pi для cursor/history/editor и extension их не перехватывает. Вход в fleet выполняется через `/ps` или зарегистрированный `Shift+Down`; после фокуса `↑/↓` двигают выбор. Aggregate/group rows видимы как headings, но не выбираются курсором и не принимаются `Enter`, `x` или `last`; direct aggregate id даёт guidance выбрать один child agent. `Cmd+Down`, `Ctrl+Down` и прежний `Alt/Option+A` не регистрируются и не показываются. Installed-Pi tmux mid-run доказал fallback `Shift+Down → focus → arrows → Enter → drill`. Slash-команда не используется как fallback, потому что текущий синхронный command handler должен сначала завершиться. `Enter` открывает drill, а `x` сначала показывает Yes/No confirmation и только после подтверждения посылает реальный abort выбранному SDK child. `Esc` внутри fleet или drill закрывает только просмотр; notification явно сообщает, что активный агент продолжает работу. Если bare `Esc` дошёл бы до стандартного Pi `app.interrupt` во время parent turn с live agent rows, extension сначала спрашивает подтверждение; отказ оставляет операцию работающей, подтверждение вызывает `ctx.abort()`. Строки, курсор и последняя строка footer всегда принадлежат `belowEditor`; editor-replacement только перехватывает клавиши и ничего не рисует. В RPC остаётся обновляемый пассивный `string[]` widget без ложного управления.
- Оператор вызывает `/ps` для fleet selection или `/ps <row-id|agent|petname|last>` для прямого перехода. `/agent ps` — стабильный fallback при collision, `/agent drill` — compatibility alias. Resolver сохраняет exact/normalized/ambiguity правила. Выбранная строка открывается как 100% overlay без frame и видимого parent content. Viewer использует Pi `AssistantMessageComponent`/`ToolExecutionComponent`, follow-tail, ручной scroll, `End`, compact/expanded tools и `Esc`/`q` return without abort. Fleet показывает последнее видимое assistant message; raw args/stdout/event names остаются detail/diagnostics.
- Основной агент или workflow runtime может вызвать primary tool `spawn_agent` (back-compat alias `task`) с optional `agent`, required `tasks` и optional top-level `parentContext`. Если `agent` отсутствует или равен `default`, runner выбирает `task`; если `agent` равен `general`, runner выбирает `task` только когда нет real project/user `general`. Каждый task спавнит реального headless child-агента через `createAgentSession` и возвращает `structuredResult`: marked JSON принимается, raw structured JSON принимается, а свободный непустой текст runtime заворачивает как `completed`. Unknown-agent и child-session failures возвращаются как `ToolResult` с `isError=true`, text `content` и structured `details`.
- Child-агент не может напрямую породить ещё одного child-агента. SDK host всегда передаёт `excludeTools: ["spawn_agent", "task"]`; Pi применяет этот deny после любого `tools` allowlist, поэтому `tools: ["*"]`, явный `task` или legacy `spawns` metadata не обходят запрет. Bundled profiles больше не объявляют `spawns`. Parser сохраняет legacy/project `spawns` только для совместимости каталога и помечает его как blocked. При этом `workflow` намеренно не исключён: child может написать `.pi/workflows/*.workflow.mjs` и вызвать tool `workflow`, если его профиль разрешает этот tool; уже workflow runtime владеет orchestration и отдельными child rows.
- Token counter обновляется на каждом `turn_end` из реального assistant `usage.input + usage.output`, то есть `↓<tok>` появляется до завершения агента. Финальный `getSessionStats().tokens` заменяет промежуточное значение cumulative truth. У каждого child остаётся собственный счётчик; summary row для `parallel`/`pipeline` workflow рекурсивно суммирует token-bearing descendants, не считая group rows повторно.
- Default output contract is text. `structuredResult` is the parent-runtime envelope, not a requirement that every child emit JSON by default. When a child does emit structured `artifacts`, the parser accepts `{ path }` entries, path-string arrays, or object maps and normalizes them to `{ path, kind?, title? }` records. Structured `diagnostics` may be an array, string, or object; object/string diagnostics are normalized instead of hiding the child result behind a parser-shape error.
- Legacy tool `runAgent` удален из active surface. Команда `/agent run` остается единственным command-gated run path.
- Агент или reviewer flow может вызвать `locus_workload_proof`, чтобы записать diagnostic persisted evidence после bounded child workload. Этот tool не является execution engine и не управляет success/failure сам по себе.

## Как работает по коду

Entrypoint `extensions/agents/index.ts` регистрирует команду `agent` через shared `registerCommandWithUiLifecycle`, primary tool `task` и proof-evidence tool `locus_workload_proof`. Перед command/tool вызовами extension обновляет список definitions через `_shared/agents.ts` из одного соглашения: ближайший project `.agents/agents`, user `~/.agents/agents`, затем bundled `.agents/agents` внутри пакета. Merge rule — first wins, поэтому project definitions перекрывают user и bundled agents, а user definitions перекрывают bundled agents.

Parser принимает YAML-like frontmatter, достаточный для локального catalog. Он сохраняет `name`, `description`, `tools`, legacy `spawns`, `model`, `thinking-level`, `blocking`, `output`, `systemPrompt`, `source` и `filePath`. `spawns` является только совместимым catalog metadata и не выдаёт runtime capability: direct nesting всегда блокирует SDK host. Nested `output` остается opaque metadata. Invalid markdown или missing required fields попадают в diagnostics и не становятся runnable definitions.

Tool execution path рабочий. `task` вызывает `_shared/agent-runner.executeAgentRunBoundary` с tool-context executor `createAgentSdkSessionExecutor` (`_shared/agent-sdk-host.ts`), который спавнит headless child через host-SDK `createAgentSession`, ждёт `agent_end`, читает `getSessionStats()`, экспортирует JSONL-транскрипт и парсит final answer. Runner по-прежнему проверяет depth, maxTurns и tool allow-list; approval prompt/deny decisions belong to Pi native approval before the tool handler runs, not to this runner. Nested `createAgentSession` currently receives `cwd`, allowed tools, model and optional `appendSystemPrompt`; no confirmed Pi SDK option is used here to inherit parent yolo/approval policy into the child session. Когда host не экспортирует `createAgentSession`, `task` честно fail-closed (`agent-sdk-session-unavailable`), без stale M11-текста. Когда child run возвращает `failed`, `blocked` или `cancelled`, `task` возвращает `isError=true` и сохраняет per-task details вместо успешного summary. Если `task` получает non-empty top-level `parentContext`, он передаёт его в каждый `createAgentRunRequest(..., { parentContext })`; пустой или absent `parentContext` не форвардится. В details результата есть `parentContextBroker: { forwarded, sources, agentDefault }` — и на top-level, и в каждом `results[]`. Empty or absent context is omitted and reports `forwarded:false`, `sources:[]`, and `agentDefault` reflects `agent.parentContextDefault`. Если definition имеет `systemPrompt`, SDK path добавляет его через `DefaultResourceLoader.appendSystemPromptOverride`, чтобы сохранить базовый Pi prompt и tool semantics.

Pi загружает каждый package entrypoint отдельным `jiti` с выключенным module cache. Поэтому module-local singleton не является меж-extension контрактом: `agents` и `workflows` раньше получали разные `agentLiveStore`, и workflow row была видна в progress, но не в `/agent drill`. Store теперь находится в versioned `globalThis[Symbol.for(...)]`; отдельный regression-тест загружает producer/consumer двумя реальными Pi entrypoints и доказывает общую строку. На `session_start` store очищается, поэтому process scope не превращает строки предыдущей сессии в текущие.

`buildAgentSystemPrompt` дополняется опционально блоком context extras, только когда `LOCUS_AGENT_CONTEXT_EXTRAS=1`. По умолчанию extras выключены. Memory extras берётся из `LOCUS_AGENT_MEMORY_FILE` или `<projectRoot>/MEMORY.md`; skill extras берутся только из явных записей `LOCUS_AGENT_PRELOAD_SKILLS` и резолвятся по строгим путям `.agents/skills/<name>/SKILL.md` или `.pi/skills/<name>/SKILL.md`. Оба блока bounded: memory берёт первые 200 строк и byte cap, skills тоже clamped, а missing files становятся diagnostics и не валят child run.

`/agent run` зовёт `runAgentLiveTask()`, который создаёт row в process-shared `agentLiveStore`, строит deterministic request capsule with resolved model-role metadata, и запускает `createAgentSdkSessionExecutor()`. Executor использует host-SDK `createAgentSession`, добавляет persona через `DefaultResourceLoader.appendSystemPromptOverride`, читает реальные child events/stats/final answer, принимает marked JSON, raw structured JSON или free text и мапит abort/cancellation в terminal row. Parent-session `ctx.newSession` и `pi.sendUserMessage` в current command path не используются.

`/ps` and its compatibility aliases open `AgentSessionViewer` only in interactive TUI with native Pi component capability. One bounded typed timeline owns stable assistant/tool blocks, structured lifecycle, `cwd`, redaction, `latestMessage`, 100 content items per message, and aggregate `512 KB`/`5000` node limits. Raw `eventLines` remains diagnostic-only. RPC/headless receives honest passive fallback; historical workflow rounds retain journal summary fallback. Global input guards yield while viewer owns the terminal, native tool components update only on state transitions, and listener/component cleanup is idempotent.

Live-row rendering is shared with workflow views through `_shared/agent-live-panel.ts`. The shared renderer owns row ordering, bounded text formatting, concrete model plus `/effort` display, activity labels, path display, tool-argument clamping, group summaries, and optional child-result fields. `/agent observe`, `/agent summary`, `/agent drill`, `/workflows run`, and the headless workflow `liveAgents` fallback all read from the same local `AgentLiveRow` contract instead of copying separate row formatters.

- Entrypoint: `./extensions/agents/index.ts`
- Manifest: `extensions/agents/manifest.json`
- Commands: `agent list`, `agent inspect`, `agent run`, `agent observe`, `agent summary`, `agent drill`
- Tools: `spawn_agent` (primary), `task` (back-compat alias), `locus_workload_proof`
- Hooks: `session_start`, `input`
- Input controls: bare `up`/`down` всегда остаются у Pi; fleet entry — `/ps` или registered fallback `shift+down`. После фокуса стрелки двигают только leaf rows; aggregate/group rows видимы, но не selectable.
- Permissions: fs.read=`.agents/agents/*.md`, `~/.agents/agents/*.md`, bundled `.agents/agents/*.md`, `MEMORY.md`, `.agents/skills/*/SKILL.md`, `.pi/skills/*/SKILL.md`, `.locus/runtime/agent-workload-proof/*.json`; fs.write=`.locus/runtime/artifacts/*.json`, `.locus/runtime/agent-workload-proof/*.json`; subprocess=none; network=none; browser=false; models=false; ui=`setWidget`, `setStatus`, `notify`, `custom`, `confirm`, `onTerminalInput`, `getEditorText`, `registerShortcut`
- State: agent definitions and diagnostics live in memory; command run reports SDK `childSessionId`, optional `parentSessionId`, `resultArtifact`, `childEntries` and `assistantToolCalls` when available; process-shared live rows keep source-backed optional metadata such as `model`, `thinking` (shown as `/effort`), `currentPath`, `currentToolArgs`, `turnCount`, `tokenCount`, `childSessionId`, `resultArtifact`, and `finalAnswer`; fleet selection is ephemeral UI state and is not persisted.
- Review: status=reviewed, source=write-from-scratch, reviewedBy=locus-pi, reviewedAt=2026-06-08, risk=high

## Ограничения и риски

Этот extension выполняет `tasks:[]` последовательно (`parallel: false`, не true-concurrent — честно отражено в tool detail) и не переносит OMP `TaskTool`. `/agent run` полезен как local catalog inspect surface, command-side SDK live runner, result-artifact writer и proof path. `task` — рабочий execution tool, доказанный на живом хосте (genuine child workload + `structuredResult`), а не skeleton.

Parser-clean `LOCUS_AGENT_RESULT_V1` без workload proof теперь может быть success, если это реальный непустой ответ ребёнка. Tool calls, tool results, transcript proof и persisted `locus_workload_proof` остаются diagnostic evidence, но не являются обязательным условием `completed`. Durable unknown-agent artifacts are diagnostic evidence only; they do not prove a child run happened.

`/agent observe` / `/agent summary` are an on-demand local live-store summarizer. Они читают bounded snapshot из `agentLiveStore.rows`, показывают total + status counts, bounded recent rows, bounded event digests, and optional row metadata when current code has a real source for it. `done`, `cancelled` и `error` rows остаются terminal, not running, без stale tool args. `observer` не является full pi-subagents parity: это local summarizer over live rows, not a background daemon.

Viewer даёт native Pi presentation parity для наблюдения, но не literal session handoff: parent runtime продолжает владеть child execution. Child editor, slash commands, model/settings и `switchSession()` transfer не заявляются.

Terminal assistant `stopReason: "error"` surfaces its `errorMessage` before the generic empty-result fallback. A `completed` child still requires a real non-empty answer; side-effect-only success and DSL changes are outside this extension contract.

## Решение

Решение: `default-command-gated-single-agent` + tool-safe `task` через host-SDK `createAgentSession`. `agents` находится в `package.json#pi.extensions`; `task` проверяется retained SDK-host tests и installed-package Pi smoke, новый entrypoint при этом не добавлялся.

## Agent profiles — when to use default vs scout

`default` is the general reasoning and delegation profile. Use it when the caller needs an owner-default child agent for broad delegated workflow steps and does not need runtime evidence. Its bundled profile sets `evidence.mode: none`.

`explore` is the read-only scout profile. Use it for fast codebase investigation and handoff context. It is expected to call at least one tool; the bundled profile sets `evidence.mode: warn` and `requireAnyToolCall: true`, so an empty tool trace produces a warning.

`task` is the full-tool worker profile. Use it for delegated implementation or multi-step work that may need file edits, shell commands, or other tools. Its bundled profile sets `evidence.mode: warn` and `requireAnyOf: [bash]`, so a run with no observed `bash` call produces a warning. Named-tool evidence comes only from exact `tool_execution_start/update/end` events seen by the SDK child subscriber, normalized by trim/deduplication/stable sort and transported as `AgentChildOutputStats.recordedToolNames`. Aggregate `toolCalls`/`toolResults` still prove that some workload occurred, but never fabricate a specific tool name; counters without a named event therefore cannot satisfy `requireAnyOf: [bash]`.
