Operations
Anything that waits on the world returns an operation. Block on it, poll it, or come back to it by id.
Some work finishes in a request. Some waits: for a user to connect an account, for a person to approve, for a model to finish a turn, for a phone call to end. That kind of work returns an operation, which you can wait on now or pick up later from anywhere.
| Started by | Its wait() returns |
|---|---|
connection.connect_link, connection.request, user.human.connect | the connected Connection |
human.ask | an Answer |
human.approve | True or False |
human.notify | None, once sent |
agent.send | an AgentReply |
child.job of parent.spawn (invite-only beta), kind agent.task | an AgentReply: the child's last reply on its task |
agent.ask (invite-only beta) | an AgentReply: the answer |
inbox.wait_for | the matching InboxMessage |
number.call | the PhoneCall, with its transcript |
sandbox.exec(..., background=True) | an ExecResult |
Wait for it
import noclick
turn = agent.send("Draft the monthly report", user="user_42")
reply = turn.wait(timeout=120)Without a timeout, wait() blocks until the operation finishes. With one, it raises noclick.OperationTimeout if the operation is still going; the operation carries on, and you can wait again.
If the operation didn't succeed, wait() raises: the operation's own error (an noclick.Error subclass), noclick.OperationCanceled, or noclick.Expired.
Come back later
Operations live on NoClick's side, so they outlive your process. Keep the id and wait from anywhere, even a different machine:
save(turn.id)
# in a background job
reply = noclick.Operation.from_id(saved_id).wait()The result comes back in the same type as it would have from the original call.
Check without waiting
op = noclick.Operation.from_id(saved_id).hydrate() # loads it now
print(op.status, op.done)An operation's status is one of:
| Status | Means |
|---|---|
queued | waiting to start |
running | working |
awaiting_human | waiting for a person: an answer, an approval (a purchase your policy holds), a connection; an agent turn whose ask waits on the person it asked (step asking), or whose browser sign-in waits on someone (invite-only beta) |
succeeded | finished; result holds the outcome |
failed | finished with an error; error says why |
canceled | stopped before it finished |
expired | nobody acted in time |
The first three are live; the last four are final. op.refresh() reloads an operation you already hold.
An agent's turn that's running when NoClick restarts goes back to queued and picks up where it left off on the next server, so wait() still returns its reply. The conversation gets a note saying so. A turn on a coding harness (its sandbox may already have the message) and other work NoClick restarts under fail with reason interrupted instead.
What started it
op.input is what an operation was started with (read on one operation, not in listings). For an agent turn that a trigger started, input["conversation"] is the conversation it ran in, the key a conversation template rendered:
op = noclick.Operation.from_id(saved_id).hydrate()
print(op.input["conversation"]) # issue-1841A turn is awaiting_human while the agent waits on a person: an answer to its ask, or a sign-in or a code its browser needs. message() keeps waiting, and returns the reply the agent writes once the person has answered.
Jobs
A child an agent spawns works on its task as a job, a noclick.Job: an operation of kind agent.task that spans every turn the task takes. While it works its step is working, or waiting when all it has left is a wake-up it set itself; it's awaiting_human while it waits on a person. job.updates() lists what the child reported along the way. A message a coordinator sends another agent without waiting is a job too, of kind agent.message. agent.ask records each question as an agent.ask operation. noclick.Job.list(status="awaiting_human") lists the jobs across your agents, Job.summary counts them by status, and agent.jobs() reads the ones one agent started.
Follow it as it runs
events() yields each status change until the operation finishes:
for event in op.events():
print(event.status, event.updated_at)See how far it got
Long work reports as it goes, and op.progress reads it: its step, its stages (each with a name, a status of done, current or pending, and when it started_at and ended_at), the last 50 lines it printed, line_count (every line so far) and updated_at, when it last said anything.
op = project.deploy()
for stage in op.progress["stages"]:
print(stage["name"], stage["status"]) # Queued done, Building current, Shipping pending, Live pending
print(op.progress["lines"][-1]) # the build's latest log line| Kind | Stages | Lines |
|---|---|---|
project.deploy | Queued, Building, Shipping, Live | The build's log, read every few seconds while it builds |
workflow.build | Building, Done (Asking between them while it waits on a question) | The builder's steps, like "Reading schema for Gmail" |
deployment.check | Opening a browser, Loading the pages | Where it loads, then each path's result, like "/pricing loaded in 0.8s (200)" (a promote that checks first adds these lines to its own) |
project.sign_in.enable, project.sign_in.upgrade | Setting up sign-in with NoClick, Creating the users tables, Adding sign-in to the code, Deploying | How each provider signs in, the tables, the commit and where it's live |
phone.call | Ringing, On the call, Ended (one nobody answered skips On the call) | What each side said, turn by turn |
agent.message | The tool calls the agent's turn makes |
It's None until the work reports something. An agent that started the operation through its NoClick tools hears each report as an operation event in its conversation's stream, so a chat can show the work live (Sharing and streaming).
Cancel it
op.cancel()Timing and history
A finished operation records how long it waited to start (wait_ms), how long it ran (run_ms) and how long it was held for a person (held_ms), plus every status change in transitions. For an agent turn, op.tool_calls() lists the tools it called.
op.console_url opens the operation in the developer console, and errors raised by wait() end with the same link.
List operations
noclick.Operation.list(status="failed", kind="agent.message", limit=20)
noclick.Operation.summary(status="live").by_status # every status: {"queued": 0, "running": 3, ...}See Activity for every filter.