Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI

on:
push:
branches: [master]
pull_request:

env:
CARGO_TERM_COLOR: always
# Fail the build on a warning rather than letting lint debt accumulate.
RUSTFLAGS: -D warnings

jobs:
check:
name: fmt, clippy, test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install the toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- uses: Swatinem/rust-cache@v2

- name: Formatting
run: cargo fmt --all --check

# --all-targets covers tests and the live suite's compilation; the live
# tests themselves are #[ignore]d and never run here. They need three
# authenticated CLIs and spend real quota, neither of which belongs in CI.
- name: Clippy
run: cargo clippy --all-targets -- -D warnings

- name: Tests
run: cargo test --all-targets

# `cargo test --all-targets` skips doctests, and the crate's usage
# examples live in them.
- name: Doctests
run: cargo test --doc

- name: Documentation builds without broken links
run: cargo doc --no-deps
env:
RUSTDOCFLAGS: -D warnings
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
127 changes: 127 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Working agreement: RustAgentAbstraction

The operating contract for **any** coding agent working in this repository. This file is
the single source of truth for the rules: Codex, Cursor and Gemini CLI read `AGENTS.md`
natively, and Claude Code loads it through the `@AGENTS.md` import in
[`CLAUDE.md`](CLAUDE.md). **Never fork these rules into a per-vendor file.**

A single Rust library crate, `agent-abstraction`: drives the Claude Code, Codex and GitHub
Copilot CLIs headlessly behind one API. Consumed as a direct dependency by the
`pathscale/agencyzero` Tauri app. See [README.md](README.md) for the API and the layout.

## Invariants (don't break these)

- **Every flag mapping must be verified against the real CLI, and the version recorded in a
comment next to it.** These CLIs change flags between releases: Copilot 1.0.75 gained a
headless session id and a JSONL event stream that oneharness (our upstream) still models as
absent. A mapping copied from documentation, from upstream, or from memory is a guess.
Check `--help`, or run the thing.
- **Never silently downgrade a capability.** If an agent cannot fork, cannot stream, or
cannot take a session id, that is an `Error::Unsupported`. A caller who asked to fork and
got a linear resume has just corrupted the conversation they meant to branch, and a loud
error is always better than a quiet wrong answer.
- **Never invent usage numbers.** `Usage` fields are `Option` because the three agents
report different subsets. Absent means "the agent did not say", never zero, and never a
figure derived from a local price table.
- **This crate is a library. It has no binary and no CLI.** If something seems to need a
command-line entry point, it belongs in the consumer, not here.
- **No shell.** Arguments are built as a `Vec<String>` and handed to `exec`. Never
interpolate a prompt into a shell string; that is how a prompt containing `$(...)` becomes
a command.
- **Cancellation is complete on Unix and incomplete on Windows.** Dropping, cancelling or
timing out a run tears down the whole process group, so the commands an agent started die
with it. On Windows only the direct child is killed: containing a tree there needs a Job
Object, which this crate does not set up. Do not describe cancellation as cross-platform,
and keep `tests/process.rs` honest by leaving it `#![cfg(unix)]` rather than making it
pass vacuously elsewhere.
- **`src/proc.rs` holds the crate's only `unsafe`.** `Cargo.toml` sets
`unsafe_code = "deny"` rather than `forbid` so that one audited call can be excepted. A
second `unsafe` anywhere is a design question, not a local decision.

## Build & run

```bash
cargo build
cargo test
cargo fmt && cargo clippy --all-targets # run after every change
```

### Live tests

```bash
cargo test --test live -- --ignored --test-threads 1
```

Spawns the real agents and consumes real quota, so it is `#[ignore]`d by default. Each test
skips itself when its binary is absent. **Run it after touching any argv mapping or output
parser**. The unit tests prove the code does what it says, only the live suite proves the
CLI agrees.

## Architecture

Pure logic and I/O are kept apart so the mappings are testable without spawning anything.

| File | Role |
|---|---|
| `src/agent.rs` | The three agents, their capabilities, and argv building. **Pure.** |
| `src/request.rs` | The fluent request builder and its resolution into a `Plan`. **Pure.** |
| `src/event.rs` | Normalizing three JSON dialects into one event vocabulary. **Pure.** |
| `src/session.rs` | Name → native-id bindings on disk. |
| `src/run.rs` | Spawning, streaming, timeouts, failure classification. |
| `src/proc.rs` | Process-group teardown. The crate's only `unsafe`. |
| `src/outcome.rs` | What a finished run produced. |
| `src/error.rs` | One error type; one variant per case a caller must branch on. |

Anything pure gets ordinary unit tests in the same file. Keep it that way: a mapping that
needs a subprocess to test is a mapping in the wrong module.

## Verification

Run what you build before reporting it done. Type-checks and tests verify code correctness,
not feature correctness. **If you can't run it, say so explicitly** rather than implying
success. If an agent CLI isn't installed and you mapped its flags from a document, say that
plainly and mark it in the code.

