| name | basic-rust |
| description | 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. |
Halter Rust harnesses
Use this skill to help users compose Halter as a Rust agent harness. Treat Halter as an embeddable runtime, not just a CLI wrapper.
Start with the harness tier
Pick the smallest tier that fits the request.
- Config-file runner: use
Halter::from_config_file("halter.toml") when the user wants a simple app, CLI wrapper, prototype, or repo-local agent.
- SDK builder: use
HalterBuilder plus ResourceCompiler when the user needs custom tools, SDK hooks, a custom session store, preloaded resources, or resource hot-swapping.
- Programmatic config: build
HarnessConfig directly when config is generated by a service, tenant, UI, or test.
- Runtime assembly: reach for
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.
Crate map
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.
Minimal config-file harness
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.
Minimal halter.toml
Start 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", "true", "cd"]
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. OpenAI can use [providers.openai].oauth instead of
api_key; the OAuth bundle must include client_id, access_token,
id_token, and refresh_token, and Halter sends access_token as the bearer
token. OAuth route rewriting sends /v1/responses, every path below that
prefix including /v1/responses/compact, and /chat/completions to
https://chatgpt.com/backend-api/codex/responses;
base_url is ignored for those paths. OAuth requests send the assembled system
prompt as top-level instructions instead of a developer/system input item,
and set store to false. OpenAI api_key and oauth are mutually
exclusive. Halter does not parse .env files.
Config details that matter
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. OpenAI also accepts provider-level oauth.
- Environment overrides include
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.
- Resource-root paths expand leading
~/ only. $VAR, ${VAR}, ~user, and shell escapes are not expanded.
- Explicit
tools.enabled is an allowlist. An empty list registers all compiled built-ins.
- Feature-gated tools must be compiled in and enabled by config before the model can use them.
Cargo features
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.
Resource loading
Use resources for durable agent behavior instead of appending instructions to every turn.
- Standalone skills are discovered recursively below
[resources.skills].roots; each skill directory must contain SKILL.md.
- Plugin roots are scanned one level deep. A plugin is a child directory with
.codex-plugin/plugin.json, .claude-plugin/plugin.json, .agent-plugin/plugin.json, or plugin.json.
- Plugin manifests need
name and version. Optional paths include skills, agents, hooks, mcpServers, lspServers, allowedHttpHosts, and allowedEnvVars.
- Plugin
skills and agents paths must be ./... or an accepted plugin alias. Parent-directory traversal is rejected.
- Agent prompt files become named
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.
Programmatic harness
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
}
For OpenAI OAuth programmatic config, set ProviderConfig::oauth instead of api_key:
use halter_config::{OpenAiOAuthConfig, ProviderConfig};
ProviderConfig {
oauth: Some(OpenAiOAuthConfig {
client_id,
access_token,
id_token,
refresh_token,
}),
..ProviderConfig::default()
}
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?.
Builder patterns
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.
- Do not combine
with_resource_snapshot(...) with with_loaded_skills(...) or with_loaded_plugins(...); the builder rejects that mix.
- Use
with_compiled_resources(...) when hooks and hook warnings should survive compilation.
- Use
with_session_store(...) to replace the configured built-in backend.
- Use
with_plugin_hook(...) or with_plugin_hook_priority(...) for in-process SDK hooks.
Custom tools
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.
Built-in tools
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
Policy is enforced by tools, not by prompting.
- Writes are limited by
allowed_write_roots.
- Reads are limited by
max_read_bytes, configured read roots inside PolicySettings, and sensitive-path patterns.
- Shell commands are parsed and checked against the allowlist. Strict mode rejects function definitions and
eval, exec, source, and . invocations.
- Network is denied unless
policy.network.enabled = true.
- Remote hosts use
allowed_hosts; loopback hosts require explicit allowed_loopback.
- Process signals are bounded by the Halter process tree when the high-level builder creates policy.
- Subagent spawning is bounded by
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.
Hooks
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.
Subagents
Use subagents when delegated work is independent enough to run in a child session.
- Enable
spawn_agent, send_input, wait_agent, and close_agent if the model should delegate.
- Configure
models.subagent for cheaper or specialized child work.
- Use plugin agent prompt files for named
agent_type values.
- Keep
fork_context = false unless the child genuinely needs the parent's accumulated transcript, summaries, and file-view cache.
- Set
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.
- Use policy limits for depth and concurrency instead of relying on prompts.
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.
Persistence and replay
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.
Event consumers
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.
Testing harnesses
Prefer deterministic tests:
- Use
halter_providers::FakeProvider for no-network runtime tests.
- Use
InMemorySessionStore unless persistence behavior is under test.
- Use temp directories and policy
allowed_write_roots for file-tool tests.
- Register only the tools needed for the test when asserting tool availability.
- Assert event payloads, replay output, and file side effects.
- Test hooks at three levels when they matter: hook logic, merge behavior, runtime integration.
- For custom tools, test JSON input decoding, policy denial, cancellation behavior if
cancellable, and ToolResult shape.
Useful verification commands:
cargo test -p halter
cargo test -p halter-runtime
cargo test -p halter-tools
cargo test -p halter-hooks
cargo check --workspace
Run the narrowest meaningful command first. Broaden to the workspace when a change touches protocol, runtime state, resource loading, provider adapters, or shared tool policy.
Common failure modes
- Missing resource snapshot: use
Halter::from_config_file, Halter::from_config(config, snapshot), or builder resources.
- Missing provider credentials: set the provider-specific env var or
[providers.<name>].api_key. For OpenAI, [providers.openai].oauth is also valid.
- Feature-gated tool unavailable: enable the Cargo feature and include the tool in
tools.enabled.
- Shell command denied: add the program to
policy.shell.allow or change the workflow.
- Path denied: make the root exist and include it in policy. Parent traversal is rejected.
- Network denied: enable network, allow the host, and explicitly allow loopback when needed.
- Unknown
agent_type: load plugin agent prompt files or omit agent_type.
- Unknown subagent model: pass
default, small, or subagent, not an upstream model string.
- SQLite config rejected: enable the
sqlite feature and set sessions.backend = "sqlite".
- Commit conflict: serialize writes to a session or reload state before retrying.
Workspace engineering guidance
When modifying Halter itself:
- Read the relevant crate README and source before changing behavior.
- Preserve public protocol shapes unless the user asked for a breaking change.
- Keep crate boundaries intact: protocol types in
halter-protocol, config in halter-config, execution in halter-runtime, tools and policy in halter-tools, hooks in halter-hooks, persistence in halter-session.
- Validate untrusted or cross-boundary data at the boundary, then pass typed values inward.
- Make spawned tasks, child processes, streams, timers, and cancellation explicit.
- For resources, keep behavior compatible with Codex and Claude Code layouts unless the request is Halter-specific.
- Report changed files, verification commands, and any unverified risk when finishing.