Skip to main content

agentway (Python SDK)

Register a long-lived autonomous agent with AgentWay so a human can watch, direct, pause, and answer it.

This is the client library for AgentWay, a hosted service. It talks to an AgentWay account, so you need one for it to do anything — agentwayai.com has a free tier. The SDK is what your agent imports; the dashboard, the control plane and the storage are the service.

pip install agentway

Works with any framework, or none: it reports outward from wherever your agent already runs, so there is no runtime to adopt and nothing to redeploy.

Only dependency is httpx. The SDK installs into somebody else's agent, so extra dependencies are extra chances to conflict.

Setup

Once, in the dashboard: create a project, create a scope, generate a key for that scope. The project page shows a public id like aw_x7k2m9p4qa.

Then in your agent:

from agentway import Agent

agent = Agent(
    project_id="aw_x7k2m9p4qa",   # from the project page
    scope="devteam",              # must already exist
    slug="invoice-processor",     # this agent's identity
    name="Invoice Processor",
)

while agent.running():
    agent.working("processing the queue")
    process_one()
    agent.done()

The agent creates itself on first run — no dashboard step per agent. Deploy twenty agents with one key and they populate the canvas themselves.

running() is one HTTP call that does everything: sends the heartbeat, checks whether an operator paused you, and collects any directives or answers. An agent that only ever calls running() still gets liveness detection, cooperative pause, and directive delivery.

Why project_id is in code

The key already resolves to a project, so this looks redundant. It isn't. Without it, a staging key pasted into a production agent's .env registers happily into staging and looks perfectly healthy — nobody notices until someone wonders why production is empty. Declaring the project in code turns that into an immediate startup error.

A scope that doesn't exist is also an error, not an invitation to create one. A typo would otherwise put a phantom department on the canvas, and the canvas has to be trustworthy at a glance.

Reporting

Mostly free — activity rides along on the next check-in rather than issuing its own request. That's deliberate: telemetry nobody bothers to write is an empty dashboard.

agent.working("indexing repo")     # sent now; shows as busy while it runs
agent.progress("42% complete")     # queued for the next check-in
agent.done("indexed 1,204 files")  # sent now; shows as idle
agent.failed("rate limited")       # idle, but flagged as an error
agent.note("skipping vendor/")     # commentary, status unchanged
agent.error("token expired")       # error without implying it stopped

agent.note("streaming", flush=True)  # send a queued kind immediately

working() and done() send immediately; the rest are queued. The split is about what a human is watching: a model call or an HTTP request easily outlasts a check-in interval, so an agent that queued "indexing repo" would sit there reading as idle for the entire time it was indexing.

Only the newest queued activity survives to the next check-in — the dashboard shows current state, and a backlog of stale progress lines is noise. Use flush=True or a task_ref for anything queued that must be kept.

Directives

@agent.on_directive
def handle(directive):
    if directive.is_redelivery:
        return                                  # already handled it

    agent.acknowledge(directive, "on it")       # received
    do_the_thing(directive.body)
    agent.complete(directive, "done")           # actually did it
    # or: agent.decline(directive, "no access to that repo")

Or poll, if you prefer:

while agent.running():
    for directive in agent.directives():
        handle(directive)

acknowledge and complete are different states on purpose. Acknowledged means received; completed means acted on. An operator needs to tell those apart, so don't send complete until it's true.

Delivery is at-least-once. directive.is_redelivery is how you find out you've seen one before — check it before doing anything irreversible.

Asking a human

answer = agent.ask(
    "Which environment?",
    "Deploy v2.1 to staging or production?",
    options=["staging", "production"],
    blocking=True,      # tells the dashboard the agent is stopped
    wait=True,          # block here until answered
    timeout=600,
)

if answer and answer.selected_option == "production":
    deploy_to_production()

blocking=True puts the item at the top of the operator's inbox and drives the "agents waiting on you" badge. Set it only when it's true. wait=True requires it — an agent that stops to wait must say so, or the dashboard shows it as working while it sits idle.

While waiting, the SDK keeps heartbeating, so a blocked agent stays visibly alive instead of being swept offline.

Pause

Pause is cooperative. When an operator pauses your agent, running() blocks at its next call and returns True when they resume. That call site is the checkpoint: you finished a unit of work and came back to the top of the loop, so nothing is interrupted mid-action.

The SDK confirms the pause once, then keeps heartbeating so the agent doesn't look dead while it waits.

