Overview
Application workers are long-lived processes that:
- Open a bidirectional
WorkgRPC stream to the runtime — one persistent connection both sides can send messages over, so the runtime can push jobs and the worker can push results without reconnecting each time. - Register the
tool@versionhandlers they implement (with capacity and contract metadata) — "here's what I can do and how many at once." - Receive
invokemessages, run handler code, and sendresult(the answer) ornack("I can't take this one"). - Send heartbeats to renew their lease — a periodic "still alive" ping so the runtime knows the worker hasn't died mid-job.
The runtime routes model-emitted calls to registered workers. Workers do not call the model; they only execute tools.
Concept examples
Register (first message on the Work stream):
worker_id: worker-7
handlers:
- tool: weather.get-forecast
version: "1.0.0"
max_concurrency: 4Invoke → result → ack:
Runtime → invoke(call_id=c1, tool=weather.get-forecast, args={ city: "Oslo" })
Worker → result(call_id=c1, payload={ temp_c: 12 })
Runtime → result_ack(call_id=c1) # worker may drop local state nowHeartbeat (keeps the lease alive):
every ~TTL/2 seconds: worker → heartbeat
lease expired + no heartbeat → runtime treats worker as goneConnect and register
Use the generated client for phrony.runtime.v1.Runtime / Work:
Runtime.Work(stream WorkClientMsg) returns (stream WorkServerMsg)First client message: register with:
| Field | Description |
|---|---|
worker_id | Stable id for this process instance |
workload_identity | Workload identity (for example SPIFFE ID, mTLS subject, or deployment label) checked against the tool allowlist when configured |
image_digest | OCI image digest of the running worker image |
handlers | List of tool, version, contract_version, descriptor_hash, max_concurrency |
in_flight | Optional call_ids still executing or holding a result awaiting runtime ack (reconnect resync) |
The runtime responds with registered and a lease TTL. Send heartbeat before the lease expires (typically at half the TTL interval).
Invocation lifecycle
sequenceDiagram participant R as Runtime participant W as Worker R->>W: invoke (call_id, tool, version, args, deadline) W->>W: Run handler W->>R: result (payload or error) R->>R: Persist outcome R->>W: result_ack (call_id)
| Server → worker | Meaning |
|---|---|
invoke | Run one call; includes call_id, session_id, args, side_effect_class, deadline |
cancel | Session cancelled; stop work for call_id |
result_ack | Durable result recorded; worker may drop local state for call_id |
| Worker → server | Meaning |
|---|---|
result | Success ( payload) or handler error (error code + message) |
nack | Worker refuses or cannot run the call (routing-level) |
Hold results until result_ack. On reconnect, re-advertise in-flight call_ids in register so the runtime can dedupe and resume.
Handler advertisement
Each handler advertises a routing key tool@version that must match the manifest binding ref and version (not necessarily the wire name shown to the model).
Example manifest binding:
spec:
tools:
- ref: weather.get-forecast
version: "1.0.0"
side_effect_class: read_onlyThe worker registers tool: weather.get-forecast, version: 1.0.0, plus contract_version and max_concurrency.
Environment variables
Typical worker configuration:
| Variable | Description |
|---|---|
PHRONY_RUNTIME_ADDR | Runtime gRPC address (default 127.0.0.1:7777) |
WORKER_ID | Instance id |
WORKER_WORKLOAD_IDENTITY | Identity for allowlist checks |
WORKER_IMAGE_DIGEST | Image digest for allowlist checks |
Example worker
The tool-worker-playground repository is a minimal Go worker that registers a weather handler and connects to a local runtime. Use it as a reference implementation for stream handling, heartbeats, and dispatch.
Interactive sessions
When a session runs with attach/streaming, clients receive tool_call and tool_result events on the interactive stream, and approval_required when HITL blocks a call. Approvals use a separate client message on the interactive RPC.
Up next