Skip to main content

The feature

pause() — optionally with a reason string, readable afterward as agent.pause_reason — suspends a running agent’s body mid-statement. resume(agent) continues it from exactly that point: same local variables, same call stack, same everything, because it’s genuinely the same in-progress execution the whole time, not a re-run or a replayed log. pause() only works inside a currently-running agent’s own body (found by walking the scope chain the same way delegation and policy are — see Sessions); calling it anywhere else raises a clear SenseRuntimeError rather than doing something unclear.

How this works, mechanically

Each agent’s body runs on its own OS thread, and only one of the caller or the agent is ever actually executing at any instant — cooperative hand-off via two threading.Events, not real concurrency.
1

start(agent) launches the thread and blocks

The agent’s body begins running on a new daemon thread. The calling thread blocks on a “settled” event.
2

pause() unblocks the caller and blocks itself

Inside the agent’s thread, pause() sets status = "paused", signals the “settled” event (waking whichever caller was waiting), and then blocks that same thread on a second “resume” event.
3

start()/resume() return once settled

The calling thread — now unblocked — sees the agent has paused (or completed, or failed) and returns control to whatever called start/resume.
4

resume(agent) signals the resume event and blocks again

This wakes the agent’s thread exactly where pause() left off — Python resumes that thread’s own call stack from inside the blocked .wait() call, so every local variable and every level of nested if/while/function-call the body was inside is simply still there. The calling thread blocks on “settled” again, and the cycle repeats until the body completes or fails.
Because the two threads never run simultaneously — one is always blocked on an Event while the other executes — there’s no locking needed around shared interpreter state. It’s genuinely cooperative, not preemptive: an agent can only be paused by its own pause() call, never from outside at an arbitrary point.

Lifecycle

An unhandled error — before any pause(), or after a resume() — sets status = "failed" and re-raises on whichever thread called start()/resume() last, so it surfaces as an ordinary SenseError right at that call site, not buried in a background thread.

A never-resumed agent doesn’t leak

The agent’s thread is a daemon thread specifically so this is safe — the process exits normally even with paused agents nobody ever got back to.

pause() carries a value back in

resume(agent, value) takes an optional second argument — and pause() returns it:
This turns pause()/resume() from a pure wake-up signal into something that can carry data across the suspend boundary — which is exactly what makes ask_human (below) possible.

ask_human: a question/answer primitive built on this

Inside a running agent, ask_human(question) is pause(question): the question becomes agent.pause_reason, and the return value is whatever resume(agent, answer) provides. Outside any agent — a plain script, or the REPL, where there’s no one to hand control back to — it falls back to printing the question and reading a real line from stdin.
There’s deliberately no way yet to require human approval as part of an action’s own declaration — an analogue to requires <capability> on action. ask_human(...) composes with .commit() by hand today (if ask_human("approve?") == "yes": a.commit()); a first-class “this action needs approval” marker is tracked future work, not built here.

What this is not

This is cooperative hand-off between exactly two logical contexts at a time, implemented with real OS threads as a mechanism — not a general concurrency or scheduling system, and not a durability mechanism.
  • No real concurrency. Only one Sense thread is ever logically “live.” Two agents cannot run simultaneously — there’s no scheduler.
  • No serialization. A paused agent’s state is a live Python call stack on a live OS thread. It cannot be written to disk and resumed after the process restarts. A genuinely persistent agent would need a different execution model — a step-based or bytecode interpreter whose frame state is actually serializable — not this thread-based approach.
  • No inter-agent pause/resume. An agent cannot pause or resume another agent from inside its own body yet.
See Agent Concurrency Model for the implementation in more depth, including the exact code paths in interpreter.py.

Continue

Agents

Back to the full agent feature set.

Tool

A capability-checked function an agent (or anything else) can call.

Architecture: agent concurrency

The implementation, for contributors and the curious.