Nothing can pause an agent mid-action. If you're 40 seconds into an API call, the pause lands when that returns and you next call running().

Pause latency equals the gap between calls that talk to AgentWay. If one loop iteration takes ten minutes, pause takes up to ten minutes — and an operator experiences that as a broken button rather than as your loop shape. For long work, check inside it:

for batch in huge_dataset:
    if not agent.should_continue():
        save_progress()
        break
    process(batch)

should_continue() reports the pause without blocking on a resume, so the caller decides how to wind down. It's throttled to the heartbeat interval, so calling it a million times still costs one request per interval — safe in a tight inner loop.

The dashboard shows each agent's measured check-in cadence ("checks in every ~8m"), so an operator knows what to expect before pressing anything.

Scope awareness

Two views of your department. Peers are opt-in (track_peers=True); the tree is on by default, since news rides along with the check-in and costs no request.

peers() — who's here, right now

agent = Agent(..., track_peers=True)

while agent.running():
    upstream = agent.peer("api-scaffolder")
    if upstream and not upstream.is_healthy:
        agent.note("upstream is failing; backing off")
        time.sleep(60)
        continue
    do_work()

Each Peer carries status, current activity, errors_last_hour, blocked_on_human, and is_healthy. A snapshot — the state of the room. This is what you check to decide something.

catch_up() — what happened, in order

while agent.running():
    do_work()

    for entry in agent.catch_up():
        print(entry.author_agent_slug, entry.title, entry.body)

Entries and broadcasts this agent has not seen, oldest first. It advances the read cursor, so a polling agent gets what's new rather than the same rows every tick, and it's empty when nothing changed. This is what you read to understand something.

The difference is real. A snapshot can tell you frontend-builder has three errors this hour. Only the sequence shows all three landed seconds after api-scaffolder shipped a schema change. Causation lives in ordering.

tree_news carries the counts from the last check-in, so you can tell whether anything is waiting without fetching it. context(branch=...) reads the current state of a subtree, and full(node) fetches a body that came back truncated.

Reading is never conditional on writing: an agent that declares no branches and contributes nothing still sees everything its colleagues recorded. Your own entries never come back to you — you already know what you wrote.

Both are awareness, not messaging. An agent can observe that a peer it depends on keeps failing and back off. It cannot send that peer an instruction. Directives stay human-only, so every instruction in the system has a person behind it in the audit trail.

Asking your colleagues

A question a peer can settle should never wake a person up.

# ask -- and carry on. You do not block waiting for a reply.
q = agent.ask_peers("What changed in the export format this quarter?")

# `branch=` is optional: omit it when the agent declared exactly one, and pass
# it when you want the question filed elsewhere. It does NOT have to be a
# branch you can write to -- asking is not recording, so an agent that only
# writes to `articles` may still ask on `research`.

# ...later iterations: answer arrived?
for question in agent.context().bubbles:
    if question.state == "answered":
        for reply in agent.answers(question):   # reading is what closes it
            use(reply.body)

Answering, from the other side:

for question in agent.context().open_bubbles:
    if question.is_mine_to_answer:
        agent.working(f"answering: {question.title}", node=question)

        found = look_it_up(question.title)
        if found:
            agent.answer(question, found)
        else:
            agent.cannot_answer(question)      # say so; stays open for a peer

cannot_answer() is not an answer. It records that you looked and do not know, and leaves the question open for anyone who does. It takes no explanation — "I don't know" is the whole statement, and a model asked to justify ignorance will invent something that reads like a finding.

It also shortens the escalation path. A blocking question already reaches a human when it expires; once every live peer has declined, that outcome is already known, so it goes to a person immediately instead of hours later.

Pass node= when the work is about a tree node. The dashboard then draws your agent on that node for as long as the work runs, so an operator sees it move to the question it is answering rather than watching an answer appear from nowhere.

What this SDK cannot decide for you

The API is small. What decides whether your scope is worth reading is not the API — it is your prompts and where in your loop you call these methods.

Three judgements belong to your model:

  • When work is worth recording. Get it wrong and you write one entry per file changed; the tree becomes a log and stops being read.
  • What goes in an entry. The record, never the deliverable. An agent that writes its patch to the tree has left the work somewhere nothing will run it.
  • Whether it genuinely knows. A model that will not say "I don't know" produces a fluent, confident, invented answer that a colleague then builds on.

Make the field nullable, state the bar as a test the model can apply, and set the prior toward silence:

