Skip to content

Commit 8a6f8d4

Browse files
nficanoclaude
andcommitted
docs: rewrite README.md from shared SDK template
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3e56b84 commit 8a6f8d4

1 file changed

Lines changed: 231 additions & 79 deletions

File tree

README.md

Lines changed: 231 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,287 @@
1-
# arcp
1+
<h3 align="center">ARCP Ruby SDK</h3>
22

3-
Ruby SDK for ARCP v1. The wire protocol is specified in
4-
[the ARCP spec](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md). This
5-
gem implements the full v1 envelope, session handshake, job lifecycle,
6-
event stream, leases, and error model.
3+
<p align="center"><strong>Ruby SDK for the Agent Runtime Control Protocol (ARCP) — submit, observe, and control long-running agent jobs from Ruby.</strong></p>
74

8-
## Install
5+
<p align="center">
6+
<a href="https://rubygems.org/gems/arcp"><img alt="gem" src="https://img.shields.io/gem/v/arcp.svg"></a>
7+
<a href="https://github.com/nficano/arpc/actions/workflows/test.yml"><img alt="CI" src="https://github.com/nficano/arpc/actions/workflows/test.yml/badge.svg"></a>
8+
<a href="https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md"><img alt="ARCP" src="https://img.shields.io/badge/ARCP-v1.1%20draft-blue"></a>
9+
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-lightgrey"></a>
10+
</p>
11+
12+
<p align="center">
13+
<a href="https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md">Specification</a> ·
14+
<a href="#concepts">Concepts</a> ·
15+
<a href="#installation">Install</a> ·
16+
<a href="#quick-start">Quick start</a> ·
17+
<a href="docs/">Guides</a> ·
18+
<a href="docs/api/">API reference</a>
19+
</p>
20+
21+
---
22+
23+
`arcp` is the Ruby reference implementation of [ARCP](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md), the Agent Runtime Control Protocol. It covers both sides of the wire — `Arcp::Client` for submitting and observing jobs, `Arcp::Runtime::Runtime` for hosting agents — so either side can talk to any conformant peer in any language without hand-rolling the envelope, sequencing, or lease enforcement.
24+
25+
ARCP itself is a transport-agnostic wire protocol for long-running AI agent jobs. It owns the parts of agent infrastructure that don't change between products — sessions, durable event streams, capability leases, budgets, resume — and stays out of the parts that do. ARCP wraps the agent function; it does not define how agents are built, how tools are exposed (that's MCP), or how telemetry is exported (that's OpenTelemetry).
26+
27+
## Installation
28+
29+
Requires Ruby 3.3 or later. The gem runs on the `socketry/async` reactor and pulls in `async-websocket` for the default networked transport and `sqlite3` for the resume log; no separate extras are needed. Add it to a `Gemfile`:
930

1031
```ruby
1132
gem 'arcp', '~> 1.0'
1233
```
1334

14-
Requires Ruby 3.3+. Runs on the `socketry/async` reactor; pairs with
15-
`falcon` for hosting and `async-websocket` for the WebSocket transport.
35+
```sh
36+
bundle install
37+
```
38+
39+
## Quick start
1640

17-
## Quickstart
41+
Connect to a runtime, submit a job, stream its events to completion:
1842

1943
```ruby
2044
require 'async'
2145
require 'arcp'
2246

47+
ECHO = lambda do |ctx|
48+
ctx.log(level: 'info', message: "echoing #{ctx.input.inspect}")
49+
ctx.progress(current: 1, total: 1, units: 'message')
50+
ctx.finish(result: { 'echoed' => ctx.input })
51+
end
52+
2353
Sync do
2454
runtime = Arcp::Runtime::Runtime.new(
2555
auth_verifier: Arcp::Auth::Bearer.from_token('demo', principal_id: 'alice'),
2656
heartbeat_interval_sec: nil
2757
)
28-
runtime.register_agent(
29-
name: 'echo', versions: ['1.0.0'], default: '1.0.0',
30-
handler: ->(ctx) {
31-
ctx.progress(current: 1, total: 1, units: 'message')
32-
ctx.finish(result: { 'echoed' => ctx.input })
33-
}
34-
)
58+
runtime.register_agent(name: 'echo', versions: ['1.0.0'], default: '1.0.0', handler: ECHO)
3559

3660
server_t, client_t = Arcp::Transport::MemoryTransport.pair
3761
server = Async { runtime.accept(server_t) }
3862

3963
client = Arcp::Client.open(
4064
transport: client_t,
41-
auth: { 'scheme' => 'bearer', 'token' => 'demo' }
65+
auth: { 'scheme' => 'bearer', 'token' => 'demo' },
66+
client_name: 'quickstart'
4267
)
68+
4369
handle = client.submit_job(agent: 'echo', input: { 'msg' => 'hi' })
44-
handle.subscribe(client: client).each { |ev| puts ev.kind }
45-
puts handle.get_result(client: client).result.inspect
70+
handle.subscribe(client: client).each { |event| puts "#{event.kind}: #{event.body.to_h}" }
71+
result = handle.get_result(client: client)
72+
puts "final: #{result.final_status} #{result.result.inspect}"
4673

4774
client.close
4875
server.stop
4976
end
5077
```
5178

52-
## What is ARCP
79+
This is the whole shape of the SDK: open a session, submit work, consume an ordered event stream, get a terminal result or error. Everything below is detail on those four moves.
80+
81+
## Concepts
82+
83+
ARCP organizes everything around four concerns — **identity**, **durability**, **authority**, and **observability** — expressed through five core objects:
84+
85+
- **Session** — a connection between a client and a runtime. A session carries identity (a bearer token), negotiates a feature set in a `hello`/`welcome` handshake, and is *resumable*: if the transport drops, you reconnect with a resume token and the runtime replays buffered events. Jobs outlive the session that started them. See [§6](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
86+
- **Job** — one unit of agent work submitted into a session. A job has an identity, an optional idempotency key, a resolved agent version, and a lifecycle that ends in exactly one terminal state: `success`, `error`, `cancelled`, or `timed_out`. See [§7](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
87+
- **Event** — the ordered, session-scoped stream a job emits: logs, thoughts, tool calls and results, status, metrics, artifact references, progress, and streamed result chunks. Events carry strictly monotonic sequence numbers so the stream survives reconnects gap-free. See [§8](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
88+
- **Lease** — the authority a job runs under, expressed as capability grants (`fs.read`, `fs.write`, `net.fetch`, `tool.call`, `agent.delegate`, `cost.budget`, `model.use`). The runtime enforces the lease at every operation boundary; a job can never act outside it. Leases may carry a budget and an expiry, and may be subset and handed to sub-agents via delegation. See [§9](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
89+
- **Subscription** — read-only attachment to a job started elsewhere (e.g. a dashboard watching a job a CLI submitted). A subscriber observes the live event stream but cannot cancel or mutate the job. Distinct from *resume*, which continues the original session and carries cancel authority. See [§7.6](https://github.com/agentruntimecontrolprotocol/spec/blob/main/docs/draft-arcp-1.1.md).
90+
91+
The SDK models each of these as first-class objects; the rest of this README shows how.
5392

54-
ARCP is a session-oriented protocol for invoking remote agents. A client
55-
opens a session, submits jobs, and receives a stream of structured events
56-
followed by a terminal result or error. The protocol covers capability
57-
negotiation, heartbeats, ordered acks, cursored job listing, cross-session
58-
observation, capability-bounded leases, and trace propagation.
93+
## Guides
94+
95+
### Sessions and resume
96+
97+
Open a session, negotiate features, and reconnect transparently after a transport drop using the resume token — jobs keep running server-side while you're gone.
98+
99+
```ruby
100+
require 'async'
101+
require 'arcp'
102+
103+
Sync do
104+
client = Arcp::Client.open(
105+
transport: transport,
106+
auth: { 'scheme' => 'bearer', 'token' => ENV.fetch('ARCP_TOKEN') },
107+
client_name: 'resumable'
108+
)
109+
110+
session_id = client.session.id
111+
resume_token = client.session.resume_token
112+
last_seq = Hash.new(0)
113+
handle = client.submit_job(agent: 'long-runner')
114+
handle.subscribe(client: client).each do |event|
115+
last_seq[handle.job_id] = event.body.respond_to?(:seq) ? event.body.seq : last_seq[handle.job_id]
116+
end
117+
118+
# ... transport drops ...
119+
120+
resumed = Arcp::Client.open(
121+
transport: new_transport,
122+
auth: { 'scheme' => 'bearer', 'token' => ENV.fetch('ARCP_TOKEN') },
123+
resume: { 'token' => resume_token, 'last_event_seq' => last_seq }
124+
)
125+
# The runtime replays every event with event_seq > last_seq, then resumes live streaming.
126+
end
127+
```
59128

60-
## Features
129+
### Submitting jobs
61130

62-
- Capability negotiation (§6.2)
63-
- Heartbeat / ping-pong (§6.4)
64-
- Application-level ack (§6.5)
65-
- Cursored `list_jobs` (§6.6)
66-
- Cross-session `job.subscribe` with history replay (§7.6)
67-
- Agent versioning with `name@version` refs (§7.5)
68-
- `result_chunk` streaming with result_id terminator (§8.4)
69-
- `progress` events (§8.2)
70-
- `lease_constraints.expires_at` (§9)
71-
- `cost.budget` capability with `BigDecimal` arithmetic (§9.6)
72-
- Resume token + last_event_seq replay (§6.3)
73-
- Trace context propagation (§11)
131+
Submit a job with an agent (optionally version-pinned as `name@version`), an input, and an optional lease request, idempotency key, and runtime limit.
74132

75-
## Architecture
133+
```ruby
134+
handle = client.submit_job(
135+
agent: 'weekly-report@2.1.0',
136+
input: { 'week' => '2026-W19' },
137+
lease_request: Arcp::Lease::LeaseRequest.new(
138+
capabilities: ['net.fetch'],
139+
expires_at: (Time.now.utc + 60).iso8601
140+
),
141+
lease_constraints: Arcp::Lease::LeaseConstraints.new(
142+
expires_at: (Time.now.utc + 300).iso8601,
143+
max_budget: nil
144+
),
145+
idempotency_key: 'weekly-report-2026-W19',
146+
max_runtime_sec: 300
147+
)
76148

149+
puts "job_id = #{handle.job_id}"
150+
puts "resolved agent = #{handle.agent.inspect}"
151+
puts "effective lease = #{handle.lease&.to_h.inspect}"
77152
```
78-
Arcp::Client # session-oriented client
79-
Arcp::Runtime::Runtime # server-side runtime; accepts transports
80-
Arcp::Runtime::JobContext # passed to agent handlers
81-
Arcp::Session::* # Hello, Welcome, CapabilitySet, Feature, AgentInventory, ...
82-
Arcp::Job::* # Submit, Accepted, Event, Result, JobError, Handle, Summary
83-
Arcp::Job::EventKind # 10 standard kinds
84-
Arcp::Lease::* # Lease, LeaseRequest, LeaseConstraints, CostBudget, Subsetting
85-
Arcp::Transport::* # MemoryTransport, WebSocketTransport, StdioTransport
86-
Arcp::Auth::* # AuthScheme, Bearer, Principal
87-
Arcp::Errors::* # 15 wire codes + 3 internal-only
88-
Arcp::Trace # Fiber-local Context, span helpers
153+
154+
### Consuming events
155+
156+
Iterate the ordered event stream — `log`, `thought`, `tool_call`, `tool_result`, `status`, `metric`, `artifact_ref`, `progress`, `result_chunk` — and optionally acknowledge progress so the runtime can release buffered events early.
157+
158+
```ruby
159+
last_seq = 0
160+
handle.subscribe(client: client).each do |event|
161+
case event.kind
162+
when Arcp::Job::EventKind::LOG
163+
puts event.body.message
164+
when Arcp::Job::EventKind::TOOL_CALL
165+
puts "-> tool #{event.body.name}(#{event.body.arguments.inspect})"
166+
when Arcp::Job::EventKind::METRIC
167+
puts "metric #{event.body.name}=#{event.body.value}#{event.body.unit}"
168+
when Arcp::Job::EventKind::PROGRESS
169+
puts "progress #{event.body.current}/#{event.body.total} #{event.body.units}"
170+
end
171+
last_seq += 1
172+
client.ack(last_seq) if (last_seq % 32).zero? # coalesced session.ack
173+
end
89174
```
90175

91-
## Transports
176+
### Leases and budgets
92177

93-
- `Arcp::Transport::MemoryTransport.pair` — in-process queue pair. Tests, embedded clients.
94-
- `Arcp::Transport::WebSocketTransport` — wraps an `Async::WebSocket::Connection`. Production transport.
95-
- `Arcp::Transport::StdioTransport` — newline-delimited JSON over a pair of IOs. Co-process agents.
178+
Request capabilities, a budget, and an expiry; read budget-remaining metrics as they arrive; handle the runtime's enforcement decisions.
96179

97-
## Deployment
180+
```ruby
181+
handle = client.submit_job(
182+
agent: 'web-research',
183+
input: { 'iterations' => 8 },
184+
lease_request: Arcp::Lease::LeaseRequest.new(
185+
capabilities: ['tool.call', 'cost.spend'],
186+
budget: Arcp::Lease::CostBudget.parse(['USD:1.00']),
187+
expires_at: (Time.now.utc + 600).iso8601
188+
)
189+
)
98190

99-
Run the runtime as a daemon under the `socketry/async` reactor. Host
100-
WebSocket endpoints with `falcon`. The runtime is fiber-based and
101-
multiplexes sessions on the reactor; do not deploy under
102-
request-per-thread servers like Puma.
191+
puts "initial budget = #{handle.lease&.budget&.to_a.inspect}"
103192

104-
## Errors
193+
handle.subscribe(client: client).each do |event|
194+
next unless event.kind == Arcp::Job::EventKind::METRIC
195+
next unless event.body.name == 'cost.budget.remaining'
105196

106-
15 wire codes, each mapped to an `Arcp::Errors::*` subclass with a
107-
`retryable?` default: `Cancelled`, `InvalidRequest`, `Unauthenticated`,
108-
`PermissionDenied`, `JobNotFound`, `AgentNotAvailable`, `DuplicateKey`,
109-
`RateLimited`, `Internal`, `HeartbeatLost`, `Backpressure`,
110-
`ProtocolViolation`, `Timeout`, `ResumeWindowExpired`,
111-
`LeaseSubsetViolation`, `AgentVersionNotAvailable`, `LeaseExpired`,
112-
`BudgetExhausted`. Three additional codes are library-internal and never
113-
appear on the wire (`UNNEGOTIATED_FEATURE`, plus the abstract
114-
`Arcp::Error` base and the generic `Internal` fallback).
197+
puts "budget remaining: #{event.body.value} #{event.body.unit}"
198+
end
115199

116-
## Documentation
200+
begin
201+
handle.get_result(client: client)
202+
rescue Arcp::Errors::BudgetExhausted, Arcp::Errors::LeaseExpired => e
203+
# Never retryable — resubmit with a fresh lease/budget instead.
204+
warn "job ended: #{e.code} #{e.message}"
205+
end
206+
```
117207

118-
See `docs/` for guides, concepts, and reference. Start with
119-
`docs/getting-started.md`.
208+
### Subscribing to jobs
120209

121-
## Conformance
210+
Attach read-only to a job submitted elsewhere and observe its live stream (with optional history replay) without cancel authority.
211+
212+
```ruby
213+
Sync do
214+
observer = Arcp::Client.open(
215+
transport: dashboard_transport,
216+
auth: { 'scheme' => 'bearer', 'token' => ENV.fetch('ARCP_TOKEN') },
217+
client_name: 'dashboard'
218+
)
122219

123-
Spec-to-code matrix in [CONFORMANCE.md](CONFORMANCE.md).
220+
running = observer.list_jobs(status: 'running', limit: 1).first
221+
stream = observer.subscribe_job(job_id: running.job_id, from_event_seq: 0, history: true)
124222

125-
## Development
223+
stream.each do |event|
224+
puts "[#{running.job_id}] #{event.kind}"
225+
end
126226

227+
observer.close
228+
end
127229
```
128-
bundle install
129-
bundle exec rake # spec + rubocop + steep
130-
bundle exec rake docs # build doc tree
230+
231+
### Error handling
232+
233+
Catch the typed error taxonomy and respect the `retryable` flag — `LEASE_EXPIRED` and `BUDGET_EXHAUSTED` are never retryable; a naive retry fails identically.
234+
235+
```ruby
236+
begin
237+
handle = client.submit_job(agent: 'flaky', input: {})
238+
handle.get_result(client: client)
239+
rescue Arcp::Errors::LeaseExpired, Arcp::Errors::BudgetExhausted => e
240+
raise e # resubmit with a fresh lease / budget instead
241+
rescue Arcp::Error => e
242+
if e.retryable?
243+
# safe to retry with backoff (e.g. INTERNAL_ERROR, RATE_LIMITED, TIMEOUT)
244+
retry_with_backoff(e)
245+
else
246+
raise
247+
end
248+
end
131249
```
132250

251+
## Feature support
252+
253+
ARCP features this SDK negotiates during the `hello`/`welcome` handshake:
254+
255+
| Feature flag | Status |
256+
|---|---|
257+
| `heartbeat` | Supported |
258+
| `ack` | Supported |
259+
| `list_jobs` | Supported |
260+
| `subscribe` | Supported |
261+
| `lease_expires_at` | Supported |
262+
| `cost.budget` | Supported |
263+
| `model.use` | Supported |
264+
| `provisioned_credentials` | Supported |
265+
| `progress` | Supported |
266+
| `result_chunk` | Supported |
267+
| `agent_versions` | Supported |
268+
269+
## Transport
270+
271+
ARCP is transport-agnostic. This SDK ships a WebSocket transport (default), a stdio transport for in-process child runtimes, and an in-memory transport for tests. WebSocket is the default for networked runtimes; stdio is used for in-process child runtimes. Select one by constructing the corresponding transport object and passing it to `Arcp::Client.open(transport:, ...)`: `Arcp::Transport::WebSocketTransport.new(connection: ws)` for production (wrap an open `Async::WebSocket::Connection`, typically hosted under `falcon`), `Arcp::Transport::StdioTransport` for co-process agents, or `Arcp::Transport::MemoryTransport.pair` for in-process tests and embedded clients.
272+
273+
## API reference
274+
275+
Full API reference — every type, method, and event payload — is in [`docs/`](docs/) (YARD-generated reference under [`docs/api/`](docs/api/), rebuilt with `bundle exec rake docs`).
276+
277+
## Versioning and compatibility
278+
279+
This SDK speaks **ARCP v1.1 (draft)**. The SDK follows semantic versioning independently of the protocol; the protocol version it negotiates is shown above and in `session.hello`. A runtime advertising a different ARCP MAJOR is not guaranteed compatible. Feature mismatches degrade gracefully: the effective feature set is the intersection of what the client and runtime advertise, and the SDK will not use a feature outside it.
280+
281+
## Contributing
282+
283+
See [`CONTRIBUTING.md`](CONTRIBUTING.md). Protocol questions and proposed changes belong in the [spec repository](https://github.com/agentruntimecontrolprotocol/spec); SDK bugs and feature requests belong here.
284+
133285
## License
134286

135-
Apache-2.0. See `LICENSE`.
287+
Apache-2.0 — see [`LICENSE`](LICENSE).

0 commit comments

Comments
 (0)