Skip to content

Commit f8faed8

Browse files
Nick Ficanoclaude
andcommitted
docs: align documentation with actual SDK surface
Reviewed every doc against the source under src/ and corrected the fictional or stale API claims that had accumulated: - Project docs (Cli, AspNetCore, Giraffe, Otel, Client, Core, Arcp) rewritten to match the real exported types and helpers. - Guides (sessions, auth, resume, jobs, job-events, errors, vendor-extensions, observability) updated for the actual ArcpClientOptions, JobHandle, ITransport, ToolOutcome, and Features shapes; removed the imagined envelope `extensions` block. - Reference docs (architecture, getting-started, transports, cli, troubleshooting, recipes) updated for the real CLI commands and transport constructors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 999a094 commit f8faed8

23 files changed

Lines changed: 1149 additions & 1059 deletions

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,5 @@
4242
- [Recipes](recipes.md) — common patterns
4343
- [Conformance](conformance.md) — spec coverage summary
4444
- [Troubleshooting](troubleshooting.md) — common errors and fixes
45-
- [Diagrams](diagrams/README.md) — Graphviz sources and rendered SVGs
45+
- [Diagrams](diagrams/README.md) — Graphviz sources and rendered SVGs (`architecture-light.dot`, `architecture-dark.dot`)
4646
- [Root conformance document](../CONFORMANCE.md)

docs/architecture.md

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,20 @@ The shared kernel — no client/runtime distinction, no I/O. Exports:
2929
- **`Envelope`** — eight-field wire record. `arcp = "1.1"`.
3030
`Payload : JsonElement` is kept lazy so decoders only pay for what
3131
they read.
32-
- **`Message` DU**one case per protocol message type. Every `match`
33-
is compile-checked; adding a new type is a compile error until all
34-
match arms are updated.
32+
- **`Message` DU**eighteen cases, one per protocol message type.
33+
Every `match` is compile-checked; adding a new type is a compile
34+
error until all match arms are updated.
3535
- **`ARCPError` DU** — exhaustive 15-case discriminated union for all
3636
spec error codes. No stringly-typed error handling.
3737
- **`LeaseGrant`**`Map<namespace, glob list>`; stateless
3838
`validateLeaseOp` runs glob match → expiry → budget in that order.
39-
- **`Features`**`Set<string>` of feature flag names; `Features.All`
40-
is the canonical SDK default.
39+
- **`Features`** — string constants plus `Features.All : Set<string>`;
40+
the canonical SDK default is the full set.
4141
- **Codec**`Codec.toEnvelope`, `Codec.toMessage`,
4242
`Codec.readEnvelope`, `Codec.writeEnvelope`. Uses
43-
`FSharp.SystemTextJson` with `JsonUnionEncoding.InternalTag` so the
44-
discriminator `type` field sits at the same level as peer fields.
43+
`FSharp.SystemTextJson` (`WithUnionExternalTag()` keyed on `"type"`,
44+
`WithUnionUnwrapRecordCases()`) so the discriminator sits at the same
45+
level as peer fields.
4546

4647
See [Arcp.Core](projects/Arcp.Core.md).
4748

@@ -57,11 +58,13 @@ the handshake, dispatches inbound envelopes, and exposes:
5758
terminal envelope.
5859
- `SubscribeAsync(jobId, opts, ct)` — attaches to an in-progress job
5960
from another session (requires `subscribe` feature).
60-
- `ListJobsAsync(filter, ct)` — paginated job listing (requires
61-
`list_jobs` feature).
61+
- `UnsubscribeAsync(jobId, ct)` — stop receiving events for a
62+
subscribed job.
63+
- `ListJobsAsync(filter, limit, cursor, ct)` — paginated job listing
64+
(requires `list_jobs` feature).
6265
- `AckAsync(seq, ct)` — explicit ack for back-pressure (requires `ack`
63-
feature).
64-
- `CloseAsync(ct)` — graceful session teardown.
66+
feature). Auto-ack runs in the background once `ack` is negotiated.
67+
- `CloseAsync(reason, ct)` — graceful session teardown via `session.bye`.
6568

