Skip to main content

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:

  1. Protocol — versioned JSON messages, a transport-agnostic encode/decode core, an optional websockets binding, and a sequencing state machine (idempotent progress, resume/replay on reconnect). Task and report schemas are imported from stapel-taskspecs, not re-invented.
  2. 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 a client_factory to test or to wire a custom client.
  • SubprocessAdapter — a JSONL stream to a child process ({"type":"turn"} in, {"type":"result","text","usage"} out). Inject spawn to 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

stapel_runner_protocol-0.1.1.tar.gz (32.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stapel_runner_protocol-0.1.1-py3-none-any.whl (32.0 kB view details)

Uploaded Python 3

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

Hashes for stapel_runner_protocol-0.1.1.tar.gz
Algorithm Hash digest
SHA256 2a55a9a40a10ada2da4ccc6c5b278b2e2039393c85e9cb6dedd19fbb047fd085
MD5 3e63a7435764c7cf84f164403fa0d231
BLAKE2b-256 5a0a4b914e97f828487b85439cfe1a3ee6180cce6de17cfa7ea65dd75b4969fc

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_runner_protocol-0.1.1.tar.gz:

Publisher: publish.yml on usestapel/stapel-runner-protocol

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stapel_runner_protocol-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for stapel_runner_protocol-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 28963009a5d7932d7941e42c9953ccbcf2911a1cf3ba44da141e425e036b6c17
MD5 6e55def53eea77f3d04367c64b082b5f
BLAKE2b-256 e88c7900ede6e928add8e785dca5cbeef46395329e4abbd19b54dd37b65ad5be

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page