- Compare against the base branch rather than asserting: a pre-existing failing test or
clippy warning is not something you introduced, and saying so requires checking.
- `cargo build` finishing in under a second means it was cached, not that it rebuilt. Touch
the sources when a rebuild is the thing you're verifying.

## PR discipline

**Always paste the full PR URL**
(`https://github.com/pathscale/RustAgentAbstraction/pull/<n>`), not just the number, so it's
clickable.

## Keeping docs honest

Hit a factual error here, such as a stale flag, a wrong version or a moved status? Fix
it in the same change. Don't open cosmetic rewording PRs.

Learned something durable, such as a CLI gotcha, a flag that changed, or a shape that
differs from the docs? It belongs **in this repo** (a comment next to the mapping, or
the README's gotchas
section), not in your agent's private memory. Repo docs are versioned, reviewable, and
visible to every agent and human; private memory dies with your machine.

## Git workflow

- **Always specify the branch when pushing**: `git push origin branch-name`
- **Branch naming**: `fix/issue-description` or `feat/issue-description`
- **Default branch is `master`**, not `main`.
- **Force-push your own branch freely.** Rebasing a feature branch onto a moved base, or
amending before review, is normal and correct. Use `--force-with-lease` so you don't
clobber someone else's push.
- **Never force-push the default branch.** That is the history everyone else builds on.

## No AI attribution

Never add AI attribution to anything in this repo or leaving it: no "Generated with
Claude Code" / robot-emoji footers, no `Co-Authored-By: Claude` (or any AI) trailers,
and no AI credit in commit messages, PR or issue titles/bodies, changelogs, release
notes, or code comments. Applies to every agent and every vendor. Work product should
be indistinguishable from a human teammate's.

## Writing style

No em dashes in prose or documentation. Restructure the sentence instead.
15 changes: 15 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@AGENTS.md

# Claude Code notes: RustAgentAbstraction

The import above is binding: [`AGENTS.md`](AGENTS.md) is the **working agreement** for this
repository, and every Claude Code session loads it automatically. Don't copy rules here,
since one source of truth means no drift. Only genuinely Claude-specific wiring belongs
below.

- This crate drives `claude` itself. When you change the Claude adapter in
[`src/agent.rs`](src/agent.rs), you are changing how a program invokes the same CLI you
are running inside. Verify against `claude --help` for the installed version rather than
against your own knowledge of the flags, which may be from a different release.
- The live test suite spawns real agents and spends real quota. Don't run it on a loop, and
don't add it to a watch task.
57 changes: 57 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[package]
name = "agent-abstraction"
version = "0.1.0"
edition = "2024"
# The floor edition 2024 requires, and where the strictest dependencies (uuid,
# getrandom) sit. Derived from the dependency graph rather than compile-tested.
rust-version = "1.85"
description = "Drive Claude Code, Codex and Copilot CLIs headlessly from Rust, as a library."
license = "MIT"
repository = "https://github.com/pathscale/RustAgentAbstraction"
readme = "README.md"
keywords = ["agent", "claude", "codex", "copilot", "cli"]
categories = ["development-tools"]

# Library only, by design. There is no binary: the consumer (an agencyzero GUI,
# a service, a test) links this crate and spawns the agent directly, so nothing
# marshals a request through a CLI and back out of stdout twice.
[lib]
name = "agent_abstraction"
path = "src/lib.rs"

[dependencies]
# `process` gives an async child with piped stdio; `io-util` the line reader that
# turns a JSONL stream into events; `time` the run timeout; `sync` the event
# channel. No `full`: the consumer picks its own runtime features.
tokio = { version = "1", features = ["process", "io-util", "sync", "time", "rt", "macros"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
# Session ids are caller-minted UUIDv4 (`claude --session-id`, and Copilot's
# handle, which the CLI never prints). v4 only, since these are opaque handles and not
# sort keys, so the v7 timestamp would leak wall-clock into a stable id.
uuid = { version = "1", features = ["v4", "serde"] }
# Resolves the agent binary on PATH so "not installed" is a typed error with an
# install hint, rather than a bare ENOENT from the spawn.

# Killing a process *group* on timeout or cancellation. Without it only the CLI
# dies and the commands it spawned keep running.
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
# The live tests mint UUIDs to prove a caller-assigned session id round-trips.
uuid = { version = "1", features = ["v4"] }

[lints.rust]
missing_docs = "warn"
# `deny` rather than `forbid`: one audited exception exists, `libc::kill` for
# process-group teardown, which has no safe wrapper.
unsafe_code = "deny"

[lints.clippy]
pedantic = { level = "warn", priority = -1 }
# The crate's own error type is deliberately wide (one variant per failure a
# caller must branch on); boxing it would push that branch into a downcast.
result_large_err = "allow"
26 changes: 26 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
MIT License

Copyright (c) 2026 PathScale
Copyright (c) 2026 Nick DeRobertis

This project is a Rust port of nickderobertis/oneharness, which is MIT
licensed. Portions of the harness flag mappings and the session-store design
are derived from that work; the copyright notice above is retained accordingly.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading