agent.run()
DOCUMENTATION / CLIENT REFERENCE

Based on AgentRun release · Synced .

parcha-agentrun

A typed Python client for an AgentRun server you host yourself, the open platform's server: one method per operation of its API, typed from the OpenAPI document it serves, plus waiting for a run and following its timeline. It is synchronous and uses the standard library only (http.client, json, ssl), on Python 3.10 or newer.

Install

pip install parcha-agentrun

Or from GitHub, or from a checkout:

pip install "git+https://github.com/Parcha-ai/agentrun#subdirectory=python"
pip install ./python

Its versions start at 0.3.0. The 0.2.x releases under the same name are the old hosted API's SDK, not this client.

It installs as parcha-agentrun and imports as agentrun. It has no dependencies, and it ships py.typed, so pyright and mypy read its types.

Construct

import os

from agentrun import AgentRun

client = AgentRun(base_url="https://agentrun.example.com/v2", api_key=os.environ["AGENTRUN_API_KEY"])

base_url defaults to AGENTRUN_BASE_URL and api_key to AGENTRUN_API_KEY. The base URL must be http or https with no query, fragment, user, password, space or . or .. segment, and the key must be a valid header value; both are checked here, before any request. The client reads no file: finding the server started on this machine (its data directory and the address it records there) is the CLI's job (npx @parcha/agentrun), not this package's. It reads no proxy from the environment either: the key goes to the base URL's origin and nowhere else. Other arguments: timeout, how long one try of a request may take in seconds, its answer read included (default 60), and max_retries (default 4).

Methods are grouped as the CLI's commands are, in snake_case: agents create is client.agents.create(body), runs start AGENT is client.runs.start(agent_id, body), mcp-servers save NAME is client.mcp_servers.save(name, body). Path parameters and the body come first, by position; query and header parameters are keyword arguments named in snake_case (limit=, cursor=, idempotency_key=, if_match=, and from_= for from); every method takes timeout= too. Bodies and answers are TypedDicts, enums are Literals, all importable from agentrun (Run, TimelinePage, CreateRunBody, ...). An operation whose answer carries an ETag (agents.get, agents.update, learning.read, learning.write) returns Tagged(body, etag): pass etag as the next write's if_match=. A body the operation also takes as bytes or text (a file upload, a learning note) is sent as given, with body_type= as its content type.

Examples

Create an agent, start a run with a spend cap and wait for it:

client.agents.create({"definition": {"id": "notes", "version": 1, "name": "Notes", "instructions": "Write what you are asked to a file.", "skills": [], "tools": ["write", "ask_person"]}})
started = client.runs.start("notes", {"input": {"content": "Write notes.md about the Moon."}, "max_cost_usd": 0.5})
run = client.runs.wait(started["run_id"], timeout=120)
print(run["status"], run["cost"]["usd"])

runs.wait returns once the run ends (completed, failed, cancelled) or waits on a person (input_required, paused). Answer a question, or raise the cap of a run stopped at it, then wait again:

pending = run.get("pending")
if pending and pending["kind"] == "question":
    client.runs.message(run["run_id"], {"content": "Europe"})
if pending and pending["kind"] == "budget":
    client.runs.raise_cap(run["run_id"], {"max_cost_usd": 1})
done = client.runs.wait(run["run_id"])

Follow the timeline as it changes, then read a file the run wrote:

for page in client.runs.follow(done["run_id"]):
    for row in page["rows"]:
        print(row["kind"])
data = client.runs.read_file(done["run_id"], "notes.md")  # bytes, byte for byte
with client.runs.read_file(done["run_id"], "notes.md", stream=True) as response:  # a StreamedResponse, unread
    for chunk in response.iter_bytes():
        ...

runs.follow reads the route's event stream and resumes a dropped connection with Last-Event-ID, so no page comes twice. It waits at least the server's retry before it reconnects, and a gateway's Retry-After. Only the server's silence counts as a dropped connection (idle_timeout=, default 45 seconds), never the time your loop spends on a page. Its arguments are checked when you call it, before anything is sent.

Errors

Every error is an AgentRunError, and its kind says which one:

Class kind When Fields
AgentRunRefused refused The server refused (4xx, 5xx); a redirect away from the base URL's origin, or one this client does not follow (code redirect); an answer the client cannot read (code bad_answer) status, code, error (the server's error object), retry_after (seconds)
AgentRunAuthError auth No key (reason no_token), a key that is not a valid header value (bad_header), or a key the server refused, 401 or 403 (refused) reason, status, code, error
AgentRunUnsupported unsupported The server does not offer the operation here (501) capability, status, code, error
AgentRunUnreachable unreachable No answer: the server is down, the connection dropped, or a try timed out cause, the failure's code (ECONNREFUSED, ENOTFOUND, timeout, CERT_...)
AgentRunUsage usage A bad argument, before anything was sent: a path argument with a . or .. segment, a header value with a line break, a timeout of 0 reason
AgentRunTimeout late runs.wait passed its timeout; the run goes on run_id, run as last read

No message holds the API key or the server's own message text: a message names the route, the status and the server's error code. No printed form of an error or of the client holds the key: not str(), repr(), a traceback, nor vars(), and the client does not pickle it. The key is cut from the server's error object.

Secret bodies

A secret body (a tool key, an MCP server's credentials: tool_keys.save, mcp_servers.save, mcp_servers.preview) goes as JSON only, and each of its string values is cut from the answer as a JSON value (as it is, JSON-escaped and URL-encoded), its structure and public fields (url, auth.type) kept, with [hidden] in its place. A refusal of it keeps only its code, and a code that held one of its values is no code. A body whose URL reaches a secret route, the base URL's own path counted, is a secret body too.

Retries and redirects

A GET, or a request that carries an Idempotency-Key, is tried again after a 429, a 5xx or a dropped connection (a body cut midway included), with backoff and never sooner than the server's Retry-After; a Retry-After over a minute is no retry. Any other request is sent once. A fresh client's first request ends at once only when no connection was made (refused, unknown name, unreachable host); a connection dropped after it was made is tried again even then. A certificate the client refuses is never tried again, and neither is a try that ran out of its time, whether it was connecting, in its TLS handshake or reading. A redirect is followed only for a GET within the base URL's origin, up to 5 hops: the key never goes to another server.

Cancellation

Python has no AbortSignal, and this client is synchronous: a timeout is the way to bound a call. The client's timeout, or a method's own timeout=, ends one try at that many seconds, its answer read included, whatever the server does; past it the call raises AgentRunUnreachable with cause timeout (or, when the answer's head had come, the refusal it carried). runs.wait(run_id, timeout=...) bounds the whole wait, and a read that stalls past it is AgentRunTimeout, never a refusal. runs.follow ends when you stop iterating it (break, or closing the generator), and its stream counts as dropped after idle_timeout of silence. Close a StreamedResponse you stop reading, or read it in a with block.

Where it comes from

The methods and types in agentrun/_generated.py are generated from the server's OpenAPI document by node scripts/generate-clients.mjs, from the same projection and resource map as the CLI's commands and the TypeScript client (@parcha/agentrun-client), under the same hash; --check fails on a stale file. The rest is hand-written and keeps the TypeScript client's rules, test for test: python3 -m unittest discover -s python/tests -v runs them against loopback servers and against the real server with a faux model (skipped when packages/serve/dist is not built), and npx pyright -p python checks the types.