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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ jobs:
env:
RUSTDOCFLAGS: -D warnings

# The published artifact is not what the other steps build: `exclude` can
# drop a file the crate needs, and nothing above would notice. Checked on
# every run rather than only on release PRs, because the mistake is made
# in the commit that edits `exclude`, not the one that bumps the version.
- name: The package builds from what would be published
run: cargo package

# The README links to it and a consumer reading it from a vendored crate
# is exactly who it is for, so it must not be excluded by accident.
- name: The changelog is in the package
run: cargo package --list | grep -Fx CHANGELOG.md

audit:
name: cargo audit
runs-on: ubicloud-standard-2
Expand Down
68 changes: 68 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Changelog

Notable changes per release. Versions follow [semantic versioning](https://semver.org),
with the 0.x caveat that a minor bump is the only signal Cargo treats as incompatible, so
behaviour changes go there rather than into a patch.

## 0.2.0

Nothing was removed or renamed, so this compiles against any 0.1 code. It is a minor bump
rather than a patch because two changes alter behaviour, and `"0.1"` would have delivered
them through an ordinary `cargo update`.

### Behaviour changes

- **An unauthenticated run is now an error.** Claude exits `0` and puts
"Not logged in · Please run /login" in its result text, so a missing login previously came
back as a successful `Outcome` whose answer was a login prompt. Auth is now checked
regardless of exit code, as quota refusals already were, and reported as
`Error::NotAuthenticated` carrying the command that fixes it. Callers matching on `Ok`
should expect this variant. `Error::is_auth_failure` distinguishes it.
- **Event payloads are bounded at `MAX_EVENT_BYTES`** (64 KiB), marked with
`TRUNCATION_MARK` when shortened. Oversized tool arguments are replaced rather than cut,
since truncated JSON no longer parses.
- **Identifiers are validated rather than truncated.** A session id, tool-call id or tool
name over `MAX_IDENTIFIER_BYTES` (4 KiB) is rejected: an oversized session id is not
captured or persisted, and an oversized tool id is dropped while its event is still
reported. Shortening one would be worse than losing it, since a truncated session id
resumes nothing and a truncated tool id matches the wrong call.
- **The pending-tool map is bounded by bytes**, not only by entry count. Counting entries
bounded nothing while the entries themselves were unbounded.

Together these give a real ceiling on queued memory. Each event is at most 64 KiB of
payload plus a few identifiers of 4 KiB, so a full 256-deep channel holds under about
21 MiB, with a further 256 KiB of pending-tool state. Before this release the figure was
unbounded in practice: identifiers were exempt from every limit, so a single line could
carry a 512 KiB id and 256 queued events could reach roughly 128 MiB.

### Fixed

- **Dropping a `Run` now reliably kills the process group.** It signalled the driver and
aborted it, which left the kill waiting on the runtime to poll the aborted task. On Linux
that did not reliably happen and grandchildren survived, while `cancel` and timeouts were
unaffected because both kill directly. `Run` now holds the pid and kills synchronously in
`Drop`.
- Output lines are bounded at `MAX_LINE`. A line that never ended could exhaust memory
before any total cap applied, because the reader accumulated until a newline.

### Added

- **`Probe`** reads a CLI's `--version` and compares it against
`Agent::verified_version`, the release its flag mappings were checked against, reporting
`Verified` / `Newer` / `Older` / `Unrecognized` with an `advisory()` written to be shown to
a person. Not automatic: probing spawns a process, so a host calls it at startup.
- **`Error::FlagRejected`**, separated from `Error::Failed` when a CLI refuses an argument.
Usually version drift, where nothing about the request is wrong and the wrapper and the CLI
simply disagree about what is accepted.
- `Agent::login_hint`, the per-agent command that resolves a missing login.
- `MAX_LINE`, `MAX_EVENT_BYTES` and `TRUNCATION_MARK` are public, so a consumer can size its
own buffers against the same numbers.

## 0.1.0

First release. Drives Claude Code, Codex and GitHub Copilot headlessly behind one request
type, one event vocabulary and one session model, with resume and fork.

Every flag mapping and output shape was verified against the installed CLIs rather than
taken from their documentation, against `claude 2.1.205`, `codex-cli 0.145.0` and
`GitHub Copilot CLI 1.0.75`.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "agent-abstraction"
version = "0.1.0"
version = "0.2.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.
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ things.

```toml
[dependencies]
agent-abstraction = "0.1"
agent-abstraction = "0.2"
```

Every flag mapping and output shape here was verified against the installed CLIs rather than
Expand Down Expand Up @@ -144,9 +144,10 @@ the child and joins its readers before returning `Error::Cancelled`. So when it
tree really is gone, which matters if the next thing you do touches the files it was working
on.

`drop` cannot await, so it signals and aborts as a backstop. That is prompt but **not
synchronous**: teardown runs when the runtime next polls the aborted task. Both kill the
tree; only `cancel` tells you when.
`drop` **synchronously signals** the process group on Unix, then aborts the driver. What it
cannot do is wait: `Drop` cannot await, so it does not block until the child has exited and
its readers are joined. Both kill the tree; only `cancel` tells you when it is gone. On
Windows only the direct child is signalled.

Each run gets its own process group on Unix, and cancellation, drop and timeout all tear
down the whole group. Killing only the CLI would orphan the commands *it* started, which
Expand Down
Loading
Loading