0.4.11
22 · Техническая документация

22. Cookbook: рецепты для разработчиков

Пошаговые how-to для типовых изменений в AstraCode. Каждый recipe — самодостаточный список шагов с реальными файлами и проверками.

Recipe: добавить новую slash-команду

Пример: добавляем /foo — открывает popup с тремя пунктами.

Шаги

1. Объявить вариант enum.

slash-commands/src/id.rs (порядок вариантов = порядок в меню):

rust
pub enum SlashCommandId {
    // ...
    #[strum(serialize = "foo")]
    Foo,
}

TUI реэкспортит его как SlashCommand автоматически: pub use astracode_slash_commands::SlashCommandId as SlashCommand; в tui/src/slash_command.rs.

2. Добавить EN/RU описания и метаданные.

В реестре slash-commands/src/commands.rs (REGISTRY) — одна запись SlashCommandSpec:

rust
SlashCommandSpec {
    id: SlashCommandId::Foo,
    description: LocalizedText {
        en: "do the foo thing",
        ru: "сделать foo",
    },
    usage: DEFAULT_USAGE,
    inline_args: OPT_ARGS,
    visibility: VisibilityRule::Always,
    availability: rule(/*during_task*/ true, /*in_side*/ false),
    feature_gates: &[],
    hidden_in_popup: false,
},

3. Флаги доступности задаются в availability/visibility записи (см. AvailabilityRule, VisibilityRule в slash-commands/src/registry.rs). inline_argsNone/Optional. Feature-gates — через FeatureGate в feature_gates.

4. Фильтр включения (если команда условная).

tui/src/bottom_pane/slash_commands.rs:

rust
.filter(|(_, cmd)| flags.foo_command_enabled || *cmd != SlashCommand::Foo)

И поле в BuiltinCommandFlags:

rust
pub(crate) foo_command_enabled: bool,

5. Реализовать диспатч.

tui/src/chatwidget/slash_dispatch.rs:

rust
SlashCommand::Foo => {
    self.open_foo_popup();
    QueuedCommandDrainResult::Stop
}

Если команда работает во время задачи, добавь в queued_command_drain_result ветку Continue.

6. Реализовать UI (popup / overlay).

Для popup'а — наследуйся от существующих в bottom_pane/. Для оверлея — используй pager_overlay::Overlay::new_static_with_lines() (как сделано для /help).

7. Добавить per-command help.

tui/src/help_text.rs:

rust
// в overview_lines_en и _ru:
cmd(&mut out, "/foo", "do the foo thing");

// в command_usage:
(SlashCommand::Foo, _) => vec!["/foo"],

// в command_details:
(SlashCommand::Foo, TuiLocale::En) => vec![
    "Detailed explanation, what menu items show up, where state is saved, ...",
],
(SlashCommand::Foo, TuiLocale::Ru) => vec![
    "Подробное описание: какие пункты меню, где сохраняется состояние, ...",
],

8. Обновить snapshot-тесты.

bash
cargo test -p astracode-tui slash_commands::tests
cargo insta review   # принять обновлённые snapshots

9. Документировать.

Добавь раздел в 04-slash-commands.md. Если команда условная — в 03-configuration.md feature flags.

Контрольный лист

  • [ ] SlashCommandId variant + strum serialize (slash-commands/src/id.rs)
  • [ ] SlashCommandSpec в реестре commands.rs (описание EN+RU, usage, availability, feature-gates, hidden_in_popup)
  • [ ] алиасы при необходимости (id.rs::aliases())
  • [ ] фильтр в builtins_for_input
  • [ ] диспатч в slash_dispatch.rs (TUI) и/или серверный handler в app-server
  • [ ] UI компонент
  • [ ] overview + per-command help (EN+RU)
  • [ ] snapshot тесты обновлены
  • [ ] документация в 04-slash-commands.md

Recipe: добавить новый встроенный tool

Пример: добавляем tool count_lines — считает строки в файле, без shell.

Шаги

