22. Cookbook: рецепты для разработчиков
Пошаговые инструкции для типовых изменений в AstraCode. Каждый recipe — самодостаточный список шагов с реальными файлами и проверками.
Рецепт: добавить новую slash-команду
Пример: добавляем /foo — открывает popup с тремя пунктами.
Шаги
1. Объявить вариант enum.
tui/src/slash_command.rs:
#[derive(strum::EnumString, strum::IntoStaticStr, ...)]
pub enum SlashCommand {
// ...
#[strum(serialize = "foo")]
Foo,
}
2. Добавить EN/RU описания.
В том же файле в fn description_for_locale:
SlashCommand::Foo => match locale {
TuiLocale::En => "do the foo thing",
TuiLocale::Ru => "сделать foo",
},
3. Установить флаги доступности.
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:
.filter(|(_, cmd)| flags.foo_command_enabled || *cmd != SlashCommand::Foo)
И поле в BuiltinCommandFlags:
pub(crate) foo_command_enabled: bool,
5. Реализовать диспатч.
tui/src/chatwidget/slash_dispatch.rs:
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:
// в 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-тесты.
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:
pub enum ToolHandlerKind {
// ...
CountLines,
}
2. Описать spec.
Новый файл tools/src/count_lines_tool.rs:
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():
plan.add(create_count_lines_tool(), ToolHandlerKind::CountLines);
4. Реализовать handler в core.
core/src/tools/handlers/count_lines.rs:
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:
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:
#[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:
#[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! { ... } блок:
client_request_definitions! {
// ...
"thread/foo/bar" => ThreadFooBarParams => ThreadFooBarResponse,
}
3. Обработать на стороне сервера.
app-server/src/astracode_message_processor.rs:
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:
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!:
"thread/foo/updated" => ThreadFooUpdatedNotification,
И эмитим из core:
session.send_notification(ServerNotification::ThreadFooUpdated(...)).await;
6. Использовать из TUI.
tui/src/app/app_server_session.rs:
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:
pub enum HookEvent {
// ...
BeforeCompact,
}
2. Поддержать парсинг из config.toml.
В config/src/types.rs или hooks/src/config.rs:
#[serde(rename = "BeforeCompact")] BeforeCompact(Vec<HookHandler>),
3. Зафайрить событие из core.
В core/src/compact.rs или где запускается compact:
self.hook_engine.fire(HookEvent::BeforeCompact, HookContext::new(...)).await?;
4. Передать matcher-переменные (если нужны).
В hooks/src/matcher.rs добавь поля, доступные для условий:
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.
// Было:
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().
let locale = session.tui_locale().await;
let msg = match locale { /* ... */ };
5. Snapshot-тесты.
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:
pub struct Config {
// ...
pub feature_foo_enabled: bool,
}
2. Парсить из ConfigToml.
В том же файле, в ConfigBuilder или прямом конструкторе из ConfigToml:
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.
cargo build --workspace # покажет где забыли инициализировать
4. Использовать в коде.
if config.feature_foo_enabled { /* ... */ }
5. Документировать.
В 03-configuration.md — секция features.
Рецепт: написать insta snapshot-тест
Шаги
1. Создать тест с #[test] и insta::assert_snapshot!.
#[test]
fn my_function_renders_correctly() {
let output = my_function(SomeInput {});
insta::assert_snapshot!(output);
}
Для структур:
insta::assert_debug_snapshot!(my_struct); insta::assert_json_snapshot!(my_struct);
2. Прогнать тест.
cargo test -p astracode-tui my_function_renders_correctly
При первом запуске snapshot создаётся в snapshots/<module>__my_function_renders_correctly.snap.new.
3. Просмотреть и принять.
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.
[[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.
export ASTRACODE_CA_CERTIFICATE=/etc/ssl/certs/company-ca.pem astracode
4. Установить API-ключ.
Через TUI: /model → выбрать провайдера → ввести ключ. Если ключ не нужен (бесплатный internal endpoint) — введи любую строку (dummy).
5. Проверка.
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_imagecontent types.
См. 10-providers.md.
Рецепт: подключить кастомный MCP-сервер
См. подробнее 06-mcp.md. Кратко:
[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:
$skill-creator
Вручную:
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
RUST_LOG=astracode_core=debug,astracode_tui=info astracode 2> /tmp/astracode.log
Посмотреть rollout текущего thread'а
В TUI:
/rollout
(требует debug-feature flag) — путь к JSONL текущей сессии.
Или из shell:
ls -t ~/.astracode/sessions/$(date +%Y/%m/%d)/ | head -1
Посмотреть конфиг с источниками
/debug-config
(Это slash-команда внутри TUI; отдельной CLI-команды astracode debug-config нет.)
Дамп state SQLite
sqlite3 ~/.astracode/state_5.sqlite "SELECT id, title, model_provider, updated_at FROM threads ORDER BY updated_at DESC LIMIT 10;"
Подсчёт токенов
/status
показывает текущий context usage.
Рецепт: вернуть AstraCode в чистое состояние
Когда что-то поломалось локально, и не хочется разбираться:
# Бэкап секретов и кастомных скиллов 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.
Рецепт: ускорить сборку при разработке
# В 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"]
Установка:
cargo install sccache sudo apt install mold clang
Эффект: cargo build -p astracode-tui падает с ~3 минут до ~30 секунд на инкрементальных билдах.
Рецепт: профилировать hot-path
# Включить 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:
// В main.rs добавить: #[cfg(feature = "tokio-console")] console_subscriber::init();
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-тестов.
Кратко (мок одного ответа модели):
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 версии для релиза
# 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.