docs(rfc): add agent-scope diagrams
This commit is contained in:
@@ -27,6 +27,24 @@ The contract has four parts:
|
||||
|
||||
The scope is deliberately flat. Resolution never walks parent or sibling scopes. Parent ownership links lifetimes without importing registrations.
|
||||
|
||||
For scope-aware registries and default listener routing, the whole mechanism can be read from left to right: the registering context chooses a layer, while the agent named by an operation chooses which one local layer joins the deployment-global layer.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
plain["Plain plugin context<br/>cleanup follows the plugin"] -->|"registers into"| globalLayer["Deployment-global layer"]
|
||||
agentAContext["agentA.ctx<br/>cleanup follows Agent A"] -->|"registers into"| agentALayer["Agent A layer"]
|
||||
agentBContext["agentB.ctx<br/>cleanup follows Agent B"] -->|"registers into"| agentBLayer["Agent B layer"]
|
||||
|
||||
operationA["Operation for Agent A"] -->|"selects"| agentAView["Agent A view<br/>eligible globals plus A local only"]
|
||||
globalLayer --> agentAView
|
||||
agentALayer --> agentAView
|
||||
operationB["Operation for Agent B"] -->|"selects"| agentBView["Agent B view<br/>eligible globals plus B local only"]
|
||||
globalLayer --> agentBView
|
||||
agentBLayer --> agentBView
|
||||
```
|
||||
|
||||
The missing cross-edges describe registry resolution and default listener routing: Agent A's registered values and ordinary scoped listeners do not enter Agent B's view, and a parent's layer does not enter a child's view merely because the parent owns the child's lifetime. For scope-filtered events, `{ global: true }` is the explicit opt-in exception; it can observe across scopes while cleanup still follows the registering agent. Registry-membership notifications are a separate unfiltered event class described below.
|
||||
|
||||
The companion [runtime-design RFC](2026-07-12-agent-scope-runtime-design.md) explains how the implementation preserves this contract under Cordis dispatch, JavaScript mutation and reentrancy, asynchronous setup, rollback, and racing disposal.
|
||||
|
||||
### Registration origin selects visibility and cleanup
|
||||
@@ -103,6 +121,26 @@ The returned promise resolves only after setup, ordered lifecycle notification,
|
||||
|
||||
The calling Cordis context and AgentLoop are structural co-owners. Unloading either disposes the agent, so creation through a short-lived plugin context intentionally gives the agent that shorter lifetime.
|
||||
|
||||
The lifecycle keeps the local layer private until setup succeeds and keeps it alive until final work has drained:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
request["Create or resume"] --> reserve["Reserve agent and session IDs"]
|
||||
reserve --> privateWorld["Load or build private session, scope, and driver"]
|
||||
privateWorld --> setup["Await setup through agent.ctx"]
|
||||
setup --> publish["Publish session and agent, then start the loop"]
|
||||
publish --> live["Return the live handle"]
|
||||
|
||||
privateWorld -->|"load or preparation failure, or owner loss"| rollback["Rollback startup<br/>no handle escapes"]
|
||||
setup -->|"setup failure or owner loss"| rollback
|
||||
publish -->|"publication failure or owner loss"| rollback
|
||||
live -->|"handle disposal, owner unload, or AgentLoop unload"| settle["Quiesce prepared or running work"]
|
||||
rollback --> settle
|
||||
settle --> detach["Detach any published agent, then session"]
|
||||
detach --> revoke["Dispose any created agent scope"]
|
||||
revoke --> release["Release acquired IDs"]
|
||||
```
|
||||
|
||||
Contributors should put agent-local activation inside `setup` and always dispose the returned handle. Code that needs to observe a live agent waits for `create()`/`resume()` to resolve rather than polling the registries during setup.
|
||||
|
||||
## Tool restrictions resolve against a live flat view
|
||||
|
||||
@@ -249,6 +249,37 @@ Two services split the public API from the implementation. `AgentRegistry`, reac
|
||||
| Publish and start | Session, agent, and lifecycle notifications appear in order | Liveness is checked between observable phases |
|
||||
| Dispose | Driver drains, registries detach, scope unwinds, IDs release | All owner paths join one completion promise |
|
||||
|
||||
The implementation treats success, rollback, handle disposal, caller unload, and AgentLoop unload as entrances to one owned transaction rather than separate cleanup algorithms:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
caller["Caller context owner"] --> transaction["Owned create or resume transaction"]
|
||||
factory["AgentLoop structural owner"] --> transaction
|
||||
|
||||
subgraph creation["Create or resume"]
|
||||
transaction --> reserve["Reserve both IDs and install trackers"]
|
||||
reserve --> prepare["Load persistence or prepare the session"]
|
||||
prepare --> lifecycle["Install the complete caller-owned lifecycle"]
|
||||
lifecycle --> setup["Await unpublished setup"]
|
||||
setup --> enter["Enter session and agent registries"]
|
||||
enter --> announce["Emit session/created, then agent/created"]
|
||||
announce --> start["Enable driving, emit agent/session-start, start driver"]
|
||||
end
|
||||
|
||||
transaction -.->|"reservation, load, or preparation failure before lifecycle handoff"| earlyRollback["Release acquired tracking and reservations"]
|
||||
start --> live["Live handle"]
|
||||
lifecycle -.->|"failure or owner loss before a handle escapes"| dispose["Join the lifecycle cleanup boundary"]
|
||||
live -->|"dispose or either owner unloads"| dispose
|
||||
|
||||
subgraph teardown["Reverse-order teardown"]
|
||||
dispose --> barrier["Wait for synchronous publication to unwind"]
|
||||
barrier --> drain["Stop driver and complete final flushes"]
|
||||
drain --> detach["Detach agent, then session"]
|
||||
detach --> scope["Dispose agent scope to quiescence"]
|
||||
scope --> release["Release session and agent IDs"]
|
||||
end
|
||||
```
|
||||
|
||||
The [public lifecycle contract](2026-07-08-agent-scope-contexts.md#creation-publishes-after-setup-disposal-revokes-after-work-stops) defines what callers observe. The following sections justify each ownership and ordering fact behind that contract.
|
||||
|
||||
### Reservations precede awaiting; lifecycle ownership precedes setup
|
||||
@@ -429,6 +460,24 @@ Before agent setup can run, the concrete agent pins its accepted ID, options, an
|
||||
|
||||
`send()` and running `steer()` resolve the message source once and materialize `{ content, source }` as one detached, deeply frozen lossless-JSON record before `agent/queued` or inbox insertion. The notification and FIFO share that accepted content and source; its metadata wrapper is frozen separately, so neither retained caller references nor an earlier notification listener can rewrite what a later listener, the session log, or the model sees. Invalid content or source throws synchronously without notification, enqueue, or loop wakeup; idle `steer()` delegates to the same `send()` boundary. The later `agent/prompt-submit` waterfall can still replace a queued prompt by returning new content; ownership forbids in-place mutation, not the explicit rewrite protocol.
|
||||
|
||||
The inbox path makes that accepted-value boundary concrete. Getter evaluation happens during materialization, so liveness is rechecked before the accepted record crosses into an inbox FIFO:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
callerInput["Caller-owned content and source"] --> initialCheck["Require a live, drive-enabled agent"]
|
||||
initialCheck --> accept["Resolve source once; materialize and deep-freeze one record"]
|
||||
accept -->|"invalid lossless JSON"| invalidReject["Throw synchronously; no inbox insertion, agent/queued, or loop wakeup"]
|
||||
accept -->|"accepted"| liveness["Recheck disposal after caller getters"]
|
||||
liveness -->|"disposed reentrantly"| disposedReject["Throw disposed; do not insert or announce the message"]
|
||||
liveness -->|"still live"| inbox["Insert the record into the queued or steering FIFO"]
|
||||
inbox -->|"same frozen content and source"| queued["Emit agent/queued with a frozen metadata wrapper"]
|
||||
inbox -->|"if later drained, read the same owned record"| drain["Loop-owned delivery"]
|
||||
inbox -->|"cancel before drain"| cancelled["Clear the pending record without delivery"]
|
||||
inbox -->|"disposal wins before drain"| disposed["Stop delivery; the disposed agent may retain the pending record"]
|
||||
drain -->|"queued prompt"| prompt["agent/prompt-submit may block or explicitly replace"]
|
||||
drain -->|"steering consumed by an active turn"| steering["Append steering/message"]
|
||||
```
|
||||
|
||||
A stateful getter shows why validation and ownership must use the same capture:
|
||||
|
||||
```js
|
||||
|
||||
Reference in New Issue
Block a user