1. Зарегистрировать ToolHandlerKind.

tools/src/tool_registry_plan_types.rs:

rust
pub enum ToolHandlerKind {
    // ...
    CountLines,
}

2. Описать spec.

Новый файл tools/src/count_lines_tool.rs:

rust
use crate::tool_spec::ToolSpec;

pub(crate) fn create_count_lines_tool() -> ToolSpec {
    ToolSpec::Function {
        name: "count_lines".into(),
        description: "Count lines in a file.".into(),
        parameters: serde_json::json!({
            "type": "object",
            "properties": {
                "path": { "type": "string", "description": "Path to the file." }
            },
            "required": ["path"]
        }),
        strict: true,
        supports_parallel_tool_calls: true,
    }
}

3. Зарегистрировать в плане.

tools/src/tool_registry_plan.rs в build_tool_registry_plan():

rust
plan.add(create_count_lines_tool(), ToolHandlerKind::CountLines);

4. Реализовать handler в core.

core/src/tools/handlers/count_lines.rs:

rust
pub(crate) async fn execute_count_lines(
    args: CountLinesArgs,
    ctx: &TurnContext,
) -> Result<String> {
    let path = ctx.resolve_path(&args.path)?;
    let content = ctx.file_system.read_to_string(&path).await?;
    Ok(format!("{}", content.lines().count()))
}

Подключить в core/src/tools/router.rs:

rust
ToolHandlerKind::CountLines => execute_count_lines(parse_args(args)?, ctx).await,

5. Sandbox / approval.

Если tool читает FS — permission_profile.read проверится автоматически через file_system.read_to_string. Если ничего «опасного» — никакого approval не требуется (как list_dir).

6. Тесты.

tools/src/count_lines_tool_tests.rs:

rust
#[test]
fn count_lines_spec_has_required_path_param() {
    let spec = create_count_lines_tool();
    // assert that JSON-schema has required: ["path"]
}

Интеграционный — в core/tests/suite/.

7. Документировать.

Добавь раздел в 18-tools-catalog.md.


Recipe: добавить новый JSON-RPC метод

Пример: новый метод thread/foo/bar — установить foo-флаг на треде.

Шаги

1. Описать параметры и ответ.

app-server-protocol/src/protocol/v2.rs:

rust
#[derive(Serialize, Deserialize, JsonSchema, TS)]
pub struct ThreadFooBarParams {
    pub thread_id: ThreadId,
    pub value: bool,
}

#[derive(Serialize, Deserialize, JsonSchema, TS)]
pub struct ThreadFooBarResponse {
    pub previous_value: bool,
}

2. Зарегистрировать в макросе.

В том же файле или в lib.rs есть client_request_definitions! { ... } блок:

rust
client_request_definitions! {
    // ...
    "thread/foo/bar" => ThreadFooBarParams => ThreadFooBarResponse,
}

3. Обработать на стороне сервера.

app-server/src/astracode_message_processor.rs:

rust
ClientRequest::ThreadFooBar(params) => {
    let response = self.thread_foo_bar(params).await?;
    Ok(JSONRPCResponse { id, result: serde_json::to_value(response)? })
}

Реализация thread_foo_bar — обычно делегируется в core.

4. Прокидываем в core (если нужно).

В core/src/astracode_thread.rs или session:

rust
impl AstraCode {
    pub async fn set_foo(&self, value: bool) -> Result<bool> {
        let mut state = self.session.state.lock().await;
        let previous = state.foo;
        state.foo = value;
        Ok(previous)
    }
}

5. Если нужна нотификация back в UI — добавь в server_notification_definitions!:

rust
"thread/foo/updated" => ThreadFooUpdatedNotification,

И эмитим из core:

rust
session.send_notification(ServerNotification::ThreadFooUpdated(...)).await;

6. Использовать из TUI.

tui/src/app_server_session.rs:

rust
self.client.thread_foo_bar(ThreadFooBarParams { thread_id, value: true }).await?;

7. Тесты.

