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

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

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

Рецепт: добавить новую slash-команду

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

Шаги

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

tui/src/slash_command.rs:

rust
#[derive(strum::EnumString, strum::IntoStaticStr, ...)]
pub enum SlashCommand {
    // ...
    #[strum(serialize = "foo")]
    Foo,
}

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

В том же файле в fn description_for_locale:

rust
SlashCommand::Foo => match locale {
    TuiLocale::En => "do the foo thing",
    TuiLocale::Ru => "сделать foo",
},

3. Установить флаги доступности.

rust
fn supports_inline_args(&self) -> bool {
    matches!(self, Self::Goal | Self::Help | /* ... | */ Self::Foo)
}

fn available_during_task(&self) -> bool { /* добавь в нужный список */ }
fn available_in_side_conversation(&self) -> bool { /* аналогично */ }

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.

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

  • [ ] enum variant + strum serialize
  • [ ] описание EN+RU
  • [ ] флаги inline-args, during-task, in-side
  • [ ] фильтр в builtins_for_input
  • [ ] диспатч в slash_dispatch.rs
  • [ ] UI компонент
  • [ ] overview + per-command help (EN+RU)
  • [ ] snapshot тесты обновлены
  • [ ] документация в 04-slash-commands.md

Рецепт: добавить новый встроенный 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::ResponsesApiTool;
use crate::tool_spec::ToolSpec;

pub(crate) fn create_count_lines_tool() -> ToolSpec {
    // `ToolSpec::Function` — tuple-вариант, оборачивающий `ResponsesApiTool`
    // (см. `tools/src/tool_spec.rs` и `tools/src/responses_api.rs`).
    ToolSpec::Function(ResponsesApiTool {
        name: "count_lines".to_string(),
        description: "Count lines in a file.".to_string(),
        strict: true,
        defer_loading: None,
        parameters: JsonSchema::object(
            BTreeMap::from([(
                "path".to_string(),
                JsonSchema::string(Some("Path to the file.".to_string())),
            )]),
            Some(vec!["path".to_string()]),
            Some(false.into()),
        ),
        output_schema: None,
    })
}

⚠️ В самом ResponsesApiTool нет поля supports_parallel_tool_calls. Параллельные вызовы настраиваются слоем выше — ConfiguredToolSpec (tools/src/tool_spec.rs):

```rust pub struct ConfiguredToolSpec { pub spec: ToolSpec, pub supports_parallel_tool_calls: bool, }

// ConfiguredToolSpec::new(spec, supports_parallel_tool_calls) let configured = ConfiguredToolSpec::new(create_count_lines_tool(), 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.


Рецепт: добавить новый 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/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 в соответствующей секции.


Рецепт: добавить новый hook event

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

Шаги

1. Добавить вариант enum.

hooks/src/event.rs:

rust
pub enum HookEvent {
    // ...
    BeforeCompact,
}

2. Поддержать парсинг из config.toml.

В config/src/types.rs или hooks/src/config.rs:

rust
#[serde(rename = "BeforeCompact")]
BeforeCompact(Vec<HookHandler>),

3. Зафайрить событие из core.

В core/src/compact.rs или где запускается compact:

rust
self.hook_engine.fire(HookEvent::BeforeCompact, HookContext::new(...)).await?;

4. Передать matcher-переменные (если нужны).

В hooks/src/matcher.rs добавь поля, доступные для условий:

rust
context.set("history_size", state.message_history.len());

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

08-hooks.md — список событий и доступных matcher-переменных.


Рецепт: локализовать новое 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 остаются английскими.

Рецепт: добавить новое поле в 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.


Рецепт: написать 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/v0.4.11/v0.4.12/g'
  • Локализация поменялась — пересмотри руками, не accept всё подряд.

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

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

Рецепт: запустить 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 -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.


Рецепт: подключить кастомный 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 для проверки.


Рецепт: добавить новый скилл

См. 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.


Рецепт: debug сессии

Включить tracing

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

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

В TUI:

text
/rollout

(требует debug-feature flag) — путь к JSONL текущей сессии.

Или из shell:

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

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

text
/debug-config

(Это slash-команда внутри TUI; отдельной CLI-команды astracode 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.


Рецепт: вернуть AstraCode в чистое состояние

Когда что-то поломалось локально, и не хочется разбираться:

bash
# Бэкап секретов и кастомных скиллов
tar czf astracode-backup.tar.gz \
  ~/.astracode/secrets/ \
  ~/.astracode/config.toml \
  $(ls -d ~/.astracode/skills/*/ 2>/dev/null | grep -v .system)

# Снести всё (ОПАСНО — секреты и сессии потеряются если бэкап не сделан)
rm -rf ~/.astracode

# Запустить заново — конфиг сгенерируется
astracode

Не забудь, что без мастер-ключа из OS keyring восстановленный local.age не дешифруется. См. 09-security.md.


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

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 секунд на инкрементальных билдах.


Рецепт: профилировать 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

Для async — tokio-console:

rust
// В main.rs добавить:
#[cfg(feature = "tokio-console")]
console_subscriber::init();
bash
cargo build --features tokio-console
tokio-console

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

Интеграционные тесты в core/tests/ поднимают мок Responses API на базе wiremock и используют хелперы из крейта core_test_support (модуль core/tests/common/responses.rs). Отдельного astracode_test_support::TestProvider нет. См. core/tests/suite/review.rs и core/tests/suite/tools.rs для примеров полноценных integration-тестов.

Кратко (мок одного ответа модели):

rust
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_astracode::test_astracode;

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn my_e2e_test() -> anyhow::Result<()> {
    skip_if_no_network!(Ok(()));

    // 1. Поднять мок-сервер Responses API.
    let server = start_mock_server().await;

    // 2. Смонтировать один SSE-ответ модели.
    let response_mock = mount_sse_once(
        &server,
        sse(vec![
            ev_response_created("resp-1"),
            ev_assistant_message("msg-1", "Hello, world!"),
            ev_completed("resp-1"),
        ]),
    )
    .await;

    // 3. Собрать тестовый AstraCode, указывающий на мок-сервер.
    let test = test_astracode().build(&server).await?;
    test.submit_turn_with_environments("Say hello", None).await?;

    // 4. Проверить, что именно ушло в модель.
    let request = response_mock.single_request();
    assert!(request.body_contains_text("Say hello"));

    Ok(())
}

Для многошаговых сценариев (ответ → tool call → ответ) монтируйте несколько ответов через mount_sse_sequence(...) и эмитьте tool call хелпером ev_function_call(call_id, name, arguments) — он мокирует обращение к встроенному tool.


Рецепт: bump версии для релиза

bash
# 1. Обновить версии (пример: текущая 0.4.11 → новая 0.4.11)
sed -i 's/version = "0.4.11"/version = "0.4.11"/' astracode-rs/Cargo.toml
sed -i 's/"version": "0.4.11"/"version": "0.4.11"/' astracode-cli/package.json

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

# 3. Сгенерировать CHANGELOG
git-cliff --tag 0.4.11 > 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 0.4.11"
git tag 0.4.11
git push origin main 0.4.11

CI должен подхватить тег и собрать Linux-бандл. macOS — собрать локально и загрузить в Package Registry вручную. См. 16-cicd.md.