AOI‑CLI#
The Agent-Operable Interface · Command‑Line Profile
The first profile of the Machine Mode standard. A typed, streamable, discoverable, bounded, safe contract for command‑line tools used by autonomous systems.
- Document: AOI‑CLI v1.0‑alpha
- Standard: Machine Mode
- Status: Alpha. Breaking change from v0.2 — see § 5.2.
- Date: 2026‑07‑30
- Editor: Kevin Hunt
- Related: Machine Mode foundations · Why a standard · In practice
Executive summary#
AOI‑CLI is the command‑line profile of the Machine Mode standard. A conforming tool exposes:
A stable machine mode that emits UTF‑8 newline‑delimited JSON event objects to stdout, never mixes human prose into that stream, provides a credential‑free schema and capability discovery command, uses a standard event envelope and error taxonomy, has explicit completion and failure semantics, and makes side effects safe, bounded, and retry‑aware.
The analogy is not "POSIX for agents." POSIX standardizes operating‑system interfaces. AOI is closer to OpenAPI, AsyncAPI, or gRPC/protobuf for command‑line tools: a typed interface contract for local process execution.
Roles in a pipeline#
An AOI‑CLI tool plays one of three roles depending on whether it consumes JSONL input, produces JSONL output, or both:
| Role | Reads JSONL on stdin? | Emits JSONL on stdout? | Example |
|---|---|---|---|
| Source | no | yes | outline search … produces hits |
| Transformer | yes | yes | summarize --input-jsonl - reads hits, emits abstracts |
| Sink | yes | yes (audit + summary only) | outline import --input-jsonl - performs side effects |
Every conforming tool — source, transformer, or sink — still emits aoi:meta first and a terminal aoi:summary last, and follows every other factor of the standard. The role only describes the tool's position in a pipeline; it is not a separate conformance category.
The spec uses these terms throughout where the producer/consumer distinction matters: § 12 Input contract applies to transformers and sinks; § 13 Pipeline composition describes how the three roles compose.
Relationship to Machine Mode#
Machine Mode is the public concept and the foundations document: the framework for thinking about agent‑operable tools, naming the ten factors that describe a well‑shaped one (Typed, Discoverable, Streamable, Bounded, Safe, Idempotent, Auditable, Verifiable, Composable, Versioned). AOI is the formal specification of that framework. AOI‑CLI, defined in this document, is the first formal profile — it specifies how each of those ten factors applies at the command‑line process boundary.
Forthcoming profiles for other interface surfaces, alternate modes beyond batch invocation (such as a JSON‑RPC session mode), alternative wire formats, and the relationships to adjacent standards (OpenAPI, MCP) are described in Direction — beyond v1.0‑alpha. Nothing in that document is normative for v1.0‑alpha.
Design goals#
A conforming AOI‑CLI tool is:
- Stable — explicit compatibility contract for machine output.
- Discoverable — schema and capabilities can be fetched without credentials, network, or side effects.
- Parseable — machine mode stdout is only JSONL event objects.
- Streamable — consumers can process records incrementally.
- Verifiable — successful completion, partial failure, truncation, and crashes have defined semantics.
- Composable — pipes and process exit status behave predictably.
- Safe — read‑only by default, destructive work explicit, secrets redacted.
- Bounded — large reads and searches have limits, cursors, and truncation signals.
- Retry‑aware — errors say whether retry may help; writes can be idempotent.
- Language‑neutral — no Python or Nushell‑specific behavior is normative.
Non‑goals#
AOI‑CLI does not standardize:
- operating‑system process APIs,
- package‑manager layouts,
- programming‑language frameworks,
- human CLI UX,
- transport protocols beyond local process stdin/stdout/stderr,
- a universal schema‑registry requirement.
Normative keywords#
The words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are used in the RFC 2119 sense.
Relationship to existing standards#
AOI‑CLI builds on existing conventions:
- POSIX utility conventions for process, arguments, stdin/stdout/stderr, and exit status: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
- NDJSON / JSON Lines for newline‑delimited JSON streams: https://github.com/ndjson/ndjson-spec and https://jsonlines.org/
- JSON Schema 2020‑12 for event and input schemas: https://json-schema.org/draft/2020-12/json-schema-core
- Modern CLI UX guidance: https://clig.dev/
- sysexits‑style exit categories where available:
sysexits.h
AOI‑CLI is a typed CLI interface convention, not an OS standard.
Part I — Normative core#
Everything in Part I is required to claim AOI-CLI conformance. It is deliberately small: an envelope, a discriminator, a declared schema version, defined completion, a portable error taxonomy, and predictable composition. A maintainer should be able to implement all of it in an afternoon.
These are interoperability invariants — they only pay off if every tool spells them identically. One tool implementing them alone gains little; a hundred tools implementing them differently gains nothing. That is what makes them a standard rather than advice, and it is why they are stated strictly.
1. Machine mode#
A conforming tool MUST expose at least one stable machine mode that emits AOI events.
Recommended invocation pattern:
outline --output jsonl ...
or equivalently:
outline --format jsonl ...
A compatibility wrapper such as outline-jsonl MAY exist, but the suffix is not the normative contract.
1.1 Machine‑mode stability#
If a tool advertises AOI conformance, its machine mode MUST be a stable, schema‑governed API. It is not "best effort JSON." The project MUST document compatibility rules for event types, field names, field types, and versioning.
1.2 Human mode#
The default unsuffixed command MAY remain human‑readable. Human output MAY include tables, color, progress bars, and prose. Machine mode MUST NOT.
1.3 Interactive flows#
Machine mode is non‑interactive by contract. A conforming tool MUST NOT prompt for input, open a browser, or otherwise require human attention while running in machine mode. This is what makes the contract callable from an agent or a non‑terminal automation context.
Interactive flows that a CLI legitimately needs — OAuth authorization, first‑run config, MFA approval, interactive credential prompts — belong in the unsuffixed human‑mode invocation. The human runs tool auth login once, completes the flow, and the resulting credential lands in the user's existing credential ecosystem (keychain, config file, environment, kubeconfig, ssh agent, etc.). After that, subsequent machine‑mode calls inherit the credential by inheriting the process environment.
If a machine‑mode invocation reaches a point where it would otherwise prompt, it MUST instead emit a structured error:
{
"type": "aoi:error",
"aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
"category": "authn",
"code": "INTERACTIVE_REQUIRED",
"message": "Interactive authentication required. Run `outline auth login` to complete the flow.",
"retryable": false,
"hint": "outline auth login"
}
The hint field is informational only; consumers MUST NOT parse it as a command to execute automatically. A reasonable agent surfaces the hint to a human operator.
This carve‑out is what lets AOI work with the full breadth of real auth ecosystems (browser‑bound OAuth, hardware MFA, enterprise SSO) without dragging interactive‑flow handling into the wire protocol.
2. JSONL event stream#
Machine‑mode stdout MUST be UTF‑8 newline‑delimited JSON.
Each line MUST be one complete JSON object.
Every event, including the terminal aoi:summary, MUST be followed by a line feed. A consumer MUST NOT treat a final line lacking a line terminator as a complete event; it MUST discard it and treat the stream as truncated. NDJSON does not settle this, and naive line iterators will happily yield {"type":"aoi:sum as a record — which would let a half‑written summary be read as a whole one, defeating § 4 entirely.
Every event object MUST include:
{"type":"..."}
Machine‑mode stdout MUST NOT include:
- banners,
- markdown headings,
- color or ANSI escape codes,
- progress bars,
- human prose,
- stack traces,
- mixed plain‑text warnings.
Recoverable diagnostics belong in structured aoi:warning events. Fatal diagnostics should be structured aoi:error events when possible; usage errors may use stderr before stream startup.
3. Discovery without credentials or network#
A conforming tool MUST provide a credential‑free, network‑free way to discover its schema and capabilities.
Recommended subcommands:
outline schema --output json
outline capabilities --output json
Equivalent flags MAY exist:
outline --schema
outline --capabilities
If both flags and subcommands exist, they MUST return equivalent information.
3.1 Schema sidecars#
Sidecar schema files MAY be provided but MUST NOT be required for conformance.
A sidecar is a packaging convenience, not the wire contract. Real installation layouts include npm/pnpm shims, Homebrew Cellar paths, immutable Nix store paths, single‑file Go/Rust binaries, container images with stripped filesystems, FHS share/<package>/ resources, and embedded schemas.
Consumers MUST be able to rely on the schema and capability command. They MAY opportunistically read local sidecars when available.
3.2 Schema command requirements#
schema MUST NOT require:
- authentication,
- config files beyond local install metadata,
- network access,
- environment secrets,
- a valid business query.
Startup cost is acceptable.
4. Completion semantics#
A normal finite operation SHOULD emit aoi:meta first and MUST emit a terminal aoi:summary event before exiting 0.
ok means the tool accomplished what was asked, not that the process avoided crashing. It has no default: a producer MUST emit it explicitly. SARIF's equivalent field had an ambiguous default and GCC consequently shipped executionSuccessful: true while failing with an error.
Once aoi:meta has been emitted, a process that exits under its own control MUST emit aoi:summary regardless of outcome. That is what makes the absence of a summary mean exactly one thing — crash, kill, or truncation — which is the whole basis of the contract below.
A consumer MUST treat EOF without a terminal aoi:summary as an incomplete stream unless the operation is explicitly documented as unbounded.
A producer that crashes cannot guarantee a terminal sentinel. Therefore the absence of aoi:summary on EOF is the cross‑language crash and truncation signal.
4.1 Finite streams#
For finite commands such as list, search, get, create, and doctor:
- exit 0 + terminal
aoi:summary.ok=truemeans success, - exit 0 without terminal
aoi:summaryMUST be treated as protocol failure, - non‑zero exit with partial events MUST be treated as failure unless the caller explicitly requested partial tolerance,
aoi:summary.partial=trueMAY indicate successful partial results when partial is a valid outcome.
4.2 Unbounded streams#
For commands like watch, tail, or event subscriptions:
- the command MUST document that the stream is unbounded,
- it MAY emit heartbeat or checkpoint events,
- on graceful cancellation it SHOULD emit
aoi:summary.ok=falsewithreason:"cancelled"when possible, - consumers cannot require a terminal
aoi:summaryuntil EOF.
5. Versioning#
AOI versioning has three separate concepts and they MUST NOT be conflated:
- Tool version — implementation release, e.g.
outline 1.8.2. - AOI version — the version of this interface convention, e.g.
aoi_version: "1.0-alpha". - Schema version — the version of a specific event/input schema, e.g.
schema_version: "1.0.0".
The aoi:meta event and the terminal aoi:summary SHOULD each carry all three, and MUST to claim the Versioned factor (§ 18). Requiring the identity triple at both ends means a consumer holding only tail -1 still knows what it read, and one holding only the first line does too.
The field name aoi_version is frozen permanently. CloudEvents failed the one version transition it existed for because the version field itself was renamed between 0.1 and 0.2, leaving old parsers unable to locate the version at all.
schema_name + schema_version are the normative identity. A schema $id URL is advisory: consumers MUST NOT be required to dereference it and MUST NOT use it as the version identity. SARIF made this mistake — its schema URL became the de-facto identity and then churned across four host variants, breaking GCC, Trivy, tflint, and both Microsoft SARIF viewers.
The cheap incompatibility check: if major(schema_version) differs from what the consumer was built against, it MUST NOT interpret domain events — but MAY still interpret aoi:* framework events if major(aoi_version) matches. That is the payoff for carrying two independent version numbers.
{
"type": "aoi:meta",
"aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
"tool": "outline",
"tool_version": "1.8.2",
"aoi_version": "1.0-alpha",
"schema_name": "com.example.outline.events",
"schema_version": "1.0.0"
}
5.1 Breaking-change policy#
A breaking machine‑output change MUST change schema_version major version.
A tool MAY expose multiple schema versions through one binary:
outline --output jsonl --schema-version 1 ...
outline --output jsonl --schema-version 2 ...
or through content negotiation:
outline --output jsonl --accept-schema com.example.outline.events@2
5.2 Breaking changes in v1.0-alpha#
v1.0-alpha is not backward compatible with v0.2. A v0.2 consumer will silently mis-handle a v1.0-alpha stream, which is precisely the failure this spec exists to prevent — so the break is stated explicitly rather than left to discovery.
| Change | v0.2 | v1.0-alpha | Migration |
|---|---|---|---|
| Framework envelope | flat, top level | nested under aoi | Read aoi.seq / aoi.run; two top-level names are reserved, type and aoi. |
category:"temporary" | present | renamed unavailable | A consumer branching on temporary stops matching silently. Update the branch. |
category:"partial" | present | removed | Emit the item's real category with scope:"item"; express partiality on aoi:summary.partial. |
retryable | optional | required, authoritative | Emit it on every aoi:error. |
retry_after_ms | undefined | defined | Replaces prose "include retry hint if known". MUST NOT appear with retryable:false. |
aoi:error.upstream | {protocol,status} | renamed wrapped | upstream now means provenance on aoi:summary — a different shape. |
| Trailing newline | unstated | MUST | A final line without a terminator is truncated, not complete. |
reason | undefined | required when ok:false | Closed enum: failed, cancelled, timeout, denied. |
5.3 Rollout rule#
Consumers SHOULD branch on schema_name and schema_version, not executable name.
6. Exit status#
AOI does not invent low‑number exit meanings that conflict with existing conventions.
Required baseline:
0— process completed successfully at the process layer. For finite streams, a terminalaoi:summary.ok=trueis still required for application‑level success.- Non‑zero — process failed or was interrupted. Inspect structured events if present.
Recommended mapping using sysexits‑compatible codes:
| Code | Constant | Meaning |
|---|---|---|
| 64 | EX_USAGE | usage error |
| 65 | EX_DATAERR | input or data validation error |
| 69 | EX_UNAVAILABLE | service or dependency unavailable |
| 70 | EX_SOFTWARE | internal software error |
| 73 | EX_CANTCREAT | cannot create output |
| 74 | EX_IOERR | I/O error |
| 75 | EX_TEMPFAIL | temporary failure, retry may succeed |
| 77 | EX_NOPERM | permission or authz failure |
| 78 | EX_CONFIG | configuration error |
| 124 | — | timeout (when produced by timeout wrappers or documented) |
| 130 | — | interrupted by SIGINT |
| 141 | — | SIGPIPE / downstream closed pipe |
Tools on platforms without sysexits MAY use local conventions but MUST document them. Consumers MUST NOT rely on exit code alone for application semantics when structured aoi:summary or aoi:error events are available.
7. Signals and cancellation#
All AOI implementations SHOULD exit cleanly on SIGPIPE/downstream pipe close without printing stack traces to stderr. This requirement is language‑neutral.
On SIGINT/SIGTERM, a tool SHOULD emit, if safe and possible:
{"type":"aoi:summary","aoi":{"seq":41,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"cancelled","event_count":42}
then exit with the conventional signal code. If emitting a final summary would corrupt state or hang, exiting promptly is more important.
8. Event model#
8.1 Common envelope#
Every event MUST include type. Every framework field that may appear on a domain event lives inside a reserved object named aoi. A framework (aoi:*) event additionally carries its own spec-defined fields at the top level — see Envelope fields below.
{"type":"hit","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"rank":1,"id":"doc_123","title":"Agent-operable tools"}
Exactly two top-level names are reserved, permanently: type and aoi. Everything else at the top level of a domain event belongs to the tool's schema and AOI will never claim it.
Why the envelope is nested when the type prefix is not#
This is the one place AOI nests, and the asymmetry is deliberate.
A colon is safe in a JSON value and hostile in a JSON key: jq 'select(.type=="aoi:summary")' works, while .aoi:seq is a syntax error requiring .["aoi:seq"]. So the framework namespace is expressed as a prefix in the type value and as a nested object in the field space. One namespace, two syntactic positions, each chosen for what that position tolerates.
The field space is the one that actually needed protecting. Domain payloads are flattened into the event, and they are owned by third parties. § 10.3 tells tools to allow unknown fields for forward compatibility, so tools will add their own. Without a reserved container, every field AOI adds in a later version can collide with one a deployed tool already emits — and the collision is live today: the hit shape in § 8.3 carries id, a name any envelope would want. Flattening the framework into that space guarantees the collision the type prefix was introduced to prevent.
Nesting costs roughly sixteen bytes per line and .aoi.seq instead of .seq. It buys unbounded framework growth with zero collision risk, and it lets a generic filter, router, or tee strip the envelope without knowing any domain schema.
Envelope fields#
| Field | Status | Meaning |
|---|---|---|
type | MUST | Event discriminator. Top level, never inside aoi. |
aoi.seq | SHOULD; MUST to claim Verifiable | Monotonic counter from 0, no gaps, one per emitted line. |
aoi.run | SHOULD; MUST alongside seq | Opaque identifier stable for this invocation. ULID RECOMMENDED. |
aoi.t | MAY | Milliseconds elapsed since aoi:meta. Absolute time is recoverable from the meta event; a monotonic offset is always determinable where a wall-clock timestamp is not. |
aoi.trace_id | MAY | Correlates this run with an external trace. |
A framework event (aoi:*) carries its own spec-defined fields at the top level — they are framework-owned by definition, so there is no third party to collide with. Tool-specific additions to a framework event MUST go in a properties object rather than at the top level:
{"type":"aoi:summary","aoi":{"seq":9,"run":"01JZQ7X4TQ9"},"ok":true,"event_count":10,"count":8,
"properties":{"vendor_cache_hits":41}}
properties is the one part of SARIF that has drawn no criticism in six years: a single closed container for vendor data, with no prefixes and no registry to administer.
Why seq and run matter#
These are SHOULD, not MUST, because a tool that omits them is still useful and the cost of retrofitting them is real. But a tool cannot claim the Verifiable factor without them, and aoi-lint reports their absence. seq closes a failure mode specific to AOI's actual consumers. The terminal-summary contract (§ 4) detects a stream cut at the end. It cannot detect a stream cut in the middle — and agent harnesses routinely elide the middle of long tool output, keeping the head and tail with a [… N lines omitted …] marker. The summary survives that elision; the data does not. The consumer then reports success while having silently lost records. A contiguous aoi.seq, reconciled against aoi:summary.event_count, makes that undetectable loss detectable. This is TAP's plan line (1..N) applied to a stream that can be truncated from the middle rather than only the end.
aoi.run makes every line attributable on its own. aoi:meta is a sibling line, not a structural wrapper, so any grep, head, split, sort, sampling step, or parallel fan-in severs a line from its metadata. An orchestrator running five tools concurrently and merging their stdout otherwise has no way to say which line came from which tool.
8.2 Type namespace#
Event type values live in one of three namespaces, and the namespace is visible on the wire by prefix convention.
| Layer | Convention | Examples |
|---|---|---|
| Framework events (defined by this spec) | aoi: prefix | aoi:meta, aoi:summary, aoi:warning, aoi:error, aoi:heartbeat, aoi:plan, aoi:check, aoi:progress |
| Recommended domain patterns (common shapes for common operations; each tool's schema owns them) | unprefixed | entry, hit, match |
| Tool‑specific domain events | unprefixed; scoped by aoi:meta.schema_name | whatever the tool defines, e.g. created, deleted, abstract, compiled, transcoded |
Rationale:
- The
aoi:prefix on framework events prevents wire‑level collision with domain events that share generic words. A tool that emits a textual document summary ({"type":"summary","text":"…"}) does not conflict with the framework's terminal{"type":"aoi:summary","ok":true,…}— they coexist in the same stream without ambiguity. - Domain types stay unprefixed because they are already scoped by the schema declared in
aoi:meta.schema_name. The full coordinate for a domain event isschema_name#type(e.g.com.example.outline.events#hit). - The visual asymmetry is a feature, not an inconsistency. A consumer reading a stream knows at a glance whether a line is framework control‑plane (parser cares) or domain data‑plane (domain code cares).
grep aoi:extracts control events from a tee'd log. - No aliases. A framework type is always written with the
aoi:prefix. Aliases would re‑introduce the ambiguity the prefix exists to prevent.
The namespace invariant. The aoi: type namespace is closed and owned by this specification. Every unprefixed type name is owned by the tool's schema, permanently. AOI will never claim an unprefixed type name.
Nothing in the bare namespace is reserved. A domain schema MAY define an event named summary, meta, or error — that is precisely the point of the prefix, and forbidding those names would make the collision argument above circular while growing a reserved-word list with every framework type ever added.
This resolves what would otherwise be a contradiction in § 8.3: entry, hit, and match are defined by this specification and yet carry no prefix. They are recommended shapes, not framework events — common field layouts for common operations, which each tool's own schema owns. They are not reserved, and per the invariant above they will never be promoted into the aoi: namespace. Prefixing them would also destroy the property that makes the prefix practically useful: grep aoi: extracts the control plane, and data-plane events must stay out of it.
Prose convention. Throughout this document, framework event types are always written with their prefix (aoi:summary), including in field paths (aoi:summary.ok). Domain event types are written bare (hit). A bare framework name in a normative sentence about the framework is an error — except where this document deliberately quotes a domain spelling to contrast it, as in the {"type":"summary","text":"…"} example above.
Verbosity cost: 4 bytes per framework event. A typical AOI stream emits ~5 framework events per invocation, so total overhead is ~20 bytes per stream — negligible against the clarity benefit.
8.3 Standard event types#
aoi:meta#
First successful event for finite commands. Describes schema, command, version, and source context. MUST NOT echo raw argv or environment wholesale.
{
"type": "aoi:meta",
"aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
"tool": "outline",
"tool_version": "1.8.2",
"aoi_version": "1.0-alpha",
"schema_name": "com.example.outline.events",
"schema_version": "1.0.0",
"command": "search"
}
entry#
One resource in a list or inventory.
hit#
One search result. Should include source, stable ID/path/URL, rank/score where meaningful, and snippet/text.
match#
One exact match inside a source, such as a file line match. Recommended fields: path, line_number, column, text, submatches.
aoi:check#
One readiness or doctor check.
{
"type": "aoi:check",
"aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
"name": "config_file",
"ok": true,
"severity": "info",
"detail": "found"
}
aoi:plan#
One planned side effect, usually emitted during --dry-run or before apply.
aoi:progress#
Long‑running operation progress. Use sparingly. Progress events MUST NOT be required for correctness.
aoi:heartbeat#
Optional for unbounded streams. Lets consumers distinguish a quiet but alive stream from an idle transport when process supervision is not enough.
aoi:warning#
Recoverable issue. The operation continues.
aoi:error#
Structured failure. May be item‑level or process‑level.
aoi:summary#
Terminal event for finite commands. Required for process‑level success.
{
"type": "aoi:summary",
"aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
"ok": true,
"count": 42,
"input_count": 50,
"warning_count": 1,
"error_count": 0,
"partial": false,
"truncated": false,
"next_cursor": null,
"elapsed_ms": 384
}
Counter semantics, which MUST NOT be conflated:
| Field | Counts |
|---|---|
count | domain events this tool emitted. Never framework events. |
input_count | domain events this tool consumed from stdin. Never framework events. A transformer with count == input_count dropped nothing. |
warning_count | aoi:warning events this tool emitted plus those it consumed from upstream. |
error_count | aoi:error events this tool emitted plus those it consumed from upstream. |
partial | true when the run produced usable output and at least one item failed. |
event_count | Recommended; required to claim the Verifiable factor (§ 18). Total lines emitted in this stream, including this one. The reconciliation counter: a consumer asserts lines received == event_count and seq contiguous over 0…event_count-1. Without it, middle-of-stream elision is undetectable. |
reason | required when ok is false. Closed enum: failed, cancelled, timeout, denied. Also valid with ok:true: skipped, meaning the operation was not applicable and did not run — distinct from count:0, which means it ran and found nothing. |
caused_by | present when ok is false due to an upstream error (§ 13.1). |
upstream | the direct upstream's identity (§ 13.1). Omitted when unknown. |
9. Error taxonomy#
AOI needs portable retry and handling logic. Tools MAY define domain‑specific codes, but MUST map them to a standard category. A code without a category is unroutable by a consumer that has never seen the tool before, which is the entire premise of the taxonomy.
{
"type": "aoi:error",
"aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
"scope": "item",
"category": "rate_limited",
"code": "UPSTREAM_RATE_LIMIT",
"domain": "outline",
"message": "Upstream API rate limit exceeded.",
"retryable": true,
"retry_after_ms": 4500,
"target": "/documents/abc123",
"item_index": 17
}
9.1 Fields#
| Field | Status | Type | Semantics |
|---|---|---|---|
type | MUST | "aoi:error" | — |
category | MUST | closed enum (§ 9.2) | Semantic class. A consumer that meets an unrecognized value MUST NOT fail (§ 9.4). |
code | MUST | [A-Z][A-Z0-9_]* | Stable, tool‑defined identifier. MUST remain stable once published. |
message | MUST | string, single line | Human‑readable. No ANSI, no stack trace, no secrets. Not machine‑parsed. |
retryable | MUST | boolean | Authoritative retry disposition for this occurrence (§ 9.3). |
retry_after_ms | MUST when known and retryable:true; MUST NOT appear when retryable:false | integer ≥ 0 | Minimum delay before retry, in milliseconds, relative to receipt. |
domain | SHOULD | string | Namespace for code. Defaults to aoi:meta.tool. |
origin | MUST when forwarded | string | Names the tool that raised this error, set when a producer forwards an upstream error rather than raising it locally (§ 13.1). Absent means the error is local. |
scope | SHOULD | process | item (default process) | process means the run is over; item means the stream continues. |
target | SHOULD | string | The resource the error concerns — path, URI, or id. |
item_index / line_number | SHOULD when scope:"item" | integer | Correlation back to the input record. |
field | MAY | string | Input path that failed, for validation. |
wrapped | MAY | {protocol, status?, code?} | Raw status from a wrapped service, where protocol is http, grpc, or exit. Named wrapped, not upstream, because aoi:summary.upstream is a different shape — one key, two meanings, is the collision this spec exists to prevent. |
hint | MAY | string | Informational. Consumers MUST NOT execute it (§ 1.3). |
docs | MAY | URL | Documentation for this condition. |
details | MAY | object | Extension bag. Consumers MUST ignore unrecognized keys. |
9.2 Standard categories#
The category enum is closed and owned by this specification. Adding a value is a minor version bump.
| Category | Default retryable | Meaning |
|---|---|---|
usage | false | Invalid CLI usage or missing required operand. |
validation | false | Input parsed but failed schema/domain validation. |
config | false | Missing or invalid configuration. |
authn | false | Missing or expired credentials. Waiting does not refresh a token. |
authz | false | Credentials valid but action not allowed. |
not_found | false | Target resource does not exist. |
conflict | false | Concurrent modification, version/ETag mismatch, or destination exists. Retry requires re‑reading state first, so the identical invocation is not retryable. |
precondition_failed | false | System state must be explicitly fixed before this can succeed (e.g. rmdir on a non‑empty directory). |
unsupported | false | The invocation is well‑formed but the capability does not exist. The version‑skew signal between an agent and a tool. |
rate_limited | true | Time‑based throttling or quota. SHOULD carry retry_after_ms. |
unavailable | true | Transient dependency, network, or service failure. |
timeout | true | Operation exceeded a deadline. |
cancelled | false | Interrupted by caller or system. Retry is the caller's decision. |
io | false | Filesystem or stdio failure. Includes BROKEN_PIPE. Transient I/O (EAGAIN, EINTR) MAY override to true. |
internal | false | Tool bug or unexpected exception. A bug does not fix itself. |
rate_limited is deliberately distinct from unavailable: a consumer's base backoff for throttling should be roughly an order of magnitude longer than for a transient failure, so collapsing them loses actionable information.
There is no partial error category. Partiality is a property of the run, not of a failure. Item‑level failures MUST use the specific category of that failure with scope:"item" and an index; aggregate partiality is expressed only by aoi:summary.partial and aoi:summary.error_count.
9.3 retryable is authoritative#
retryable is the retry disposition for this occurrence. Consumers MUST honor it over the default listed in § 9.2. The Default column applies only when retryable is absent, which producers MUST NOT allow.
category:"rate_limited" with retryable:false is not a contradiction — it is the most important case in the taxonomy. It means throttling was hit and retrying will not help: a hard quota, a depleted retry budget, or an explicit upstream instruction not to retry.
A producer MUST set retryable:false when it cannot determine whether a side effect was applied. Retry safety is a joint property of the error and the operation's idempotency. A tool invoked with --idempotency-key MAY report retryable:true for a mutating operation that would otherwise be false, and MUST document that it does so.
Consumers MUST NOT automatically retry a failed automatic retry without applying their own attempt limit, and SHOULD treat retry_after_ms as a minimum with jitter applied, never as a schedule.
Branch on retryable for disposition; branch on category for routing.
9.4 Forward compatibility#
A consumer that encounters an unrecognized category MUST NOT fail. It MUST use retryable for disposition and SHOULD route the error as internal. This lets the enum grow without breaking deployed consumers.
Fine‑grained identification is the open, namespaced domain + code pair — there is deliberately no central registry of AOI error codes. A closed semantic enum plus a tool‑owned code namespace is what gRPC, Kubernetes, and RFC 9457 all converge on in practice.
9.5 Mapping from wrapped protocols#
A tool wrapping an HTTP or gRPC service SHOULD use this mapping so that agents get identical retry behavior across wrappers.
| gRPC | HTTP | AOI category | retryable |
|---|---|---|---|
INVALID_ARGUMENT | 400, 422 | validation | false |
UNAUTHENTICATED | 401 | authn | false |
PERMISSION_DENIED | 403 | authz | false |
NOT_FOUND | 404, 410 | not_found | false |
UNIMPLEMENTED | 405, 501 | unsupported | false |
DEADLINE_EXCEEDED | 408, 504 | timeout | true |
ABORTED, ALREADY_EXISTS | 409 | conflict | false |
FAILED_PRECONDITION | 412, 428 | precondition_failed | false |
RESOURCE_EXHAUSTED | 429 | rate_limited | true |
UNAVAILABLE | 502, 503 | unavailable | true |
INTERNAL, UNKNOWN, DATA_LOSS | 500 | internal | false |
CANCELLED | 499 | cancelled | false |
Unlisted statuses: unknown 4xx → validation/false, unknown 5xx → unavailable/true. This is a mechanical heuristic, not a semantic claim — RFC 9110 assigns no retry semantics to the 4xx/5xx classes, and 408 is a retryable 4xx in the same document. An implementer with real knowledge of the upstream MUST override it.
Retry timing maps as: Retry-After: 120 → 120000; Retry-After: <HTTP-date> → max(0, date − now) in ms; grpc-retry-pushback-ms: 4500 → 4500; a negative pushback → omit and set retryable:false.
A UNAVAILABLE on a non‑idempotent operation invoked without --idempotency-key MUST be downgraded to retryable:false.
10. Schema rules#
10.1 Discovery#
The schema command SHOULD emit JSON Schema to stdout:
outline schema --output json
It MAY support:
outline schema --output json --schema-version 1
outline schema --output json --name com.example.outline.events
10.2 $id#
Schema $id MUST NOT be a machine‑local file URI unless the schema is explicitly local‑only.
Preferred:
{
"$id": "https://schemas.example.com/outline/events/1.0.0/schema.json"
}
10.3 Strictness#
Recommended policy:
- Event/output schemas SHOULD be permissive for forward‑compatible extension: allow unknown fields unless a specific event class has a reason to forbid them.
- Input schemas SHOULD be strict: reject unknown fields to catch agent typos and hallucinated parameters.
10.4 Event discriminators#
Prefer discriminator‑style schemas using type with if/then or $defs keyed by event type. Avoid examples that produce opaque oneOf matched zero schemas errors.
11. Confidentiality and argument redaction#
aoi:meta.args is optional and dangerous. A conforming framework SHOULD NOT echo raw argv by default.
If arguments are included, the tool MUST redact sensitive values.
Sensitive patterns include at least:
- flags containing
token,secret,password,passwd,key,credential,cookie,authorization,auth,bearer,private,client-secret, - URLs with credentials, tokens, signatures, SAS parameters, or query keys such as
sig,token,access_token,code,key, - custom headers,
- environment variable values,
- config file contents.
Preferred aoi:meta pattern:
{
"type": "aoi:meta",
"aoi": { "seq": 0, "run": "01JZQ7X4TQ9" },
"command": "search",
"input_fingerprint": "sha256:...optional...",
"args_redacted": true
}
The caller already knows what it passed. Do not leak secrets for convenience.
12. Input contract#
Output typing is not enough. Tools acting as transformers or sinks (those that consume JSONL on stdin) need equal rigor on the input side.
12.1 Input modes#
A tool MAY accept:
- ordinary CLI operands and options,
- stdin text via
-, - JSON input via
--input-json -, - JSONL input via
--input-jsonl -, - file paths.
If accepting structured stdin, the tool SHOULD provide an input schema:
outline input-schema --output json --command import
12.2 Malformed JSONL input#
For JSONL input streams, tools MUST document whether failures are:
- whole‑input fail‑fast — any malformed line aborts the whole command,
- per‑line tolerant — malformed lines emit item‑level errors and processing continues,
- configurable —
--fail-fast/--continue-on-error.
Recommended default:
- Mutating imports — fail fast before side effects where possible.
- Read‑only transforms — item‑level errors may continue if
--continue-on-erroris set.
13. Pipeline composition#
An AOI pipeline composes sources → transformers → sinks. A source produces events. A transformer reads upstream events and produces its own. A sink reads events and produces side effects (with a corresponding audit trail in its own output). Any role may be a conforming AOI‑CLI tool; standard shell pipes are the canonical transport.
A transformer or sink that accepts AOI JSONL input SHOULD:
- accept and parse upstream
aoi:meta,aoi:warning,aoi:error, andaoi:summaryevents, - ignore unknown unprefixed event types, which MUST always be safe to ignore,
- treat an unknown
aoi:event type asymmetrically: it MAY be ignored, but a consumer MUST NOT report success on a stream containing an unrecognizedaoi:event it did not interpret, because framework events can carry failure semantics, - decide whether upstream errors are data or control signals.
Recommended default:
- Upstream
entry,hit,match, and domain events are data. - Upstream
aoi:errorevents cause downstreamaoi:summary.ok=falseunless--continue-on-erroris set. - Upstream missing terminal
aoi:summarycauses downstream failure if EOF occurs.
13.1 Framework events across a pipe#
Scope. This section binds an AOI producer — a tool that emits framework events of its own. A transparent relay — a tool that emits no framework events and passes the stream through unchanged (tee, grep, head, jq -c ., stdbuf, pv) — is out of scope and MAY forward everything byte for byte. Relays are not transformers in the § 13 sense even though they read and write JSONL; the distinction is whether the tool speaks the framework itself.
An AOI producer MUST NOT forward upstream aoi:meta or aoi:summary to its own stdout. These two are cardinality‑critical: a consumer must be able to read aoi:meta as "the description of the stream I am holding" and a terminal aoi:summary as "the outcome of the process that produced it." A stream therefore carries at most one aoi:meta, which when present is its first event, and at most one aoi:summary, which when present is its last.
An AOI producer MAY forward upstream aoi:error and aoi:warning, and when it does it MUST tag them with an origin naming the emitting tool, so a consumer can tell a local failure from an inherited one:
{"type":"aoi:error","aoi":{"seq":7,"run":"01JZQ7X4TQ9"},"origin":"outline","category":"rate_limited","code":"UPSTREAM_RATE_LIMIT","message":"…","retryable":true,"retry_after_ms":7200}
Whether or not it forwards them, it MUST account for upstream outcome in its own terminal event:
- Upstream
aoi:errorsetsaoi:summary.ok=falseunless--continue-on-erroris set. - Upstream EOF without a terminal
aoi:summarysetsaoi:summary.ok=falseand SHOULD emit anaoi:errorwithcategory:"io"first. - Upstream
aoi:warningevents MUST be counted intoaoi:summary.warning_count.
Retry detail MUST survive the pipe#
Collapsing an upstream failure to ok:false destroys the retry semantics of § 9. A rate_limited error carrying retry_after_ms:7200 is actionable; {"ok":false,"error_count":1} is not. When aoi:summary.ok is false because of an upstream error, the summary MUST carry caused_by reproducing the retry‑critical fields of the most severe upstream error:
{
"type": "aoi:summary",
"aoi": { "seq": 14, "run": "01JZQ7X4TQ9" },
"ok": false,
"reason": "failed",
"event_count": 15,
"count": 12,
"warning_count": 0,
"error_count": 1,
"partial": true,
"caused_by": {
"origin": "outline",
"category": "rate_limited",
"code": "UPSTREAM_RATE_LIMIT",
"retryable": true,
"retry_after_ms": 7200
},
"upstream": {
"tool": "outline",
"tool_version": "1.8.2",
"schema_name": "com.example.outline.events",
"schema_version": "1.0.0"
}
}
Provenance lives on the summary, not the meta#
upstream records the direct upstream only — one object, not an array. It is not transitive: a three‑stage pipeline does not accumulate a chain. Each stage names the stage that fed it, and a caller reconstructs the chain by reading the stages it launched.
It is carried on aoi:summary rather than aoi:meta for a structural reason: aoi:meta must be the first event, but upstream identity is not known until the upstream's own aoi:meta has been read — which is after the first byte of stdin. Requiring it on aoi:meta would force a transformer to block its own first event on a possibly‑hung upstream, making "starting" indistinguishable from "wedged." By the time the summary is emitted, upstream identity is known or definitively absent.
If upstream identity is unavailable (an empty stream, or a non‑conforming producer that emitted no aoi:meta), the producer MUST omit the upstream key rather than emit nulls.
Fan‑in is not defined in v1.0‑alpha. § 12.1 specifies a single stdin, so a conforming pipeline is linear. Merging two AOI streams into one tool — and how two upstream outcomes combine into one ok — is deferred; see Direction.
13.2 Pipefail#
Shell pipelines should use process exit status and terminal aoi:summary checks together:
set -o pipefail
producer --output jsonl ... \
| consumer --input-jsonl - --output jsonl ... \
| tee output.jsonl \
| jq -e 'select(.type=="aoi:summary") | .ok == true'
A library wrapper should validate:
- every stdout line is valid JSON,
- EOF includes terminal
aoi:summaryfor finite commands, - process exit is acceptable,
aoi:summary.okis true unless partial success was explicitly accepted.
Part II — Recommended profile#
Nothing in Part II is required for conformance. These are properties that pay off for a single tool immediately, with no coordination: safe defaults, dry-run plans, confirmation guards, idempotency keys, bounded reads, a predictable flag vocabulary.
They are unilateral quality properties, not interoperability invariants. --dry-run is a good idea whether or not any other tool has it, and a read-only tool that omits --confirm is fully conforming because it has nothing to confirm. Conformance is asserted per (factor × command) — see § 18.1 — so a tool adopts from Part II what its surface actually warrants.
The distinction matters because the two halves have opposite adoption dynamics. Part I is worthless until it is universal. Part II is valuable to the first tool that adopts it. Conflating them is what makes specifications feel too large to start.
14. Safety and side effects#
14.1 Read‑only by default#
Commands SHOULD default to read‑only behavior. Mutating operations must be obvious in the command or options.
14.2 Dry‑run#
Non‑trivial mutating operations SHOULD support --dry-run and emit aoi:plan events.
14.3 Confirmation#
Destructive operations MUST require explicit confirmation such as --confirm.
Bulk destructive operations SHOULD require --confirm-count N or an equivalent guard to prevent accidental broad deletes.
14.4 Idempotency#
Retryable writes SHOULD support:
--idempotency-key KEY
or a documented natural key.
14.5 Auditability#
Mutating operations SHOULD emit stable IDs/paths/URLs for resources created, updated, or deleted.
15. Bounds, pagination, and truncation#
List, search, and read operations SHOULD be bounded by default. Recommended options:
--limit N
--cursor TOKEN
--timeout SECONDS
--max-bytes N
--max-lines N
If output is truncated, aoi:summary.truncated MUST be true and aoi:summary.next_cursor SHOULD be set when more data can be fetched.
{"type":"aoi:summary","aoi":{"seq":101,"run":"01JZQ7X4TQ9"},"ok":true,"event_count":102,"count":100,"truncated":true,"next_cursor":"opaque-token"}
16. Standard CLI surface#
16.1 Global options#
| Option | Status | Meaning |
|---|---|---|
--help, -h | MUST | Human‑readable help. |
--version | SHOULD | Tool version. |
--output jsonl / --format jsonl | MUST | Enter AOI JSONL mode unless the tool is AOI‑only. |
--schema-version VERSION | SHOULD | Select supported schema version where applicable. |
--timeout SECONDS | SHOULD | Bound external calls. |
--limit N | SHOULD | Bound result count. |
--cursor TOKEN | SHOULD | Continue from prior cursor. |
--dry-run | SHOULD | Plan without side effects. |
--idempotency-key KEY | SHOULD | Safe retry key. |
--confirm | MUST¹ | Required for destructive operations. |
--confirm-count N | SHOULD | Guard expected affected count. |
--no-color | SHOULD | Disable color. Machine mode never uses color. |
--debug | MAY | Extra diagnostics, redacted, not corrupting JSONL stdout. |
¹ --confirm is MUST for any tool that exposes a destructive operation, and not applicable to a tool that exposes none. A read-only tool is fully conforming without it. Per § 18.1, conformance is asserted per (factor × command); a requirement that cannot apply to a command is not a requirement that command fails.
16.2 Discovery subcommands#
| Subcommand | Meaning |
|---|---|
schema | Emit event schema JSON. Credential‑free and network‑free. |
input-schema | Emit input schema JSON for a command or input mode. |
capabilities | Emit supported commands, schema versions, event types, input modes, auth requirements. |
doctor | Check credentials, dependencies, connectivity. Emits AOI events in machine mode. |
16.3 Resource subcommands#
Use boring verbs:
| Subcommand | Meaning |
|---|---|
list | Bounded collection. |
get | One resource by stable key. |
search | Ranked or filtered results. |
create | Create resource. |
update | Update resource. |
delete | Delete resource. Confirmation required. |
move | Move or rename resource. |
copy | Copy resource. |
send | Send message, email, notification. |
sync | Reconcile state. Must state direction and planned changes. |
watch | Unbounded stream. Must document completion semantics. |
status | Current state. |
16.4 Flag names are conventions, not prefixed reservations#
The flag and subcommand names this section lists — --output, --format, --limit, --cursor, --dry-run, --confirm, --idempotency-key, schema, capabilities, doctor, and so on — are conventions intended to make a conforming tool feel the same to an agent across every implementation. They are deliberately not namespaced (e.g. --aoi-output, aoi-schema).
This is a different design call from the one made for event types (§ 8.2). Event types appear inline in the output stream where parser ambiguity is real; the aoi: prefix prevents collision with domain payloads. Flag names appear in the input the operator or agent writes; collisions there are configuration questions, not parse questions, and the per‑invocation byte cost of a prefix would land on every call.
Where a tool already uses a flag name AOI also defines (for example, an existing --output FILE for an output destination), the tool MAY:
- Use the listed equivalent — e.g.
--format jsonlinstead of--output jsonl(already a recommended alternate in § 16.1). - Document a tool‑specific alias and advertise both in
capabilities. - Resolve the conflict locally however it sees fit; AOI does not mandate a global rename.
The standard's leverage comes from consistent flag names where possible, not from prefixed reservations. Tools whose existing surface conflicts with AOI's recommended names should use the listed alternates rather than ship --aoi-output, which would defeat the convention‑is‑the‑value‑proposition that motivates AOI's flag vocabulary in the first place.
The same logic applies to subcommand names. A tool whose domain already includes a schema subcommand (a database CLI managing literal database schemas) may rename its conformance discovery to aoi-schema and document the alias; AOI does not prescribe a global subcommand prefix.
17. Capability manifest#
capabilities --output json SHOULD return a single JSON object, not JSONL, because it is discovery metadata.
{
"tool": "outline",
"tool_version": "1.8.2",
"aoi_versions": ["1.0-alpha"],
"outputs": ["jsonl"],
"schemas": [
{
"name": "com.example.outline.events",
"versions": ["1.0.0", "2.0.0"],
"default": "2.0.0"
}
],
"commands": [
{
"name": "search",
"read_only": true,
"bounded": true,
"supports_cursor": true,
"event_types": ["aoi:meta", "hit", "aoi:warning", "aoi:error", "aoi:summary"]
},
{
"name": "delete",
"read_only": false,
"destructive": true,
"requires_confirm": true,
"supports_dry_run": true,
"supports_idempotency_key": true
}
]
}
Part III — Conformance and reference#
Part III is neither normative requirement nor optional profile: it defines how conformance to Part I is asserted and checked, and collects examples, implementation notes, and open questions. The Part II statement that nothing is required for conformance does not extend here.
18. Conformance — aoi-lint#
18.1 Per‑factor, not per‑level#
Conformance in AOI is asserted per factor and per command, not as an aggregate level. A tool is Typed for search, or Safe for delete, or not yet Auditable. There is no "AOI Level 2"; there is the set of (factor × command) pairs the tool passes.
This is intentional. A read‑only tool that lacks Idempotent is still a legitimate, useful, conforming tool — there is nothing to be idempotent about. Aggregating factors into levels would either hide that nuance or force every tool to implement requirements it doesn't need. The granularity is the truth; aoi-lint reports it.
18.2 Lint checks#
aoi-lint runs the following minimum checks against a command invocation. Each one maps to one or more factors:
| # | Check | Factor(s) |
|---|---|---|
| 1 | schema --output json succeeds without credentials or network. Schema is valid JSON Schema. capabilities --output json succeeds if advertised. | Discoverable |
| 2 | Machine mode emits only valid JSONL objects to stdout. Every event has type. No ANSI/control/prose pollution. | Typed |
| 3 | Finite success exits 0 with terminal aoi:summary.ok=true. EOF without aoi:summary is detected as failure. | Verifiable |
| 4 | aoi:meta, aoi:summary, aoi:warning, aoi:error, and aoi:check events validate against the declared schema when emitted. | Typed · Verifiable |
| 5 | Malformed usage exits non‑zero. Every aoi:error carries category, code, message, and retryable. retry_after_ms is absent whenever retryable is false. No error uses the removed partial category. | Verifiable |
| 6 | Destructive commands refuse to run without confirmation. Dry‑run emits aoi:plan events and performs no side effects when advertised. | Safe |
| 7 | Mutations accept an --idempotency-key and replay returns the prior result without doubling the effect. | Idempotent |
| 8 | Mutating events carry stable IDs for created/updated/deleted resources. | Auditable |
| 9 | Known secret‑looking args are not echoed raw in output. | Safe |
| 10 | Early pipe close does not produce stack traces. SIGINT/SIGTERM produce a reason:"cancelled" summary when safe. | Composable |
| 11 | Commands advertising --limit report truncated and cursor behavior consistently. | Bounded · Streamable |
| 12 | Malformed input JSONL line handling matches declared mode. | Composable |
| 13 | schema_version is reported in aoi:meta and is honored when negotiated. | Versioned |
| 14 | A stream carries at most one aoi:meta (first if present) and at most one aoi:summary (last if present). | Composable · Verifiable |
| 15 | When aoi.seq is emitted it is contiguous from 0, aoi.run accompanies it, and both reconcile with aoi:summary.event_count; when absent, the Verifiable factor is not claimed. | Verifiable |
| 16 | An AOI producer fed a known upstream stream forwards no upstream aoi:meta or aoi:summary, tags any forwarded aoi:error/aoi:warning with origin, and reproduces upstream retry fields in aoi:summary.caused_by when failing. | Composable |
A tool is conforming for the (factor × command) pairs it passes. It claims conformance via the Machine Mode Ready badge and the entries in its capabilities manifest. Verification is aoi-lint's job — the badge is the claim, not the proof.
19. Examples#
19.1 Finite search#
outline search "agent operable" --output jsonl --limit 2
{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","tool_version":"1.8.2","aoi_version":"1.0-alpha","schema_name":"com.example.outline.events","schema_version":"1.0.0","command":"search"}
{"type":"hit","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"rank":1,"id":"doc_123","title":"Agent-Operable Tools","snippet":"A conforming tool exposes a stable interface..."}
{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"ok":true,"count":1,"warning_count":0,"error_count":0,"partial":false,"truncated":false}
Consumer rule: if EOF arrives before the aoi:summary, the stream failed.
19.2 Schema discovery#
outline schema --output json --schema-version 1
Returns JSON Schema to stdout. No credentials. No network. No business query.
19.3 Doctor#
outline doctor --output jsonl
{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","aoi_version":"1.0-alpha","schema_name":"com.example.outline.events","schema_version":"1.0.0","command":"doctor"}
{"type":"aoi:check","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"name":"config_file","ok":true,"severity":"info","detail":"found"}
{"type":"aoi:check","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"name":"api_token","ok":false,"severity":"error","detail":"missing"}
{"type":"aoi:summary","aoi":{"seq":3,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"failed","event_count":4,"count":0,"error_count":0,"properties":{"checks_failed":1}}
19.4 Input JSONL error#
outline import --input-jsonl - --output jsonl --continue-on-error
{"type":"aoi:meta","aoi":{"seq":0,"run":"01JZQ7X4TQ9"},"tool":"outline","command":"import","input_mode":"jsonl","continue_on_error":true}
{"type":"aoi:error","aoi":{"seq":1,"run":"01JZQ7X4TQ9"},"scope":"item","category":"validation","code":"INPUT_JSONL_PARSE_ERROR","line_number":17,"message":"Invalid JSON on input line 17","retryable":false}
{"type":"aoi:summary","aoi":{"seq":2,"run":"01JZQ7X4TQ9"},"ok":false,"reason":"failed","event_count":32,"count":29,"error_count":1,"partial":true}
20. Implementation guidance (non‑normative)#
Python#
- Compact JSON per line.
- Handle
BrokenPipeError/SIGPIPE cleanly. - Do not dump raw
argparse.Namespaceintoaoi:meta. - Usage errors can exit before
aoi:meta.
Node / Bun#
- Handle EPIPE on stdout and stderr.
- Avoid console logging in machine mode except through the event emitter.
- Ensure async streams flush before exit when emitting terminal
aoi:summary.
Go#
- Treat broken pipe as normal termination.
- Keep event structs versioned.
- Avoid logging to stdout in machine mode.
Rust#
- Handle
BrokenPipewithout panic output. - Serialize event enums with
typediscriminator. - Keep stderr diagnostics redacted.
Shell wrappers#
- Prefer wrapping upstream JSON APIs over parsing human tables.
- Use
jq -cfor emitted objects. - Keep stderr separate.
- Be careful with
set -o pipefailand partial output.
21. Anti‑patterns#
--jsonsometimes emits prose, warnings, or pretty tables.- Machine output is a giant JSON array, blocking all results until completion.
- Schema discovery requires login, network, or a real query.
- Schema
$idisfile:///opt/data/bin/...in a public spec. - Raw argv or env is echoed into
aoi:meta. - Consumers treat EOF without
aoi:summaryas success. - Exit code alone is used as application semantics.
- Every tool invents error codes with no category mapping.
- An error carries
categoryandretryablethat contradict each other with no stated precedence. - Partial-run status is expressed as an error rather than on the terminal summary.
- Destructive bulk operations run without preview or count confirmation.
- Language‑specific stack traces appear on pipe close.
- Wrapper names become the only versioning mechanism.
22. Adoption path#
Tighten this into a public specification.Done — this document, at v1.0-alpha, served at/spec,/spec.md, and/llms-full.txt.- Build
aoi-lintaround testable conformance checks. - Provide reference implementations in Python, Node, Go, and Rust.
- Local
*-jsonlwrappers remain a useful convenience profile, not the universal rule. - Publish a small example repo with schema discovery, finite search, doctor checks, input JSONL import, cancellation/pipe tests, and redaction tests.
23. Open questions#
- Should
--output jsonlor--format jsonlbe the preferred flag name? - Should
aoi:summarybe mandatory for every finite non‑zero failure onceaoi:metahas emitted, or only best effort? - How strict should the default
capabilitiesschema be? - Should
schemaemit all event schemas by default or require a schema name? - Should the protocol define a standard
watchheartbeat interval hint?
Open questions concerning forthcoming modes, profiles, and adjacent‑standard relationships are tracked separately in Direction § 06.
Cite as#
Hunt, K. (2026). AOI‑CLI: Agent-Operable Interface,
command-line profile. Machine Mode v1.0‑alpha (draft).
https://machinemode.io/spec