Тест в app-server-test-client или core/tests/.

8. Документировать.

Обнови 20-protocol.md в соответствующей секции.


Recipe: добавить новый hook event

Пример: event BeforeCompact — стрелять до compact'а.

Шаги

1. Расширить wire/config contract.

Добавь BeforeCompact в HookEventName протокола и поле с #[serde(rename = "BeforeCompact")] в config/src/hook_config.rs. Обнови is_empty, handler_count и into_matcher_groups.

2. Определить stdin/stdout schema и event runner.

Добавь типы события в hooks/src/events/, зарегистрируй их в events/mod.rs и registry.rs, затем добавь input/output schema в hooks/src/schema.rs. Сгенерированные fixtures должны отражать новый JSON через stdin и поддерживаемый JSON через stdout.

3. Определить matcher input.

Matcher — exact/pipe/regex по одной строке, а не expression context. Если для события нужен matcher, явно выбери стабильную строку и добавь ветку в events/common.rs::matcher_pattern_for_event.

4. Вызвать runner из владельца lifecycle.

Собери request со стабильными полями (session_id, turn_id, cwd, model и данные события), вызови соответствующий метод hook registry до compact и обработай outcome по fail-open/fail-closed политике события.

5. Тесты и документация.

Покрой config parsing, discovery, matcher selection, stdin serialization, stdout parsing и интеграционный вызов. Обнови 08-hooks.md и generated schemas.

08-hooks.md — события, matcher semantics и stdin/stdout contract.


Recipe: локализовать новое user-facing сообщение

В AstraCode локализация точечная — каждое сообщение явно выбирает строку по TuiLocale. Универсального i18n-resource bundle нет.

Шаги

1. Найди вызывающий код.

Если сообщение в TUI — это tui/src/.... Если из core — нужно протащить TuiLocale сюда (см. как сделано для review-interrupted, файл core/src/tasks/review.rs).

2. Замени строковый литерал на match.

rust
// Было:
let msg = "Something happened.".to_string();

// Стало:
let msg = match locale {
    TuiLocale::En => "Something happened.".to_string(),
    TuiLocale::Ru => "Что-то произошло.".to_string(),
};

3. Если функция-донор не знает локаль — пробрось параметр.

Часто это значит добавить locale: TuiLocale в сигнатуру помощника и обновить call-sites.

4. Если сообщение из core — используй Session::tui_locale().await.

Накопительно: в core::Config есть tui_locale: Option<TuiLocale>, и Session::tui_locale() возвращает её или TuiLocale::default().

rust
let locale = session.tui_locale().await;
let msg = match locale { /* ... */ };

5. Snapshot-тесты.

bash
cargo test -p astracode-tui
cargo insta review

6. Документировать.

Если сообщение крупное (раздел, оверлей) — упомяни в 11-localization.md.

Подводные камни

  • YAML значения с двоеточием в скиллах оборачивай в кавычки — иначе парсер падает (баг был с code-review-context).
  • Идентификаторы в backticks оставляй на английском (как в keymap-ошибках): Конфликт привязок `tui.keymap.composer.submit`: ...
  • Не локализуй имена slash-команд/cd, /help остаются английскими.

Recipe: добавить новое поле в Config

Пример: новое булевое поле feature_foo_enabled в [features].

Шаги

1. Добавить в struct Config.

core/src/config/mod.rs:

rust
pub struct Config {
    // ...
    pub feature_foo_enabled: bool,
}

2. Парсить из ConfigToml.

В том же файле, в ConfigBuilder или прямом конструкторе из ConfigToml:

rust
feature_foo_enabled: cfg.features.as_ref()
    .map(|f| f.foo.unwrap_or(false))
    .unwrap_or(false),

И в ConfigToml::features добавь поле foo: Option<bool>.

3. Обновить тесты.

В core/src/config/config_tests.rs найди все места создания Config { ... } вручную и добавь feature_foo_enabled: false,. Аналогично — в thread-manager-sample/src/main.rs.

