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
- Sign up / dashboard — agentwayai.com
- Documentation — agentwayai.com/docs
- Full working agent — agentwayai.com/docs/full-agent
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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4cbb480ddb5f0539a596a1810abf84186acd2534c82dff1c918ae11055316a1a
|
|
| MD5 |
687b8efe9de8e0a9d928399c6861dde4
|
|
| BLAKE2b-256 |
4d0dbafb3eb4871d9d5bfa0cfcb482eea1380f4f1d1300f332852c61842bbe56
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bedd363e184aea24db831349518bc3bfa08b714ee162093f7ee075f131797585
|
|
| MD5 |
2e5f67c41c731da20f2da44faf93a095
|
|
| BLAKE2b-256 |
aab7a6a92e23ecddc774846fe12cacfe306509430aa93d30534d999270f00f90
|