agcli
agcli is a no-bloat Rust crate for building agent-native CLIs.
It is built around the design in design.md:
- JSON-only envelopes
- HATEOAS
next_actions - self-documenting root command tree
- typed exit codes for agent self-correction
- agent-native output flags (
--select,--compact,--quiet) on every command - context-safe output truncation and bounded list output
- built-in
doctorhealth checks and a static command-tree self-audit - typed NDJSON streaming with terminal
result/error
Why terminal envelopes and truncation pointers matter
- Terminal
result/errorenvelopes give agents a deterministic finish state, so they can branch on structured outcomes instead of fragile text parsing. - Structured
errorenvelopes support reliable retries, escalation, and fallback actions, whileresultenvelopes make successful completion explicit and machine-verifiable. - Truncation with file pointers lets CLIs cap large outputs safely while preserving continuity: agents can follow the pointer to full logs or artifacts without overflowing context windows.
- This improves reliability and debuggability for long-running automation while reducing token pressure in agent loops.
Install
[]
= "0.9.1"
= "1"
= { = "1", = ["macros", "rt-multi-thread"] }
The crate is 100% async (since v0.8). Handlers, the NDJSON emitter, and
truncation I/O all return Futures. Wire up a tokio runtime in your binary —
the snippet below uses #[tokio::main].
Quick start
use ;
use json;
async
Flag parsing
agcli accepts both --key=value and --key value (space-separated) for
value flags. This matches the HATEOAS [--flag <value>] template form used
throughout the crate's docs.
To disambiguate boolean flags from value flags without a schema layer, the
parser reads each command's .usage(...) string at runtime and treats any
bracketed flag without a <placeholder> (e.g. [--no-git], [--follow])
as a pure boolean. Declared boolean flags will never consume the next token,
so mycli submit --no-git ./plan.html works as expected.
For value flags or undeclared flags, a bare --key followed by a non-flag
token consumes that token as the value (--key value ≡ --key=value). Use
--key=true or -- to force a positional after an undeclared boolean.
Agent-native flags
Every command supports a reserved vocabulary of agent-native flags without declaring them. The framework applies three of them to the result centrally, so any command gets token-economy output for free:
| Flag | Effect |
|---|---|
--select=a,b,c |
Project the result to only these fields (top-level keys or a.b dot paths; maps over arrays). |
--compact |
Drop null and empty fields. A command may instead declare CommandOutput::compact_fields([...]) to keep an explicit high-gravity allowlist. |
--quiet |
Omit next_actions from the envelope. |
The rest are parsed as booleans anywhere on the line and exposed via typed
accessors on CommandRequest, for the handler to act on:
--dry-run → req.dry_run(), --yes/--no-input → req.assume_yes(),
--no-cache → req.no_cache(), --no-color → req.no_color(),
--stdin → req.wants_stdin() (pair with agcli::read_stdin().await).
These names are reserved while enabled (the default). If a command needs one
with conflicting semantics, opt out per-CLI with AgentCli::reserved_flags(false).
// `app get 1 --select id,name --compact` → result projected and compacted,
// even though `get` never declared those flags.
Typed exit codes
Every envelope carries a typed exit code as a first-class field — both as the
process exit status (Execution::exit_code()) and in the JSON
("exit_code": N) — so an agent can branch on the failure class whether it
reads $? from a shell or parses stdout. Framework usage errors (unknown
command, bad flag, missing handler) return 2; handler errors default to 1
and opt into a typed code via CommandError::exit_code(...).
ExitCode |
Value | Meaning |
|---|---|---|
SUCCESS |
0 | Command succeeded |
ERROR |
1 | Generic failure (default for handler errors) |
USAGE |
2 | Bad invocation (framework-raised) |
NOT_FOUND |
3 | Requested resource missing |
AUTH |
4 | Auth/authorization failure |
API |
5 | Upstream/API call failed |
RATE_LIMITED |
7 | Back off and retry later |
Err
Bounded lists
CommandOutput::list(items) and CommandOutput::list_truncated(items, total)
emit a bounded { items, count, total, truncated } result. When truncated,
a guidance field tells the agent how to narrow the query.
Built-in doctor and self-audit
AgentCli::doctor(checks) registers a doctor command that runs your
[Check]s and reports { healthy, checks: [...] }. A failing check still
produces an ok: true envelope (the report ran) but carries that check's
exit code (e.g. ExitCode::AUTH), so the shell sees a non-zero status.
AgentCli::audit() statically validates the command tree and returns an
AuditReport: it flags dangling next_action templates (HATEOAS integrity),
dead-end commands, and missing usage/descriptions. Use it in a test:
assert!;
Performance
agcli targets macOS and Linux only. The crate ships with optimized release/bench profiles. To maximize runtime performance in a downstream binary:
Recommended Cargo.toml
[]
= "0.9.1"
[]
= 3
= "thin"
= 1
The default system allocator is the right choice for short-lived CLI
processes. If you build a long-running, allocation-heavy CLI and measure a
win, add tikv-jemallocator (or another allocator) directly in your binary
via #[global_allocator] — agcli does not bundle one.
Build-machine-specific codegen
RUSTFLAGS="-C target-cpu=native"
Do not commit this into the repo — it breaks cross-compilation portability.
PGO (Profile-Guided Optimization)
Full example
See examples/ops.rs — a runnable ops CLI demonstrating:
- the self-documenting command tree
- contextual
next_actions - log truncation with file pointers