Files
pi-auto-reinject/SPEC.md
T
grayhook 0622ccbb32 Add initial spec for pi-skill-reinject extension.
Document skill tracking, post-compaction re-inject, pi-auto-compact
compatibility, and configuration via /skill-reinject commands.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 08:58:00 +07:00

32 KiB
Raw Blame History

ТЗ: pi-skill-reinject

Extension для Pi Coding Agent, который отслеживает уже вызванные skills и повторно инжектит их в контекст после auto compaction.

Статус: draft
Репозиторий: pi-auto-reinjection
Целевая платформа: upstream Pi (earendil-works/pi), не форки (oh-my-pi и т.п.)


1. Проблема

Pi использует progressive disclosure для skills: в system prompt всегда только name + description, полный SKILL.md попадает в контекст через read или /skill:name (skills.md).

При compaction старые сообщения суммаризуются; полный текст skill-блоков из «сжатой» части истории теряется (compaction.md). После auto compaction:

  • descriptions skills остаются в system prompt;
  • модель не обязана снова читать SKILL.md (skills.md — «models don't always do this»);
  • встроенного механизма re-inject в upstream Pi нет.

2. Цель

Универсальный extension, который:

  1. Отслеживает skills, реально использованные в сессии.
  2. После auto compaction повторно инжектит их в контекст (как при /skill:name).
  3. По умолчанию выключен; включается командой на уровне сессии или глобально (навсегда).

3. Не-цели (v1)

  • Замена/обёртка стандартного compaction summary.
  • Re-inject после ручного /compact (опционально в v2, см. §8).
  • Поддержка oh-my-pi-специфики (compaction.autoContinue, snapcompact и т.д.).
  • Re-inject skills, которые модель «прочитала» через read, но пользователь явно не вызывал — только если включена опция trackReadPaths (см. §6.2, по умолчанию true).
  • Изменения в сторонних extensions (в т.ч. pi-auto-compact) — только опциональная интеграция на нашей стороне с автоопределением (§16).

4. Ссылки на документацию Pi

Тема Ссылка
Extensions (API, события, команды) packages/coding-agent/docs/extensions.md
Compaction (когда срабатывает, что теряется) packages/coding-agent/docs/compaction.md
Skills (вызов, формат, progressive disclosure) packages/coding-agent/docs/skills.md
Settings (глобальные / project overrides) packages/coding-agent/docs/settings.md
Примеры extensions packages/coding-agent/examples/extensions/
sendUserMessage send-user-message.ts
Custom compaction hook custom-compaction.ts
parseSkillBlock (формат skill-блока) agent-session.ts
_expandSkillCommand (как Pi разворачивает /skill:name) agent-session.ts
Agent Skills standard agentskills.io
pi-auto-compact (совместимость) github.com/capyup/pi-auto-compact
pi-auto-compact исходник extensions/auto-compact.ts
Inter-extension events event-bus.ts example
before_agent_start (inject message) extensions.md — before_agent_start

5. Поведение

5.1. Режимы включения

Три независимых слоя (приоритет сверху вниз):

Слой Хранение Default Описание
Session override pi.appendEntry("skill-reinject:config", …) null (нет override) Вкл/выкл только в текущей сессии
Global default ~/.pi/agent/settings.jsonskillReinject.enabled false «Навсегда» для всех новых сессий
Effective вычисляется false sessionOverride ?? globalDefault

Команда /skill-reinject (см. §7) меняет session override или global default.

Важно: session override не переживает /resume в другую сессию — восстанавливается из entries этой сессии. Global default читается при session_start.

5.2. Когда делать re-inject

Триггер: событие extension session_compact (extensions.md — session_before_compact / session_compact).

Условия (все должны выполниться):

  1. effectiveEnabled === true
  2. Compaction не отменён (session_compact успешно отработал)
  3. Источник compaction = auto (threshold или overflow), не manual /compact
    → см. §8 про детекцию источника
  4. Есть хотя бы один tracked skill, отсутствующий в kept-части контекста (§6.4)
  5. Skill всё ещё зарегистрирован в resourceLoader (не удалён с диска)

Не делать re-inject:

  • при session_before_compact с cancel: true;
  • если список skills для re-inject пуст;
  • если агент в момент session_compact стримит и willRetry === true (overflow recovery) — отложить до agent_end или использовать deliverAs: "followUp" (§6.5).

5.3. Что именно инжектить

Повторять поведение Pi _expandSkillCommand: user message с блоком

<skill name="{name}" location="{filePath}">
References are relative to {baseDir}.

{body без YAML frontmatter}
</skill>

Опционально добавлять служебный суффикс (конфигурируемо):

[skill-reinject] Re-applied after compaction. Follow this skill's workflow.

Не использовать цепочку /skill:a /skill:b в одном сообщении — Pi разворачивает только один /skill: на сообщение.

Для N skills при доставке через sendUserMessage: отдельное сообщение на skill, порядок = порядок первого вызова в сессии.

При доставке через before_agent_start (§6.5, режим defer, в т.ч. с pi-auto-compact): одно injected message со всеми skill-блоками подряд (меньше turn'ов, нет гонки с follow-up).


6. Отслеживание skills

6.1. Структура состояния

interface TrackedSkill {
  name: string;           // frontmatter name
  filePath: string;       // абсолютный путь к SKILL.md
  baseDir: string;        // директория skill
  firstSeenAt: number;    // timestamp
  lastSeenAt: number;
  sources: Array<"slash" | "skill-block" | "read">;
}

interface ExtensionState {
  version: 1;
  sessionOverride: boolean | null;
  skills: TrackedSkill[];  // dedupe by name, preserve insertion order
  lastCompactionSource: "auto" | "manual" | null;
  /** Skills, ожидающие re-inject на следующем before_agent_start (§6.5) */
  pendingReinject: string[];  // skill names
}

/** Runtime-only, не персистить */
interface RuntimeFlags {
  autoCompactDetected: boolean;
  autoCompactIntegration: "auto" | "defer" | "immediate" | "off";
}

In-memory кэш + персистенция через pi.appendEntry при каждом изменении tracked skills / session override.

6.2. Источники детекции

# Событие Условие source
1 input event.text matches /^\/skill:([a-z0-9-]+)/ slash
2 message_end (user) текст содержит <skill name="…" (regex из parseSkillBlock) skill-block
3 tool_call / tool_result toolName === "read" и path совпадает с SKILL.md известного skill read

Для (3): сопоставлять path с skill.filePath из ctx / getSkills() (formatSkillsForPrompt).

Опция skillReinject.trackReadPaths (default true): если false, игнорировать детекцию через read.

6.3. Восстановление при session_start / /resume

На session_start (включая reason: "reload"):

  1. Пройти ctx.sessionManager.getBranch():
    • entries type === "custom" с customType === "skill-reinject:state" → восстановить state;
    • user messages → re-scan skill blocks;
  2. Если entries state нет — full rescan ветки.

6.4. Skills уже в kept-контексте

После compaction часть recent messages сохраняется (keepRecentTokens, default 20k — settings.md).

Перед re-inject:

  1. Взять entries от compactionEntry.firstKeptEntryId до хвоста ветки.
  2. Для каждого tracked skill: если в kept user messages есть <skill name="{name}"пропустить re-inject для этого skill.

6.5. Доставка после compaction

Два режима доставки (выбор — §6.5.1 / §16):

6.5.1. Режим defer (рекомендуемый по умолчанию при наличии pi-auto-compact)

  1. На session_compact: записать имена skills в state.pendingReinject (не вызывать sendUserMessage).
  2. На следующем before_agent_start: если pendingReinject не пуст — вернуть injected message с объединёнными skill-блоками, очистить очередь.

Skills попадают в контекст в том же turn, что и kickoff-сообщение (в т.ч. auto-continue от pi-auto-compact), без гонки sendUserMessage.

6.5.2. Режим immediate (sendUserMessage)

Использовать pi.sendUserMessage (extensions.md — sendUserMessage):

Ситуация Стратегия
Агент idle после compaction Первый skill → sendUserMessage(expandedBlock); остальные → followUp
Overflow recovery (willRetry) Все skills → sendUserMessage(..., { deliverAs: "followUp" })
Агент стримит deliverAs: "followUp" для всех

Не использовать immediate, если обнаружен pi-auto-compact и autoCompactIntegration !== "off" — см. §16.

6.5.3. Выбор режима

Условие Режим доставки
autoCompactIntegration: "defer" всегда defer
autoCompactIntegration: "immediate" всегда sendUserMessage
autoCompactIntegration: "off" по triggerTurn (§7.3)
autoCompactIntegration: "auto" (default) + pi-auto-compact обнаружен defer
autoCompactIntegration: "auto" + pi-auto-compact не обнаружен + triggerTurn: false defer (ждать следующий user prompt)
autoCompactIntegration: "auto" + pi-auto-compact не обнаружен + triggerTurn: true immediate

Важно: встроенный Pi auto-compaction (_runAutoCompaction) при reason: "threshold" не продолжает turn (willRetry: false). С установленным pi-auto-compact продолжение обеспечивает он, не Pi — наш defer рассчитан на этот сценарий.


7. Команда /skill-reinject

Зарегистрировать через pi.registerCommand("skill-reinject", …) (extensions.md — registerCommand).

7.1. Синтаксис

/skill-reinject                          # статус
/skill-reinject on                       # вкл в этой сессии
/skill-reinject off                      # выкл в этой сессии
/skill-reinject reset                    # сброс session override → global default
/skill-reinject global on                # вкл навсегда (~/.pi/agent/settings.json)
/skill-reinject global off               # выкл навсегда
/skill-reinject list                     # tracked skills
/skill-reinject clear                    # очистить tracked skills (не трогает toggle)
/skill-reinject now                      # принудительный re-inject (debug)
/skill-reinject integration auto|defer|immediate|off   # режим доставки (§6.5, §16)

Алиасы (опционально): /sr, /skills-reinject.

7.2. Вывод /skill-reinject (status)

skill-reinject: off (session override) | on (global) | on (session)
delivery: defer (pi-auto-compact detected) | immediate | defer
tracked: 2 skills — redmine-issue-context, fup-blame-commits
pending reinject: 0
last compaction: auto @ 14:32

Footer status через ctx.ui.setStatus("skill-reinject", "on·2") (status-line.ts example).

7.3. Global settings

Писать в ~/.pi/agent/settings.json (merge, не затирать файл целиком):

{
  "skillReinject": {
    "enabled": false,
    "trackReadPaths": true,
    "triggerTurn": false,
    "reinjectOnManualCompaction": false,
    "autoCompactIntegration": "auto",
    "suffix": "[skill-reinject] Re-applied after compaction."
  }
}
Поле Default Описание
autoCompactIntegration "auto" "auto" | "defer" | "immediate" | "off" — см. §6.5.3, §16

Project override: .pi/settings.json → тот же ключ (merged по правилам Pi, settings.md — Project Overrides).


8. Детекция auto vs manual compaction

Extension event session_compact не передаёт reason (SessionCompactEvent).

Стратегия v1:

// input handler (до expansion)
if (text.startsWith("/compact")) pendingCompactionSource = "manual";

// session_before_compact
if (pendingCompactionSource !== "manual") pendingCompactionSource = "auto";

// session_compact
const shouldReinject =
  effectiveEnabled &&
  (pendingCompactionSource === "auto" || settings.reinjectOnManualCompaction);
pendingCompactionSource = null;

Ручной RPC compact ({type: "compact"}) тоже помечать как manual если extension получает соответствующий сигнал (v2).


9. Архитектура файлов

pi-auto-reinjection/
├── SPEC.md                 # это ТЗ
├── README.md
├── package.json            # pi package manifest (опционально npm)
├── src/
│   ├── index.ts            # export default function(pi: ExtensionAPI)
│   ├── state.ts            # load/save/merge state, appendEntry
│   ├── detect.ts           # skill detection helpers
│   ├── expand.ts           # expand skill → block (mirror Pi logic)
│   ├── reinject.ts         # post-compaction reinject orchestration
│   ├── auto-compact.ts     # detect @capyup/pi-auto-compact, delivery mode (§16)
│   ├── settings.ts         # read/write skillReinject.* in settings.json
│   └── commands.ts         # /skill-reinject handler
├── test/
│   ├── detect.test.ts
│   ├── kept-window.test.ts
│   └── expand.test.ts
└── .gitignore

9.1. Установка (целевая)

# dev
pi -e ~/Documents/repos/pi-auto-reinjection/src/index.ts

# production
cp -r src/index.ts ~/.pi/agent/extensions/skill-reinject.ts
# или через pi package / settings.extensions

См. extensions.md — Extension Locations: ~/.pi/agent/extensions/, .pi/extensions/.


10. Зависимости

{
  "pi": {
    "extensions": ["./src/index.ts"]
  },
  "devDependencies": {
    "@earendil-works/pi-coding-agent": "workspace:* или latest",
    "typescript": "^5"
  }
}

Импорты только из публичного API Pi (extensions.md — Available Imports).

Для expand skill body: readFileSync + strip YAML frontmatter (как Pi), без дублирования приватных internal imports если возможно; иначе — локальная копия логики с комментарием «mirror agent-session._expandSkillCommand».


11. Edge cases

Case Поведение
Skill удалён с диска skip + ui.notify warning
Skill disable-model-invocation: true re-inject всё равно (explicit inject)
Два skill с одним name (collision) использовать первый из resourceLoader; warn
Compaction во время стрима followUp delivery
Пустой tracked list no-op
/tree branch switch state из entries новой ветки; rescan
session_shutdown flush pending appendEntry
RPC / print mode (hasUI === false) команды работают; notify → no-op; reinject без UI feedback

12. Тестирование

12.1. Unit

  • parseSkillBlock / detect в user text
  • kept-window: skill in / not in kept messages
  • expand: frontmatter strip, path resolution
  • settings merge read/write

12.2. Manual E2E (standalone)

  1. pi + extension, /skill-reinject on
  2. /skill:some-skill → длинная сессия → дождаться auto compaction (или уменьшить keepRecentTokens / reserveTokens в .pi/settings.json)
  3. Проверить: после compaction в контексте снова есть <skill name="some-skill"…>
  4. /skill-reinject off → compaction → re-inject не происходит
  5. /skill-reinject global on → новая сессия → re-inject без on

12.3. Manual E2E (с pi-auto-compact)

Предусловие: pi install npm:@capyup/pi-auto-compact (README).

  1. Оба extension загружены; /skill-reinject on
  2. /skill-reinject → status показывает delivery: defer (pi-auto-compact detected)
  3. /skill:some-skill → сессия до срабатывания pi-auto-compact (порог по умолчанию 90% context window)
  4. После auto-compaction:
    • pi-auto-compact отправляет follow-up («Auto-compact ran… Continue…»);
    • агент продолжает работу без idle;
    • в контексте turn'а есть skill-блок(и) до follow-up текста
  5. В логах/TUI нет ошибки Agent is already processing / Failed to send queued message
  6. Ручной /compact → pi-auto-compact не шлёт follow-up → re-inject только если reinjectOnManualCompaction: true, иначе pendingReinject сбрасывается на следующем user prompt
  7. Во время compaction пользователь печатает сообщение → pi-auto-compact молчит → re-inject через before_agent_start на turn пользователя

13. Критерии приёмки (v1)

  • Default off; /skill-reinject on включает re-inject в сессии
  • /skill-reinject global on сохраняет в ~/.pi/agent/settings.json и переживает restart
  • После auto compaction re-injectятся все tracked skills, отсутствующие в kept window
  • После manual /compact re-inject не происходит (при default reinjectOnManualCompaction: false)
  • Tracked skills: /skill:name, skill-block в user msg, read на SKILL.md
  • State переживает /resume той же сессии
  • Footer status показывает on/off и count
  • Нет duplicate skill blocks для skills уже в kept window
  • С pi-auto-compact: auto-detect, режим defer, нет гонки sendUserMessage, continue после compaction работает
  • С pi-auto-compact: ручной /compact не ломает ни один extension

14. Будущие улучшения (v2+)

  • reinjectOnManualCompaction: true как осознанный opt-in
  • session_before_compact: дописывать в custom summary список active skills
  • Опциональный pi.events протокол с pi-auto-compact (встраивать skill-блоки в follow-up текст — без PR в capyup не обязателен)
  • Pi package на npm (keywords: ["pi-package"])
  • Опция re-inject только «последнего активного» skill, а не всех

15. Риски

Риск Митигация
Re-inject раздувает контекст (N больших skills) skillReinject.maxSkills (default unlimited; soft warn > 3)
sendUserMessage запускает нежеланный turn defer по умолчанию при pi-auto-compact; иначе triggerTurn: false
Гонка sendUserMessage с pi-auto-compact follow-up §16: не слать в session_compact; before_agent_start
Нельзя отличить auto/manual без эвристики input hook на /compact; pi-auto-compact manual /compact не в его onComplete
Двойной compaction (Pi default + pi-auto-compact) документировать; опционально warn в status
Pi изменит формат skill block version в state; тест на parseSkillBlock regex

16. Совместимость с @capyup/pi-auto-compact

Типичная установка пользователя: pi install npm:@capyup/pi-auto-compact. Extension проактивно компактирует на turn_start / turn_end / emergency context и автоматически продолжает работу после auto-compaction.

16.1. Как работает pi-auto-compact (релевантное нам)

Источник: extensions/auto-compact.ts, README.

Аспект Поведение
Триггер compaction ctx.compact({ customInstructions, onComplete, onError }) — не ручной /compact
Follow-up после auto-compact onCompletesetImmediate(() => { if (ctx.isIdle()) pi.sendUserMessage(AUTO_COMPACT_FOLLOW_UP[phase]); })
Ручной /compact Не попадает в их onComplete → follow-up не отправляется
Тексты follow-up "Auto-compact ran before this turn. Continue with the current task." и ещё 3 фазы (mid-turn, emergency, session-resume)
Гонки Специально откладывают follow-up в setImmediate, чтобы не проиграть flush compactionQueuedMessages Pi
Keep budget keepRecentPercent (default 15% context window), не keepRecentTokens Pi

16.2. Конфликт без интеграции

Если skill-reinject на session_compact вызовет sendUserMessage синхронно или в том же setImmediate:

  1. Агент перестаёт быть idle → pi-auto-compact не шлёт follow-up → сессия замирает после compaction.
  2. Или оба шлют сообщения в одном tick → "Agent is already processing" (комментарий в auto-compact.ts).

Вывод: при обнаружении pi-auto-compact re-inject обязан идти через defer + before_agent_start, не через конкурирующий sendUserMessage.

16.3. Целевой совместный flow

turn_start / turn_end / emergency
        │
        ▼
pi-auto-compact: ctx.compact()
        │
        ├── session_before_compact (наш hook: только mark source=auto)
        ├── … Pi summarization …
        ├── session_compact (наш hook: pendingReinject := skills  kept)
        └── onComplete → setImmediate → sendUserMessage("Auto-compact ran…")
                    │
                    ▼
            prompt() → before_agent_start (наш hook)
                    │
                    ├── inject: <skill>…</skill> × N   ← re-inject
                    └── user prompt: "Auto-compact ran…"  ← kickoff pi-auto-compact
                    │
                    ▼
              agent продолжает с skills + summary + kept tail

16.4. Автоопределение pi-auto-compact

Без зависимости от npm-пакета и без правок в capyup:

function detectPiAutoCompact(pi: ExtensionAPI): boolean {
  return pi.getCommands().some((c) => c.name === "auto-compact");
}
  • Вызывать на session_start (reason: "startup" | "resume" | "reload" | "switch").
  • Кэшировать в RuntimeFlags.autoCompactDetected.
  • При false → status delivery: defer (standalone) или immediate по triggerTurn.

Не полагаться на путь к файлу / package.json — только публичный API (getCommands).

Опция skillReinject.autoCompactIntegration:

Значение Поведение
"auto" (default) defer если detectPiAutoCompact(), иначе по triggerTurn
"defer" всегда before_agent_start, даже без pi-auto-compact
"immediate" всегда sendUserMessage (для отладки; с pi-auto-compact — риск §16.2)
"off" игнорировать detect; только triggerTurn

Команда /skill-reinject integration auto|defer|immediate|off — session override для autoCompactIntegration (персистить в skill-reinject:config entry).

16.5. Что мы не ломаем в pi-auto-compact

Инвариант Как обеспечиваем
Follow-up только когда idle Не вызываем sendUserMessage в session_compact / onComplete tick
Follow-up только для своего auto-compact Не трогаем ctx.compact() / onComplete
Ручной /compact без follow-up Наш pendingReinject на manual: либо ждём user prompt (defer), либо skip (default)
Emergency context truncation session_compact всё равно приходит после ctx.compact() — тот же defer

16.6. Что pi-auto-compact не ломает в нас

Сценарий Результат
Auto-compact + follow-up Следующий before_agent_start — наш inject; skills в контексте
User печатает во время compaction pi-auto-compact молчит; user prompt → наш inject на его turn
pi-auto-compact keep-bookends / summarize-all Kept window определяем по compactionEntry.firstKeptEntryId в session branch (§6.4), не по их процентам
Два compaction подряд Каждый session_compact пересчитывает pendingReinject; дедуп по kept window

16.7. Coexistence с Pi default auto-compaction

Оба механизма могут сработать в одной сессии (Pi: после agent_end; pi-auto-compact: до turn). Рекомендация в README:

  • при использовании pi-auto-compact рассмотреть "compaction.enabled": false в settings.json или оставить оба — skill-reinject отработает после каждого session_compact.

Extension не отключает чужой compaction; максимум — ui.notify hint при первом detect обоих.

16.8. Будущий опциональный протокол (v2, не блокирует v1)

Через pi.events:

// skill-reinject emits (optional, v2):
pi.events.emit("skill-reinject:pending", { skills: ["foo", "bar"] });

// pi-auto-compact could listen and append to follow-up — requires PR upstream

v1 не требует изменений в pi-auto-compact.

16.9. Константы для тестов (follow-up фразы pi-auto-compact)

Скопировать в auto-compact.ts как PI_AUTO_COMPACT_FOLLOW_UP_PREFIXES для документирования/тестов, не для матчинга в runtime v1:

  • "Auto-compact ran before this turn."
  • "Auto-compact ran mid-turn."
  • "Emergency auto-compact ran."
  • "Auto-compact ran on session resume."

Runtime v1 матчит follow-up не нужен — достаточно pendingReinject + любой следующий before_agent_start.