bash
cargo build --workspace  # покажет где забыли инициализировать

4. Использовать в коде.

rust
if config.feature_foo_enabled { /* ... */ }

5. Документировать.

В 03-configuration.md — секция features.


Recipe: написать insta snapshot-тест

Шаги

1. Создать тест с #[test] и insta::assert_snapshot!.

rust
#[test]
fn my_function_renders_correctly() {
    let output = my_function(SomeInput {});
    insta::assert_snapshot!(output);
}

Для структур:

rust
insta::assert_debug_snapshot!(my_struct);
insta::assert_json_snapshot!(my_struct);

2. Прогнать тест.

bash
cargo test -p astracode-tui my_function_renders_correctly

При первом запуске snapshot создаётся в snapshots/<module>__my_function_renders_correctly.snap.new.

3. Просмотреть и принять.

bash
cargo insta review
# 'a' — принять, 's' — пропустить, 'r' — отклонить

4. Принятый snapshot переименовывается в .snap и коммитится.

При следующем запуске сравнивается с принятым.

Когда переснимать

  • Изменился UI / форматирование вывода.
  • Bump версии (snapshot'ы со «AstraCode (vX.Y.Z)»): bash find astracode-rs/tui/src/status/snapshots -name '*.snap' \ | xargs sed -i 's/vOLD/vX.Y.Z/g'
  • Локализация поменялась — пересмотри руками, не accept всё подряд.

Анти-паттерны

  • Не принимать снапшот вслепую — это скрывает регрессии.
  • Не делать снапшот случайных значений (timestamp, random ID) — используй deterministic фикстуры или insta::dynamic_redactions.

Recipe: запустить on-prem с собственным провайдером

Шаги

1. Поднять OpenAI-compatible endpoint.

vLLM, llama.cpp server, Ollama (--openai-compat), внутренний gateway — любой POST /v1/chat/completions.

2. Добавить в config.toml.

toml
[[providers]]
id = "company-llm"
display_name = "Company LLM Gateway"
base_url = "https://llm.company.internal/v1"

default_provider = "company-llm"
model = "llama-3.3-70b"

3. Если self-signed CA.

bash
export ASTRACODE_CA_CERTIFICATE=/etc/ssl/certs/company-ca.pem
astracode

4. Установить API-ключ.

Через TUI: /model → выбрать провайдера → ввести ключ. Если ключ не нужен (бесплатный internal endpoint) — введи любую строку (dummy).

5. Проверка.

bash
astracode --no-onboarding -c model_provider=company-llm
# В TUI:
/status

Должен показать Company LLM Gateway / llama-3.3-70b.

Подводные камни

  • Если провайдер не поддерживает streaming → AstraCode будет блокирующе ждать; UI будет «висеть».
  • Если caching / prompt_caching не поддерживается — токены будут считаться полностью каждый turn.
  • Multi-modal (изображения) работает только если endpoint поддерживает image_url / input_image content types.

См. 10-providers.md.


Recipe: подключить кастомный MCP-сервер

См. подробнее 06-mcp.md. Кратко:

toml
[mcp_servers.my_python_server]
command = "python"
args = ["-m", "my_mcp_module"]
env_vars = ["MY_API_KEY"]
default_tools_approval_mode = "prompt"
startup_timeout_sec = 30

Затем — перезапуск AstraCode, /mcp verbose в TUI для проверки.


Recipe: добавить новый скилл

См. 05-skills.md. Самый быстрый путь — встроенный skill-creator:

text
$skill-creator

Вручную:

bash
mkdir -p ~/.astracode/skills/my-skill/agents
cat > ~/.astracode/skills/my-skill/SKILL.md << 'EOF'
---
name: my-skill
description: "Concise description of what this skill does and when to use it."
metadata:
  short-description: "Brief one-liner (EN)"
  short-description-ru: "Краткое описание (RU)"
---

# my-skill

Detailed instructions for the agent...
EOF

После — перезапуск или /skills → refresh.


Recipe: debug сессии

Включить tracing

bash
RUST_LOG=astracode_core=debug,astracode_tui=info astracode 2> /tmp/astracode.log

Посмотреть rollout текущего thread'а

В TUI:

text
/rollout

(дебаг-команда, доступна только в debug-сборке cfg!(debug_assertions), не в release) — путь к JSONL текущей сессии.

Или из shell:

bash
ls -t ~/.astracode/sessions/$(date +%Y/%m/%d)/ | head -1

Посмотреть конфиг с источниками

text
/debug-config

Дамп state SQLite

bash
sqlite3 ~/.astracode/state_5.sqlite "SELECT id, title, model_provider, updated_at FROM threads ORDER BY updated_at DESC LIMIT 10;"

Подсчёт токенов

text
/status

показывает текущий context usage.


Recipe: вернуть AstraCode в чистое состояние

Это аварийная процедура с потерей активного состояния. Полностью остановите все процессы AstraCode и переместите $ASTRACODE_HOME целиком в backup вне этой директории:

bash
mv ~/.astracode /path/to/astracode-data-backup
astracode

Новый запуск создаст чистое хранилище. Старый local.age восстанавливается только вместе с local.key при file storage либо соответствующим OS credential при keyring storage. Не удаляйте backup до проверки config, sessions, memories, skills и secrets. См. 09-security.md.


Recipe: ускорить сборку при разработке

bash
# В Cargo.toml workspace добавить sccache
# В ~/.cargo/config.toml:
[build]
rustc-wrapper = "sccache"
incremental = true

[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

Установка:

bash
cargo install sccache
sudo apt install mold clang

Эффект: cargo build -p astracode-tui падает с ~3 минут до ~30 секунд на инкрементальных билдах.


Recipe: профилировать hot-path

bash
# Включить debug-symbols в release-builds (временно)
RUSTFLAGS="-g" cargo build --release --bin astracode

# Perf record
perf record --call-graph dwarf ./target/release/astracode
perf report

# Flamegraph
cargo install flamegraph
cargo flamegraph --bin astracode --release

Recipe: писать integration-тесты с мок-LLM

В core/tests/ есть фикстуры с моками ModelProvider на базе wiremock. См. core/tests/common/responses.rs (ResponseMock, mount_sse_sequence) и core/tests/suite/review.rs для примера полноценного integration-теста ревью с моками.

Кратко:

rust
use core_test_support::responses::{ResponseMock, mount_sse_sequence};
use wiremock::MockServer;

#[tokio::test]
async fn my_e2e_test() {
    let server = MockServer::start().await;
    // bodies — SSE-фреймы (ResponseItem'ы), которые сервер отдаст последовательно
    let request_log = mount_sse_sequence(&server, bodies).await;

    let session = make_test_session(server.uri()).await;
    let result = session.run_turn("Count lines in /tmp/foo.txt").await;
    assert!(result.contains("42 lines"));
}

Recipe: bump версии для релиза

bash
# 1. Обновить версии
# Установить X.Y.Z в `[workspace.package].version`:
$EDITOR astracode-rs/Cargo.toml
# Установить тот же X.Y.Z в поле `version`:
$EDITOR astracode-cli/package.json

# 2. Обновить snapshot-тесты
find astracode-rs/tui/src/status/snapshots -name '*.snap' \
  | xargs sed -i 's/AstraCode (vOLD)/AstraCode (vX.Y.Z)/g'

# 3. Сгенерировать CHANGELOG
git-cliff --tag X.Y.Z > CHANGELOG.md

# 4. Прогнать тесты
cd astracode-rs && cargo test --workspace
cargo insta review

# 5. Commit + tag + push
git add -A
git commit -m "chore(release): bump to X.Y.Z"
git tag X.Y.Z
git push origin main X.Y.Z

Upstream CI должен подхватить тег и собрать raw binaries Linux/macOS/Windows; текущий downstream создаёт пользовательские архивы Linux и macOS. См. 16-cicd.md.