stapel-runner-protocol
The wire protocol between the Stapel Studio orchestrator (hub) and the in-container runner, plus the harness-adapter seam the runner uses to drive a concrete model↔harness backend. This is the OSS half of the seam (studio-design §2.1): the community brings harness backends; the brains (anti-cheat, permissions, prompts, routing) stay private.
A small, Django-free, pure-Python library. Two halves:
- Protocol — versioned JSON messages, a transport-agnostic encode/decode
core, an optional
websocketsbinding, and a sequencing state machine (idempotent progress, resume/replay on reconnect). Task and report schemas are imported fromstapel-taskspecs, not re-invented. - Harness adapters — an ABC plus reference adapters for claude-agent-sdk (persistent session) and a subprocess JSONL harness.
Install
pip install stapel-runner-protocol
# optional extras:
pip install "stapel-runner-protocol[websockets]" # the WS transport binding
pip install "stapel-runner-protocol[agent-sdk]" # the claude-agent-sdk adapter
Runtime deps are just stapel-taskspecs and jsonschema. Import is lazy
(PEP 562): import stapel_runner_protocol pulls in nothing heavy until you
touch a symbol.
The protocol (studio-design §2.1)
The connection is always outbound from the runner (the project container holds no inbound management ports, S3), authorized by a project-scoped short-lived token.
runner → hub: hello {protocol_version, project_id, runner_version, capabilities[], resume{}}
hub → runner: task.assign {task: TaskSpec, role, invocation{backend, model, session,
system_prompt_ref, allowed_tools[], llm_base_url, budget}, seq}
runner → hub: task.progress {task_id, seq, kind: state|tool|controls|usage, payload}
runner → hub: task.report {report: TaskReport, seq} # status done|blocked|failed
hub → runner: task.cancel {task_id, reason, seq} # graceful, at an automaton boundary
both: ping / pong
resume: after a reconnect the runner declares last_seq per task in `hello.resume`;
the hub re-sends / replays anything after it (checkpoint = git commit)
| Message | Schema | Model |
|---|---|---|
| hello | schemas/hello.v1.json |
Hello |
| task.assign | schemas/task_assign.v1.json |
TaskAssign (embeds a taskspecs TaskSpec) |
| task.progress | schemas/task_progress.v1.json |
TaskProgress |
| task.report | schemas/task_report.v1.json |
TaskReportMessage (embeds a taskspecs TaskReport) |
| task.cancel | schemas/task_cancel.v1.json |
TaskCancel |
| ping / pong | schemas/ping.v1.json / pong.v1.json |
Ping / Pong |
Every frame carries schema_version: 1. Envelopes are forward-compatible:
unknown top-level fields are preserved (in model.extra) and re-emitted.
Encode / decode
from stapel_runner_protocol import Hello, encode, decode
frame = encode(Hello(project_id="brave-falcon-1042", runner_version="0.1.0",
capabilities=["agent-sdk", "subprocess"]))
msg = decode(frame, validate=True) # dispatches on `type`, validates the schema
Idempotency and resume
Progress is idempotent by (task_id, seq); hub→runner commands are buffered and
replayed. The state machine is transport-agnostic and synchronous:
from stapel_runner_protocol import ConnectionState, TaskCancel
hub, runner = ConnectionState("hub"), ConnectionState("runner")
c1 = hub.emit(TaskCancel(task_id="T-1", reason="a")) # seq assigned = 1
c2 = hub.emit(TaskCancel(task_id="T-1", reason="b")) # seq = 2
runner.receive(c1) # runner got 1, then dropped
resume = runner.resume_map() # {"T-1": 1} -> goes in hello.resume
replay = hub.replay_for(resume) # {"T-1": [c2]} -> hub re-sends the tail
A runner may safely re-send progress from its last checkpoint (a git commit) after a reconnect — the hub drops the duplicate seqs.
Harness adapters
adapter.open_session(role_spec) -> Session
Session.turn(prompt) -> TurnResult # {text, usage}
adapter.oneshot(role_spec, prompt) -> TurnResult
Session.close() / adapter.close()
TurnResult.usage is always the strict five-component
stapel_taskspecs.UsageSplit and is validated on construction — a protocol
condition, not a convenience (without the thinking column, reasoning-class
economics are understated, studio-design 7.16).
from stapel_runner_protocol import RoleSpec, SubprocessAdapter
role = RoleSpec.from_invocation(assign.invocation, role="coder")
adapter = SubprocessAdapter(command=["my-harness", "--json"])
result = adapter.oneshot(role, "implement the task")
result.text, result.usage.total_tokens
Reference adapters:
AgentSdkAdapter— a persistent claude-agent-sdk session per task (append-only, cache_read-friendly — the coder's slice configuration). The SDK is an optional extra; inject aclient_factoryto test or to wire a custom client.SubprocessAdapter— a JSONL stream to a child process ({"type":"turn"}in,{"type":"result","text","usage"}out). Injectspawnto test against a fake child.
Out of scope (moat)
Anti-cheat rules, permission policies, concrete system prompts, and routing config are not here and never will be. Roles, progress kinds, capabilities and statuses are open strings, not closed enums (a closed enum would leak private routing policy into an OSS schema). This library carries the shape and the sequencing discipline, nothing of the pipeline's brains.
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stapel_runner_protocol-0.1.1.tar.gz.
File metadata
- Download URL: stapel_runner_protocol-0.1.1.tar.gz
- Upload date:
- Size: 32.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a55a9a40a10ada2da4ccc6c5b278b2e2039393c85e9cb6dedd19fbb047fd085
|
|
| MD5 |
3e63a7435764c7cf84f164403fa0d231
|
|
| BLAKE2b-256 |
5a0a4b914e97f828487b85439cfe1a3ee6180cce6de17cfa7ea65dd75b4969fc
|
Provenance
The following attestation bundles were made for stapel_runner_protocol-0.1.1.tar.gz:
Publisher:
publish.yml on usestapel/stapel-runner-protocol
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_runner_protocol-0.1.1.tar.gz -
Subject digest:
2a55a9a40a10ada2da4ccc6c5b278b2e2039393c85e9cb6dedd19fbb047fd085 - Sigstore transparency entry: 2568438860
- Sigstore integration time:
-
Permalink:
usestapel/stapel-runner-protocol@201e4df59c6b030508a1d97020ee02db29cc35e1 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@201e4df59c6b030508a1d97020ee02db29cc35e1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file stapel_runner_protocol-0.1.1-py3-none-any.whl.
File metadata
- Download URL: stapel_runner_protocol-0.1.1-py3-none-any.whl
- Upload date:
- Size: 32.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
28963009a5d7932d7941e42c9953ccbcf2911a1cf3ba44da141e425e036b6c17
|
|
| MD5 |
6e55def53eea77f3d04367c64b082b5f
|
|
| BLAKE2b-256 |
e88c7900ede6e928add8e785dca5cbeef46395329e4abbd19b54dd37b65ad5be
|
Provenance
The following attestation bundles were made for stapel_runner_protocol-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on usestapel/stapel-runner-protocol
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_runner_protocol-0.1.1-py3-none-any.whl -
Subject digest:
28963009a5d7932d7941e42c9953ccbcf2911a1cf3ba44da141e425e036b6c17 - Sigstore transparency entry: 2568438883
- Sigstore integration time:
-
Permalink:
usestapel/stapel-runner-protocol@201e4df59c6b030508a1d97020ee02db29cc35e1 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@201e4df59c6b030508a1d97020ee02db29cc35e1 -
Trigger Event:
push
-
Statement type: