Skip to main content

salvor (Python)

A thin Python client for the Salvor control plane.

pip install salvor
from salvor import Client

with Client("http://127.0.0.1:8080") as client:
    agent = client.register_agent(open("agent.toml").read())
    run_id = client.start_run(agent, {"question": "..."})

    for event in client.stream_events(run_id):
        print(event.seq, event.kind)

    state = client.get_run(run_id)
    print(state.status.state)

You need a control plane to talk to: npm install -g salvor && salvor serve, or see the repository for other install routes.

What the control plane is

Salvor is a durable execution runtime for AI agents. A run is an append-only log of events: every model call and every tool call is recorded before the run moves on, so a process that dies mid-flight is recovered from the log and finished from exactly where it stopped, with no completed step run twice.

The control plane is a small HTTP and server-sent-events server that puts that runtime on a network. It owns one event store and drives runs in the background. You submit an agent definition and an input, then read the run's events as they land. The full contract is in crates/salvor-server/API.md.

Why the SDK is thin

The durability guarantees stay in one Rust process. Exact replay, crash-safe resume, and the write-ahead rule that parks a run whose write was recorded but never completed all live server-side, enforced by the same runtime the CLI uses. So this SDK is a few hundred lines: it submits data, reads events, and maps the server's error envelope to exceptions. It holds no agent loop, no run state, and no durability logic of its own. Because the server does all the work, the SDK stays consistent with it by construction.

Install

pip install salvor

The one runtime dependency is httpx. To work on the SDK itself, install it from a checkout instead: pip install -e sdks/python.

The client surface

from salvor import Client

client = Client("http://127.0.0.1:8080", token=None)

agent    = client.register_agent(toml_or_dict)      # -> agent hash
run_id   = client.start_run(agent, input=None)      # -> run id
state    = client.get_run(run_id)                   # -> RunState
runs     = client.list_runs()                       # -> list[RunSummary]
stream   = client.stream_events(run_id, from_seq=None)  # -> EventStream
result   = client.resume(run_id, input=None)        # -> ResumeResult
state    = client.resolve(run_id, output)           # record a dangling write
projected = client.replay(run_id)                   # -> ReplayState (dry run)

register_agent accepts a TOML string (sent as application/toml) or a dict of the same fields (sent as application/json). An agent is data, so it has a content hash; submit it once and reference it by that hash on every start.

The streaming and cursor model

stream_events returns an EventStream you iterate for Event objects in sequence order:

stream = client.stream_events(run_id)
for event in stream:
    print(event.seq, event.kind)
print(stream.end.status.state)   # the resting status the end frame carried

On connect the server replays every recorded event at or after the cursor, then tails new events as they land, then sends one terminal end frame and closes. A run's log has contiguous, ascending sequence numbers, so the stream is gap-free and duplicate-free by construction, and the client only has to track one number: the next sequence to expect.

That same number is what makes a dropped connection recoverable. If the socket drops mid-tail, the client reconnects with ?from_seq=<next> and the server resumes from there. Any event that arrived just before the drop is skipped by sequence number, so the merged stream stays gap-free and duplicate-free across the reconnect. Iteration stops at the end frame; its status (and a detached flag, set when the run is mid-step with no driver in this server process) is then on stream.end.

Errors

Every server error is decoded from the one JSON envelope ({"error": {"code", "message", "details?}}) into a SalvorAPIError carrying the stable code and the message. The one refusal with structured evidence, a resume blocked because a write was recorded but never completed, raises NeedsReconciliationError, whose .intent is the recorded write. Verify what that write did, then call resolve(run_id, output) to record its completion so replay never re-runs it.

from salvor import NeedsReconciliationError

try:
    client.resume(run_id)
except NeedsReconciliationError as e:
    print("stuck on write:", e.intent.get("tool"), e.intent.get("input"))
    client.resolve(run_id, output={"charged": True})
    client.resume(run_id)

The two modes

Salvor has two modes, and this SDK speaks both. The one above is server-driven: start_run hands the agent loop to the server, which drives it in a background task, and you read the events it produces. The second is client-driven: your code owns the loop and streams the events it produces, while the server still owns the durable log and, on every append, re-folds the log to confirm the incoming event is the one legal next event. The two never collide: a client-driven run and a server-driven run cannot share an id, and each surface serves only its own runs.

Open a client-driven run and drive it with a ClientRunDriver:

from salvor import Client

with Client("http://127.0.0.1:8080") as client:
    run = client.open_client_run(record_prompts=False)   # -> ClientRunDriver

    # The client emits its own control and context events through the guarded
    # append; the server confirms each is the legal next event before recording.
    run.append([run.envelope(0, "RunStarted", agent_def_hash=agent, input=task)])

    # The one side-effecting step the server must perform (it holds the key):
    result = run.model_step(1, request)          # -> ModelStepResult (response, usage)
    # or stream it, painting a live ticker:
    stream = run.model_step_stream(1, request)
    for delta in stream:
        ...                                      # {"type": "text_delta", ...}
    completion = stream.completion               # -> ModelStepResult

    # A tool the server's registry holds:
    output = run.tool_step(3, "render", {"doc": "plan.typ"})

    run.append([run.envelope(5, "RunCompleted", output=answer)])

The driver's full surface: open (also re-opens, i.e. resumes, an existing run), log(from_seq=0), append(events), model_step, model_step_stream, tool_step, and resolve(output). Re-opening a run returns its recorded log on run.log_envelopes and mints a fresh drive token (the single-writer lease every append presents), so a refreshed client rebuilds its cursor and re-drives from the log, paying nothing for a step the log already covers. A client-driven append the log rejects raises DivergenceError; a tool step that lands on a dangling write raises NeedsReconciliationError (whose .intent is the recorded write), which resolve(output) clears.

examples/browser-client-run drives this same client-driven surface from a browser page, and example/client_run_loop.py drives it from Python.

Runnable example

example/agent.toml is a model-only agent that answers one question. It is the Python mirror of examples/web-research, driven over the control plane instead of the CLI. Start a server with a key on its environment, then run the script:

npm install -g salvor          # or: cargo install salvor-cli

ANTHROPIC_API_KEY=sk-ant-... \
    salvor serve --bind 127.0.0.1:8080 --store /tmp/answer.db &

pip install salvor
python example/answer.py http://127.0.0.1:8080    # from sdks/python in a checkout

It registers the agent, starts a run, streams every event to completion, and prints the final answer, the event count, and the token usage.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

salvor-0.5.2.tar.gz (33.9 kB view details)

Uploaded Source

Built Distribution

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

salvor-0.5.2-py3-none-any.whl (25.0 kB view details)

Uploaded Python 3

File details

Details for the file salvor-0.5.2.tar.gz.

File metadata

  • Download URL: salvor-0.5.2.tar.gz
  • Upload date:
  • Size: 33.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for salvor-0.5.2.tar.gz
Algorithm Hash digest
SHA256 7a1935a6e47d0fb169734a6435d46eacc6f9a4f484d6f218502cd126e633e158
MD5 758d49711d8b7f14d2a18dba795c9c16
BLAKE2b-256 c48f8d23a4f9fd67f0a0b3c96e9c0a4c958a053857d9ac8658b76f17f7ad726a

See more details on using hashes here.

Provenance

The following attestation bundles were made for salvor-0.5.2.tar.gz:

Publisher: pypi.yml on joseym/salvor

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

File details

Details for the file salvor-0.5.2-py3-none-any.whl.

File metadata

  • Download URL: salvor-0.5.2-py3-none-any.whl
  • Upload date:
  • Size: 25.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for salvor-0.5.2-py3-none-any.whl
Algorithm Hash digest
SHA256 746a9c48d6d16d7195d5d16b44823423fe439c405873d7983139bf145fdaf28f
MD5 7141e5020000717482d3a3d4f920d202
BLAKE2b-256 b723e5caf7348022f6b7ec1a8e27bdaab6cdf8ad20360e760a140014adaf1fa4

See more details on using hashes here.

Provenance

The following attestation bundles were made for salvor-0.5.2-py3-none-any.whl:

Publisher: pypi.yml on joseym/salvor

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

Release history Release notifications | RSS feed

0.10.0

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.3

2 files

This release

0.5.2 This release

2 files

0.5.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page