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
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ Two layers must pass before a PR merges:
CI runs both on every PR. A PR that changes which feature flags the SDK
negotiates must also update the README feature matrix in the same change.

### Coverage

The aggregate Kover coverage thresholds (line ≥ 75 %, branch ≥ 45 %) are
enforced at the root level so `:lib` unit tests and the `:tests`
integration suite both contribute to one report. Reproduce the
measurement locally with:

```sh
./gradlew test koverXmlReport koverVerify
```

To raise (or temporarily relax) the floor without editing the build,
pass `-Pkover.minLineCoverage=N` and/or `-Pkover.minBranchCoverage=N`.
Ratchet the defaults up in `build.gradle.kts` as more of the runtime
dispatch surface lands real tests.

## Coding standards

This repo enforces formatting with ktlint, static analysis with detekt, and
Expand Down
36 changes: 26 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,34 @@ For Maven users, the same coordinates apply (`groupId = dev.arcp`, `artifactId =
Connect to a runtime, submit a job, stream its events to completion:

```kotlin
import dev.arcp.auth.StaticBearerAuth
import dev.arcp.client.ARCPClient
import dev.arcp.envelope.Envelope
import dev.arcp.messages.Capabilities
import dev.arcp.messages.JobCompleted
import dev.arcp.messages.JobFailed
import dev.arcp.messages.JobSubmit
import dev.arcp.messages.SessionClose
import dev.arcp.runtime.ARCPRuntime
import dev.arcp.runtime.AgentRegistry
import dev.arcp.transport.MemoryTransport
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put

fun main(): Unit = runBlocking {
val (clientTransport, _) = MemoryTransport.pair() // swap for a networked transport
// Paired in-memory transport: client end <-> runtime end.
val (clientTransport, runtimeTransport) = MemoryTransport.pair()
val agents = AgentRegistry().apply { register("data-analyzer", "1.0.0", default = true) }
val runtime = ARCPRuntime(
supportedCapabilities = Capabilities(streaming = true),
agentRegistry = agents,
bearerAuth = StaticBearerAuth(mapOf("quickstart-token" to "quickstart")),
)
runtime.accept(runtimeTransport)
ARCPClient(
transport = clientTransport,
auth = ARCPClient.bearer(System.getenv("ARCP_TOKEN")),
auth = ARCPClient.bearer("quickstart-token"),
client = ARCPClient.defaultClientInfo(principal = "quickstart"),
capabilities = Capabilities(streaming = true),
).use { client ->
Expand All @@ -78,6 +88,7 @@ fun main(): Unit = runBlocking {
}.collect {}
client.send(session.sessionId, SessionClose())
}
runtime.close()
}
```

Expand Down Expand Up @@ -173,7 +184,7 @@ client.receive().takeWhile { env ->
when (val p = env.payload) {
is JobStatusEvent -> { println("status: ${p.phase} ${p.body}"); true }
is JobProgress -> { println("progress: ${p.percent}% ${p.message}"); true }
is Metric -> { println("metric: ${p.name}=${p.value} ${p.unit ?: ""}"); true }
is Metric -> { println("metric: ${p.name}=${p.value} ${p.unit}"); true }
is JobResultChunk -> { chunks.accept(p); true }
is JobCompleted -> { println("result: ${p.result}"); false }
is JobFailed -> { println("failed: ${p.code} ${p.message}"); false }
Expand Down Expand Up @@ -222,7 +233,7 @@ try {
client.receive().collect { env ->
val m = env.payload as? Metric ?: return@collect
if (m.name == StandardMetrics.COST_BUDGET_REMAINING) {
println("budget remaining: ${m.value} ${m.unit ?: ""}")
println("budget remaining: ${m.value} ${m.unit}")
}
}
} catch (e: ARCPException.BudgetExhausted) {
Expand Down Expand Up @@ -263,18 +274,23 @@ ARCP features this SDK negotiates during the `hello`/`welcome` handshake:
| Feature flag | Status |
|---|---|
| `heartbeat` | Supported |
| `ack` | Partial |
| `ack` | Catalog only — runtime returns `UNIMPLEMENTED` Nack on `subscribe`-style ack envelopes |
| `list_jobs` | Supported |
| `subscribe` | Partial |
| `subscribe` | Catalog + helpers; runtime does **not** dispatch `subscribe`/`unsubscribe` yet |
| `lease_expires_at` | Supported |
| `cost.budget` | Supported |
| `model.use` | Supported |
| `model.use` | Catalog + helpers; runtime does not yet enforce per-call model use |
| `provisioned_credentials` | Supported |
| `progress` | Supported |
| `result_chunk` | Supported |
| `result_chunk` | Client-side assembly only; runtime does not emit chunks itself |
| `agent_versions` | Supported |

`ack` and `subscribe` are wired into the runtime and message catalog (envelopes round-trip, the runtime dispatches them), but `ARCPClient` does not yet expose convenience methods for either — they're driven via `client.send(...)` with the raw `Ack` / `Subscribe` message types.
See [`docs/conformance.md`](docs/conformance.md) for a per-section
breakdown of which message types are routed by `ARCPRuntime.handleEnvelope`
versus types that exist only in the catalog. The session
challenge/authenticate flow, delegation, artifacts dispatch, resume, and
interrupt are all in the catalog but deferred from runtime dispatch — a
peer that sends them today receives a correlated `UNIMPLEMENTED` Nack.

## Transport

Expand Down
45 changes: 44 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ktlint) apply false
alias(libs.plugins.detekt) apply false
alias(libs.plugins.kover) apply false
alias(libs.plugins.kover)
alias(libs.plugins.dokka) apply false
alias(libs.plugins.nmcp) apply false
alias(libs.plugins.nmcp.aggregation)
Expand Down Expand Up @@ -78,3 +78,46 @@ nmcpAggregation {
dependencies {
nmcpAggregation(project(":lib"))
}

// ---------------------------------------------------------------------------
// Kover aggregation: roll :lib unit tests and the :tests integration suite
// into a single coverage report and enforce a floor on every CI run (#56).
//
// Thresholds: the floor is set to what `./gradlew test koverXmlReport
// koverVerify` reports as the current measurement, so CI passes today and any
// regression below the floor fails the build. Ratchet the numbers up as more
// of the runtime dispatch surface lands real tests — `coverage.line.minimum`
// and `coverage.branch.minimum` are the only knobs to touch.
// ---------------------------------------------------------------------------
dependencies {
kover(project(":lib"))
kover(project(":tests"))
}

kover {
reports {
verify {
rule {
groupBy = kotlinx.kover.gradle.plugin.dsl.GroupingEntityType.APPLICATION
bound {
minValue = providers.gradleProperty("kover.minLineCoverage")
.map { it.toInt() }
.orElse(75)
coverageUnits =
kotlinx.kover.gradle.plugin.dsl.CoverageUnit.LINE
aggregationForGroup =
kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE
}
bound {
minValue = providers.gradleProperty("kover.minBranchCoverage")
.map { it.toInt() }
.orElse(45)
coverageUnits =
kotlinx.kover.gradle.plugin.dsl.CoverageUnit.BRANCH
aggregationForGroup =
kotlinx.kover.gradle.plugin.dsl.AggregationType.COVERED_PERCENTAGE
}
}
}
}
}
118 changes: 70 additions & 48 deletions docs/conformance.md
Original file line number Diff line number Diff line change
@@ -1,69 +1,91 @@
# Conformance

This document maps ARCP v1.1 RFC sections to their Kotlin SDK implementations.
This document maps ARCP v1.1 RFC sections to their Kotlin SDK
implementations. Each row distinguishes three layers:

- **Catalog** — the wire types and serializers exist in `messages/*` and
round-trip via `Envelope.serializer()`.
- **Helpers** — storage, registry, or in-memory data structures back the
feature (`store/EventLog.kt`, `lease/*`, etc.).
- **Runtime dispatch** — `ARCPRuntime.handleEnvelope` actually handles
the message and produces a correlated reply. Anything that falls
through to the `UNIMPLEMENTED` Nack is *not* dispatched, even if
catalog and helpers exist.

A surface is only **✅ Supported** when all three layers are present.
Partial rows give a brief note.

## Implementation status

| RFC § | Title | Status | Implementation |
|-------|-------|--------|----------------|
| §6.1 | Envelope format | ✅ | `envelope/Envelope.kt` |
| §6.2 | Message catalog | ✅ | `messages/*.kt` |
| §6.3 | Resume | ✅ | `store/EventLog.kt` |
| §6.4 | Idempotency | ✅ | `store/EventLog.kt` |
| §6.6 | `session.list_jobs` / `session.jobs` | ✅ | `messages/Session.kt`, `runtime/ARCPRuntime.kt` |
| §7 | Capability negotiation | ✅ | `runtime/CapabilityNegotiation.kt` |
| §7.5 | Agent versioning (`name@version`) | ✅ | `runtime/AgentRegistry.kt` |
| §8 | Session handshake | ✅ | `runtime/ARCPRuntime.kt`, `client/ARCPClient.kt` |
| §8.2 | Authentication (`bearer`, `signed_jwt`) | ✅ | `auth/BearerAuth.kt`, `auth/JwtAuth.kt` |
| §8.4 | `result_chunk` streaming | ✅ | `messages/Execution.kt`, `client/ARCPClient.kt` |
| §9 | Leases & budgets | ✅ | `lease/` |
| §9.6 | `cost.budget` lease | ✅ | `lease/CostBudget.kt`, `lease/BudgetRegistry.kt` |
| §9.7 | `model.use` lease | ✅ | `lease/ModelUseLease.kt` |
| §9.8 | Provisioned credentials | ✅ | `credentials/` |
| §10 | Cancellation & delegation | ✅ | `messages/Control.kt`, `runtime/ARCPRuntime.kt` |
| §11 | Observability / metrics | ✅ | `messages/Telemetry.kt`, `trace/TraceContext.kt` |
| §12 | Error taxonomy | ✅ | `error/ErrorCode.kt`, `error/ARCPException.kt` |
| §15 | Vendor extensions | ✅ | `extensions/ExtensionRegistry.kt` |
| §16 | Artifacts | ✅ | `messages/Artifacts.kt` |
| §17.1 | Distributed tracing (W3C TraceContext) | ✅ | `trace/TraceContext.kt` |
| §18 | Error codes | ✅ | `error/ErrorCode.kt` |
| §19 | Session resume | ✅ | `store/EventLog.kt` |
| §21 | Extension naming (`arcpx.*`) | ✅ | `extensions/ExtensionRegistry.kt` |
| §22 | Reference transports | ✅ (memory) | `transport/MemoryTransport.kt` |
| WebSocket transport | — | 🔜 v0.2 | `transport/WebSocketTransport.kt` |
| Stdio transport | — | 🔜 v0.2 | `transport/StdioTransport.kt` |
| RFC § | Title | Catalog | Helpers | Dispatch | Notes |
|-------|-------|---------|---------|----------|-------|
| §6.1 | Envelope format | ✅ | ✅ | ✅ | `envelope/Envelope.kt` |
| §6.2 | Message catalog | ✅ | ✅ | partial | runtime handles ping, list_jobs, job.submit, metric, cancel, terminal job events, session.close; everything else returns `UNIMPLEMENTED` Nack |
| §6.3 | Resume | ✅ | ✅ | ⚠ deferred | `EventLog.replay()` exists; the runtime does not yet drive a `session.resume` flow |
| §6.4 | Idempotency | ✅ | ✅ | partial | `EventLog.recordIdempotent/lookupIdempotent`; runtime does not yet replay idempotent outcomes on resubmit |
| §6.6 | `session.list_jobs` / `session.jobs` | ✅ | ✅ | ✅ | `messages/Session.kt`, `runtime/JobInventory.kt`, `ARCPRuntime.handleListJobs` |
| §7 | Capability negotiation | ✅ | ✅ | ✅ | `runtime/CapabilityNegotiation.kt` |
| §7.5 | Agent versioning (`name@version`) | ✅ | ✅ | ✅ | `runtime/AgentRegistry.kt` |
| §8.1 | Session handshake (direct-credential) | ✅ | ✅ | ✅ | `runtime/ARCPRuntime.kt`, `client/ARCPClient.kt` |
| §8.1 | Session challenge/authenticate flow | ✅ | — | ⚠ deferred | runtime returns explicit `UNIMPLEMENTED` Nack; client cannot respond to a challenge |
| §8.2 | Authentication (`bearer`, `signed_jwt`) | ✅ | ✅ | ✅ | `auth/BearerAuth.kt`, `auth/JwtAuth.kt` (clock skew + optional issuer) |
| §8.4 | `result_chunk` streaming | ✅ | ✅ | partial | client-side `ResultChunkAssembler`; runtime does not emit result chunks itself |
| §9 | Leases & budgets | ✅ | ✅ | partial | runtime enforces `cost.budget` on `Metric` envelopes; lease subset checks live in `lease/LeaseSubset.kt` |
| §9.6 | `cost.budget` lease | ✅ | ✅ | ✅ | `lease/CostBudget.kt`, `lease/BudgetRegistry.kt`, `ARCPRuntime.handleMetric` |
| §9.7 | `model.use` lease | ✅ | ✅ | partial | parsed at submit; runtime does not yet enforce per-call model use |
| §9.8 | Provisioned credentials | ✅ | ✅ | ✅ | issued at job submit, revoked at terminal cleanup, retried with exponential backoff |
| §10.4 | Cancellation | ✅ | ✅ | ✅ | `messages/Control.kt`, `ARCPRuntime.handleCancel` (job-target only) |
| §10.5 | Interrupt | ✅ | — | ⚠ deferred | `Capabilities.interrupt` is negotiated; runtime does not yet dispatch the interrupt flow |
| §11 | Observability / metrics | ✅ | ✅ | partial | `Metric` is dispatched for `cost.*`; other metrics flow through as informational |
| §12 | Error taxonomy | ✅ | ✅ | ✅ | `error/ErrorCode.kt`, `error/ARCPException.kt` |
| §13 | Subscriptions | ✅ | partial | ⚠ deferred | `runtime/SubscriptionManager.kt`, `CompiledSubscriptionFilter.kt` exist; runtime does not dispatch `subscribe`/`unsubscribe` |
| §14 | Agent handoff / delegation | ✅ | — | ⚠ deferred | wire types exist; runtime returns `UNIMPLEMENTED` Nack |
| §15 | Vendor extensions | ✅ | ✅ | ✅ | `extensions/ExtensionRegistry.kt`; unknown extensions are silently dropped from negotiation rather than rejecting the session (RFC §21) |
| §16 | Artifacts | ✅ | ✅ | partial | `runtime/ArtifactStore.kt` for in-process use; runtime does not yet route `artifact.put`/`artifact.fetch` envelopes |
| §17.1 | Distributed tracing (W3C TraceContext) | ✅ | ✅ | ✅ | `trace/TraceContext.kt` |
| §18 | Error codes | ✅ | ✅ | ✅ | `error/ErrorCode.kt` |
| §19 | Session resume | ✅ | ✅ | ⚠ deferred | log-side replay works; runtime does not yet honor a `session.resume` envelope |
| §21 | Extension naming (`arcpx.*`) | ✅ | ✅ | ✅ | `extensions/ExtensionRegistry.kt`; unknown extensions on the wire produce `UNIMPLEMENTED` Nack at dispatch time |
| §22 | Reference transports | ✅ | ✅ (memory) | ✅ (memory) | `transport/MemoryTransport.kt` |
| WebSocket transport | — | — | 🔜 v0.2 | not on the public API |
| Stdio transport | — | — | 🔜 v0.2 | not on the public API |

## Notable v1.1 additions

- **`session.list_jobs` / `session.jobs`** (§6.6): principal-scoped in-memory
inventory with cursor pagination.
- **Agent versioning** (§7.5): `name@version` parsing, advertised descriptors,
and `AGENT_VERSION_NOT_AVAILABLE` error.
- **`result_chunk`** (§8.4): wire payloads plus client-side chunk assembly.
- **`session.list_jobs` / `session.jobs`** (§6.6): principal-scoped
in-memory inventory with opaque, sort-key-tied cursors.
- **Agent versioning** (§7.5): `name@version` parsing, advertised
descriptors, and `AGENT_VERSION_NOT_AVAILABLE` error.
- **`result_chunk`** (§8.4): wire payloads plus client-side chunk
assembly with content equality on the assembled bytes.
- **`cost.budget`** (§9.6): budget parser, counters, subset checks, and
`BUDGET_EXHAUSTED` error.
- **`model.use` and provisioned credentials** (§9.7, §9.8): lease matching,
credential wire types, provisioner interface, in-memory implementation,
redaction, issue/revoke hooks, and rotation status events.
- **Error taxonomy** (§12): `BUDGET_EXHAUSTED`, `AGENT_VERSION_NOT_AVAILABLE`,
and `LEASE_SUBSET_VIOLATION` are recognized wire codes.
`BUDGET_EXHAUSTED` error. The counter clamps over-spend so a misbehaving
agent cannot push the balance negative.
- **`model.use` and provisioned credentials** (§9.7, §9.8): lease
matching, credential wire types, provisioner interface, in-memory
implementation, redaction, issue/revoke hooks, and rotation status
events.
- **Error taxonomy** (§12): `BUDGET_EXHAUSTED`,
`AGENT_VERSION_NOT_AVAILABLE`, and `LEASE_SUBSET_VIOLATION` are
recognized wire codes.

## Conformance testing

Integration tests live in `:tests` and target the public SDK surface over
`MemoryTransport`. Run with:
Integration tests live in `:tests` and target the public SDK surface
over `MemoryTransport`. Run with:

```bash
./gradlew :tests:test
```

For cross-language conformance tracking, refer to the ARCP spec repository
and shared issue milestones.
For cross-language conformance tracking, refer to the ARCP spec
repository and shared issue milestones.

## Version reporting

The Gradle artifact ships as `dev.arcp:arcp:1.1.0`, but the in-process
`dev.arcp.Version.SDK_VERSION` constant (which feeds `RuntimeIdentity.version`
on handshakes and the CLI `version` output) currently reads `0.1.0`. The
two are intentionally decoupled while the protocol-driving CLI subcommands
catch up; both numbers will align in a future release.
`dev.arcp.Version.SDK_VERSION` constant (which feeds
`RuntimeIdentity.version` on handshakes and the CLI `version` output)
currently reads `0.1.0`. The two are intentionally decoupled while the
protocol-driving CLI subcommands catch up; both numbers will align in a
future release.
19 changes: 14 additions & 5 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,37 @@ are declared as `api` dependencies and are pulled in automatically.

## Minimal example

The snippet below opens a session, submits a job, and closes cleanly.
It uses `MemoryTransport` — the same transport the integration tests use;
swap it for `WebSocketTransport` or `StdioTransport` in production.
The snippet below pairs a client and runtime over `MemoryTransport`,
registers one agent, opens a session, submits a job, and closes
cleanly. It is the same shape the integration tests use and runs
end-to-end as written.

`MemoryTransport` is the only transport on the v0.1 public API surface
(`dev.arcp.transport.Transport`); the WebSocket and stdio transports
referenced elsewhere in the docs are planned for a future SDK release.

```kotlin
import dev.arcp.client.ARCPClient
import dev.arcp.messages.Capabilities
import dev.arcp.runtime.ARCPRuntime
import dev.arcp.runtime.AgentRegistry
import dev.arcp.transport.MemoryTransport
import dev.arcp.auth.StaticBearerAuth
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
// 1. Paired in-memory transport (client ↔ runtime).
val (clientTransport, runtimeTransport) = MemoryTransport.pair()

// 2. Runtime with one registered agent.
// 2. Runtime with one registered agent. The bearer auth on both
// sides must agree — here the runtime maps "my-token" to a
// principal name, and the client carries the same token.
val registry = AgentRegistry()
registry.register("summarise", "1.0.0", default = true)
val runtime = ARCPRuntime(
supportedCapabilities = Capabilities(streaming = true),
agentRegistry = registry,
bearerAuth = StaticBearerAuth(mapOf("my-token" to "quickstart")),
)

// 3. Let the runtime accept the connection in the background.
Expand Down Expand Up @@ -90,5 +99,5 @@ cd kotlin-sdk
## Next steps

- [Architecture](architecture.md) — understand the layering before writing more code
- [Transports](transports.md) — connect over WebSocket or stdio
- [Transports](transports.md) — currently only `MemoryTransport` is public; WebSocket/stdio are planned
- [Guides](README.md#guides) — deep-dives on sessions, jobs, leases, and more
4 changes: 3 additions & 1 deletion docs/guides/job-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ progress, heartbeat, chunk, and status events.
Sent when the agent begins executing:

```kotlin
is JobStarted -> println("Job ${msg.jobId} is now running")
// JobStarted carries only startedAt on its payload; the job id lives on
// the envelope (see Execution.kt).
is JobStarted -> println("Job ${env.jobId} started at ${msg.startedAt}")
```

## JobProgress
Expand Down
Loading
Loading