Skip to main content

The original design, and why it broke

The first implementation of set delegation and policy used a single Interpreter-global stack: entering a session or starting an agent pushed a copy of the current rules, and exiting popped it. It worked, and it was simple — right up until agent pause/resume needed to exist. The problem: a stack models exactly one thing correctly — a single, uninterrupted sequence of nested scopes, entered and exited in strict order. Pause/resume breaks that assumption on purpose. When an agent pauses mid-body, its delegation/policy frame is still logically “open” — but control has been handed back to whatever called start(), which has its own, completely different notion of “current scope.” With a shared global stack, “top of stack” stops reliably meaning “the right scope” the moment two independent logical contexts — a paused agent, and whatever resumed it — can each expect their own current scope at the same time.

The fix: attach state to Environment, not the interpreter

Each Environment — the same object that already holds a scope’s variables — gets three more optional slots: model (a set delegation override), policy (a dict of capability → allowed), and agent (set only on an agent’s own scope, so pause() can find “which agent am I inside”). Looking up “the current model” walks the parent chain exactly the way Environment.get(name) already does for variables — first non-None value wins, defaulting to None (no model configured) if nothing in the chain ever set one. set delegation = x mutates only the current env’s own model slot — never a parent’s — so it’s automatically scoped to wherever it’s written and automatically visible to anything nested inside, with zero explicit push/pop/copy bookkeeping anywhere.
This is strictly more robust than the stack it replaced: state now lives wherever the logical scope is (an Environment object reachable from wherever execution currently is, on whichever thread), not in a single mutable structure shared across every context that happens to be interpreting Sense code at once.

A side effect worth knowing about

Because every scope — including an ordinary function call’s own environment — now gets its own optional override for free, a plain function’s set delegation no longer leaks to its caller either. Under the old stack-based design, only session and agent bodies got their own frame; a function call didn’t push one, so delegation set inside a function used to affect the caller too. That was a documented limitation. It disappeared as a consequence of fixing the pause/resume bug, without anyone writing function-call-specific scoping logic — every scope just follows the same one rule now.

Policy uses the identical mechanism

Same walk-the-chain pattern, same reasoning, same fix for the same underlying class of bug. See Policy for the capability-gating semantics this enables, and Actions for how .verify()/.commit() each call this independently rather than caching a result.

Continue

Agent concurrency model

The pause/resume mechanism this redesign was made to support.

Sessions

The user-facing behavior this architecture produces.