6669
Transports: `MemoryTransport`, `StdioTransport`,
6770
`WebSocketClientTransport`. See [transports.md](transports.md).
@@ -81,13 +84,15 @@ server.SetDefaultAgentVersion("hello", "2.0")
8184
server.HandleSessionAsync(transport, ct) // runs one session
8285
```
8386

84-
`JobContext` is the agent's window into the runtime. It emits all
85-
eight reserved event kinds (`EmitLogAsync`, `EmitThoughtAsync`,
86-
`EmitToolCallAsync`, `EmitToolResultAsync`, `EmitStatusAsync`,
87-
`EmitProgressAsync`, `EmitMetricAsync`, `EmitArtifactRefAsync`),
88-
exposes `Lease`, `LeaseConstraints`, and `CancellationToken`, and
89-
provides v1.1 streaming via `BeginStreamingResult()` /
90-
`EmitResultChunkAsync`.
87+
`JobContext` is the agent's window into the runtime. It emits every
88+
reserved event kind (`EmitLogAsync`, `EmitThoughtAsync`,
89+
`EmitStatusAsync`, `EmitProgressAsync`, `EmitToolCallAsync`,
90+
`EmitToolResultAsync`, `EmitMetricAsync`, `EmitArtifactRefAsync`,
91+
`EmitDelegateAsync`) plus `EmitVendorEventAsync` for the `x-vendor.*`
92+
namespace, exposes `Lease`, `LeaseConstraints`, `Credentials`,
93+
`RemainingBudget`, and `CancellationToken`, and provides v1.1
94+
streaming via `BeginStreamingResult()` / `EmitResultChunkAsync`.
95+
Lease enforcement runs through `ValidateOpAsync`.
9196

9297
`type ArcpAgentHandler = JobContext -> Task<JsonElement>`
9398

@@ -104,11 +109,14 @@ See [Arcp.AspNetCore](projects/Arcp.AspNetCore.md) and
104109

105110
## `Arcp.Otel`
106111

107-
Provides `ArcpActivitySource.Instance` (the shared `ActivitySource`)
108-
and `ArcpSpanAttributes` constants (`arcp.session_id`, `arcp.job_id`,
109-
`arcp.agent`, `arcp.lease.capabilities`, `arcp.lease.expires_at`,
110-
`arcp.budget.remaining`). Your instrumentation code uses these
111-
constants to attach well-known attributes to spans.
112+
Provides `ArcpActivitySource.Instance` (the shared `ActivitySource`,
113+
name `"ARCP"`), `ArcpSpanAttributes` constants (`arcp.session_id`,
114+
`arcp.job_id`, `arcp.agent`, `arcp.lease.capabilities`,
115+
`arcp.lease.expires_at`, `arcp.budget.remaining`), and two `ArcpOtel`
116+
helpers (`beginJobSpan`, `recordBudgetRemaining`) for runtime
117+
implementers who want to wrap a job in an `Activity`. Trace context is
118+
carried as the envelope's top-level `trace_id` field — no
119+
`extensions` block is emitted by this SDK.
112120

113121
See [Arcp.Otel](projects/Arcp.Otel.md).
114122

@@ -120,7 +128,7 @@ Every message is one JSON object with eight top-level fields:
120128
| ------------ | ------------------------------------------- | -------------------------------------------- |
121129
| `arcp` | always | `"1.1"` (the protocol version literal) |
122130
| `id` | always | ULID or UUIDv7, unique per message |
123-
| `type` | always | discriminator (`"session.hello"`, `"job.submit"`, ) |
131+
| `type` | always | discriminator (`"session.hello"`, `"job.submit"`, ...) |
124132
| `payload` | always | type-specific body; decoded lazily |
125133
| `session_id` | after handshake | absent on pre-session frames |
126134
| `job_id` | job-scoped envelopes | set on `job.*` types |

docs/cli.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@ dotnet tool install --global --add-source ./artifacts Arcp.Cli
99

1010
## `arcp serve`
1111

12-
Start a runtime listening on stdio:
12+
Start a runtime listening on stdio. A single `echo` agent is registered;
13+
it is intended as a smoke-test runtime, not a production binary.
1314

1415
```
15-
arcp serve [--stdio] [--token TOKEN]
16+
arcp serve [--stdio | -s] [--token TOKEN]
1617
```
1718

18-
| Option | Default | Notes |
19-
| --------- | --------------- | ------------------------------------------------------- |
20-
| `--stdio` | always set | Only stdio transport is supported in v1. |
21-
| `--token` | `$ARCP_TOKEN` | Bearer token. Falls back to the `ARCP_TOKEN` env var. If neither is set, a dev-mode verifier is used that accepts any token. |
19+
| Option | Default | Notes |
20+
| --------- | ----------- | ---------------------------------------------------------------------------------------------------------------- |
21+
| `--stdio` | (implied) | Only stdio is supported; the flag is reserved for future transports. |
22+
| `--token` | dev mode | When set, only this exact bearer token is accepted (`StaticBearerVerifier`). When unset and `$ARCP_TOKEN` is empty, a permissive dev-mode verifier accepts any non-empty token. |
2223

23-
Example — spawn a runtime as a child process with a fixed token:
24+
`ARCP_TOKEN` is consulted only when `--token` is omitted.
25+
26+
Example — run with a fixed token:
2427

2528
```bash
2629
ARCP_TOKEN=secret arcp serve --stdio
@@ -44,8 +47,10 @@ arcp send --url URL --agent AGENT [--token TOKEN] [--input JSON]
4447
| `--token` | no | Bearer token. Falls back to `$ARCP_TOKEN`. |
4548
| `--input` | no | JSON-encoded input. Defaults to `null`. |
4649

