Stopping
A flow ends when its run returns. Most interesting flows never return, and a Ralph loop is a while True, so you end them from outside. You reach for stopping when a flow is running and you want it to end now.
Try it
Press ctrl+c twice in the interface while a flow is running. Twice, because a day's work is behind a key that is also pressed by mistake: the first press says press ctrl+c again to stop the flow, and the second one does it.
Or type /stop and send it, which is the same stop asked once.
The four ways to stop
| ctrl+c twice, in the interface | Stops the flow — the whole flow, not just the turn. Clears what is half-typed first, if anything is. |
/stop, at the prompt | The same, asked once. |
ctrl+c, on a hmz exec command line | The same. |
agent.stop(), from anywhere | The same, for that agent. |
/stop is not asked twice. The key is, because a finger lands on it by mistake; nothing is typed by mistake, so writing the command out and sending it is the deliberation the second press stands in for. It says so where there is nothing to stop, which the key never does — with nothing running the key is the one that leaves, and what it says is about leaving.
It also leaves no half-made gesture behind it. A ctrl+c pressed before a /stop and one pressed after it are not two presses of one gesture: the command came between them, so the press after it starts again from the beginning — the third press below while the flow is still unwinding, and otherwise the first of a fresh one.
A third press does not wait for it. A flow told to stop unwinds in its own time — a loop sleeps off its round, a server is given its seconds — and the press after the one that stopped it closes every conversation still open under whatever turn it is in. That is the backend's process going, so the flow reads a turn that failed rather than an agent that was stopped, and nothing is left reading as a run in progress. It is the last thing a key can do about a run.
esc does not stop anything. It is pressed to dismiss whatever is on the screen everywhere else in the interface, so it is not the key that ends a day's work: it opens /monitor instead. With nothing running at all, two presses of ctrl+c leave the interface.
What a stop does to the turn under way
The turn is closed out, and every later call into that agent raises Stopped.
A stop leaves the turn where it got to. It does not wait for the turn, because a stop that waited would not read as a stop. A model can think for minutes, and a key that took four of them to have an effect is a key nobody trusts.
A file the agent had half-written stays half-written. What ends is the agent's part in it, which includes the CLI process the turn was running in and whatever that process had started: a stop that left the agent still writing would not be a stop.
To end one turn without ending the run, a flow has session.interrupt, and a turn can be given a budget that cuts it off on its own.
To have a run stop itself rather than wait for a key, give it an allowance: hours on the clock, millions of output tokens, dollars. Every run has one, and a run that reached the end of it is stopped exactly as this key stops one — turns left where they got to, state kept, and the run worth picking up.
After a stop
A stop is what makes a run worth picking up. Where the flow says it can be picked up, /resume at the prompt carries the last run here on from where it stopped — its own flow, its own agents, its own task, and whatever it had written down by the time the key was pressed. Nothing carries on by itself: stopped means stopped, and the run that carries on is a run somebody asked for.
Wait for it to go, though. A flow told to stop unwinds in its own time and writes down where it got to as it goes, so /resume in that window is refused with no picking a run up while the flow is still stopping — picked up from a state still moving under it, the next run would do a round the stopped one had already recorded. A flow that will not unwind at all is what the third press is for: it leaves nothing reading as a run in progress, and /resume is answerable again. A second /stop in that window is no help either — it says the flow is already stopping rather than telling it again, since the agents it is holding are what that press reaches.
What stopping is not
Not /clear. That clears the screen and nothing else. It clears the conversation being read, not the others, and nothing that is running.
Not choosing another flow. /flow is refused while one is running, with no choosing a flow while a flow is running: ctrl+c twice stops it first. A flow drives the agents it was handed, and it must not have them swapped underneath it. Stop it first, then choose. Looking at /flow and leaving without choosing changes nothing.
Not a question ending. A question still up when the flow ends or is stopped ends with it. Stopping is never blocked on one.
Why suppress=True does not catch a stop
The other side of that key press is the loop a weaver wrote, which has to let it out. suppress turns a failed turn into an empty answer:
agent(task, suppress=True) # a turn that failed answers ""; the loop goes round againIt deliberately does not catch Stopped. A loop that carried on past a stop would never end:
while True:
agent(task, suppress=True) # ← Stopped comes out of here, and the flow unwindsStopped is not a subprocess.CalledProcessError. Nothing that catches a failed turn catches this by accident. Let it propagate. The epic then records the run as stopped by hand rather than as one that finished — the difference between "it decided it was done" and "somebody stopped it", and the only place that distinction is written down.
There is one other thing suppress does not catch, for the same reason. An Unrecoverable is a turn that failed for a reason no other try could come out differently on — a conversation longer than the model's context window, a session id the backend will not answer under. A while True that swallowed one would go round on the same failure until somebody stopped it, so it comes out of the loop and the run ends with it. Unlike a stop, it is a CalledProcessError, so a flow that really does want to catch everything still can.
agent.prompted() raises Stopped too, so a run ended while it waited also reads as ended by hand. agent.stopped is the quiet way to ask the same question — a bool, and never a raise:
agent.prompted() # waiting for the next thing to say; raises if the wait ended in a stop
agent.stopped # whether it has been told to stop; answers True, and never raisesA hook that raises is normally the hook's own problem: a flow must not fail because something hung off it did. Stopped is the one exception, and it is let out.
See also
- Picking a run up — carrying on from where a stop left it
- Talking to a running turn — when a steer is enough
- Being away
- Flows › Stopping