22. Cookbook: рецепты для разработчиков
Пошаговые how-to для типовых изменений в AstraCode. Каждый recipe — самодостаточный список шагов с реальными файлами и проверками.
Recipe: добавить новую slash-команду
Пример: добавляем /foo — открывает popup с тремя пунктами.
Шаги
1. Объявить вариант enum.
slash-commands/src/id.rs (порядок вариантов = порядок в меню):
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:
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_args — None/Optional. Feature-gates — через FeatureGate в feature_gates.
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.
// в 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.
Контрольный лист
- [ ]
SlashCommandIdvariant + 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:
pub enum ToolHandlerKind {
// ...
CountLines,
}
2. Описать spec.
Новый файл tools/src/count_lines_tool.rs:
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():
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.
Recipe: добавить новый 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_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 в соответствующей секции.
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.
// Было:
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остаются английскими.
Recipe: добавить новое поле в Config
Пример: новое булевое поле feature_foo_enabled в [features].
Шаги
1. Добавить в struct Config.
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.
Recipe: написать 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/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.
[[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 --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_imagecontent types.
См. 10-providers.md.
Recipe: подключить кастомный 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 для проверки.
Recipe: добавить новый скилл
См. 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.
Recipe: debug сессии
Включить tracing
RUST_LOG=astracode_core=debug,astracode_tui=info astracode 2> /tmp/astracode.log
Посмотреть rollout текущего thread'а
В TUI:
/rollout
(дебаг-команда, доступна только в debug-сборке cfg!(debug_assertions), не в release) — путь к JSONL текущей сессии.
Или из shell:
ls -t ~/.astracode/sessions/$(date +%Y/%m/%d)/ | head -1
Посмотреть конфиг с источниками
/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.
Recipe: вернуть AstraCode в чистое состояние
Это аварийная процедура с потерей активного состояния. Полностью остановите все
процессы AstraCode и переместите $ASTRACODE_HOME целиком в backup вне этой
директории:
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: ускорить сборку при разработке
# В 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 секунд на инкрементальных билдах.
Recipe: профилировать 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
Recipe: писать integration-тесты с мок-LLM
В core/tests/ есть фикстуры с моками ModelProvider на базе wiremock. См. core/tests/common/responses.rs (ResponseMock, mount_sse_sequence) и core/tests/suite/review.rs для примера полноценного integration-теста ревью с моками.
Кратко:
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 версии для релиза
# 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.