From halter-rust
Use when building, explaining, testing, or modifying Rust agent harnesses with the Halter library. Applies to simple one-shot runners, chat sessions, embedded services, custom tools, hook policy, skills/plugins, subagents, persistence, provider configuration, event consumers, and Halter workspace Rust changes.
How this skill is triggered — by the user, by Claude, or both
Slash command
/halter-rust:basic-rustThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill to help users compose Halter as a Rust agent harness. Treat Halter as an embeddable runtime, not just a CLI wrapper.
Use this skill to help users compose Halter as a Rust agent harness. Treat Halter as an embeddable runtime, not just a CLI wrapper.
Pick the smallest tier that fits the request.
Halter::from_config_file("halter.toml") when the user wants a simple app, CLI wrapper, prototype, or repo-local agent.HalterBuilder plus ResourceCompiler when the user needs custom tools, SDK hooks, a custom session store, preloaded resources, or resource hot-swapping.HarnessConfig directly when config is generated by a service, tenant, UI, or test.halter-runtime, halter-tools, halter-providers, and halter-session directly only when replacing core services such as the prompt assembler, context manager, model registry, provider adapters, or event plumbing.Use the high-level halter crate first. It assembles config, providers, resources, built-in tools, hooks, policy, session storage, runtime services, and subagent tools.
halter: facade, Halter, HalterBuilder, ResourceCompiler, resource loaders, session-store override.halter-config: HarnessConfig, TOML loading, environment overrides, JSON Schema export, provider credential resolution.halter-protocol: canonical messages, turns, events, tools, resources, subagent requests, provider metadata.halter-runtime: sessions, turn loop, event stream, context planning, compaction, prompt assembly, hooks, subagents, shutdown.halter-providers: provider trait, OpenAI, Anthropic, OpenRouter, fake provider, model registry.halter-tools: built-in tool catalog, Tool trait, ToolContext, policy, subagent-control tools.halter-hooks: hook file parsing, SDK hooks, hook merge semantics, permission decisions.halter-session: in-memory and SQLite session stores, replay, optimistic commit conflict type.halter-cli: thin command-line wrapper over the SDK; useful as an event-output reference.Use this for the first working version of most apps:
use futures::StreamExt;
use halter::prelude::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let harness = Halter::from_config_file("halter.toml").await?;
let session = harness.new_session(SessionInit::default()).await?;
let mut events = session
.submit_turn(Turn::user("Summarize this repository"))
.await?;
while let Some(event) = events.next().await {
match event?.payload {
SessionEventPayload::DeltaItem { delta } => print!("{}", delta.text),
SessionEventPayload::ToolOutput { chunk, .. } => print!("{chunk}"),
SessionEventPayload::TurnCompleted { usage, .. } => {
eprintln!("\nusage: in={} out={}", usage.input_tokens, usage.output_tokens);
break;
}
SessionEventPayload::TurnFailed { error, .. } => anyhow::bail!(error),
_ => {}
}
}
session.shutdown("complete").await?;
let _ = harness.shutdown(std::time::Duration::from_secs(10)).await;
Ok(())
}
Always consume SessionEventPayload, not only text deltas. Tool activity, hook runs, warnings, compaction, child-session events, failures, and shutdown all flow through the same event stream.
halter.tomlStart with this shape and add only the tools and policies the agent needs:
version = 1
[models.default]
provider = "openai"
model = "gpt-5"
reasoning = "medium"
[models.subagent]
provider = "openai"
model = "gpt-5-mini"
reasoning = "medium"
[resources.skills]
roots = ["./.agent/skills"]
[resources.plugins]
roots = ["./.agent/plugins"]
[tools]
enabled = ["read", "glob", "grep", "write", "edit", "shell", "task"]
[policy]
allowed_write_roots = ["./", "/tmp/halter"]
max_read_bytes = 1048576
max_subagent_depth = 3
max_concurrent_subagents = 8
[policy.shell]
enabled = true
allow = ["git", "cargo", "rg", "ls", "find"]
timeout_secs = 30
[policy.network]
enabled = false
allowed_hosts = []
[sessions]
backend = "memory"
Credentials are resolved for every selected model role. Configured API keys win; otherwise Halter reads OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY. Halter does not parse .env files.
models.default is required. models.small and models.subagent fall back to default when omitted.reasoning accepts low, medium, high, and xhigh.tokens_per_minute defaults to 500_000; use it for proactive rate limiting.providers.<name>.base_url, api_key, headers, and temperature are provider-level, not role-level.HALTER_TOOLS_ENABLED, HALTER_SKILL_ROOTS, HALTER_PLUGIN_ROOTS, HALTER_POLICY_SHELL_ALLOW, HALTER_POLICY_SHELL_ENABLED, HALTER_POLICY_NETWORK_ENABLED, HALTER_POLICY_ALLOWED_HOSTS, and HALTER_SESSION_BACKEND.~/ only. $VAR, ${VAR}, ~user, and shell escapes are not expanded.tools.enabled is an allowlist. An empty list registers all compiled built-ins.Enable features on the halter crate, then expose the matching tools in config:
advanced-tools: faster grep paths for larger files.ast-tools: ast_grep.browser-tools: browser.image-tools: image.pty: pty.profiling: profile.full: all optional tool families except SQLite.sqlite: SQLite session persistence and sessions.backend = "sqlite".Do not tell users that adding a tool name to tools.enabled compiles the feature. It does not.
Use resources for durable agent behavior instead of appending instructions to every turn.
[resources.skills].roots; each skill directory must contain SKILL.md..claude-plugin/plugin.json, .agent-plugin/plugin.json, .halter-plugin/plugin.json, or plugin.json.name and version. Optional paths include skills, agents, hooks, mcpServers, lspServers, allowedHttpHosts, and allowedEnvVars.skills and agents paths must be ./... or an accepted plugin alias. Parent-directory traversal is rejected.agent_type values for spawn_agent.hooks/hooks.json is used when the manifest does not name a hooks file.Compiled skills are inserted as prefix-cacheable prompt segments between system instructions and transcript history. Recompile resources with ResourceCompiler::from_config(&config).compile().await?, then call harness.replace_resources(resources) when a long-lived process needs future turns to see updated skills, agents, plugins, or hooks.
Use programmatic config when the app owns provider/model/tool/policy selection:
use halter::prelude::*;
use halter_config::{
ConfiguredProvider, HarnessConfig, ModelConfig, ModelsConfig, ProviderConfig,
ProvidersConfig,
};
use halter_protocol::ReasoningEffort;
async fn build() -> anyhow::Result<Halter> {
let config = HarnessConfig {
providers: ProvidersConfig {
openai: Some(ProviderConfig {
api_key: Some(std::env::var("OPENAI_API_KEY")?),
..ProviderConfig::default()
}),
..ProvidersConfig::default()
},
models: ModelsConfig {
default: Some(ModelConfig {
provider: ConfiguredProvider::OpenAi,
model: "gpt-5".to_owned(),
max_input_tokens: Some(200_000),
max_output_tokens: Some(8_192),
reasoning: Some(ReasoningEffort::Medium),
tokens_per_minute: Some(500_000),
}),
..ModelsConfig::default()
},
..HarnessConfig::default()
};
Halter::from_config(config, ResourceSnapshot::empty()).await
}
Use ResourceSnapshot::empty() only when the harness intentionally has no loaded skills, agents, plugins, or hooks. If resource roots matter, compile them and use Halter::from_compiled_resources(config, resources).await?.
Use HalterBuilder when the harness needs extension points:
use std::sync::Arc;
use halter::{HalterBuilder, ResourceCompiler};
use halter_config::load_path;
async fn build_custom(tool: Arc<dyn halter_tools::Tool>) -> anyhow::Result<halter::Halter> {
let config = load_path("halter.toml").await?;
let resources = ResourceCompiler::from_config(&config).compile().await?;
HalterBuilder::new()
.with_config(config)
.with_compiled_resources(resources)
.with_tool(tool)
.build()
.await
}
Builder rules:
build() requires either a resource snapshot, compiled resources, or loaded skills/plugins to compile.with_resource_snapshot(...) with with_loaded_skills(...) or with_loaded_plugins(...); the builder rejects that mix.with_compiled_resources(...) when hooks and hook warnings should survive compilation.with_session_store(...) to replace the configured built-in backend.with_plugin_hook(...) or with_plugin_hook_priority(...) for in-process SDK hooks.Implement halter_tools::Tool. Keep the provider-visible schema strict, decode JSON into a typed input struct, enforce policy through ToolContext, and return ToolResult::Json when callers need structured data.
Tool spec choices matter:
ToolConcurrency::Exclusive: mutates shared state, writes files, runs shell/process operations, or must preserve order.ToolConcurrency::ReadOnly: can run beside other non-mutating reads.ToolConcurrency::ParallelSafe: safe beside any other parallel-safe call.ToolCapabilities.mutating: true for writes or externally visible side effects.requires_approval: true when the harness or hook stack should mediate risk.cancellable: true only when ToolContext.cancel is honored.long_running: true for subprocesses, network calls, browser sessions, subagents, profiling, and other slow operations.Register custom tools with HalterBuilder::with_tool(Arc::new(MyTool)). Built-ins are registered first; a custom tool with the same name replaces the registered entry in ToolRuntime.
Base tools:
read, glob, grep: file discovery and reads.write, edit: atomic file writes.shell: persistent shell session, gated by shell policy and allowlist.process: process-tree inspection and termination.task: in-memory todo list scoped to the session.Optional tools:
pty: bounded interactive terminal sessions.ast_grep: syntax-aware search and replacement.image: local image info, resize, and convert.browser: remote browser automation over CDP; also requires browser provider environment and network policy.profile: profiling and instrumentation workflows.Subagent-control tools:
spawn_agent: start a child session.send_input: send follow-up work to a child.wait_agent: wait for terminal child state.close_agent: close a child control surface.For subagent tools, the model argument is a registered model id such as default, small, or subagent, not a provider model name like gpt-5.
Policy is enforced by tools, not by prompting.
allowed_write_roots.max_read_bytes, configured read roots inside PolicySettings, and sensitive-path patterns.eval, exec, source, and . invocations.policy.network.enabled = true.allowed_hosts; loopback hosts require explicit allowed_loopback.max_subagent_depth and max_concurrent_subagents.If a user asks for more tool power, change config or PolicySettings deliberately. Do not work around policy failures in tool code.
Use hooks for policy, interception, annotation, and workflow control. Do not use hooks to implement a new tool, provider, or session backend.
Important events include SessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, Notification, Stop, SubagentStart, SubagentStop, PreCompact, PostCompact, PermissionRequest, and PermissionDenied.
SDK hook pattern:
use halter_hooks::{Hook, HookEventName, HookResponse};
use halter_protocol::PluginId;
let hook = Hook::callback(HookEventName::PreToolUse, |input| async move {
if input.tool_name() == Some("shell")
&& input.payload.to_string().contains("rm -rf /")
{
return HookResponse::block("dangerous shell command blocked");
}
HookResponse::passthrough()
});
let builder = Halter::builder()
.with_plugin_hook(PluginId::from("sdk-policy"), hook);
Continue the builder chain with config, resources, and build().await?. File hooks support command, HTTP, prompt, and agent handlers. Callback and function handlers are SDK-only. Multiple hook outputs are merged; any blocking hook matters, and competing input/output rewrites can fail.
Use subagents when delegated work is independent enough to run in a child session.
spawn_agent, send_input, wait_agent, and close_agent if the model should delegate.models.subagent for cheaper or specialized child work.agent_type values.fork_context = false unless the child genuinely needs the parent's accumulated transcript, summaries, and file-view cache.subagent_event_forwarding = "all" only when the parent event stream needs raw child events. Forwarded events keep the child session_id; the cap emits Lagged when reached.Named agents are prompt specializations. They do not create new Rust types; the runtime appends the agent prompt segment to the child session's system prompt seed.
Use memory sessions for tests and short-lived processes. Use SQLite when sessions must survive restarts:
[sessions]
backend = "sqlite"
sqlite_path = "./.halter/sessions.db"
The embedding build must enable the sqlite feature. Session stores persist blueprints, state, snapshots, and committed SessionEvents. Use session.replay().await? for UI hydration, debugging, audit trails, and tests.
For full traces, set runtime.traces_dir; Halter writes one JSONL .txt file per root session with a header, live pending_event preview lines, committed SessionEvent lines, and subagent headers/events appended to the root trace.
Build UIs and automation around canonical events:
DeltaItem: assistant text deltas.MessageItem: durable transcript messages, including final assistant messages.ToolExecutionStarted, ToolOutput, ToolExecutionCompleted: tool progress and results.HookStarted, HookCompleted: hook audit trail.ContextCompacted: compaction happened.TurnCompleted: final usage for a turn.TurnFailed: provider, runtime, or cancellation failure. Check cancelled and retryable.Lagged: forwarded events were capped or event delivery fell behind.The CLI's run --streaming-json emits canonical SessionEvent JSON. Its default JSON-result mode returns the latest assistant MessageItem when TurnCompleted arrives.
Prefer deterministic tests:
halter_providers::FakeProvider for no-network runtime tests.InMemorySessionStore unless persistence behavior is under test.allowed_write_roots for file-tool tests.cancellable, and ToolResult shape.Useful verification commands:
cargo test -p haltercargo test -p halter-runtimecargo test -p halter-toolscargo test -p halter-hookscargo check --workspaceRun the narrowest meaningful command first. Broaden to the workspace when a change touches protocol, runtime state, resource loading, provider adapters, or shared tool policy.
Halter::from_config_file, Halter::from_config(config, snapshot), or builder resources.[providers.<name>].api_key.tools.enabled.policy.shell.allow or change the workflow.agent_type: load plugin agent prompt files or omit agent_type.default, small, or subagent, not an upstream model string.sqlite feature and set sessions.backend = "sqlite".When modifying Halter itself:
halter-protocol, config in halter-config, execution in halter-runtime, tools and policy in halter-tools, hooks in halter-hooks, persistence in halter-session.Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub pbdeuchler/halter --plugin halter-rust