47-
Events are printed to stdout as they arrive. On completion the final
48-
result or error is printed and the process exits.
50+
Each received event prints one `event: <kind>` line to stdout. On a
51+
`job.result` the inline result (or the literal `null`) is printed and
52+
the process exits with code 0; on a `job.error` the error code and
53+
message are written to stderr and the process exits with code 1.
4954

5055
```bash
5156
arcp send \
@@ -60,9 +65,14 @@ arcp send \
6065
Prints the SDK version and the ARCP protocol version it targets:
6166

6267
```
63-
arcp --version
68+
$ arcp --version
69+
arcp 1.0.0 (protocol 1.1)
6470
```
6571

72+
There is no `arcp cancel`, `arcp status`, `arcp events`, or `arcp ls`
73+
the CLI is intentionally tiny. Drive those operations from a script
74+
that uses `ArcpClient` directly.
75+
6676
## Samples
6777

6878
```bash

docs/getting-started.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ let server =
3535
3636
server.RegisterAgent("echo", fun ctx ->
3737
task {
38-
do! ctx.EmitStatusAsync("running", ctx.CancellationToken)
38+
do! ctx.EmitStatusAsync("running", None, ctx.CancellationToken)
3939
do! ctx.EmitLogAsync(LogLevel.Info, "received", ctx.CancellationToken)
4040
return Json.serializeToElement<{| echoed: obj |}> {| echoed = ctx.Input |}
4141
})
@@ -62,8 +62,15 @@ let request : JobSubmitRequest = {
6262
}
6363
6464
let handle = (client.SubmitAsync(request, CancellationToken.None)).Result
65-
let result = handle.Result.Result
66-
// → JsonElement: { "echoed": { "hi": 1 } }
65+
66+
// handle.Result : Task<Result<JobResultPayload, ARCPError>>
67+
match handle.Result.Result with
68+
| Ok payload ->
69+
payload.Result
70+
|> Option.iter (fun el -> printfn "%s" (el.GetRawText()))
71+
// → { "echoed": { "hi": 1 } }
72+
| Error err ->
73+
eprintfn "[%s] %s" (ARCPError.code err) (ARCPError.message err)
6774
```
6875

6976
You should see two events (`status: running`, `log: info`) arriving before
@@ -82,15 +89,17 @@ using ARCP.Runtime;
8289
var server = new ArcpServer(ArcpServerOptions.defaults);
8390
server.RegisterAgent("echo", async ctx =>
8491
{
85-
await ctx.EmitStatusAsync("running", ctx.CancellationToken);
92+
await ctx.EmitStatusAsync("running", null, ctx.CancellationToken);
8693
await ctx.EmitLogAsync(LogLevel.Info, "received", ctx.CancellationToken);
8794
return JsonSerializer.SerializeToElement(new { echoed = ctx.Input });
8895
});
8996

9097
var (clientT, serverT) = MemoryTransport.CreatePair();
9198
_ = server.HandleSessionAsync(serverT, CancellationToken.None);
9299

93-
await using var client = new ArcpClient(
100+
// `ArcpClientOptions` is an F# record; from C# you build it with the
101+
// generated constructor, passing every field.
102+
using var client = new ArcpClient(
94103
clientT,
95104
new ArcpClientOptions(
96105
// "1.0" is this demo client's application version, not the ARCP protocol version.
@@ -129,12 +138,24 @@ app.MapArcp("/arcp", server) |> ignore
129138
app.Run("http://localhost:7878")
130139
```
131140

132-
On the client side use `WebSocketClientTransport`:
141+
On the client side use `WebSocketClientTransport.connectAsync`, which
142+
opens a `ClientWebSocket`, attaches the bearer token (if any) as an
143+
`Authorization` header on the upgrade, and returns an `ITransport`:
133144

134145
```fsharp
135-
let transport = new WebSocketClientTransport(uri) :> ITransport
136-
let client = new ArcpClient(transport, ArcpClientOptions.defaults)
137-
let _ = client.ConnectAsync(CancellationToken.None).Result
146+
open ARCP.Client.Transport
147+
148+
task {
149+
let! transport =
150+
WebSocketClientTransport.connectAsync
151+
(System.Uri "ws://localhost:7878/arcp")
152+
(Some "demo") // bearer token, or None
153+
CancellationToken.None
154+
155+
let client = new ArcpClient(transport, ArcpClientOptions.defaults)
156+
let! _session = client.ConnectAsync CancellationToken.None
157+
return client
158+
}
138159
```
139160

