Agent-scope runtime design and correctness
Agent 作用域运行时设计与正确性
The [agent-scope contract](2026-07-08-agent-scope-contexts.md) is simple for contributors: register through `agent.ctx`, resolve one global-plus-agent view, publish only after setup, and retain the scope until work stops. The runtime must preserve that contract across a cooperative plugin framework, asynchronous creation, reentrant listeners, durable session commits, and worker or process failure. The main design ris
English
Problem
The agent-scope contract is simple for contributors: register through agent.ctx, resolve one global-plus-agent view, publish only after setup, and retain the scope until work stops. The runtime must preserve that contract across a cooperative plugin framework, asynchronous creation, reentrant listeners, durable session commits, and worker or process failure.
The main design risk is adding a second mechanism for every race. Separate reservations, readiness sentinels, cancellation relays, snapshot layers, and protection registries can mirror the same fact until no reader can tell which one is authoritative. That machinery also encourages the runtime to treat trusted typed calls as hostile serialization boundaries.
The implementation needs enough state to preserve real ownership and settlement boundaries, but no more. A correctness reviewer must be able to follow one fact from acceptance through publication and teardown without reconciling parallel representations.
Decision
The runtime uses one mechanism per independent fact. Scope routing has an opaque carrier and shared layer store; each live registry object has one entry record; each create or resume operation has one transaction; typed same-process calls borrow readonly values; real data boundaries materialize once; the cooperative prompt-assembly result is authoritative; and worker/process code retains separate terminal and quiescence state only where different owners can genuinely race.
The design can be skimmed as seven choices:
| Problem | Authoritative mechanism |
|---|---|
| Select global plus one agent's registrations | Opaque scope key, routing carrier, and shared layer store |
| Own one live agent or session | One registry entry captured by its disposer |
| Coordinate create/resume | One AgentCreationTransaction |
| Protect durable, queued, model, or wire data | Materialize once at that boundary |
| Pass typed values inside one process | Readonly borrowed contract |
| Compose the model-visible prompt and tool set | One shared tool view plus the authoritative assembly-waterfall result |
| Coordinate subagent, worker, and process shutdown | One cancellation signal plus the independent terminal/quiescence facts of that boundary |
The rest of this Agent Note expands those choices in dependency order: Cordis mechanics, scope routing, creation and session commit, tools and prompts, subagents and workflows, then executable checks.
The July 8 Agent Note remains the contributor contract. The separate subagent composition-controls Agent Note owns persona, toolFilter, and maxDepth; this document discusses only how their setup fits the lifecycle.
Cordis model: context, fiber, effect, receiver, and waterfall
Five Cordis ideas are required to understand the implementation. A context selects services and registration ownership; a fiber is one live plugin or child lifecycle; an effect attaches cleanup to a fiber; an event receiver selects listeners; and a waterfall lets listeners transform or short-circuit an operation in sequence.
A context is an ownership path through one service graph
All agents share one Cordis service graph. A derived context does not clone ToolRuntime, SystemPrompt, persistence, or model adapters; it changes how registrations made through that context are tagged and which effects own their cleanup.
agent.ctx is such a derived context. Service calls still reach the shared instances, while a registration can inspect its calling context and store a contribution under the nearest scope key. Ordinary plugin contexts carry no scope key and therefore register globally.
Fibers and effects make cleanup structural
A Cordis fiber is the live instance created when a plugin or child context is activated. Its state records whether that lifecycle is active, unloading, failed, or disposed. ctx.effect() and ctx.on() return disposers and also attach those disposers to the registering fiber, so unloading a plugin or agent scope removes everything registered through that context without a separate inventory.
The vendored Cordis fiber implementation establishes ownership before arbitrary setup or internal/plugin observers run. A reentrant unload can see the child fiber or effect that has started, reject effects added after unload begins, and join cleanup already started through a public single-shot disposer. Teardown observers are contained individually so one callback cannot prevent structural cleanup.
These are framework lifecycle guarantees rather than agent-specific policy. Agent creation depends on them because setup can activate arbitrary plugins and synchronously reenter owner disposal.
Receivers route listeners; waterfalls compose decisions
Cordis filters listeners using the dispatch receiver (this), while harness listeners need an explicit agent, execution, request, or other subject. Scoped<T> marks the receiver expected by a scoped event declaration, but the runtime carrier deliberately exposes no subject API.
Product helpers therefore construct the carrier and pass the domain subject separately. This prevents listener routing from becoming an alternate object model and keeps event signatures understandable without knowledge of carrier internals.
A Cordis waterfall is middleware-style dispatch. Each listener receives next(): calling it delegates to the remaining listeners and base operation, while returning without it short-circuits or replaces the downstream result. Waterfalls power prompt assembly and tool policy; ordinary emit events notify synchronously, and parallel events await all listeners without a veto result.
Scope routing: one opaque key selects one layer
The scope package implements the smallest object needed for Cordis routing. Its carrier holds only a composed service filter and scope predicate, while the package records the opaque key privately and exposes the scope fiber's quiescent disposer separately.
Scope identity uses object identity
A ScopeKey is an opaque object compared by identity. The harness uses the live Agent as its own key, but the primitive is domain-neutral and supports other scoped owners.
createScope(parent, key) returns a scope whose ctx shares the parent's services and whose effects are tagged with that key. scopeOf(ctx) reads the nearest registration key. scopeTarget(base, key) creates the event receiver whose filter preserves the base receiver's Cordis service filter, then admits unscoped listeners and listeners with that exact key.
The receiver is a small carrier rather than a transparent proxy for the domain object. Code that needs the agent receives the explicit event argument; code that needs registration ownership receives agent.ctx.
Registry reads overlay one exact layer
Scope-aware registries use ScopedLayers to own one eager global aggregate and lazily created identity-keyed aggregates. A read resolves the global layer and at most one exact local layer; it never creates state or traverses parentage. Registration visibility and Cordis effect ownership derive from the same context, and reclamation waits until the concrete layer's complete aggregate is empty (decision).
Each service retains its domain rule. Named command and prompt views use the shared insertion-ordered shadow merge; tools keep a richer resolver because restrictions filter globals before local tools are added and the reserved Code Mode transport is inserted separately. Prompt variables and tool guards retain live iteration, while tool-provider membership is materialized per assembly. Scope supplies storage lifecycle and named shadowing, not a universal registry view.
Fused dispatch helpers prevent subject drift
agentEvents(context, agent) constructs the agent's carrier and injects the same agent as the event subject. Session, tool, approval, prompt, and subagent services likewise derive routing from the object they already own instead of accepting an unrelated key.
The type marker rejects ordinary bare-receiver mistakes, and development invariants cover direct JavaScript or casted dispatch. The subject remains explicit because routing correctness and useful event data are different concerns.
Agent creation: one transaction owns the complete operation
Create and resume are one asynchronous lifecycle with several phases, not several lifecycles. AgentCreationTransaction owns caller and factory liveness, optional cancellation, private resources, publication, rollback, and the memoized teardown observed by every owner.
Registry entries are the only live identity records
AgentRegistry and SessionStore each keep one entry per live object. The entry holds the stable ID, object, scoped carrier, and the small amount of publication or append state that belongs to that object.
A detach closure captures its exact entry. It deletes only when the map still points to that entry, so an old disposer cannot delete a later object that reuses the same ID. No registry rereads a mutable caller object to decide identity.
There is no reservation API. Caller-supplied IDs are admitted at final entry. Concurrent same-ID operations may both complete private setup; exactly one final enter() succeeds, and every loser rolls its private resources back. Sequential reuse is valid after the earlier disposer reaches quiescence.
The transaction owns preparation before awaiting it
The transaction is installed under both the calling Cordis context and the concrete AgentLoop factory before persistence load or setup can suspend. It also observes an optional create/resume signal until the public operation settles.
Create prepares a new Session. Resume loads and validates the persisted Session before preparing the same live session identity. Both paths then build the scope, agent, and driver and invoke the same setup/publication algorithm.
The factory stores concrete trace targets but invokes them through a caller-bound Cordis trace. This preserves dependency origin and caller ownership without stacking trace proxies.
Setup is trusted composition inside a private world
Setup receives the full child context and may await plugin activation. It can register tools, prompt sections, restrictions, listeners, and other effects, but the public contract does not support driving or publishing the in-flight agent through casts or internal registry calls.
The transaction races asynchronous load and setup against deactivation rather than waiting forever for a promise owned by external code. If cancellation or owner unload wins, public creation rejects after transaction-owned cleanup even when the external promise never settles.
Publication has one ordered commit path
Publication admits and announces resources in the order required by observers:
- Enter the session.
- Enter the agent.
- Announce
session/created. - Announce
agent/created. - Enable public driving.
- Emit
agent/session-start. - Start the driver.
The agent never drives before both registries and creation notifications agree. A synchronous listener may veto or dispose an owner; the transaction records publication in progress and waits for that callback stack to unwind before teardown continues. Every creation announcement that begins has a matching disposal announcement during rollback.
The sequence diagram isolates the non-obvious race: a synchronous creation listener can request disposal while the publication call stack still owns both registry entries. Teardown must deactivate immediately but wait for that stack to unwind before stopping and detaching anything.
sequenceDiagram
participant Tx as AgentCreationTransaction
participant Registries
participant Listener as Synchronous listener
participant Driver
Tx->>Tx: mark publication in progress
Tx->>Registries: announce agent/created
Registries->>Listener: invoke inside the same call stack
Listener->>Tx: dispose reentrantly
Tx->>Tx: deactivate, teardown waits for publication
Tx-->>Listener: disposal request accepted
Listener-->>Registries: return
Registries-->>Tx: announcement unwound
Tx->>Tx: resolve publication settlement
Tx->>Driver: stop and drain
Tx->>Registries: detach agent, then session
Tx->>Tx: dispose scope and resolve teardown
Teardown preserves work before revoking registrations
Every teardown request joins one memoized path. The order is:
- Deactivate creation or driving and let synchronous publication finish.
- Stop and drain the driver, discarding any injection that remains pending.
- Detach the agent.
- Detach the session.
- Dispose the agent scope.
- Retire transaction ownership tracking.
This order lets final agent and session events use the matching scoped listeners and keeps persistence observers attached through the final flush. Scope disposal comes last because registration revocation is the externally visible lifetime boundary.
Session append: materialize, validate, commit, notify
Session events cross a durable boundary, so append owns their data. The rest of the algorithm uses one attached entry and one commit point.
Durable data is materialized once
Session headers, seeds, and appended events are lossless JSON data. The Session constructor or append path materializes and validates them before storage and exposes frozen snapshots, so later caller mutation cannot change persistence, replay, or model reconstruction.
This is a real ownership boundary: the values leave the caller, may be persisted, and must reconstruct the same request later. It is intentionally stricter than a typed same-process callback or registry definition.
Pre-commit listeners can veto; post-commit observers cannot
Append follows one sequence:
- Materialize the durable event and surface intent.
- Claim the SessionEntry and reject reentrant append on that entry.
- Resolve scoped callbacks and run internal invariant validation.
- Push exactly once; this is the commit point.
- Notify each observer independently, containing synchronous and asynchronous failures.
- Release append state and honor a detach requested during publication.
No observer error makes a committed event look uncommitted, and one bad listener cannot starve later listeners. Session invariants stage their transition before commit and apply it only when the same event reaches the contained post-commit observer.
flush() starts every persistence listener and awaits every result before reporting failure. This deliberate all-settled behavior prevents a synchronous failure from starving another backend or final flush.
Trust boundaries: copy only when ownership actually changes
The runtime distinguishes typed in-process contracts from serialization and durability boundaries. This is the main simplification rule for values and callbacks.
| Boundary | Ownership rule |
|---|---|
| Typed service/plugin call in the same process | Borrow readonly values and callbacks |
| Parsed plugin configuration or external file | Validate semantic and structural input |
| Queued inbox message | Materialize before asynchronous consumption |
| Model/tool JSON input or output | Materialize at the model/tool boundary |
| Durable session or persistence data | Materialize and validate before commit |
| Worker, process, or wire message | Serialize, validate, and own the decoded value |
Tests that fabricate hostile getters, replace typed callbacks after handoff, or cast fake service objects do not define a production contract by themselves. The runtime keeps checks where data crosses a parser, queue, model, durable, file, worker, process, or wire boundary and relies on readonly types plus plugin discipline inside the trusted process.
Callback containment is separate from data ownership. Listeners are arbitrary extension code and can throw even when their arguments are trusted; publication and post-commit paths still contain failures according to their event contract.
Tools and prompts: one view, authoritative assembly, committed outcomes
Tool presentation and execution share one private resolver. Prompt assembly remains trusted cooperative composition: registries supply the ordered input, and the assembly waterfall's returned value is exactly what the loop logs and sends. Execution uses separate one-way boundaries only where policy or outcome settlement must be monotonic.
One resolver defines the tool view
The private resolver applies the current presentation mode, live global restrictions, exact local overlay, and local shadowing. Schemas, lookup, execution, Code Mode SDK generation, and restriction validation all use that resolver or its pre-restriction global-name view.
The subagent composition-controls Agent Note owns the user-visible allow/deny semantics. The implementation requirement is agreement: a filtered-away global cannot remain executable through a different lookup path, and a locally shadowed definition is the same definition presented and executed.
ToolRestriction accepts readonly allow/deny names and compiles them into internal sets. Multiple restrictions intersect. Public visible() and knownNames() methods are unnecessary because only the registry needs the intermediate views.
Tool execution owns identity and boundary materialization
The registry assigns every execution a fresh branded Symbol token. Nested Code Mode calls carry the outer token as parent, so structured output can correlate an inner capture with its enclosing run_code result by identity.
A fresh registry-assigned Symbol provides collision-free execution identity without a WeakSet membership registry. Callers cannot supply the execution's own token through ToolExecutionInput; they only receive the pipeline-owned ToolExecution after the registry creates it. This is a trusted typed contract, not a runtime defense against arbitrary casts or JavaScript callers.
Arguments are materialized once where model/tool JSON enters the pipeline. Pre-, around-, and post-execute listeners operate on the typed execution and decisions. Call ID correlation, approval, monotonic guards, and Code Mode nesting remain explicit relational checks.
After post-execute or outer pipeline normalization, the registry losslessly snapshots the candidate result, converting a snapshot failure into an ordinary error, invokes the call's snapshotted optional ToolDefinition.finalizeContent callback, then materializes and freezes the accepted final result once. The callback may replace only content, so structured error identity, contexts, and metadata remain registry-owned even when a tool enforces a last-mile result bound. Every synchronous tools/result observer receives that exact committed object, and observer failures are contained individually. An outer pipeline or candidate-snapshot failure is normalized before final content, so observers can discard staged work against the same authoritative boundary.
The assembly waterfall owns the final model-visible composition
SystemPrompt first resolves the global-plus-agent sections, variables, and tool providers into a deterministic registry contribution. The scope-filtered system-prompt/assemble waterfall may then reorder, replace, add, or remove any section, variable, or schema. Its returned assembly is authoritative; there is no later restoration pass and no finality metadata on ordinary prompt sections, tool definitions, or provider results.
This is a trusted same-process extension point, not an authority boundary. A listener that changes Code Mode's run_code schema or tools:sdk instructions, or a structured child's capture schema or instruction, owns preserving a coherent protocol in the assembly it returns. ToolRuntime still reserves run_code against ordinary tool registration and restriction because those are registry invariants, but assembly middleware remains free to transform the final model-visible surface.
Scope solves the real isolation problem directly. Structured-output contributions register in the child's exact scope, while Code Mode derives its transport and SDK from the same resolved tool view. A second named-protection system would need another ownership and collision rule across arbitrary schema providers—including providers that intentionally contribute duplicate names—without creating a new trust boundary.
Structured output commits only authoritative outcomes
Structured output combines child-scoped composition with a two-phase execution commit. The child registers its structured_output tool and instruction before publication; a trusted assembly listener may transform those ordinary contributions and is responsible for preserving the protocol if the child is expected to complete. The tool body validates a candidate and stages it by the current ToolExecution, but successful capture is decided only by immutable tools/result observations.
For a native call, the observer deletes the stage and commits its value only when that exact execution's final result succeeds. A post-execute block or outer pipeline failure therefore cannot leave a captured value behind.
For a Code Mode SDK call, the inner successful result records { parentToken, value } rather than committing. The observer waits for the run_code execution whose token matches parentToken and commits only if that outer final result also succeeds. Program failure, runtime abort, or outer post-policy denial discards the pending value.
Once a value is pending or committed, a scoped monotonic guard denies later tool calls. The successful structured-output execution calls exec.concludeTurn(), so its own immutable result carries concludesTurn: true and the loop ends the tool loop at that step. A schema-validation failure remains an ordinary INVALID_ARGS tool error and leaves the child able to retry within the same turn.
Pure Code Mode's registry contribution omits structured_output from native wire schemas and exposes it through the generated SDK. The assembly waterfall may deliberately change that presentation; execution still validates against the child-scoped definition, and the listener owns the consistency of any alternate model-visible route it creates.
Three execution boundaries are deliberately one-way
Prompt assembly is intentionally cooperative, but three execution facts need one-way settlement after their extensible stages:
| Boundary | Final power | Why ordinary listener order is insufficient |
|---|---|---|
| Tool pre-policy | Deny monotonically | A later listener must not re-allow an already denied call |
| Tool result | Observe the immutable committed outcome | Structured output must commit only the result that actually escaped the pipeline |
| Turn continuation | Conclude through the committed tool result | A committed terminal output must end the turn |
ToolGuard is the monotonic policy registry. Committed tool observation is the contained tools/result point described above. Terminal structured output marks its own execution with concludesTurn, so terminality is data on the authoritative result rather than a separate hook decision.
Skill and approval services trust typed callers
Skill registry definitions and approval policies are readonly same-process contracts. Their services do not clone callback objects or defend against post-handoff callback replacement.
Skill still validates external skill files and parsed provider output, routes catalogs through the calling agent's tool view, and disposes registrations exactly. Approval still resolves policy, observes cancellation, routes approval/request by request.agent, records the durable audit pair, and contains answerer and post-commit observer failures.
Subagents: publication is the start promise
Subagent startup has one ownership transfer. The provider owns unpublished resources until its start promise fulfills with a published run; the caller owns the returned run and must dispose it.
The service contract has one cancellation channel
SubagentProvider.start() and SubagentRuntime.start() return Promise<SubagentRun>. The promise fulfills after the backend crosses its publication boundary, so callers and subagent/start observers never need a second run.started promise. Provider work that fails before publication rejects start(); prompt, turn, cancellation, and infrastructure outcomes after publication settle through SubagentRun.result without hiding the child id, as required by the durable catalog decision.
SubagentStartRequest.signal is required. Aborting it requests cancellation during startup and across the published run's remaining readiness or turn work. SubagentRun.dispose() also requests cancellation and awaits quiescence. There is no separate public run.cancel() channel.
Continuable conversations use their separate creation and follow-up operations and have no SubagentRun; their manager owns each resident AgentHandle.
The service validates provider capabilities and request semantics before calling the provider. A provider rejection cleans unpublished resources before the rejection escapes and emits no subagent/start/subagent/end pair. After fulfillment, the service attaches result observation, emits scoped start, and returns the run; a post-publication result rejection closes that pair. Provider removal prevents later starts but does not revoke a run already accepted by the provider.
In-process providers reuse the core transaction
Spawn and fork share one in-process driver. It creates the child through parent.ctx, passes the required signal into the core creation transaction, and installs persona, tool restriction, and structured-output contributions during unpublished setup.
The provider awaits creation and returns only the published run. At the handoff, core creation detaches its creation-only abort listener; the provider immediately rechecks the signal before installing the live-run listener, so an abort in that narrow interval disposes the new handle instead of escaping cancellation. Parent teardown follows the child because the operation belongs to parent.ctx; provider unload blocks new starts but does not become a second revocation owner for accepted runs. The run disposer cancels the child and awaits the AgentHandle's ordered teardown.
Spawn uses an empty session seed. Fork uses a validated completed-turn prefix. Conversation seeding changes history only and does not import scope, tools, services, or authority.
ACP providers own the process until readiness or cleanup
An ACP provider crosses a real process and wire boundary, so it retains validation, environment scrubbing, message serialization, abort/process races, and kill-to-exit quiescence.
Start resolves only after initialize and newSession succeed. Abort, spawn failure, RPC failure, or invalid startup response reaps the process before rejection. After readiness, result maps the ACP prompt outcome and streamed output; dispose requests cancellation, closes the connection, and awaits process exit through one memoized path.
Workflows and ACP processes: retain only independent async facts
Worker and child-process bridges need more state than same-process registries because messages, process death, and cleanup can settle independently. Their state is organized around those real facts rather than duplicate cancellation protocols.
Workflow children are pending starts or published records
The workflow host keeps pending provider-start promises and published child records. A child moves from pending to published only when async SubagentRuntime.start() fulfills; rejected starts clean their partial provider work and produce no child lifecycle pair.
One host-owned AbortController supplies the required signal to pending and live children. Closing workflow admission aborts that signal, so there is no duplicate ChildCancel worker RPC or explicit host-side run.cancel() fanout. Quiescence waits for both pending starts and published child disposal.
The worker boundary still serializes requests and outcomes. The host retains first-terminal-outcome arbitration, exact child accounting, worker-death handling, grace termination, late/duplicate message rejection, and bounded cleanup because result receipt, worker exit, and child quiescence are genuinely independent facts.
Terminal result and physical cleanup remain separate
The workflow result records the first accepted terminal outcome according to the public precedence rules. Cleanup can continue after that result is chosen: live children still need disposal, a worker still needs termination, and a slow external backend may outlive the configured grace bound.
Public disposal claims its memoized promise before invoking callbacks. Worker death closes admission before processing any queued late child request, synthesizes missing lifecycle ends, and starts child/process cleanup without rewriting an outcome already claimed.
ACP prompt settlement does not depend on update delivery
The automation-only ACP bridge correlates one in-flight prompt with its observed user-message turn directly. It does not scan from a log watermark or use session status as a second reconciliation oracle.
The session-event listener settles correlation from the matching turn/end even when a committed-message update cannot reach the client. Update delivery therefore cannot leave the session permanently in flight. ACP creates server-assigned fresh session ids and owns every resulting agent handle until connection teardown.
Correctness enforcement
The design is enforced at types, runtime escape points, generated contracts, and behavioral tests. No one layer is asked to prove what it cannot observe.
Types make the ordinary path hard to misuse
Readonly contracts describe borrowed same-process values. Scoped<T> marks event receivers, agentEvents() fuses carrier and subject, tool inputs omit registry-owned tokens, and subagent async return types expose publication and settlement directly.
TypeScript cannot govern JavaScript casts, direct Cordis dispatch, process messages, or durable files, so runtime enforcement remains at those escape points.
Runtime invariants cover cross-service facts
The dsh-scope/invariant companion verifies, when selected, that every declared scoped event uses a marked carrier and that event families exposing a subject use the matching key. The separate dsh-session/invariant contribution stages trace validation before append commit and advances after the same event commits; both register through ctx.invariants.
The plugin does not police trusted setup by scanning registries or reject prompt assembly objects fabricated through casts. Those checks would turn composition contracts into speculative runtime machinery without protecting a real external boundary.
Generated artifacts keep public contracts aligned
The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, type-equivalence blocks, and scoped-event resolver map are generated or freshness-gated from source. The TypeScript semantic-gates Agent Note owns Program construction, semantic event discovery, and resolver-generation rules.
Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, cooperative prompt assembly, structured-output commit in native and Code Mode, async subagent startup and signal cancellation, worker terminal arbitration, ACP settlement, and process teardown.
Alternatives considered
The July 8 Agent Note owns alternatives to the public flat-scope contract. The alternatives here concern implementation shape.
Use a transparent proxy as the scope carrier
A proxy that impersonates the subject must preserve property, callable, constructable, private-field, descriptor, and proxy-invariant behavior that listener routing never needs. A small opaque carrier keeps the filter and key while the explicit event argument carries the subject.
Reserve agent and session IDs before setup
Reservations prevent duplicate private setup work but require cross-service capabilities, release ordering, abandoned-reservation cleanup, and prepared-object binding. IDs are caller-supplied and concurrent reuse is caller error; final entry can choose the winner while the losing transaction rolls back cleanly.
Snapshot every typed same-process argument
Universal copying defends against stateful getters and callers that violate readonly contracts, but it adds allocation, duplicated validators, and paths that can forget to copy. Materialization belongs at parser, queue, model, durable, worker, process, and wire boundaries where ownership actually changes.
Give readiness, cancellation, and disposal separate controllers
Parallel sentinels can all mirror whether one operation is live. One transaction or start promise owns the operation; separate promises remain only where publication unwind, external work, terminal result, and physical quiescence can settle independently.
Keep synchronous subagent start plus run.started
This splits provider acceptance from publication and forces every consumer to register a partial run, attach result observation, await publication, and clean up publication failure. An async start promise keeps provider-to-caller ownership transfer at publication; the existing result promise owns any remaining readiness instead of adding another lifecycle promise.
Restore selected prompt or tool contributions after assembly
A post-waterfall restoration pass would create a second composition rule after the documented cooperative waterfall. Correctly assigning canonical presence or absence would also require provider ownership and collision rules for arbitrary tool-schema providers, whose ordinary output may contain duplicate names. Scoped registration already supplies the required per-agent isolation, and trusted assembly listeners own the protocol consistency of what they return, so named restoration adds machinery without establishing an independent boundary.
Remove worker/process lifecycle guards with same-process hardening
Worker messages, process death, and durable input do cross ownership and serialization boundaries. First-outcome arbitration, validation, environment scrubbing, and quiescent process cleanup remain necessary even though hostile same-process callback machinery does not.
Consequences
The implementation is smaller and its proof follows the same shape as its ownership graph. One key selects a layer, one entry owns a live registry object, one transaction owns creation, one resolver owns a tool view, and one async promise transfers subagent ownership.
What the design guarantees
- A scoped contribution is visible only in its exact agent view and is disposed with that scope.
- Create and resume expose no partially configured handle; final-entry losers and publication failures clean every prepared resource.
- Disposal retains scoped listeners and persistence through driver drain and final session work, then revokes the scope.
- Durable, queued, model, worker, process, and wire values are owned at their real boundary; typed same-process values follow readonly contracts.
- ToolRuntime's presentation, lookup, and execution resolve the same live view before expert assembly transforms, and committed results have one immutable observation point.
- Registry contributions are deterministic inputs, while the trusted assembly waterfall owns the final model-visible composition.
- Subagent start returns only a published run, required signals cancel pending or live work, and disposal reaches the backend's quiescence contract.
- Worker/process result precedence and cleanup remain correct under death, late messages, and bounded teardown.
Costs and limits
Scope-aware services still maintain global and identity-keyed maps, and operations must carry their real agent explicitly. Async create/resume and subagent start require callers to await ownership transfer and dispose returned handles.
A trusted system-prompt/assemble listener can remove or replace Code Mode and structured-output protocol pieces. This is deliberate: the listener owns final composition and must preserve any protocol the deployment expects to remain usable.
The design trusts typed plugins in the same process. It does not defend against arbitrary casts, stateful getters, mutation that violates readonly contracts, or a plugin deliberately using ambient service access outside the supported composition API.
The security and authority non-goal remains fundamental. These mechanisms prove registration composition, publication, and lifetime ownership; they do not prove confinement or parent-to-child non-escalation.
中文
问题
agent(智能体)作用域约定对贡献者而言很简单:通过 agent.ctx 注册,解析出一个全局加单 agent 的视图,仅在 setup 完成后发布,并保持作用域直到工作停止。运行时必须在协作式插件框架、异步创建、可重入监听器、持久化会话提交以及 worker 或进程故障等场景下维护这份约定。
主要的设计风险是为每个竞态条件引入第二套机制。独立的预留、就绪哨兵、取消中继、快照层和保护注册表可能镜像同一个事实,直到没有读者能分辨哪个才是权威的。这些机制还会诱使运行时把可信的类型化调用当作敌对的序列化边界来处理。
实现需要足够的状态来维护真实的所有权和结算边界,但不能更多。正确性审查者必须能够从接受、发布到拆除,沿着一条事实链跟踪下去,而无需在并行的表示之间做调和。
决策
运行时对每个独立事实使用一种机制。作用域路由有一个不透明载体与共享 layer store;每个活跃的注册表对象有一条注册表条目;每个创建或恢复操作有一个事务;类型化的同进程调用借用 readonly 值;真实数据边界只物化一次;协作式提示词组装的结果即为权威;worker/进程代码仅在不同所有者确实可能竞争时才保留独立的终止态和完全停稳态。
该设计可概括为七项选择:
| 问题 | 权威机制 |
|---|---|
| 选择全局加某个 agent 的注册 | 不透明作用域键、路由载体与共享 layer store |
| 拥有一个活跃的 agent 或会话 | 由其 disposer 捕获的单条注册表条目 |
| 协调创建/恢复 | 单个 AgentCreationTransaction |
| 保护持久化、队列、模型或协议格式数据 | 在该边界处一次性物化 |
| 在同一进程内传递类型化值 | Readonly 借用约定 |
| 组合模型可见的提示词与工具集 | 单个共享工具视图加权威的 assembly-waterfall(瀑布式事件)结果 |
| 协调 subagent、worker 和进程关闭 | 单个取消信号加该边界独立的终止态/完全停稳态事实 |
本 Agent Note 余下部分按依赖顺序展开这些选择:Cordis 机制、作用域路由、创建与会话提交、工具与提示词、subagent 与工作流,最后是可执行检查。
7 月 8 日 Agent Note 仍然是贡献者约定。独立的 subagent 组合控制 Agent Note 拥有 persona、toolFilter 和 maxDepth;本文仅讨论它们的 setup 如何融入生命周期。
Cordis 模型:上下文、fiber、effect、receiver 与 waterfall
理解实现需要五个 Cordis 概念。上下文选择服务和注册所有权;fiber 是一个活跃的插件或子生命周期;effect 将清理逻辑附加到 fiber;事件接收器选择监听器;waterfall 让监听器按顺序变换或短路一个操作。
上下文是贯穿单个服务图的所有权路径
所有 agent 共享一个 Cordis 服务图。派生的上下文不会克隆 ToolRuntime、SystemPrompt、持久化或模型适配器;它改变的是:通过该上下文进行的注册如何被标记,以及哪些 effect 拥有其清理逻辑。
agent.ctx 就是这样一个派生上下文。服务调用仍然到达共享实例,而注册操作可以检查其调用上下文并将贡献存储在最近的作用域键下。普通的插件上下文不携带作用域键,因此注册到全局。
Fiber 与 effect 使清理成为结构性的
Cordis fiber 是插件或子上下文被激活时创建的活跃实例。其状态记录该生命周期是 active、unloading、failed 还是 disposed。ctx.effect() 和 ctx.on() 返回 disposer,同时将这些 disposer 附加到注册所在的 fiber,因此卸载一个插件或 agent 作用域会移除通过该上下文注册的一切,无需单独的清单。
vendor 中的 Cordis fiber 实现在任意 setup 或 internal/plugin 观察者运行之前就建立了所有权。可重入的卸载可以看到已启动的子 fiber 或 effect,拒绝卸载开始后添加的 effect,并通过一个公开的一次性 disposer 加入已启动的清理。拆除观察者被逐个隔离,因此一个回调无法阻止结构性清理。
这些是框架生命周期保证,而非 agent 特有的策略。Agent 创建依赖它们,因为 setup 可以激活任意插件并同步重入所有者的 dispose(资源释放)。
Receiver 路由监听器;waterfall 组合决策
Cordis 使用 dispatch receiver(this)过滤监听器,而 harness 的监听器需要一个显式的 agent、execution、request 或其他主体。Scoped<T> 标记作用域事件声明所期望的 receiver,但运行时载体刻意不暴露主体 API。
因此,产品辅助函数构造载体并单独传递领域主体。这防止监听器路由变成另一套对象模型,并使事件签名在不了解载体内部的情况下也可理解。
Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 next():调用它则委托给剩余监听器和基础操作,不调用则短路或替换下游结果。Waterfall 驱动提示词组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。
<a id="scope-routing-one-opaque-key-selects-one-layer"></a>
作用域路由:一个不透明键选择一层
scope 包实现了 Cordis 路由所需的最小对象。其载体仅持有一个组合的服务过滤器和作用域谓词,而包私有地记录不透明键,并单独暴露会等待作用域 fiber 完全停稳的 disposer。
作用域标识使用对象标识
ScopeKey 是一个按标识比较的不透明对象。harness 使用活跃的 Agent 作为自身的键,但该原语与领域无关,支持其他作用域所有者。
createScope(parent, key) 返回一个作用域,其 ctx 共享父级的服务,其 effect 被标记为该键。scopeOf(ctx) 读取最近的注册键。scopeTarget(base, key) 创建事件接收器,其过滤器保留 base receiver 的 Cordis 服务过滤器,然后接纳无作用域的监听器和具有该确切键的监听器。
Receiver 是一个小型载体而非领域对象的透明代理。需要 agent 的代码接收显式的事件参数;需要注册所有权的代码接收 agent.ctx。
注册表读取叠加一个精确 layer
作用域感知的注册表使用 ScopedLayers,拥有一个即时创建的全局 aggregate 和按标识键惰性创建的 aggregate。读取解析全局 layer 和至多一个精确局部 layer;它不创建状态,也从不遍历父级链。注册可见性与 Cordis effect 所有权都从同一个上下文派生,而回收会等待具体 layer 的完整 aggregate 变空(见决策)。
每个服务保留其领域规则。命名 command 和提示词视图使用共享的、保持插入顺序的 shadow 合并;工具保留更丰富的 resolver,因为限制会在加入局部工具前过滤全局工具,保留的 Code Mode transport 则单独插入。提示词变量和工具 guard 保持实时迭代,而工具提供方成员关系按每次 assembly 物化。Scope 提供存储生命周期和命名遮蔽,而非通用的注册表视图。
融合 dispatch 辅助函数防止主体漂移
agentEvents(context, agent) 构造 agent 的载体并注入同一个 agent 作为事件主体。会话、工具、approval、提示词和 subagent 服务同样从它们已拥有的对象派生路由,而非接受一个无关的键。
类型标记拒绝普通的裸 receiver 误用,开发环境不变式覆盖直接 JavaScript 或强制转换的 dispatch。主体保持显式,因为路由正确性和有用的事件数据是不同的关注点。
Agent 创建:一个事务拥有完整操作
创建和恢复是一个具有多个阶段的异步生命周期,而非多个生命周期。AgentCreationTransaction 拥有调用方和工厂的活跃性、可选取消、私有资源、发布、回滚,以及每个所有者观察到的记忆化拆除。
注册表条目是唯一的活跃标识记录
AgentRegistry 和 SessionStore 各为每个活跃对象保留一条注册表条目。注册表条目持有稳定 ID、对象、作用域载体,以及属于该对象的少量发布或追加状态。
detach 闭包捕获其确切注册表条目。它仅在映射仍指向该注册表条目时才删除,因此旧的 disposer 无法删除一个复用相同 ID 的后续对象。注册表不会重读可变的调用方对象来决定标识。
没有预留 API。调用方提供的 ID 在最终写入注册表时被接纳。并发的同 ID 操作可能都完成私有 setup;恰好一个最终 enter() 成功,每个失败者回滚其私有资源。前一个 disposer 达到完全停稳态后,顺序复用即为有效。
事务在等待之前就拥有准备工作
事务在持久化加载或 setup 可能挂起之前,就被安装到调用方的 Cordis 上下文和具体的 AgentLoop 工厂下。它还在公开操作结算之前观察可选的创建/恢复信号。
创建准备一个新 Session。恢复加载并验证持久化的 Session,然后准备相同的活跃会话标识。两条路径随后构建作用域、agent 和 driver,并调用相同的 setup/发布算法。
工厂存储具体的 trace 目标,但通过调用方绑定的 Cordis trace 调用它们。这保留了依赖来源和调用方所有权,而不堆叠 trace 代理。
Setup 是私有世界内的可信组合
Setup 接收完整的子上下文,可以等待插件激活。它可以注册工具、提示词段、限制、监听器和其他 effect,但公开约定不支持通过强制转换或内部注册表调用来驱动或发布正在创建中的 agent。
事务将异步加载和 setup 与停用进行竞争,而非无限等待外部代码拥有的 promise。如果取消或所有者卸载获胜,即使外部 promise 永不结算,公开创建也会在事务拥有的清理之后拒绝。
发布有一条有序的提交路径
发布按观察者所需的顺序接纳和宣告资源:
- 将会话写入注册表。
- 将 agent 写入注册表。
- 宣告
session/created。 - 宣告
agent/created。 - 启用公开驱动。
- 发射
agent/session-start。 - 启动 driver。
Agent 在两个注册表和创建通知都达成一致之前绝不驱动。同步监听器可以否决或 dispose 一个所有者;事务记录发布进行中,并等待该回调栈展开后再继续拆除。每个已开始的创建宣告在回滚期间都有匹配的销毁宣告。
以下序列图隔离了非显而易见的竞态:同步创建监听器可以在发布调用栈仍拥有两个注册表条目时请求 dispose。拆除必须立即停用,但要等待该栈展开后才停止和分离任何东西。
sequenceDiagram
participant Tx as AgentCreationTransaction
participant Registries
participant Listener as Synchronous listener
participant Driver
Tx->>Tx: mark publication in progress
Tx->>Registries: announce agent/created
Registries->>Listener: invoke inside the same call stack
Listener->>Tx: dispose reentrantly
Tx->>Tx: deactivate, teardown waits for publication
Tx-->>Listener: disposal request accepted
Listener-->>Registries: return
Registries-->>Tx: announcement unwound
Tx->>Tx: resolve publication settlement
Tx->>Driver: stop and drain
Tx->>Registries: detach agent, then session
Tx->>Tx: dispose scope and resolve teardown
拆除在撤销注册之前保留工作
每个拆除请求加入一条记忆化路径。顺序为:
- 停用创建或驱动,让同步发布完成。
- 停止并排空 driver,丢弃仍处于待处理状态的注入。
- 分离 agent。
- 分离会话。
- dispose agent 作用域。
- 退役事务所有权追踪。
此顺序让最终的 agent 和会话事件能使用匹配的作用域监听器,并使持久化观察者在最终刷新完成前保持附加。作用域 dispose 放在最后,因为注册撤销是外部可见的生命期边界。
<a id="session-append-materialize-validate-commit-notify"></a>
会话追加:物化、验证、提交、通知
会话事件跨越持久化边界,因此追加操作拥有其数据。算法的其余部分使用一条已附加的注册表条目和一个提交点。
持久化数据一次性物化
Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造函数或追加路径在存储前物化并验证它们,并暴露冻结的快照,因此后续调用方的修改无法改变持久化、回放或模型重建。
这是一个真实的所有权边界:值离开调用方,可能被持久化,且必须在之后重建相同的请求。这比类型化的同进程回调或注册表定义有意更严格。
提交前监听器可以否决;提交后观察者不能
追加遵循一个序列:
- 物化持久化事件和表层意图。
- 取得 SessionEntry 的独占所有权,并拒绝该注册表条目上的重入追加。
- 解析作用域回调并运行内部不变式验证。
- 恰好推送一次;这是提交点。
- 逐个通知每个观察者,隔离同步和异步失败。
- 释放追加状态并兑现发布期间请求的 detach。
没有观察者错误能让已提交的事件看起来未提交,一个坏的监听器也无法饿死后续监听器。Session 不变式在提交前暂存其转换,仅当同一事件到达被隔离的提交后观察者时才应用。
flush() 启动每个持久化监听器并等待所有结果后再报告失败。这种有意的 all-settled 行为防止同步失败饿死另一个后端或最终刷新。
信任边界:仅在所有权真正变更时复制
运行时区分类型化的进程内约定与序列化及持久化边界。这是值和回调的主要简化规则。
| 边界 | 所有权规则 |
|---|---|
| 同进程内的类型化服务/插件调用 | 借用 readonly 值和回调 |
| 解析的插件配置或外部文件 | 验证语义和结构输入 |
| 队列中的收件箱消息 | 在异步消费前物化 |
| 模型/工具 JSON 输入或输出 | 在模型/工具边界处物化 |
| 持久化会话或持久化数据 | 在提交前物化并验证 |
| Worker、进程或协议格式消息 | 序列化、验证并拥有解码后的值 |
测试中构造恶意 getter、在交接后替换类型化回调、或强制转换伪造服务对象的做法本身不定义生产约定。运行时在数据跨越解析器、队列、模型、持久化、文件、worker、进程或协议格式(wire format)边界时保留检查,并在可信进程内依赖 readonly 类型加插件纪律。
回调隔离与数据所有权是分开的。监听器是任意扩展代码,即使其参数是可信的也可能抛出异常;发布和提交后路径仍按其事件约定隔离失败。
工具与提示词:单一视图、权威组装、已提交的结果
工具展示和执行共享一个私有解析器。提示词组装仍然是可信的协作式组合:注册表提供有序输入,assembly waterfall 的返回值就是 agent loop(智能体循环)记录和发送的内容。执行仅在策略或结果结算必须单调时才使用独立的单向边界。
一个解析器定义工具视图
私有解析器应用当前展示模式、活跃的全局限制、精确的局部叠加和局部遮蔽。Schema、查找、执行、Code Mode SDK 生成和限制验证都使用该解析器或其限制前的全局名称视图。
subagent 组合控制 Agent Note 拥有用户可见的 allow/deny 语义。实现要求是一致性:被过滤掉的全局工具不能通过另一条查找路径仍可执行,局部遮蔽的定义就是被展示和执行的同一个定义。
ToolRestriction 接受 readonly 的 allow/deny 名称并将其编译为内部集合。多个限制取交集。公开的 visible() 和 knownNames() 方法是不必要的,因为只有注册表需要中间视图。
工具执行拥有标识和边界物化
注册表为每次执行分配一个新的带品牌的 Symbol token。嵌套的 Code Mode 调用将外层 token 作为 parent 携带,因此结构化输出可以通过标识将内层捕获与其外层 run_code 结果关联。
注册表分配的新 Symbol 提供无碰撞的执行标识,无需 WeakSet 成员注册表。调用方无法通过 ToolExecutionInput 提供执行自身的 token;它们仅在注册表创建后接收流水线拥有的 ToolExecution。这是一个可信的类型化约定,而非针对任意强制转换或 JavaScript 调用方的运行时防御。
参数在模型/工具 JSON 进入流水线时一次性物化。Pre-、around- 和 post-execute 监听器操作类型化的 execution 和决策。Call ID 关联、审批、单调守卫和 Code Mode 嵌套仍然是显式的关系检查。
在 post-execute 或外层流水线完成规范化后,注册表先为候选结果创建无损快照,并将快照失败转为普通错误;随后调用在本次调用创建时已快照的可选 ToolDefinition.finalizeContent 回调,最后一次性物化并冻结被接受的最终结果。该回调只能替换内容,因此即使工具强制最后一道结果上限,结构化错误标识、上下文与元数据仍由注册表拥有。每个同步的 tools/result 观察者接收该确切的已提交对象,观察者失败被逐个隔离。外层流水线失败或候选快照失败会在最终内容处理之前被规范化,因此观察者可以丢弃针对同一权威边界的暂存工作。
Assembly waterfall 拥有最终的模型可见组合
SystemPrompt 首先将全局加 agent 的段、变量和工具提供方解析为确定性的注册表贡献。作用域过滤的 system-prompt/assemble waterfall 随后可以重排、替换、添加或移除任何段、变量或 schema。其返回的组装结果即为权威;没有后续的恢复步骤,普通提示词段、工具定义或提供方结果上也没有终态元数据。
这是一个可信的同进程扩展点,而非权限边界。修改 Code Mode 的 run_code schema 或 tools:sdk 指令,或结构化子级的捕获 schema 或指令的监听器,有责任在其返回的组装中保持协议的一致性。ToolRuntime 仍然保留 run_code 不受普通工具注册和限制影响,因为那些是注册表不变式,但 assembly 中间件仍然可以自由变换最终的模型可见表面。
Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子级的精确作用域中,而 Code Mode 从同一个已解析的工具视图派生其传输和 SDK。第二套命名保护系统需要另一套所有权和碰撞规则来覆盖任意 schema 提供方(包括有意贡献重复名称的提供方),却不创建新的信任边界。
<a id="structured-output-commits-only-authoritative-outcomes"></a>
结构化输出仅提交权威结果
结构化输出将子作用域组合与两阶段执行提交相结合。子级在发布前注册其 structured_output 工具和指令;可信的 assembly 监听器可以变换这些普通贡献,并有责任在期望子级完成时保持协议。工具体验证候选值并按当前 ToolExecution 暂存,但成功捕获仅由不可变的 tools/result 观察决定。
对于原生调用,观察者仅在该确切执行的最终结果成功时才删除暂存并提交其值。因此 post-execute 阻止或外层流水线失败不会留下已捕获的值。
对于 Code Mode SDK 调用,内层成功结果记录 { parentToken, value } 而非提交。观察者等待 token 匹配 parentToken 的 run_code 执行,仅在该外层最终结果也成功时才提交。程序失败、运行时中止或外层 post-policy 拒绝会丢弃待定值。
一旦值处于待定或已提交状态,作用域单调守卫拒绝后续工具调用。成功的结构化输出执行会调用 exec.concludeTurn(),因此其自身不可变结果携带 concludesTurn: true,循环在该步骤结束工具循环。Schema 验证失败仍然是普通的 INVALID_ARGS 工具错误,子级可以在同一轮次内重试。
纯 Code Mode 的注册表贡献从原生 wire schema 中省略 structured_output,并通过生成的 SDK 暴露它。Assembly waterfall 可以有意改变该展示;执行仍然针对子作用域定义进行验证,监听器拥有其创建的任何替代模型可见路由的一致性。
<a id="three-execution-boundaries-are-deliberately-one-way"></a>
三个执行边界有意设为单向
提示词组装有意是协作式的,但三个执行事实在其可扩展阶段之后需要单向结算:
| 边界 | 最终权力 | 为何普通监听器顺序不够 |
|---|---|---|
| 工具 pre-policy | 单调拒绝 | 后续监听器不得重新允许已被拒绝的调用 |
| 工具结果 | 观察不可变的已提交结果 | 结构化输出必须仅提交实际逃出流水线的结果 |
| 轮次 continuation | 通过已提交工具结果终止 | 已提交的终端输出必须结束轮次 |
ToolGuard 是单调策略注册表。已提交的工具观察是上述被隔离的 tools/result 点。终端结构化输出在自身执行上标记 concludesTurn,因此终止性成为权威结果上的数据,而不是独立 hook 决策。
skill(技能)和 approval 服务信任类型化调用方
Skill 注册表定义和 approval 策略是 readonly 的同进程约定。它们的服务不克隆回调对象,也不防御交接后的回调替换。
Skill 仍然验证外部 skill 文件和解析的提供方输出,通过调用 agent 的工具视图路由目录,并精确 dispose 注册。Approval 仍然解析策略、观察取消、按 request.agent 路由 approval/request、记录持久化审计对,并隔离应答者和提交后观察者的失败。
subagent:发布即 start promise
subagent 启动有一次所有权转移。提供方拥有未发布资源,直到其 start promise 以一个已发布 run 兑现;调用方拥有返回的 run 并必须 dispose 它。
服务约定有一个取消通道
SubagentProvider.start() 和 SubagentRuntime.start() 返回 Promise<SubagentRun>。Promise 会在后端跨过发布边界后兑现,因此调用方和 subagent/start 观察者从不需要第二个 run.started promise。提供方工作如果在发布前失败,start() 就会被拒绝;发布后的提示词、轮次、取消与基础设施结果会通过 SubagentRun.result 结算,且不会隐藏 child id,这也是持久化目录决策所要求的约定。
SubagentStartRequest.signal 是必需的。中止它会在启动期间,以及已发布 run 的剩余就绪或轮次工作中请求取消。SubagentRun.dispose() 也请求取消并等待完全停稳。没有单独的公开 run.cancel() 通道。
可继续对话使用各自独立的创建和后续操作,并且没有 SubagentRun;其管理器拥有每个驻留中的 AgentHandle。
服务在调用提供方之前验证提供方能力和请求语义。提供方 rejection 在逃出之前清理未发布资源,且不发射 subagent/start/subagent/end 对。兑现之后,服务附加结果观察、发射作用域 start 并返回 run;发布后的结果 rejection 会结束该事件对。提供方移除会阻止后续 start,但不撤销提供方已接受的 run。
进程内提供方复用核心事务
spawn 和 fork 共享一个进程内 driver。它通过 parent.ctx 创建子级,将必需的 signal 传入核心创建事务,并在未发布的 setup 期间安装 persona、工具限制和结构化输出贡献。
提供方等待创建并仅返回已发布的 run。在交接时,核心创建分离其仅用于创建的 abort 监听器;提供方在安装活跃 run 监听器之前立即重新检查 signal,因此在那个窄窗口中的 abort 会 dispose 新句柄而非逃脱取消。父级拆除会一并拆除子级,因为操作属于 parent.ctx;提供方卸载阻止新 start 但不成为已接受 run 的第二个撤销所有者。Run disposer 取消子级并等待 AgentHandle 的有序拆除。
spawn 使用空会话种子。fork 使用经验证的已完成轮次前缀。对话种子仅改变历史,不导入作用域、工具、服务或权限。
ACP(Agent Client Protocol)提供方拥有进程直到就绪或清理
ACP 提供方跨越真实的进程和协议格式边界,因此它保留验证、环境清洗、消息序列化、abort/进程竞争,以及从 kill 到进程退出并完全停稳的过程。
Start 仅在 initialize 和 newSession 成功后才 resolve。Abort、spawn 失败、RPC 失败或无效启动响应在拒绝前回收进程。就绪后,result 映射 ACP 提示词结果和流式输出;dispose 请求取消、关闭连接并通过一条记忆化路径等待进程退出。
工作流与 ACP 进程:仅保留独立的异步事实
Worker 和子进程桥接比同进程注册表需要更多状态,因为消息、进程死亡和清理可以独立结算。它们的状态围绕这些真实事实组织,而非重复的取消协议。
<a id="workflow-children-are-pending-starts-or-published-records"></a>
工作流子级是待定 start 或已发布记录
工作流宿主保持待定的提供方 start promise 和已发布的子级记录。子级仅在异步 SubagentRuntime.start() 兑现时才从待定变为已发布;被拒绝的 start 清理其部分提供方工作且不产生子级生命周期对。
一个宿主拥有的 AbortController 向待定和活跃子级提供必需的 signal。关闭工作流准入中止该 signal,因此没有重复的 ChildCancel worker RPC 或显式的宿主侧 run.cancel() 扇出。完全停稳需要等待待定 start 和已发布子级 dispose 两者。
Worker 边界仍然序列化请求和结果。宿主保留首个终端结果仲裁、精确的子级计数、worker 死亡处理、优雅终止、迟到/重复消息拒绝和有界清理,因为结果接收、worker 退出和子级完全停稳是真正独立的事实。
终端结果与物理清理保持分离
工作流结果按公开优先级规则记录首个被接受的终端结果。该结果选定后清理可以继续:活跃子级仍需 dispose,worker 仍需终止,慢速外部后端可能超出配置的优雅期限。
公开 dispose 在调用回调之前取得其记忆化 promise 的所有权。Worker 死亡在处理任何排队的迟到子级请求之前关闭准入,合成缺失的生命周期结束,并启动子级/进程清理而不重写已声明的结果。
ACP 提示词结算不依赖更新投递
仅面向自动化的 ACP 桥接层直接将一个进行中的提示词与其观察到的用户消息轮次关联。它不从日志水位线扫描,也不使用会话状态作为第二个调和预言机。
即使已提交消息的更新无法送达客户端,会话事件监听器也会从匹配的 turn/end 结算关联。因此更新投递不能让会话永久处于进行中状态。ACP 创建由服务器分配 id 的全新会话,并拥有由此产生的每个 agent 句柄,直到连接拆除。
正确性强制
该设计通过类型、运行时逃逸点、生成的约定和行为测试来强制执行。没有哪一层被要求证明它无法观察到的东西。
类型使常规路径难以误用
Readonly 约定描述借用的同进程值。Scoped<T> 标记事件接收器,agentEvents() 融合载体和主体,工具输入省略注册表拥有的 token,subagent 异步返回类型直接暴露发布与结算。
TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进程消息或持久化文件,因此运行时强制保留在这些逃逸点。
运行时不变式覆盖跨服务事实
dsh-scope/invariant 配套插件在被选用时验证每个声明的作用域事件使用带标记的载体,以及暴露主体的事件族使用匹配的键。独立的 dsh-session/invariant 贡献在追加提交前暂存 trace 验证,并在同一事件提交后推进;二者都通过 ctx.invariants 注册。
该插件不通过扫描注册表来管控可信 setup,也不拒绝通过强制转换构造的提示词 assembly 对象。这些检查会将组合约定变成推测性的运行时机制,却不保护真实的外部边界。
生成的产物使公开约定保持对齐
事件目录、服务目录、生产者/消费方矩阵、配置目录、模块图、工具目录、type-equiv 块和作用域事件解析器映射都是从源码生成或受新鲜度门禁约束的。TypeScript 语义门禁 Agent Note 拥有 Program 构造、语义事件发现和解析器生成规则。
行为测试固定了作用域路由和 dispose、最终写入注册表时的碰撞清理、发布回滚、有序完全停稳、持久化前/后提交行为、跨展示和执行的活跃工具过滤、协作式提示词组装、原生和 Code Mode 中的结构化输出提交、异步 subagent 启动和信号取消、worker 终端仲裁、ACP 结算和进程拆除。
曾考虑的替代方案
7 月 8 日 Agent Note 拥有公开扁平作用域约定的替代方案。此处的替代方案关注实现形态。
使用透明代理作为作用域载体
模拟主体的代理必须保持属性、可调用、可构造、私有字段、描述符和代理不变式行为,而监听器路由从不需要这些。一个小型不透明载体保持过滤器和键,而显式事件参数携带主体。
在 setup 前预留 agent 和会话 ID
预留防止重复的私有 setup 工作,但需要跨服务能力、释放排序、废弃预留清理和已准备对象绑定。ID 由调用方提供,并发复用是调用方错误;最终写入注册表时可以选择赢家,而失败的事务干净地回滚。
对每个类型化的同进程参数做快照
通用复制防御有状态 getter 和违反 readonly 约定的调用方,但增加分配、重复验证器和可能遗忘复制的路径。物化属于解析器、队列、模型、持久化、worker、进程和协议格式边界——即所有权真正变更的地方。
为就绪、取消和 dispose 提供独立控制器
并行哨兵可能都镜像一个操作是否活跃。一个事务或 start promise 拥有操作;独立 promise 仅在发布展开、外部工作、终端结果和物理层面的完全停稳可以独立结算时才保留。
保留同步 subagent start 加 run.started
这将提供方接受与发布分离,迫使每个消费方注册部分 run、附加结果观察、等待发布并清理发布失败。异步 start promise 将提供方到调用方的所有权转移保持在发布边界;现有的结果 promise 负责所有剩余就绪工作,无需增加另一个生命周期 promise。
在 assembly 之后恢复选定的提示词或工具贡献
Waterfall 之后的恢复步骤会在文档化的协作式 waterfall 之后创建第二套组合规则。正确分配规范的存在或缺失还需要为任意工具 schema 提供方制定所有权和碰撞规则,而这些提供方的普通输出可能包含重复名称。作用域注册已经提供了所需的按 agent 隔离,可信的 assembly 监听器拥有其返回内容的协议一致性,因此命名恢复增加了机制却不建立独立边界。
用同进程加固替代 worker/进程生命周期守卫
Worker 消息、进程死亡和持久化输入确实跨越所有权和序列化边界。首个结果仲裁、验证、环境清洗和使进程完全停稳的清理即使在敌对的同进程回调机制不存在时仍然必要。
后果
实现更小,其证明与所有权图具有相同的形状。一个键选择一层,一条注册表条目拥有一个活跃注册表对象,一个事务拥有创建,一个解析器拥有工具视图,一个异步 promise 转移 subagent 所有权。
设计保证的内容
- 作用域贡献仅在其精确的 agent 视图中可见,并随该作用域一起 dispose。
- 创建和恢复不暴露部分配置的句柄;最终写入注册表时的失败者和发布失败清理每个已准备的资源。
- dispose 在 driver 排空和最终会话工作期间保留作用域监听器和持久化,然后撤销作用域。
- 持久化、队列、模型、worker、进程和协议格式的值在其真实边界处被拥有;类型化的同进程值遵循 readonly 约定。
- ToolRuntime 的展示、查找和执行在专家 assembly 变换之前解析相同的活跃视图,已提交的结果有一个不可变的观察点。
- 注册表贡献是确定性输入,而可信的 assembly waterfall 拥有最终的模型可见组合。
- subagent start 仅返回已发布的 run,必需的 signal 取消待定或活跃的工作,dispose 到达后端的完全停稳约定。
- Worker/进程结果优先级和清理在死亡、迟到消息和有界拆除下保持正确。
代价与局限
作用域感知服务仍然维护全局和按标识键索引的映射,操作必须显式携带其真实 agent。异步创建/恢复和 subagent start 要求调用方等待所有权转移并 dispose 返回的句柄。
可信的 system-prompt/assemble 监听器可以移除或替换 Code Mode 和结构化输出协议片段。这是有意为之:监听器拥有最终组合,必须保持部署期望仍可用的任何协议。
该设计信任同进程中的类型化插件。它不防御任意强制转换、有状态 getter、违反 readonly 约定的修改,或插件有意在支持的组合 API 之外使用环境服务访问。
安全与权限非目标仍然是根本性的。这些机制证明注册组合、发布和生命期所有权;它们不证明隔离或父到子的非升权。