| title | Architecture | ||||||
|---|---|---|---|---|---|---|---|
| sdk | java | ||||||
| spec_sections |
|
||||||
| kind | concept | ||||||
| since | 1.0.0 |
Every wire message rides in one JSON envelope:
| Field | Java type | Required | Notes |
|---|---|---|---|
arcp |
String |
yes | Envelope.VERSION = "1.1" |
id |
MessageId |
yes | ULID, monotonic per process |
type |
discriminator → Message.Type |
yes | |
payload |
JsonNode, decoded per type |
yes | |
session_id |
SessionId |
absent on session.hello, required after |
|
trace_id |
TraceId |
optional | propagated end-to-end for §11 |
job_id |
JobId |
required on job-scoped messages | |
event_seq |
Long |
required on job.event |
monotonic, gap-free per session |
Unknown top-level fields are dropped on parse — Jackson
ArcpMapper
sets FAIL_ON_UNKNOWN_PROPERTIES=false for forward compatibility.
Seventeen wire types, all members of the sealed
Message
interface. Dispatch is exhaustive at compile time.
type |
Java record | Direction |
|---|---|---|
session.hello |
SessionHello |
client → runtime |
session.welcome |
SessionWelcome |
runtime → client |
session.bye |
SessionBye |
either |
session.ping |
SessionPing |
runtime → client |
session.pong |
SessionPong |
client → runtime |
session.ack |
SessionAck |
client → runtime |
session.list_jobs |
SessionListJobs |
client → runtime |
session.jobs |
SessionJobs |
runtime → client |
job.submit |
JobSubmit |
client → runtime |
job.accepted |
JobAccepted |
runtime → client |
job.event |
JobEvent |
runtime → client |
job.result |
JobResult |
runtime → client |
job.error |
JobError |
runtime → client |
job.cancel |
JobCancel |
client → runtime |
job.subscribe |
JobSubscribe |
client → runtime |
job.subscribed |
JobSubscribed |
runtime → client |
job.unsubscribe |
JobUnsubscribe |
client → runtime |
Ten event kinds, sealed under
EventBody.
kind |
Java record | Notes |
|---|---|---|
log |
LogEvent |
structured log line from agent |
thought |
ThoughtEvent |
agent reasoning step |
tool_call |
ToolCallEvent |
outbound tool invocation |
tool_result |
ToolResultEvent |
inbound tool response |
status |
StatusEvent |
lifecycle phase change |
metric |
MetricEvent |
numeric measurement, drives cost.budget |
artifact_ref |
ArtifactRefEvent |
reference to a produced artifact |
delegate |
DelegateEvent |
sub-agent job dispatch record |
progress |
ProgressEvent |
current / total progress tuple |
result_chunk |
ResultChunkEvent |
one piece of a chunked final result |
A session is one transport connection with one negotiated feature set.
- Client sends
session.hellocarryingauth(bearer token) and afeatureslist of capability strings. - Runtime verifies auth, intersects feature lists
(
Capabilities.intersect), and replies withsession.welcomecarrying the intersection plus a freshresume_token. - Both peers MUST NOT use any feature outside the negotiated intersection.
When both peers negotiate heartbeat, the runtime schedules a session.ping
after each idle interval. The client replies with session.pong. Two missed
intervals on either side surface HEARTBEAT_LOST (retryable — resume the
session). See guides/sessions.md for details.
A client with ack negotiated emits session.ack { last_processed_seq }
periodically so the runtime can free buffered events earlier. Advisory, not
flow-controlling. See guides/sessions.md.
A fresh session.hello carrying resume_token and last_event_seq from a
prior session replays buffered events from the in-memory
ResumeBuffer.
See guides/resume.md.
A job is one agent invocation. State machine:
PENDING → RUNNING → { SUCCESS | ERROR | CANCELLED | TIMED_OUT }
- Submit:
client.submit(jobSubmit)blocks untiljob.acceptedreturns with the resolvedagent@version, effective lease, and initial budget. - Events:
handle.events()is a bufferedFlow.Publisher<EventBody>. Late subscribers see the full history (replaying publisher). - Result:
handle.result()is aCompletableFuture<JobResult>that completes onjob.resultor fails with the matchingArcpExceptiononjob.error. - Cancel:
handle.cancel()sendsjob.cancel; the runtime interrupts the worker virtual thread. Agents checkctx.cancelled()cooperatively.
Terminal states are sticky — competing transitions lose to the first writer via
CAS on
JobRecord.Status.
A Lease is a bag of namespace → glob-pattern lists. Reserved namespaces:
| Namespace | Meaning | Spec |
|---|---|---|
fs.read |
Filesystem path globs for reading | §9.2 |
fs.write |
Filesystem path globs for writing | §9.2 |
net.fetch |
Outbound URL globs | §9.2 |
tool.call |
Tool-name globs | §9.2 |
agent.delegate |
Sub-agent name globs | §9.2 |
cost.budget |
currency:decimal upper bounds |
§9.6 |
model.use |
Model-ID globs (v1.1) | §9.7 |
Agents authorize per-operation via ctx.authorize(namespace, pattern). The
LeaseGuard
matches globs (* = any chars except /, ** = any chars including /),
checks expires_at if set, and throws PermissionDeniedException on miss.
See guides/leases.md for full details.
The agent field in job.submit accepts name or name@version. The
runtime advertises available versions in session.welcome. A bare name
resolves to the registered default; name@version requires an exact match.
ArcpRuntime runtime = ArcpRuntime.builder()
.agent("code-refactor", "1.0.0", v1)
.agent("code-refactor", "2.0.0", v2)
.build();
runtime.agents().setDefault("code-refactor", "2.0.0");Grammar: AgentRef.parse;
resolution: AgentRegistry.resolve.
Fifteen canonical
ErrorCode
values. The sealed
ArcpException
hierarchy splits at RetryableArcpException / NonRetryableArcpException —
a generic retry loop cannot accidentally retry LEASE_EXPIRED or
BUDGET_EXHAUSTED.
| Code | Retryable | Java exception |
|---|---|---|
PERMISSION_DENIED |
no | PermissionDeniedException |
LEASE_SUBSET_VIOLATION |
no | LeaseSubsetViolationException |
JOB_NOT_FOUND |
no | JobNotFoundException |
DUPLICATE_KEY |
no | DuplicateKeyException |
AGENT_NOT_AVAILABLE |
no | AgentNotAvailableException |
AGENT_VERSION_NOT_AVAILABLE |
no | AgentVersionNotAvailableException |
CANCELLED |
no | CancelledException |
TIMEOUT |
yes | TimeoutException |
RESUME_WINDOW_EXPIRED |
no | ResumeWindowExpiredException |
HEARTBEAT_LOST |
yes | HeartbeatLostException |
LEASE_EXPIRED |
no | LeaseExpiredException |
BUDGET_EXHAUSTED |
no | BudgetExhaustedException |
INVALID_REQUEST |
no | InvalidRequestException |
UNAUTHENTICATED |
no | UnauthenticatedException |
INTERNAL_ERROR |
yes | InternalErrorException |
See guides/errors.md for the full table plus a retry helper pattern.
- Virtual threads (JEP 444, stable JDK 21) drive every per-job worker and every transport publisher dispatch.
- One
ScheduledExecutorServiceper runtime fires heartbeat ticks and lease-expiry watchdogs (platform threads for the scheduler, virtual-thread runnables for the callbacks — the JDK does not provide a virtual-threadScheduledExecutorService). StructuredTaskScopeis intentionally absent from published bytecode: it is preview in JDK 21 and finalised with a different shape in JDK 25; the SDK targets--release 21.
| Setting | Reason |
|---|---|
registerModule(new JavaTimeModule()) |
Instant for §9.5 expires_at |
enable(USE_BIG_DECIMAL_FOR_FLOATS) |
§9.6 budget arithmetic — no double-precision drift |
disable(FAIL_ON_UNKNOWN_PROPERTIES) |
forward-compatible envelope |
disable(WRITE_DATES_AS_TIMESTAMPS) |
ISO-8601 on the wire |
setSerializationInclusion(NON_NULL) |
omit null fields globally |
Override per runtime or per client via ArcpRuntime.Builder.mapper(ObjectMapper)
and ArcpClient.Builder.mapper(ObjectMapper).