140161
## Run over stdio

docs/guides/auth.md

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,24 @@ let options =
3030

3131
### Dev-mode verifier (default)
3232

33-
`ArcpServerOptions.defaults` includes a permissive verifier that
34-
accepts any non-empty token and maps it to a principal of `"dev"`. This
35-
is useful for local testing but must never be used in production.
33+
`ArcpServerOptions.defaults` wires in `DevModeBearerVerifier`, which
34+
accepts any non-empty token and maps it to a principal id of
35+
`"dev:<token>"`. This is useful for local testing but must never be
36+
used in production.
3637

3738
### Static verifier
3839

39-
`StaticBearerVerifier` accepts a map of `token → principal`:
40+
`StaticBearerVerifier` accepts a `IReadOnlyDictionary<string, string>`
41+
of `token → principal-id`:
4042

4143
```fsharp
42-
open ARCP.Runtime
44+
open ARCP.Runtime.Auth
4345
4446
let verifier =
4547
StaticBearerVerifier(
46-
Map.ofList [
47-
("token-alice", "alice")
48-
("token-bob", "bob")
48+
readOnlyDict [
49+
"token-alice", "alice"
50+
"token-bob", "bob"
4951
])
5052
5153
let options =
@@ -57,27 +59,32 @@ let server = ArcpServer(options)
5759
### Custom verifier
5860

5961
Implement `IBearerVerifier` to add JWT validation, database lookups, or
60-
any other auth logic:
62+
any other auth logic. `VerifyAsync` returns
63+
`Task<Result<IPrincipal, ARCPError>>` — on rejection, supply the
64+
`ARCPError.Unauthenticated` case you want surfaced:
6165

6266
```fsharp
67+
open ARCP.Core
68+
open ARCP.Runtime.Auth
69+
6370
type JwtVerifier(signingKey: string) =
6471
interface IBearerVerifier with
65-
member _.VerifyAsync(token, ct) =
72+
member _.VerifyAsync(token, _ct) =
6673
task {
67-
let principal = validateJwt signingKey token
68-
return
69-
if principal <> null then Some principal
70-
else None
74+
match validateJwt signingKey token with
75+
| Some claims -> return Ok(StringPrincipal(claims.Subject) :> IPrincipal)
76+
| None -> return Error(ARCPError.Unauthenticated "Invalid bearer token")
7177
}
7278
7379
let options =
7480
{ ArcpServerOptions.defaults with
7581
BearerVerifier = JwtVerifier("my-secret") }
7682
```
7783

78-
`VerifyAsync` should return `Some principal` for a valid token, or
79-
`None` to reject. A rejected token causes the runtime to send
80-
`session.error` with `UNAUTHENTICATED` and close the transport.
84+
Returning `Error` causes the runtime to send `session.error` with
85+
`UNAUTHENTICATED` and close the transport. `StringPrincipal` lives in
86+
`ARCP.Runtime.Auth`; you can also implement `IPrincipal` directly to
87+
expose `Labels`.
8188

8289
## Wire shape
8390

@@ -99,17 +106,25 @@ schemes requires explicit handling in a custom verifier.
99106

100107
## Vendor auth schemes
101108

102-
The spec reserves `x-vendor.<vendor>.<scheme>` for custom auth. To
103-
accept vendor schemes, implement `IBearerVerifier` and inspect the
104-
raw `AuthPayload.Scheme` field:
109+
The spec reserves `x-vendor.<vendor>.<scheme>` for custom auth. The
110+
current SDK only passes the bearer `token` to the verifier — `Auth`'s
111+
raw `Scheme` field is consumed by the handshake before `VerifyAsync`
112+
is called and is not surfaced here. To support vendor schemes today,
113+
embed the scheme distinction inside the token itself and dispatch on
114+
it from a custom verifier:
105115

106116
```fsharp
117+
open ARCP.Core
118+
open ARCP.Runtime.Auth
119+
107120
type MultiSchemeVerifier() =
108121
interface IBearerVerifier with
109-
member _.VerifyAsync(token, ct) =
122+
member _.VerifyAsync(token, _ct) =
110123
task {
111-
// 'token' is the raw Auth.Token value; scheme is inspected upstream
112-
return Some "any-principal"
124+
if token.StartsWith("acme:") then
125+
return Ok(StringPrincipal(token.Substring 5) :> IPrincipal)
126+
else
127+
return Error(ARCPError.Unauthenticated "Unknown auth scheme")
113128
}
114129
```
115130

0 commit comments

Comments
 (0)