Skip to main content

Declaring one doesn’t run it

This is the key difference from a plain function or a session: agent name: ... declares an entity. The interpreter creates a SenseAgent — identity (name), a persistent scope (env, a child of the declaring scope, so the body closes over its surroundings the same way a function does), and status: "created" — and binds it, same as any other value. Nothing in the body runs yet.

start(agent) runs it

start(...) advances status to "running", executes the body, then to "completed" (or "failed" if an unhandled error occurred) — and returns the agent. An agent can only be started once from "created"; calling start again is a SenseRuntimeError.

Reading state back out

Member access on a completed (or even paused) agent reads whatever variables the body set in its own persistent scope — the exact same mechanism a module’s import ... as m; m.some_fn uses. .status, .name, and .pause_reason are reserved fields that always refer to the agent itself, taking precedence over a same-named body variable.

Agents close over their declaring scope

If you want the body’s x to be genuinely its own, use local inside it, exactly as you would inside any other block.

Delegation and policy are scoped like session

See Sessions for exactly how this scoping works — agent uses the identical mechanism.

Agents can pause themselves mid-task

This is real: local variables, control-flow position, everything survives the pause exactly as written. resume(agent, value) can also carry an answer back into the paused body — which is exactly how ask_human(question) works, a dedicated human-in-the-loop primitive built on the same mechanism. It’s substantial enough to have its own page covering how it works and what it doesn’t cover yet.

Concurrency: async agent

A plain agent (everything above) is always sync: start()/resume() block until the body settles. async agent makes that optional — the caller and the agent’s body (or two async agents) can genuinely be executing at the same wall-clock time:
1

start()/resume() skip their blocking wait

Everything else — pause(), resuming with a value, .status/ .pause_reason — is unchanged. Only whether the caller blocks differs.
2

await(agent) gets the result when you need it

Blocks (if still "running") until the agent settles, then applies the exact same result-collection logic start()/resume() already use, including re-raising the body’s error on the awaiting thread. await() on an already-settled agent — including any sync agent, always already settled by the time start()/resume() return — is a harmless no-op, so it can be used uniformly regardless of sync/async.
Concurrency is opt-in, only at the agent/tool boundary — a plain function call is never affected, and never forces a caller to become async too (“function coloring,” the usual cost of async-by-default languages).
No synchronization primitives exist (no locks, channels, atomics) — two async bodies mutating the same shared scope or memory concurrently can race, exactly like raw Python threading with no lock. CPU-bound Sense code in two async bodies still won’t run simultaneously either (the same GIL constraint any Python threading has) — only I/O-bound work (a sleep, a network call via import python, a model call) actually overlaps.

Communication: send / receive

Two async agents racing on a shared variable is genuinely unsafe — the same limitation the concurrency warning above already flags. send(agent, message) / receive(timeout?) give every agent its own mailbox instead, a thread-safe FIFO queue that exists from the moment it’s declared:
send() never blocks and works from anywhere — top-level code, another agent’s body, even before the target has started (the message just waits). receive() blocks the calling agent’s own thread until a message arrives, or returns nil once an optional timeout elapses — only legal inside a running agent’s own body, same lookup pause() uses. Messages are delivered in send order even with several senders at once, queue.Queue handling that safely with no new synchronization primitive of Sense’s own. See the builtins reference for the full signatures, and examples/agent_communication.sns in the repo for a fuller scenario — a coordinator fanning work out to two concurrent workers and collecting both results back.

What this doesn’t cover yet

An agent’s body can call anything the surrounding scope can, beyond what an individual action inside it explicitly requires. Full agent-level sandboxing is broader future work.
async agent gives real OS-thread concurrency (see above), not a scheduler managing fairness/priority/limits across many agents — each async call is just a fresh daemon thread. See Agent Concurrency Model for the mechanism.
A paused agent’s state lives in a live thread’s call stack — it can’t be serialized to disk and resumed after a restart. That would need a genuinely different execution model (e.g. a step-based/bytecode interpreter with serializable frames).
Not built. Tracked on the Roadmap.

Continue

Pause & resume

How genuine suspension works, mechanically.

Actions & policy

How an agent does something to the outside world safely.