Java SDK for the Agent Runtime Control Protocol (ARCP). Targets JDK 21
LTS; tested on 21. Depends on Jackson and SLF4J only at the api level —
no logging binding shipped.
dependencies {
implementation("dev.arcp:arcp:1.0.0") // umbrella
// or, granular:
implementation("dev.arcp:arcp-client:1.0.0") // client only
implementation("dev.arcp:arcp-runtime:1.0.0") // runtime only
implementation("dev.arcp:arcp-runtime-jetty:1.0.0") // WebSocket server
implementation("dev.arcp:arcp-otel:1.0.0") // OpenTelemetry tracing
}<dependency>
<groupId>dev.arcp</groupId>
<artifactId>arcp</artifactId>
<version>1.0.0</version>
</dependency>import dev.arcp.client.ArcpClient;
import dev.arcp.client.JobHandle;
import dev.arcp.core.transport.MemoryTransport;
import dev.arcp.runtime.ArcpRuntime;
import dev.arcp.runtime.agent.JobOutcome;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
class Quickstart {
public static void main(String[] args) throws Exception {
MemoryTransport[] pair = MemoryTransport.pair();
ArcpRuntime runtime = ArcpRuntime.builder()
.agent("echo", "1.0.0",
(input, ctx) -> JobOutcome.Success.inline(input.payload()))
.build();
runtime.accept(pair[0]);
try (ArcpClient client = ArcpClient.builder(pair[1]).build()) {
client.connect(java.time.Duration.ofSeconds(5));
JobHandle handle = client.submit(ArcpClient.jobSubmit(
"echo@1.0.0", JsonNodeFactory.instance.objectNode().put("hi", 1)));
System.out.println(handle.result().get().result());
}
runtime.close();
}
}For a WebSocket-backed runtime, swap MemoryTransport for
dev.arcp.runtime.jetty.ArcpJettyServer on the runtime side and
dev.arcp.client.WebSocketTransport.connect(uri) on the client side.
For a child-process agent, both peers wrap their stdin / stdout in
dev.arcp.core.transport.StdioTransport (§4.2 newline-delimited JSON).
| Artifact | What's in it | Depends on |
|---|---|---|
arcp-core |
Wire types, errors, capability, ids, lease, transport SPI | none |
arcp-client |
ArcpClient, JobHandle, ResultStream, JDK WebSocket transport |
arcp-core |
arcp-runtime |
ArcpRuntime, session FSM, job FSM, lease enforcement, budget counters |
arcp-core |
arcp |
Umbrella; re-exports client + runtime | arcp-client, arcp-runtime |
arcp-runtime-jetty |
Embedded Jetty 12 WebSocket server transport | arcp-runtime |
arcp-middleware-spring-boot |
Spring Boot 3.x auto-config + WebSocket handler | arcp-runtime |
arcp-middleware-jakarta |
Plain Jakarta WebSocket adapter (ServerEndpointConfig) |
arcp-runtime |
arcp-middleware-vertx |
Vert.x 5 WebSocket handler | arcp-runtime |
arcp-otel |
OpenTelemetry adapter (transport-wrapping Tracer) |
arcp-core, opentelemetry-api |
arcp-tck |
Reusable JUnit 5 @TestFactory conformance suite |
arcp-client, arcp-runtime |
java-sdk/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/libs.versions.toml
├── arcp-core/
├── arcp-client/
├── arcp-runtime/
├── arcp/ # umbrella
├── arcp-runtime-jetty/
├── arcp-otel/
├── arcp-middleware-jakarta/
├── arcp-middleware-spring-boot/
├── arcp-middleware-vertx/
├── arcp-tck/
├── docs/
│ ├── 00-overview.md
│ ├── 01-quickstart.md
│ ├── 02-concepts.md
│ ├── 03-features/ # heartbeats, ack, list-jobs, subscribe, progress,
│ │ # result-chunk, lease-expires-at, cost-budget,
│ │ # agent-versions
│ ├── 05-reference/ # api-overview, error-taxonomy, wire-format
│ └── diagrams/ # 6 Graphviz diagrams (light + dark SVGs)
└── examples/
├── submit-and-stream/
├── cancel/
├── heartbeat/
├── cost-budget/
├── result-chunk/
├── agent-versions/
├── list-jobs/
├── lease-expires-at/
├── idempotent-retry/
└── custom-auth/
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
./gradlew build # compile + test all modules
./gradlew :examples:submit-and-stream:run
./gradlew :arcp-runtime-jetty:test- §5.1 envelope with
arcp: "1"andFAIL_ON_UNKNOWN_PROPERTIES=false - §6.1 bearer auth via
BearerVerifierSPI;acceptAnyandstaticTokenhelpers - §6.2 capability intersection with rich
agentsshape (name + versions + default) - §6.3 resume buffer (in-memory ring) and rotating
resume_token - §6.4 heartbeats: scheduler-driven ping; client and runtime treat two missed
intervals as
HEARTBEAT_LOST - §6.5
session.ackwith auto-emit rate limit on the client side - §6.6
session.list_jobsscoped to the session's principal - §7.1
job.submitwithlease_request,lease_constraints,idempotency_key - §7.2 idempotency: identical
(principal, key, payload)reuses the priorjob_id; conflicting payload yieldsDUPLICATE_KEY - §7.4 cooperative cancellation via
JobContext.cancelled()+Thread.interrupt - §7.5 agent versioning:
name@versiongrammar; bare names resolve to advertised default; unknown versions surfaceAGENT_VERSION_NOT_AVAILABLE - §7.6 subscribe / unsubscribe with optional history replay; subscribers do not carry cancel authority
- §8.2 ten event kinds via sealed
EventBody, includingprogress(current ≥ 0) - §8.4
result_chunkreassembly viaResultStream(in-memory orOutputStreamsink) - §9 lease grammar + subset check (
Lease.contains);LeaseGuardenforces glob patterns with*and**semantics - §9.5 lease expiration: strict UTC-
Zparsing; scheduled watchdog terminates jobs whose lease expires while running - §9.6
cost.budgetvia per-currencyAtomicReference<BigDecimal>counters withUSE_BIG_DECIMAL_FOR_FLOATSon the wire mapper - §11 OpenTelemetry trace propagation via
ArcpOtel.withTracing(transport, tracer);arcp.session_id/arcp.job_id/arcp.trace_idattributes on every span - §12 fifteen-code error taxonomy with sealed
ArcpException/RetryableArcpException/NonRetryableArcpExceptionsplit
Virtual threads (JEP 444, stable in JDK 21) drive every per-job worker and
every transport publisher. StructuredTaskScope is intentionally not used
in published bytecode: it's preview in JDK 21 and finalized in JDK 25 with
a different shape, and the SDK targets --release 21.
A single ScheduledExecutorService per runtime drives heartbeat ticks and
lease expiry watchdogs; client-side, a similar scheduler emits session.ack
and watches the inbound idle timer.
See CONFORMANCE.md for the spec §-keyed table with file:line references. Tests at a glance:
arcp-core:test— envelope round-trip, unknown-field tolerance, capability intersection, feature decodearcp-runtime:test— agent version resolution, budget counters, lease guard, expiry, subset checksarcp-client:test— smoke round-trip, idempotency reuse + conflict, subscribe with history replay, result-chunk reassemblyarcp-otel:test— outbound + inbound spans throughInMemorySpanExporterarcp-runtime-jetty:test— end-to-end client + runtime over loopback WebSocketarcp-middleware-spring-boot:test— Spring Boot 3.x autoconfig + WebSocket handler driven from a@SpringBootTestwith an embedded Tomcatarcp-middleware-jakarta:test— Jakarta WebSocketServerEndpointConfigregistered against an embedded Jetty containerarcp-middleware-vertx:test— Vert.x 5HttpServer.webSocketHandlerdriven from aVertx.vertx()test fixturearcp-tck:test— seven dynamic conformance tests via JUnit@TestFactory, reusable by downstream JVM implementations
Diagrams under docs/diagrams/: module graph, session
lifecycle, job lifecycle, capability negotiation, heartbeat + ack, result-chunk
reassembly. Light + dark variants render via make -C docs/diagrams.
Maven Central credentials and PGP signing are wired in the root
build.gradle.kts. Set the following Gradle properties (in
~/.gradle/gradle.properties or as ORG_GRADLE_PROJECT_* env vars):
ossrhUsername=...
ossrhPassword=...
signingKey=... # PGP private key, in-memory format
signingPassword=...
Snapshot vs release routing is automatic from the version qualifier;
signing is required only when the PGP credentials are present (local
builds skip it silently). Publish a SNAPSHOT to local Maven with:
./gradlew :arcp-core:publishToMavenLocalPIT is wired on arcp-core and arcp-runtime as an opt-in Gradle task:
./gradlew :arcp-core:pitest
./gradlew :arcp-runtime:pitestReports land under each module's build/reports/pitest/. The mutation
threshold is set to 0 so the run never fails the build; treat the HTML
report as a code-review signal, not a gate.