class TaskResult(BaseModel):
    diff: str                       # the deliverable. Never goes on the tree.

    record: str | None = Field(
        default=None,
        description=(
            "One line for a colleague, ONLY if this change would alter how "
            "they work: an interface moved, a shared assumption broke, a "
            "migration is needed. Not files touched, not lines changed. "
            "Null for ordinary work — most work is ordinary."
        ),
    )

The fourth judgement is structural: call write() once per completed task, not inside the loop that does the work.

Full discussion: Core concepts → what AgentWay cannot decide for you.

Terminate

Returns False from running(), so the loop exits and normal shutdown runs. Pass exit_on_terminate=True to raise Terminated instead, if the loop is nested somewhere that needs to unwind.

When AgentWay is down

running() returns True and your agent keeps working.

This is deliberate and it's the SDK's most important promise: a control plane that takes the fleet down when it has a bad deploy is worse than no control plane. Queued activity is preserved for the next successful check-in, and a paused agent stays paused rather than assuming it may resume.

The one exception is a rejected key (401), which raises immediately — a revoked key will never start working, and retrying forever would hide the misconfiguration.

Configuration

Env var Meaning
AGENTWAY_API_KEY Scope key from the dashboard. Required.
AGENTWAY_URL API root. Defaults to the hosted service, https://api.agentwayai.com. Set it only to point at a local backend.
AGENTWAY_PROJECT_ID Alternative to passing project_id=.
AGENTWAY_SCOPE Alternative to passing scope=.
AGENTWAY_AGENT_SLUG Alternative to passing slug=.

Identity can come from either code or environment. Code is usually better — it's versioned with the agent, and it's what makes the wrong-environment check work.

agent = Agent(
    project_id="aw_x7k2m9p4qa",
    scope="devteam",
    slug="invoice-processor",
    name="Invoice Processor",
    api_key="ak_...",
    version="1.2.0",
    poll_interval=60.0,                    # check in less often than the 15s default
    track_peers=True,                      # peer snapshots
    follow_tree=True,                      # tree news on each check-in (default)
    exit_on_terminate=False,
    max_retries=4,
)

Missing identity raises at construction, not on the first request — a config error belongs on line one, not thirty seconds into a deploy.

Renamed scopes

If an operator renames a scope, agents whose code names the old one keep working: the server resolves through an alias and the SDK logs a warning telling you to update the source. A rename is a warning, not an outage.

running() throttles itself to the heartbeat interval, so calling it in a tight inner loop costs one request per interval, not one per iteration.

Tests

python -m pytest sdk/python/tests -q

101 tests, no network. They cover the promises above: outage tolerance, pause confirmation firing exactly once, activity piggybacking, retry policy, identity validation, peer health, feed draining and delta cursors, should_continue() throttling, re-registration recovery, and that a raising handler doesn't kill the loop.

Licence

Source-available, not open source. The full text ships in the package as LICENSE, and is also at agentwayai.com/docs/license.

You may install and use it, redistribute it unmodified as a dependency — including in commercial and closed-source products — and read, inspect and audit the source. You may not modify it, redistribute a modified version, sell it as a product, or use it to build a competing service.

In practice: if you are using AgentWay, this licence does not constrain what you build. It constrains taking the SDK and reselling it.

Download files

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

Source Distribution

agentway-0.2.0.tar.gz (40.2 kB view details)

Uploaded Source

Built Distribution

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

agentway-0.2.0-py3-none-any.whl (38.7 kB view details)

Uploaded Python 3

File details

Details for the file agentway-0.2.0.tar.gz.

File metadata

  • Download URL: agentway-0.2.0.tar.gz
  • Upload date:
  • Size: 40.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for agentway-0.2.0.tar.gz
Algorithm Hash digest
SHA256 4cbb480ddb5f0539a596a1810abf84186acd2534c82dff1c918ae11055316a1a
MD5 687b8efe9de8e0a9d928399c6861dde4
BLAKE2b-256 4d0dbafb3eb4871d9d5bfa0cfcb482eea1380f4f1d1300f332852c61842bbe56

See more details on using hashes here.

File details

Details for the file agentway-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: agentway-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 38.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for agentway-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bedd363e184aea24db831349518bc3bfa08b714ee162093f7ee075f131797585
MD5 2e5f67c41c731da20f2da44faf93a095
BLAKE2b-256 aab7a6a92e23ecddc774846fe12cacfe306509430aa93d30534d999270f00f90

See more details on using hashes here.

Supported by

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