Explorar o código

refactor(agent): make runtime identity explicit

_Kerman hai 1 semana
pai
achega
ebce3a5f04
Modificáronse 61 ficheiros con 280 adicións e 395 borrados
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml
  2. 3 1
      .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md
  3. 3 1
      .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
  5. 5 3
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
  6. 5 3
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml
  8. 4 4
      .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md
  9. 4 4
      .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md
  10. 2 2
      docs/subsystems/core.i18n.yaml
  11. 15 9
      docs/subsystems/core.md
  12. 15 9
      docs/subsystems/core.zh.md
  13. 1 4
      packages/acp/acp/src/session.ts
  14. 3 8
      packages/api/gateway/src/index.ts
  15. 3 1
      packages/api/gateway/src/types.ts
  16. 15 85
      packages/api/gateway/tests/gateway-stream.host.spec.ts
  17. 0 1
      packages/api/gateway/tests/gateway.host.spec.ts
  18. 2 0
      packages/api/remotes/package.json
  19. 7 6
      packages/api/remotes/src/index.ts
  20. 11 3
      packages/api/remotes/tests/remote-events.host.spec.ts
  21. 6 6
      packages/api/session-controller/src/agent.ts
  22. 1 4
      packages/api/session-controller/tests/agent.host.spec.ts
  23. 2 2
      packages/api/session-controller/tests/session-fork.host.spec.ts
  24. 2 3
      packages/api/session-controller/tests/session-presets.host.spec.ts
  25. 2 2
      packages/bundle/headless/tests/headless.spec.ts
  26. 2 2
      packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts
  27. 2 2
      packages/core/agent-loop/README.i18n.yaml
  28. 2 2
      packages/core/agent-loop/README.md
  29. 2 2
      packages/core/agent-loop/README.zh.md
  30. 1 1
      packages/core/agent-loop/src/agent.ts
  31. 23 10
      packages/core/agent-loop/src/index.ts
  32. 3 3
      packages/core/agent-loop/tests/agent-initiator.spec.ts
  33. 3 3
      packages/core/agent-loop/tests/resume.spec.ts
  34. 6 9
      packages/core/agent-loop/tests/scope-lifecycle.spec.ts
  35. 2 2
      packages/core/agent/README.i18n.yaml
  36. 2 2
      packages/core/agent/README.md
  37. 2 2
      packages/core/agent/README.zh.md
  38. 18 28
      packages/core/agent/src/index.ts
  39. 22 9
      packages/core/agent/tests/agent.spec.ts
  40. 15 15
      packages/extensions/tool-cordis/src/api-catalog.ts
  41. 1 1
      packages/schedule/schedule/tests/plugin.spec.ts
  42. 1 1
      packages/sdk/server/tests/built-scope-carrier.e2e.ts
  43. 8 8
      packages/sdk/server/tests/server.spec.ts
  44. 3 3
      packages/subagent/subagent-in-process-driver/src/index.ts
  45. 4 4
      packages/subagent/subagent/src/continuation.ts
  46. 5 9
      packages/subagent/tool-subagent/src/index.ts
  47. 2 2
      packages/typert/protocol/README.i18n.yaml
  48. 1 1
      packages/typert/protocol/README.md
  49. 1 1
      packages/typert/protocol/README.zh.md
  50. 0 2
      packages/typert/protocol/src/index.ts
  51. 8 34
      packages/typert/protocol/src/types.ts
  52. 2 2
      packages/typert/registry/README.i18n.yaml
  53. 1 1
      packages/typert/registry/README.md
  54. 1 1
      packages/typert/registry/README.zh.md
  55. 0 17
      packages/typert/registry/src/service.ts
  56. 0 31
      packages/typert/registry/tests/typert.spec.ts
  57. 1 4
      packages/webhook/webhook/src/session.ts
  58. 10 9
      packages/webhook/webhook/tests/runtime.spec.ts
  59. 6 4
      packages/webhook/webhook/tests/session.spec.ts
  60. 3 0
      pnpm-lock.yaml
  61. 0 1
      scripts/gen-cordis-catalog.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md
-2026-07-08-agent-scope-contexts.md: eb3f6f247bac1a1d81aa2644132c7b9cc04d602c
-2026-07-08-agent-scope-contexts.zh.md: a0f4ffb0ef80dd2fc1ee61c9ab3f4730c28c687e
+2026-07-08-agent-scope-contexts.md: a1fa4527db4dea17e3b97f47cdc5613967b6c23a
+2026-07-08-agent-scope-contexts.zh.md: 80062d2a73548a0057cc7dbdd4afcb3e1102daa9

+ 3 - 1
.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md

@@ -16,6 +16,8 @@ The mechanism also needs a publication boundary. An agent must not become visibl
 
 Every live agent owns one flat registration layer exposed as `agent.ctx`. Code registers through the context that owns a contribution; scope-aware services combine deployment-global registrations with exactly one matching agent layer; operations choose that layer from their real agent; and the layer exists for the agent's complete published lifetime.
 
+`agent.ctx` carries registration ownership and the scope key; it does not expose a reverse `agent` property. Code that needs the domain subject receives it explicitly: `AgentSetup` receives `(agentCtx, agent)`, and scoped events carry their subject in the payload.
+
 Cordis is the plugin framework underneath the SDK. A Cordis **context** is the object plugins use to access services and register effects whose cleanup follows that context. The [Cordis primer](../../../../docs/cordis-primer.md) explains the framework in more detail.
 
 For most contributors, the complete contract is four rules:
@@ -88,7 +90,7 @@ await handle.dispose()
 ctx.tools.get('review_summary', handle.agent)  // undefined: scope is gone
 ```
 
-Setup receives a full trusted Cordis context so it can compose ordinary plugins and services. Its contract is composition-only: driving or publishing the in-flight agent through casts or internal registry calls is unsupported.
+Setup receives the full trusted Cordis context and unpublished Agent so it can compose ordinary plugins and services while reading the exact child Session when needed. Its contract is composition-only: driving or publishing the in-flight agent through casts or internal registry calls is unsupported.
 
 ### The operation chooses the view
 

+ 3 - 1
.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md

@@ -16,6 +16,8 @@ Status: implemented
 
 每个存活的 agent 拥有一个扁平的注册层,通过 `agent.ctx` 暴露。代码通过拥有某项贡献的上下文进行注册;具备作用域感知的服务将部署全局注册与恰好一个匹配的 agent 层合并;操作从其真实 agent 选择该层;该层在 agent 的完整发布生命周期内存在。
 
+`agent.ctx` 携带注册所有权和作用域键,不暴露反向的 `agent` 属性。需要领域主体的代码会显式接收它:`AgentSetup` 接收 `(agentCtx, agent)`,作用域事件则在 payload 中携带主体。
+
 Cordis 是 SDK 底层的插件框架。Cordis **上下文**是插件用来访问服务和注册效果的对象,效果的清理跟随该上下文。[Cordis 入门](../../../../docs/cordis-primer.zh.md)对该框架有更详细的说明。
 
 对大多数贡献者而言,完整约定是四条规则:
@@ -88,7 +90,7 @@ await handle.dispose()
 ctx.tools.get('review_summary', handle.agent)  // undefined: scope is gone
 ```
 
-setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通插件和服务。其约定仅限组合:不支持通过 cast 或内部注册表调用来驱动或发布正在构建中的 agent。
+setup 接收完整的受信 Cordis 上下文和未发布的 Agent,因此可以组合普通插件和服务,也能在需要时读取确切的子 Session。其约定仅限组合:不支持通过 cast 或内部注册表调用来驱动或发布正在构建中的 agent。
 
 ### 操作选择视图
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
-2026-07-12-agent-scope-runtime-design.md: 7aebac35d1a5477f0b1d3857e983680ea38bd9f5
-2026-07-12-agent-scope-runtime-design.zh.md: c636bf6f51c58aa5d9f1e3b6f9988b4f4ab5b86c
+2026-07-12-agent-scope-runtime-design.md: 06f16e59357e6cca8382952bcc81dbe95fdd5583
+2026-07-12-agent-scope-runtime-design.zh.md: 12a3f916c5d5f2af0c929b7575064ce92ecd261e

+ 5 - 3
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md

@@ -42,6 +42,8 @@ All agents share one Cordis service graph. A derived context does not clone `Too
 
 `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.
 
+The Agent context is exactly the context returned by `createScope`; it carries no second reverse association to the Agent. Subject-bearing APIs pass the Agent explicitly, leaving one formal scope mechanism for registration ownership and routing.
+
 ### 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.
@@ -68,7 +70,7 @@ A `ScopeKey` is an opaque object compared by identity. The harness uses the live
 
 `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`.
+The receiver is a small carrier rather than a transparent proxy for the domain object. Code that needs the agent receives an explicit setup parameter or event argument; code that needs registration ownership receives `agent.ctx`.
 
 ### Registry reads overlay one exact layer
 
@@ -100,11 +102,11 @@ The transaction is installed under both the calling Cordis context and the concr
 
 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.
+The factory stores concrete trace targets but invokes them through a caller-bound Cordis trace. A runtime child creator passes the parent Agent explicitly, and AgentRegistry forwards it separately from the caller Context. This preserves dependency origin and both ownership facts without stacking trace proxies or attaching a domain object to the Context. Scoped Remote event adapters likewise receive the Agent in the request, verify that it is the carrier key, and project its Context and wire identity directly. No scope index reconstructs an Agent from a Context.
 
 ### 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.
+Setup receives the full child context and the exact unpublished Agent, and may await plugin activation. It can register tools, prompt sections, restrictions, listeners, and other effects, and consumers that need the child's Session read it from the Agent parameter. 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.
 

+ 5 - 3
.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md

@@ -42,6 +42,8 @@ Status: implemented
 
 `agent.ctx` 就是这样一个派生上下文。服务调用仍然到达共享实例,而注册操作可以检查其调用上下文并将贡献存储在最近的作用域键下。普通的插件上下文不携带作用域键,因此注册到全局。
 
+Agent 上下文就是 `createScope` 返回的上下文,不携带第二份指回 Agent 的关联。需要主体的 API 显式传递 Agent,因此注册所有权与路由只依赖一种正式的作用域机制。
+
 ### Fiber 与 effect 使清理成为结构性的
 
 Cordis fiber 是插件或子上下文被激活时创建的活跃实例。其状态记录该生命周期是 active、unloading、failed 还是 disposed。`ctx.effect()` 和 `ctx.on()` 返回 disposer,同时将这些 disposer 附加到注册所在的 fiber,因此卸载一个插件或 agent 作用域会移除通过该上下文注册的一切,无需单独的清单。
@@ -70,7 +72,7 @@ scope 包实现了 Cordis 路由所需的最小对象。其载体仅持有一个
 
 `createScope(parent, key)` 返回一个作用域,其 `ctx` 共享父级的服务,其 effect 被标记为该键。`scopeOf(ctx)` 读取最近的注册键。`scopeTarget(base, key)` 创建事件接收器,其过滤器保留 base receiver 的 Cordis 服务过滤器,然后接纳无作用域的监听器和具有该确切键的监听器。
 
-Receiver 是一个小型载体而非领域对象的透明代理。需要 agent 的代码接收显式的事件参数;需要注册所有权的代码接收 `agent.ctx`。
+Receiver 是一个小型载体而非领域对象的透明代理。需要 agent 的代码接收显式的 setup 参数或事件参数;需要注册所有权的代码接收 `agent.ctx`。
 
 ### 注册表读取叠加一个精确 layer
 
@@ -102,11 +104,11 @@ detach 闭包捕获其确切注册表条目。它仅在映射仍指向该注册
 
 创建准备一个新 Session。恢复加载并验证持久化的 Session,然后准备相同的活跃会话标识。两条路径随后构建作用域、agent 和 driver,并调用相同的 setup/发布算法。
 
-工厂存储具体的 trace 目标,但通过调用方绑定的 Cordis trace 调用它们。这保留了依赖来源和调用方所有权,而不堆叠 trace 代理
+工厂存储具体的 trace 目标,但通过调用方绑定的 Cordis trace 调用它们。运行时子 Agent 的创建方显式传入父 Agent,AgentRegistry 再将其与调用方 Context 分开转交。这既保留了依赖来源和两种所有权事实,又不堆叠 trace 代理,也不把领域对象附着到 Context。作用域 Remote 事件适配器同样从 request 接收 Agent,校验它就是 carrier key,再直接投影其 Context 与 wire identity。系统不会通过作用域索引从 Context 重建 Agent
 
 ### Setup 是私有世界内的可信组合
 
-Setup 接收完整的子上下文,可以等待插件激活。它可以注册工具、提示词段、限制、监听器和其他 effect,但公开约定不支持通过强制转换或内部注册表调用来驱动或发布正在创建中的 agent。
+Setup 接收完整的子上下文和确切的未发布 Agent,可以等待插件激活。它可以注册工具、提示词段、限制、监听器和其他 effect;需要子 Session 的消费者从 Agent 参数读取它。公开约定不支持通过强制转换或内部注册表调用来驱动或发布正在创建中的 agent。
 
 事务将异步加载和 setup 与停用进行竞争,而非无限等待外部代码拥有的 promise。如果取消或所有者卸载获胜,即使外部 promise 永不结算,公开创建也会在事务拥有的清理之后拒绝。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md
-2026-07-15-agent-initiator-scope.md: 63540c0ec6b29a10613e01f1ed9ced24e8f2d277
-2026-07-15-agent-initiator-scope.zh.md: 3ea893aa5f6992bf09965436c1db3144d2fae5ac
+2026-07-15-agent-initiator-scope.md: 48962fb64e4cc0034d8e181d385e0fe86cc84675
+2026-07-15-agent-initiator-scope.zh.md: a41545576187ee1b88923b8925e3e44be6c811a3

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md

@@ -6,7 +6,7 @@ English | [中文](2026-07-15-agent-initiator-scope.zh.md)
 
 ## Problem
 
-The harness has two useful but different notions of context. A Cordis `Context` selects services, registration ownership, and lifetime; `agent.ctx` is the flat registration scope owned by one live Agent. Agent and Session identity instead describe the subject of an asynchronous operation. Changing a root `ctx.agent` to mean “whichever Agent is running” would conflate those meanings and fail when one process drives Agents concurrently.
+The harness has two useful but different notions of context. A Cordis `Context` selects services, registration ownership, and lifetime; `agent.ctx` is the flat registration scope owned by one live Agent. Agent and Session identity instead describe the subject of an asynchronous operation. A dynamic `ctx.agent` meaning “whichever Agent is running” would conflate those meanings and fail when one process drives Agents concurrently.
 
 Deep process-local infrastructure sometimes needs a trusted initiating Agent below explicit loop, tool, and request parameters—for example, a host-aware transport, tracing helper, logger, or gateway client. Requiring every private helper to forward `agent` adds repetition, while a process-global mutable slot is incorrect across `await`. Model-visible arguments are unsuitable because a model must not choose a trusted Session or routing header. The carrier belongs to the Agent service rather than optional model-visible context.
 
@@ -18,9 +18,9 @@ The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the in
 
 `AgentLoop` already injects `ctx.agents` and wraps each concrete driver's complete `runLoop` lifetime in `agents.withInitiator(agent, ...)`. Its package-private loop, turn, step, and tool-call orchestration entries recover the exact Agent from `ctx.agents`, derive `agent.session` once, and let operation-local helpers capture it instead of forwarding the concrete driver or `Session` through shallow interfaces. A leaf helper keeps a narrow `Session` parameter when that is its actual interface rather than accepting a broader `Context` only for an ambient lookup.
 
-Concurrent drivers receive independent stores. A child driver's continuations carry the child, while the caller resumes in its prior store as soon as `withInitiator()` returns; active-run tracking keeps the returned Promise in the teardown drain until it settles. Creation, persistence load, and unpublished `setup(agentCtx)` remain outside the child's driver boundary: creation initiated by a parent runs under the parent identity, while `agentCtx.agent` explicitly identifies the child.
+Concurrent drivers receive independent stores. A child driver's continuations carry the child, while the caller resumes in its prior store as soon as `withInitiator()` returns; active-run tracking keeps the returned Promise in the teardown drain until it settles. Creation, persistence load, and unpublished `setup(agentCtx, childAgent)` remain outside the child's driver boundary: creation initiated by a parent runs under the parent identity, while the explicit `childAgent` parameter identifies the child.
 
-Ambient identity does not replace explicit contracts. `ToolExecution.agent`, `AssembleContext.agent`, `GenerateOptions.sessionId`, job ownership, parent/child requests, `ctx.agent`, `agentCtx.agent`, approval and hook subjects, `cwd` selection, cancellation, worker/process messages, persistence records, and wire identity remain explicit. A remote boundary materializes the identity it needs into its typed request because ALS is process-local.
+Ambient identity does not replace explicit contracts. `ToolExecution.agent`, `AssembleContext.agent`, the Agent parameter of `AgentSetup`, `GenerateOptions.sessionId`, job ownership, parent/child requests, approval and hook subjects, `cwd` selection, cancellation, worker/process messages, persistence records, and wire identity remain explicit. A remote boundary materializes the identity it needs into its typed request because ALS is process-local.
 
 `AgentRegistry` owns an ordered initiator lifecycle. Teardown first rejects new boundaries; removing `ctx.agents` then drains injected dependents such as AgentLoop, and the registry waits for active returned-Promise boundaries before calling `AsyncLocalStorage.disable()`. If a boundary's inherited async chain starts an owning Cordis fiber's unload, the private run-token lineage releases that nested boundary chain from the drain, which prevents teardown from waiting on itself while unrelated boundaries still drain. `currentInitiator()` and `requireInitiator()` remain usable through a retained in-flight service reference while the ordinary drain runs; after disposal, initiator methods throw `agent initiator scope is disposed`. Root Context disposal may start sibling fiber teardown concurrently, so active-boundary counting remains necessary in addition to Cordis dependency ordering.
 
@@ -40,7 +40,7 @@ A test-double host-aware transport derives `X-Harness-Session-Id` internally and
 
 **Pass Agent through every function.** Public, worker, process, persistence, and wire boundaries continue to do this, but requiring every process-local private helper to carry Agent adds repetitive forwarding without improving trust. ALS is confined to the asynchronous chain inside those explicit boundaries.
 
-**Make `ctx.agent` dynamic.** `ctx.agent` already means the static Agent associated with an Agent-scoped Cordis context. Changing the root meaning would mix registration and execution scopes and make concurrent behavior surprising.
+**Expose a dynamic `ctx.agent`.** Context carries registration ownership, not a domain subject. Adding an accessor for the executing Agent would mix registration and execution scopes and make concurrent behavior surprising.
 
 **Add a separate `ctx.agentExecution` service.** The carrier has no independent backend, configuration, or identity type: it stores the same `Agent` that `ctx.agents` already owns, and AgentLoop already depends on that service. A second mandatory provider would add package, composition, lifecycle, generated-catalog, and test-harness wiring without separating a real capability.
 

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md

@@ -6,7 +6,7 @@ Status: implemented
 
 ## 问题
 
-harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负责选择服务、注册归属和生命周期;`agent.ctx` 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若把根 `ctx.agent` 改成「当前正在运行的 Agent」,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。
+harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负责选择服务、注册归属和生命周期;`agent.ctx` 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若提供表示「当前正在运行的 Agent」的动态 `ctx.agent`,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。
 
 进程内深层基础设施有时需要在显式传递的循环、工具及请求参数之下获取可信的发起 Agent,例如宿主感知传输层、追踪辅助函数、日志器或网关客户端。要求每个私有辅助函数都转发 `agent` 会造成重复,而进程级可变槽会在跨 `await` 时发生并发错误。模型可见参数也不适用,因为模型不得选择可信的会话或路由请求头。该载体归 Agent 服务所有,而非模型可见的可选上下文。
 
@@ -18,9 +18,9 @@ harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负
 
 `AgentLoop` 已经注入 `ctx.agents`,并用 `agents.withInitiator(agent, ...)` 包裹每个具体驱动的完整 `runLoop` 生命周期。循环、轮次、步骤和工具调用的包内私有入口从 `ctx.agents` 恢复同一个 Agent,一次推导 `agent.session`,再由操作内辅助函数捕获该值,避免在浅层接口中转发具体驱动或 `Session`。若 `Session` 本身就是底层辅助函数的实际接口,该函数会保留狭窄的 `Session` 参数,而不会只为隐式查找而接收更宽泛的 `Context`。
 
-因此,并发驱动使用彼此独立的存储。子驱动的异步延续携带子 Agent;`withInitiator()` 返回后,调用方立即恢复之前的存储,而活动运行计数仍持续跟踪返回的 Promise,直到其结束。创建、持久化加载和尚未发布的 `setup(agentCtx)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而 `agentCtx.agent` 显式标识子 Agent。
+因此,并发驱动使用彼此独立的存储。子驱动的异步延续携带子 Agent;`withInitiator()` 返回后,调用方立即恢复之前的存储,而活动运行计数仍持续跟踪返回的 Promise,直到其结束。创建、持久化加载和尚未发布的 `setup(agentCtx, childAgent)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而显式的 `childAgent` 参数标识子 Agent。
 
-隐式身份不会取代显式约定。`ToolExecution.agent`、`AssembleContext.agent`、`GenerateOptions.sessionId`、任务归属、父子请求、`ctx.agent`、`agentCtx.agent`、审批与 hook 主体、`cwd` 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。
+隐式身份不会取代显式约定。`ToolExecution.agent`、`AssembleContext.agent`、`AgentSetup` 的 Agent 参数、`GenerateOptions.sessionId`、任务归属、父子请求、审批与 hook 主体、`cwd` 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。
 
 `AgentRegistry` 管理一个有序的发起方生命周期。teardown 会先拒绝新边界;移除 `ctx.agents` 后,AgentLoop 等注入方开始排空,注册表随后等待活动的返回 Promise 边界,最后调用 `AsyncLocalStorage.disable()`。如果某个边界继承的异步调用链启动所属 Cordis fiber 的卸载,私有运行标记谱系会从排空范围中释放该嵌套边界链,从而避免 teardown 等待自身完成,同时继续排空无关边界。在普通排空期间,进行中代码可通过保留的服务引用继续调用 `currentInitiator()` 和 `requireInitiator()`;dispose(资源释放)后,发起方方法会抛出 `agent initiator scope is disposed`。根 Context dispose 可能并发启动同级 fiber 的 teardown,因此除 Cordis 依赖顺序外仍必须统计活动边界。
 
@@ -40,7 +40,7 @@ Agent 服务测试锁定可选与必需读取、同步值及跨 realm Promise 
 
 **在每个函数中传递 Agent。** 公开、worker、进程、持久化和协议边界继续显式传递,但要求每个进程内私有辅助函数都携带 Agent 只会造成重复转发,不会提高可信度。ALS 仅限于这些显式边界内部的异步调用链。
 
-**让 `ctx.agent` 变成动态值。** `ctx.agent` 已经表示与 Agent 作用域 Cordis 上下文静态关联的 Agent。改变根上下文的含义会混合注册作用域与执行作用域,并让并发行为变得意外。
+**暴露动态的 `ctx.agent`。** Context 携带注册所有权,而非领域主体。为正在执行的 Agent 新增 accessor 会混合注册作用域与执行作用域,并让并发行为变得意外。
 
 **新增独立的 `ctx.agentExecution` 服务。** 该载体没有独立后端、配置或身份类型:它存储的是 `ctx.agents` 已经管理的同一个 `Agent`,而 AgentLoop 本就依赖该服务。第二个必需提供方会增加包、组合、生命周期、生成目录及测试 harness 接线,却没有拆出真实能力。
 

+ 2 - 2
docs/subsystems/core.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/core.md
-core.md: dbe12dfc9bfdafe0f59d7e52eefc5695b1c8d063
-core.zh.md: 50fd2e4e76ee72653c02c2ffeca06042191aa9c4
+core.md: 9f05fc3520439837f8f7b1e15fe7aa19a71ca19c
+core.zh.md: b0ac0200cefc5a1e6f0f61c0ad117bafd7c1323f

+ 15 - 9
docs/subsystems/core.md

@@ -46,9 +46,9 @@ interface AgentHandle {
 }
 ```
 
-`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, seed boundary, origin classification, delegation depth), an optional `seed` replay prefix for forks, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) composes the agent's scoped world while both ids are still unpublished — everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly — and may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal rolls the transaction back without publishing either id.
+`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: session metadata (`meta` — validated `cwd`, fork lineage, seed boundary, origin classification, delegation depth), an optional `seed` replay prefix for forks, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `agentOptions`, `signal`, and `setup`. The `setup` callback (`AgentSetup`) receives `(agentCtx, agent)` while both ids are still unpublished: the context owns scoped registrations, while the explicit Agent supplies the exact child Session without a reverse property on the Context. Everything registered through `agentCtx` exists before `agent/created` and the first prompt assembly. Setup may return a synchronous commit invoked immediately before publication; a setup rejection, commit throw, or owner disposal rolls the transaction back without publishing either id.
 
-`AgentFactory` is the creation interface behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers use `ctx.agents` without depending on the concrete loop package. The exact `create`/`resume` signatures and rollback contracts are in the [generated section](#ctxagents--agentregistry) below.
+`AgentFactory` is the creation interface behind the registry: the loop registers its factory via `ctx.agents.setFactory()`, so consumers use `ctx.agents` without depending on the concrete loop package. A runtime child creator passes its parent Agent explicitly; the registry passes that value separately from the caller Context to the factory. The exact `create`/`resume` signatures and rollback contracts are in the [generated section](#ctxagents--agentregistry) below.
 
 ## The agent handle
 
@@ -368,17 +368,19 @@ create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd
  * Create an owned agent on a caller-supplied session id.
  * @param ownerCtx - caller context that structurally owns the lifecycle.
  * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
+ * @param owner - live Agent that owns the new Agent at runtime, or undefined for a root owner.
  * @returns the published handle.
  */
-async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>
+async createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
 
 /**
  * Resume an owned agent from the configured persistence service.
  * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
  * @param options - persisted identity, loop options, setup, and cancellation.
+ * @param owner - live Agent that owns the resumed Agent at runtime, or undefined for a root owner.
  * @returns the published handle.
  */
-async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
+async resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
 ```
 
 Types: [SessionHeader](persistence.md)
@@ -621,7 +623,8 @@ Initiator methods provide same-process causal attribution only. Ambient presence
  * Read the Agent that initiated the inherited asynchronous driver chain.
  * Use this optional form for logging, tracing, metrics, or host attribution
  * that also supports agentless calls. When a parent creates a child, setup
- * reports the causal parent while `agentCtx.agent` identifies the child.
+ * reports the causal parent while the setup callback's Agent parameter
+ * identifies the child.
  * @returns the inherited Agent, or `undefined` outside an initiator boundary
  *   and inside an explicit clearing boundary.
  * @throws when this service instance has been disposed.
@@ -687,18 +690,20 @@ setFactory(factory: AgentFactory): () => void
  * registered or creation/setup fails. The resolved {@link AgentHandle} lets
  * the owner tear down exactly this agent.
  * @param options - shared identity, session seed/metadata, and agent options.
+ * @param owner - explicit live runtime owner, or undefined for a root Agent.
  * @returns the handle after setup, rollback-covered publication, and loop start complete.
  */
-async create(options: CreateAgentOptions): Promise<AgentHandle>
+async create(options: CreateAgentOptions, owner?: Agent): Promise<AgentHandle>
 
 /**
  * Load a persisted session and resume an agent on it through the registered
  * factory. Rejects if no factory is registered; the factory rejects if
  * session persistence is not configured or persistence/setup fails.
  * @param options - persisted identity, configuration, and optional setup.
+ * @param owner - explicit live runtime owner, or undefined for a root Agent.
  * @returns the handle after setup, rollback-covered publication, and loop start complete.
  */
-async resume(options: ResumeAgentOptions): Promise<AgentHandle>
+async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>
 
 /**
  * Register a live agent. Throws if an agent with the same id is already
@@ -709,6 +714,7 @@ async resume(options: ResumeAgentOptions): Promise<AgentHandle>
  * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
  * requires passing the carrier). Returns the disposer.
  * @param agent - the already-constructed agent to record in the store.
+ * @param owner - explicit live runtime owner, or undefined for a root Agent.
  * @returns the EXACT Cordis effect disposer (single-shot; a repeat call
  *   returns undefined without awaiting an in-flight teardown). Exact
  *   identity is load-bearing: a composite (generator) effect that owns a
@@ -718,7 +724,7 @@ async resume(options: ResumeAgentOptions): Promise<AgentHandle>
  *   owner unload, unregistering the agent (and emitting `agent/disposed`)
  *   while its final turn is still draining.
  */
-register(agent: Agent): () => void
+register(agent: Agent, owner?: Agent): () => void
 
 /**
  * Insert an already-constructed agent without announcing it. This is the
@@ -727,7 +733,7 @@ register(agent: Agent): () => void
  * returned detach closure into its pre-installed composite teardown before
  * calling {@link announce}. Ordinary callers use {@link register}.
  * @param agent - the prepared, unpublished agent.
- * @param owner - live agent whose scoped context created this agent, or
+ * @param owner - explicitly supplied live runtime owner, or
  *   undefined for a top-level runtime root. This is runtime ownership, not
  *   the resumed session's durable parent lineage.
  * @returns an idempotent closure that removes this exact entry and emits

+ 15 - 9
docs/subsystems/core.zh.md

@@ -48,9 +48,9 @@ interface AgentHandle {
 }
 ```
 
-`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、seed 边界、来源分类、委派深度)、fork 用的可选 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应项:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在两个 id 都尚未发布时组装 agent 的作用域世界——凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在——并可返回一个在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose(资源释放)都会回滚事务,两个 id 均不发布。
+`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、seed 边界、来源分类、委派深度)、fork 用的可选 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应项:`resumeSessionId`、`agentOptions`、`signal` 与 `setup`。`setup` 回调(`AgentSetup`)在两个 id 都尚未发布时接收 `(agentCtx, agent)`:上下文拥有作用域注册,显式 Agent 则提供确切的子 Session,Context 无需反向属性。凡经 `agentCtx` 注册的内容都先于 `agent/created` 与第一次提示词组装存在。Setup 可以返回一个在发布前一刻调用的同步 commit;setup 拒绝、commit 抛出或所有者 dispose(资源释放)都会回滚事务,两个 id 均不发布。
 
-`AgentFactory` 是注册表背后的创建接口:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方使用 `ctx.agents` 时无需依赖具体循环包。确切的 `create`/`resume` 签名及回滚约定见下方[生成区块](#ctxagents--agentregistry)。
+`AgentFactory` 是注册表背后的创建接口:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方使用 `ctx.agents` 时无需依赖具体循环包。运行时子 Agent 的创建方显式传入父 Agent;注册表将该值与调用方 Context 分开传给工厂。确切的 `create`/`resume` 签名及回滚约定见下方[生成区块](#ctxagents--agentregistry)。
 
 <a id="the-agent-handle"></a>
 
@@ -378,17 +378,19 @@ create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd
  * Create an owned agent on a caller-supplied session id.
  * @param ownerCtx - caller context that structurally owns the lifecycle.
  * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
+ * @param owner - live Agent that owns the new Agent at runtime, or undefined for a root owner.
  * @returns the published handle.
  */
-async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>
+async createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
 
 /**
  * Resume an owned agent from the configured persistence service.
  * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
  * @param options - persisted identity, loop options, setup, and cancellation.
+ * @param owner - live Agent that owns the resumed Agent at runtime, or undefined for a root owner.
  * @returns the published handle.
  */
-async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
+async resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
 ```
 
 Types: [SessionHeader](persistence.zh.md)
@@ -631,7 +633,8 @@ Initiator methods provide same-process causal attribution only. Ambient presence
  * Read the Agent that initiated the inherited asynchronous driver chain.
  * Use this optional form for logging, tracing, metrics, or host attribution
  * that also supports agentless calls. When a parent creates a child, setup
- * reports the causal parent while `agentCtx.agent` identifies the child.
+ * reports the causal parent while the setup callback's Agent parameter
+ * identifies the child.
  * @returns the inherited Agent, or `undefined` outside an initiator boundary
  *   and inside an explicit clearing boundary.
  * @throws when this service instance has been disposed.
@@ -697,18 +700,20 @@ setFactory(factory: AgentFactory): () => void
  * registered or creation/setup fails. The resolved {@link AgentHandle} lets
  * the owner tear down exactly this agent.
  * @param options - shared identity, session seed/metadata, and agent options.
+ * @param owner - explicit live runtime owner, or undefined for a root Agent.
  * @returns the handle after setup, rollback-covered publication, and loop start complete.
  */
-async create(options: CreateAgentOptions): Promise<AgentHandle>
+async create(options: CreateAgentOptions, owner?: Agent): Promise<AgentHandle>
 
 /**
  * Load a persisted session and resume an agent on it through the registered
  * factory. Rejects if no factory is registered; the factory rejects if
  * session persistence is not configured or persistence/setup fails.
  * @param options - persisted identity, configuration, and optional setup.
+ * @param owner - explicit live runtime owner, or undefined for a root Agent.
  * @returns the handle after setup, rollback-covered publication, and loop start complete.
  */
-async resume(options: ResumeAgentOptions): Promise<AgentHandle>
+async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>
 
 /**
  * Register a live agent. Throws if an agent with the same id is already
@@ -719,6 +724,7 @@ async resume(options: ResumeAgentOptions): Promise<AgentHandle>
  * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
  * requires passing the carrier). Returns the disposer.
  * @param agent - the already-constructed agent to record in the store.
+ * @param owner - explicit live runtime owner, or undefined for a root Agent.
  * @returns the EXACT Cordis effect disposer (single-shot; a repeat call
  *   returns undefined without awaiting an in-flight teardown). Exact
  *   identity is load-bearing: a composite (generator) effect that owns a
@@ -728,7 +734,7 @@ async resume(options: ResumeAgentOptions): Promise<AgentHandle>
  *   owner unload, unregistering the agent (and emitting `agent/disposed`)
  *   while its final turn is still draining.
  */
-register(agent: Agent): () => void
+register(agent: Agent, owner?: Agent): () => void
 
 /**
  * Insert an already-constructed agent without announcing it. This is the
@@ -737,7 +743,7 @@ register(agent: Agent): () => void
  * returned detach closure into its pre-installed composite teardown before
  * calling {@link announce}. Ordinary callers use {@link register}.
  * @param agent - the prepared, unpublished agent.
- * @param owner - live agent whose scoped context created this agent, or
+ * @param owner - explicitly supplied live runtime owner, or
  *   undefined for a top-level runtime root. This is runtime ownership, not
  *   the resumed session's durable parent lineage.
  * @returns an idempotent closure that removes this exact entry and emits

+ 1 - 4
packages/acp/acp/src/session.ts

@@ -150,10 +150,7 @@ export class AcpSession {
       resumeSessionId: options.sessionId,
       agentOptions: options.agentOptions,
       signal: options.signal,
-      setup: async (agentCtx) => {
-        const agent = agentCtx.agent
-        /* v8 ignore next -- Agent factory setup always carries its unpublished Agent. */
-        if (agent === undefined) throw new Error('acp: resumed Agent is absent during setup')
+      setup: async (agentCtx, agent) => {
         modelControl = new AcpModelControl(
           ctx.llm,
           selectionFor(agent.session.requestHeader(), options.fallbackSelection),

+ 3 - 8
packages/api/gateway/src/index.ts

@@ -461,12 +461,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
   private startRemoteEvent(source: TypertRemoteEventInvocation): void {
     try {
       assertRemoteEventName(source)
-      const context = this.ctx.typert.contexts.identifyHost(source.context.value)
-      if (context === undefined) {
-        source.resolve({ kind: 'next' })
-        return
-      }
-      if (context.kind !== 'agent' || !isRemoteEventAgentId(context.identity)) {
+      if (!isRemoteEventAgentId(source.context.agentId)) {
         throw new TypeError(
           'typert gateway: scoped Remote events require a non-empty Agent identity',
         )
@@ -480,7 +475,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
           () => () => {
             this.cancelRemoteEvent(
               pending,
-              new Error(`typert gateway: Remote event Context ${JSON.stringify(context.kind)} was released`),
+              new Error('typert gateway: Remote event Agent Context was released'),
             )
           },
           `api-gateway: Remote event ${JSON.stringify(source.event)}`,
@@ -504,7 +499,7 @@ export class TypertGatewayService extends Service implements TypertGateway {
           type: 'waterfall',
           event: source.event,
           eventId: id,
-          agentId: context.identity,
+          agentId: source.context.agentId,
           request: projected.request,
         },
         deliveries: new Set(),

+ 3 - 1
packages/api/gateway/src/types.ts

@@ -28,10 +28,12 @@ export interface TypertRemoteEventFrame {
 
 /** Live Host values used to project one scoped Remote Event. */
 export interface TypertRemoteEventContext {
-  /** Live Host Context identified by the registered Host adapters. */
+  /** Live Agent Context that owns cancellation of the forwarded waterfall. */
   readonly value: Context
   /** Agent object carried directly by the waterfall request. */
   readonly subject: object
+  /** Agent identity read directly from the scoped event subject. */
+  readonly agentId: string
 }
 
 /** Result returned from a Client waterfall, or delegation back to the Host chain. */

+ 15 - 85
packages/api/gateway/tests/gateway-stream.host.spec.ts

@@ -185,6 +185,7 @@ function pendingInvocation(
   context: Context,
   signal?: AbortSignal,
   prompt = 'ship',
+  identity: unknown = agentId('agent-1'),
 ): PendingInvocationProbe {
   const subject = { ctx: context }
   const settled = Promise.withResolvers<TypertRemoteEventOutcome>()
@@ -198,7 +199,7 @@ function pendingInvocation(
     dispatch: {
       event: 'fixture/approval',
       request: { prompt, agent: subject, ...(signal === undefined ? {} : { signal }) },
-      context: { value: context, subject },
+      context: { value: context, subject, agentId: identity as string },
       resolve,
       reject,
     },
@@ -463,13 +464,7 @@ describe('Typert Remote streams', () => {
   it('cancels a pending waterfall when its source rejects during removal', async () => {
     const { ctx } = await setup(true)
     const agent = ctx.extend()
-    ctx.typert.contexts.registerHost('agent', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: candidate => candidate === agent ? agentId('agent-removal') : undefined,
-      resolve: id => id === 'agent-removal' ? agent : undefined,
-    })
-    const pending = pendingInvocation(agent)
+    const pending = pendingInvocation(agent, undefined, 'ship', agentId('agent-removal'))
     const rejected = expect(pending.outcome).rejects.toThrow(
       'forwarded Remote event source was removed',
     )
@@ -494,7 +489,7 @@ describe('Typert Remote streams', () => {
     client.socket.close()
   })
 
-  it('delegates unavailable Contexts and rejects malformed scoped invocations', async () => {
+  it('rejects malformed scoped invocations and delegates a released Context', async () => {
     const { ctx } = await setup(false)
     const source = new RemoteEventSourceProbe()
     const unregister = ctx.typertGateway.registerRemoteEvents(source.source, REMOTE_HOST)
@@ -511,28 +506,15 @@ describe('Typert Remote streams', () => {
       await rejected
     }
 
-    const unavailable = pendingInvocation(ctx)
-    source.push(unavailable.dispatch)
-    await expect(unavailable.outcome).resolves.toEqual({ kind: 'next' })
-    expect(unavailable.reject).not.toHaveBeenCalled()
-
     let selected = ctx.extend()
-    let identity: unknown = 1n
-    ctx.typert.contexts.registerHost('agent', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: candidate => candidate === selected ? identity as AgentWireId : undefined,
-      resolve: () => selected,
-    })
-    const nonJsonIdentity = pendingInvocation(selected)
+    const nonJsonIdentity = pendingInvocation(selected, undefined, 'ship', 1n)
     const nonJsonRejected = expect(nonJsonIdentity.outcome).rejects.toThrow(
       'require a non-empty Agent identity',
     )
     source.push(nonJsonIdentity.dispatch)
     await nonJsonRejected
 
-    identity = 'agent-invalid-request'
-    const invalidRequest = pendingInvocation(selected)
+    const invalidRequest = pendingInvocation(selected, undefined, 'ship', agentId('agent-invalid-request'))
     const invalidRequestRejected = expect(invalidRequest.outcome).rejects.toThrow(
       'must carry its scoped Agent directly',
     )
@@ -545,18 +527,16 @@ describe('Typert Remote streams', () => {
     const staleFiber = ctx.plugin(() => {})
     await staleFiber
     selected = staleFiber.ctx
-    identity = 'agent-stale'
     await staleFiber.dispose()
-    const stale = pendingInvocation(selected)
+    const stale = pendingInvocation(selected, undefined, 'ship', agentId('agent-stale'))
     source.push(stale.dispatch)
     await expect(stale.outcome).resolves.toEqual({ kind: 'next' })
     expect(stale.reject).not.toHaveBeenCalled()
 
     selected = ctx.extend()
-    identity = 'agent-cancelled'
     const abort = new AbortController()
     abort.abort('fixture non-error cancellation')
-    const cancelled = pendingInvocation(selected, abort.signal)
+    const cancelled = pendingInvocation(selected, abort.signal, 'ship', agentId('agent-cancelled'))
     const cancelledOutcome = expect(cancelled.outcome).rejects.toMatchObject({
       message: 'typert gateway: Remote event was cancelled',
       cause: 'fixture non-error cancellation',
@@ -594,19 +574,13 @@ describe('Typert Remote streams', () => {
     const source = new RemoteEventSourceProbe()
     const unregister = ctx.typertGateway.registerRemoteEvents(source.source, REMOTE_HOST)
     const agent = ctx.extend()
-    ctx.typert.contexts.registerHost('agent', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: candidate => candidate === agent ? agentId('agent-collision') : undefined,
-      resolve: id => id === 'agent-collision' ? agent : undefined,
-    })
     const firstId = '00000000-0000-4000-8000-000000000001' as ReturnType<typeof randomUUID>
     const secondId = '00000000-0000-4000-8000-000000000002' as ReturnType<typeof randomUUID>
     randomUuid.mockReturnValueOnce(firstId).mockReturnValueOnce(firstId).mockReturnValueOnce(secondId)
     const firstAbort = new AbortController()
     const secondAbort = new AbortController()
-    const first = pendingInvocation(agent, firstAbort.signal, 'first')
-    const second = pendingInvocation(agent, secondAbort.signal, 'second')
+    const first = pendingInvocation(agent, firstAbort.signal, 'first', agentId('agent-collision'))
+    const second = pendingInvocation(agent, secondAbort.signal, 'second', agentId('agent-collision'))
 
     source.push(first.dispatch)
     await vi.waitFor(() => { expect(randomUuid).toHaveBeenCalledTimes(1) })
@@ -648,12 +622,6 @@ describe('Typert Remote streams', () => {
     const source = new RemoteEventSourceProbe()
     const unregister = ctx.typertGateway.registerRemoteEvents(source.source, REMOTE_HOST)
     const agent = ctx.extend()
-    ctx.typert.contexts.registerHost('agent', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: candidate => candidate === agent ? agentId('agent-1') : undefined,
-      resolve: id => id === 'agent-1' ? agent : undefined,
-    })
     const first = await openEventClient(ctx, 'events-a')
     const second = await openEventClient(ctx, 'events-b')
     const pending = pendingInvocation(agent)
@@ -702,14 +670,8 @@ describe('Typert Remote streams', () => {
     const source = new RemoteEventSourceProbe()
     const unregister = ctx.typertGateway.registerRemoteEvents(source.source, REMOTE_HOST)
     const agent = ctx.extend()
-    ctx.typert.contexts.registerHost('agent', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: candidate => candidate === agent ? agentId('agent-rejected') : undefined,
-      resolve: id => id === 'agent-rejected' ? agent : undefined,
-    })
     const client = await openEventClient(ctx, 'events-rejected')
-    const pending = pendingInvocation(agent)
+    const pending = pendingInvocation(agent, undefined, 'ship', agentId('agent-rejected'))
     source.push(pending.dispatch)
     await vi.waitFor(() => { expect(deliveredInvocation(client)).toBeDefined() })
     const frame = deliveredInvocation(client)!
@@ -742,12 +704,6 @@ describe('Typert Remote streams', () => {
     const source = new RemoteEventSourceProbe()
     const unregister = ctx.typertGateway.registerRemoteEvents(source.source, REMOTE_HOST)
     const agent = ctx.extend()
-    ctx.typert.contexts.registerHost('agent', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: candidate => candidate === agent ? agentId('agent-1') : undefined,
-      resolve: id => id === 'agent-1' ? agent : undefined,
-    })
     const first = await openEventClient(ctx, 'events-next-a')
     const second = await openEventClient(ctx, 'events-next-b')
     const pending = pendingInvocation(agent)
@@ -775,13 +731,7 @@ describe('Typert Remote streams', () => {
     const source = new RemoteEventSourceProbe()
     const unregister = ctx.typertGateway.registerRemoteEvents(source.source, REMOTE_HOST)
     const agent = ctx.extend()
-    ctx.typert.contexts.registerHost('agent', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: candidate => candidate === agent ? agentId('agent-late-client') : undefined,
-      resolve: id => id === 'agent-late-client' ? agent : undefined,
-    })
-    const pending = pendingInvocation(agent, undefined, 'before-connect')
+    const pending = pendingInvocation(agent, undefined, 'before-connect', agentId('agent-late-client'))
 
     source.push(pending.dispatch)
     await vi.waitFor(() => { expect(randomUuid).toHaveBeenCalledTimes(1) })
@@ -808,12 +758,6 @@ describe('Typert Remote streams', () => {
     const source = new RemoteEventSourceProbe()
     const unregister = ctx.typertGateway.registerRemoteEvents(source.source, REMOTE_HOST)
     const agent = ctx.extend()
-    ctx.typert.contexts.registerHost('agent', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: candidate => candidate === agent ? agentId('agent-1') : undefined,
-      resolve: id => id === 'agent-1' ? agent : undefined,
-    })
     const original = await openEventClient(ctx, 'events-original')
     const pending = pendingInvocation(agent)
     source.push(pending.dispatch)
@@ -845,24 +789,10 @@ describe('Typert Remote streams', () => {
     const contextFiber = ctx.plugin(() => {})
     await contextFiber
     const contextAgent = contextFiber.ctx
-    ctx.typert.contexts.registerHost('agent', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: (candidate) => {
-        if (candidate === signalAgent) return agentId('agent-signal')
-        if (candidate === contextAgent) return agentId('agent-context')
-        return undefined
-      },
-      resolve: (id) => {
-        if (id === 'agent-signal') return signalAgent
-        if (id === 'agent-context') return contextAgent
-        return undefined
-      },
-    })
     const client = await openEventClient(ctx, 'events-cancel')
 
     const abort = new AbortController()
-    const signalPending = pendingInvocation(signalAgent, abort.signal, 'signal')
+    const signalPending = pendingInvocation(signalAgent, abort.signal, 'signal', agentId('agent-signal'))
     source.push(signalPending.dispatch)
     await vi.waitFor(() => { expect(deliveredInvocation(client)).toBeDefined() })
     const signalFrame = deliveredInvocation(client)!
@@ -883,7 +813,7 @@ describe('Typert Remote streams', () => {
       })
     })
 
-    const contextPending = pendingInvocation(contextAgent, undefined, 'context')
+    const contextPending = pendingInvocation(contextAgent, undefined, 'context', agentId('agent-context'))
     source.push(contextPending.dispatch)
     let contextFrame: RemoteEventInvocationFrame | undefined
     await vi.waitFor(() => {
@@ -896,7 +826,7 @@ describe('Typert Remote streams', () => {
           && Reflect.get(value, 'eventId') !== signalFrame.eventId) as RemoteEventInvocationFrame | undefined
       expect(contextFrame).toBeDefined()
     })
-    const contextOutcome = expect(contextPending.outcome).rejects.toThrow('Context "agent" was released')
+    const contextOutcome = expect(contextPending.outcome).rejects.toThrow('Agent Context was released')
     await contextFiber.dispose()
     await contextOutcome
     await vi.waitFor(() => {

+ 0 - 1
packages/api/gateway/tests/gateway.host.spec.ts

@@ -1340,7 +1340,6 @@ function contextProvider(context: Context) {
   return {
     wire: 'agentId',
     wireTypeSymbol: '@fixture/domain#AgentId',
-    identity: (candidate: Context) => candidate === context ? 'agent-1' : undefined,
     resolve: (id: string) => id === 'agent-1' ? context : undefined,
   }
 }

+ 2 - 0
packages/api/remotes/package.json

@@ -60,6 +60,7 @@
   },
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
+    "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-agent-presets": "workspace:^",
     "@deepseek-ai/dsh-api-gateway": "workspace:^",
     "@deepseek-ai/dsh-api-session-controller": "workspace:^",
@@ -83,6 +84,7 @@
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
+    "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-agent-presets": "workspace:^",
     "@deepseek-ai/dsh-api-gateway": "workspace:^",
     "@deepseek-ai/dsh-api-session-controller": "workspace:^",

+ 7 - 6
packages/api/remotes/src/index.ts

@@ -2,6 +2,7 @@
 
 import { homedir } from 'node:os'
 import type { Context } from '@deepseek-ai/cordis'
+import type { Agent } from '@deepseek-ai/dsh-agent'
 import type {
   TypertRemoteEventDispatch,
   TypertRemoteEventInvocation,
@@ -56,17 +57,17 @@ function remoteEventSource(ctx: Context): TypertRemoteEventSource {
         request: object,
         next: () => unknown,
       ) {
-        const subject = carrierKeyOf(this)
-        if (subject === undefined) return next()
-        const value = Reflect.get(subject, 'ctx') as unknown
-        if (typeof value !== 'object' || value === null) {
-          throw new TypeError(`forwarded scoped event ${JSON.stringify(event)} has no live Context`)
+        const carrierAgent = carrierKeyOf(this)
+        if (carrierAgent === undefined) return next()
+        const agent = (request as { readonly agent?: Agent }).agent
+        if (agent === undefined || agent !== carrierAgent) {
+          throw new TypeError(`forwarded scoped event ${JSON.stringify(event)} must carry its Agent directly`)
         }
         return forwardWaterfall(
           queue,
           event,
           request,
-          { value: value as Context, subject },
+          { value: agent.ctx, subject: agent, agentId: agent.id },
           next,
         )
       }) as never)

+ 11 - 3
packages/api/remotes/tests/remote-events.host.spec.ts

@@ -154,10 +154,18 @@ describe('Remote event Host source', () => {
     const abort = new AbortController()
     const iterator = sourceOf(gateway)(abort.signal)[Symbol.asyncIterator]()
     const agentCtx = ctx.extend()
-    const agent = { ctx: agentCtx }
+    const agent = { id: 'agent-1', ctx: agentCtx }
     const target = scopeTarget(ctx, agent)
     const request = { questions: [], agent }
 
+    await expect(async () => waterfallRaw(
+      ctx,
+      target,
+      'user-questions/request',
+      [{ questions: [], agent: { id: 'agent-2', ctx: ctx.extend() } }],
+      () => Promise.resolve('host fallback'),
+    )).rejects.toThrow('must carry its Agent directly')
+
     const claimed = waterfallRaw(
       ctx,
       target,
@@ -169,7 +177,7 @@ describe('Remote event Host source', () => {
     expect(claimedDispatch).toMatchObject({
       event: 'user-questions/request',
       request,
-      context: { value: agentCtx, subject: agent },
+      context: { value: agentCtx, subject: agent, agentId: 'agent-1' },
     })
     claimedDispatch.resolve({ kind: 'result', value: 'client answer' })
     await expect(claimed).resolves.toBe('client answer')
@@ -211,7 +219,7 @@ describe('Remote event Host source', () => {
     const abort = new AbortController()
     const iterator = sourceOf(gateway)(abort.signal)[Symbol.asyncIterator]()
     const delivery = iterator.next()
-    const agent = { ctx: ctx.extend() }
+    const agent = { id: 'agent-1', ctx: ctx.extend() }
     const reason = new Error('forwarded event source removed')
     const pending = waterfallRaw(
       ctx,

+ 6 - 6
packages/api/session-controller/src/agent.ts

@@ -380,12 +380,14 @@ export class ApiSessionAgentController {
     readonly setup: AgentSetup
   }> {
     const presets = this.ctx.get('agentPresets')
-    if (presets === undefined) return { setup: (agentCtx) => { this.installSelection(agentCtx) } }
+    if (presets === undefined) {
+      return { setup: (_agentCtx, agent) => { this.installSelection(agent) } }
+    }
     const resolvedId = (await presets.resolve(presetId)).id
     return {
       agentPreset: resolvedId,
-      setup: async (agentCtx) => {
-        this.installSelection(agentCtx)
+      setup: async (agentCtx, agent) => {
+        this.installSelection(agent)
         await presets.mount(agentCtx, resolvedId)
       },
     }
@@ -494,9 +496,7 @@ export class ApiSessionAgentController {
     return { provider, model }
   }
 
-  private installSelection(agentCtx: Context): void {
-    const agent = agentCtx.agent
-    if (agent === undefined) throw new Error('api-session: Agent setup has no scoped Agent')
+  private installSelection(agent: Agent): void {
     this.selectionFor(agent)
   }
 

+ 1 - 4
packages/api/session-controller/tests/agent.host.spec.ts

@@ -421,15 +421,12 @@ describe('ApiSession create or adoption', () => {
       .rejects.toBeInstanceOf(ApiSessionCwdConflict)
   })
 
-  it('surfaces directory creation failure and rejects setup without a scoped Agent', async () => {
+  it('surfaces directory creation failure', async () => {
     const { agents } = await harness()
     const parent = mkdtempSync(join(tmpdir(), 'dsh-session-controller-file-'))
     const file = join(parent, 'file')
     writeFileSync(file, 'not a directory')
     await expect(agents.ensureSession(SessionId('mkdir-failure'), join(file, 'child'), false))
       .rejects.toThrow('failed to ensure project directory')
-
-    const composition = await agents.composeAgent(undefined)
-    expect(() => composition.setup(new Context())).toThrow('Agent setup has no scoped Agent')
   })
 })

+ 2 - 2
packages/api/session-controller/tests/session-fork.host.spec.ts

@@ -34,9 +34,9 @@ async function composed(workspaces: readonly Workspace[] = []): Promise<Context>
         ...options.meta === undefined ? {} : { meta: options.meta },
       })
       const agent = {} as Agent
-      const agentCtx = ownerCtx.extend({ agent })
+      const agentCtx = ownerCtx
       Object.assign(agent, { id: session.id, session, status: 'idle', ctx: agentCtx })
-      await options.setup?.(agentCtx)
+      await options.setup?.(agentCtx, agent)
       ctx.agents.register(agent)
       return { agent, dispose: () => Promise.resolve() }
     },

+ 2 - 3
packages/api/session-controller/tests/session-presets.host.spec.ts

@@ -49,9 +49,8 @@ async function harness(presets?: readonly string[]) {
         options.meta === undefined ? {} : { meta: options.meta },
       )
       const agent = stubAgent(session)
-      const agentCtx = ctx.extend({ agent })
-      ;(agent as { ctx?: Context }).ctx = agentCtx
-      await options.setup?.(agentCtx)
+      ;(agent as { ctx?: Context }).ctx = ctx
+      await options.setup?.(ctx, agent)
       const unregister = ctx.agents.register(agent)
       return { agent, dispose: () => { unregister(); return Promise.resolve() } }
     },

+ 2 - 2
packages/bundle/headless/tests/headless.spec.ts

@@ -67,7 +67,7 @@ async function bench(script: Script): Promise<{
       })
       let idle = Promise.resolve()
       const agent = {} as Agent
-      const agentCtx = ownerCtx.extend({ agent })
+      const agentCtx = ownerCtx
       Object.assign(agent, {
         id: session.id,
         options: options.agentOptions ?? {},
@@ -86,7 +86,7 @@ async function bench(script: Script): Promise<{
         inject: () => {},
         whenIdle: () => idle,
       } satisfies Partial<Agent>)
-      await options.setup?.(agentCtx)
+      await options.setup?.(agentCtx, agent)
       script.before?.(session)
       ctx.agents.register(agent)
       return { agent, dispose: () => Promise.resolve() }

+ 2 - 2
packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts

@@ -339,7 +339,7 @@ describe('context-overflow recovery across the real loop and compaction-basic',
             provider: 'unconfigured-agent-fallback',
             model: 'unconfigured-agent-fallback',
           },
-        })
+        }, undefined)
 
         agent.followup(createUserMessage({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } }))
         await agent.whenIdle()
@@ -413,7 +413,7 @@ describe('context-overflow recovery across the real loop and compaction-basic',
         sessionId: SessionId('alternating-recovery'),
         seed: overflowHistorySeed(),
         agentOptions: { provider: 'mock', model: 'mock' },
-      })
+      }, undefined)
       agent.followup(createUserMessage({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } }))
       await agent.whenIdle()
 

+ 2 - 2
packages/core/agent-loop/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md
-README.md: f1830c3293b4ca37fe102aa6868bbb612dc773a1
-README.zh.md: 671a117d64baeb77f083a6d429e5bb205e667fe6
+README.md: 0ab167ab3b6c44fea509cae53cbac3b12514f65c
+README.zh.md: c1afc008eb1f1e2a1950aeb9f829e6fca015a770

+ 2 - 2
packages/core/agent-loop/README.md

@@ -64,7 +64,7 @@ Plugins and hosts create agents through `ctx.agents.create()` and resume persist
 const handle = await ctx.agents.create({
   sessionId,
   agentOptions: { provider: 'deepseek', model: 'deepseek-chat' },
-  setup: (agentCtx) => { /* scoped tools, prompt sections, listeners */ },
+  setup: (agentCtx, agent) => { /* scoped registrations plus explicit unpublished Agent */ },
 })
 ```
 
@@ -103,7 +103,7 @@ After `agent/request`, `ctx.llm.prepareCall()` validates adapter-owned fields an
 
 ### Creation and teardown
 
-Creation is one rollback-covered transaction: construct a private session, concrete agent, and scoped context; await optional setup; enter both registries; announce `session/created` then `agent/created`; emit `agent/session-start`; only then start the driver. A setup throw, commit failure, or owner disposal rolls the transaction back without publishing either id. Teardown runs stop-and-drain, unwind the scope, detach the agent, then detach the session, and every detach is bound to the exact entered object so a stale disposer cannot remove a later same-id replacement.
+Creation is one rollback-covered transaction: construct a private session, concrete agent, and scoped context; await optional setup with the context and Agent passed separately; enter both registries; announce `session/created` then `agent/created`; emit `agent/session-start`; only then start the driver. A caller creating a runtime child passes the parent Agent explicitly; the caller Context separately owns the transaction and live handle. A setup throw, commit failure, or owner disposal rolls the transaction back without publishing either id. Teardown runs stop-and-drain, unwind the scope, detach the agent, then detach the session, and every detach is bound to the exact entered object so a stale disposer cannot remove a later same-id replacement.
 
 ### Turn and step flow
 

+ 2 - 2
packages/core/agent-loop/README.zh.md

@@ -64,7 +64,7 @@ kind: "package-reference"
 const handle = await ctx.agents.create({
   sessionId,
   agentOptions: { provider: 'deepseek', model: 'deepseek-chat' },
-  setup: (agentCtx) => { /* scoped tools, prompt sections, listeners */ },
+  setup: (agentCtx, agent) => { /* scoped registrations plus explicit unpublished Agent */ },
 })
 ```
 
@@ -103,7 +103,7 @@ const handle = await ctx.agents.create({
 
 ### 创建与拆除
 
-创建是同一个受回滚保护的事务:构造私有会话、具象 agent 与带作用域上下文;等待可选 setup;进入两个注册表;依次宣告 `session/created` 与 `agent/created`;发出 `agent/session-start`;此后才启动驱动器。Setup 抛出、commit 失败或所有者 dispose 都会回滚事务而不发布任一 id。Teardown 顺序是停止并排空、撤销作用域、detach agent、再 detach 会话,且每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。
+创建是同一个受回滚保护的事务:构造私有会话、具象 agent 与带作用域上下文;等待分别传入上下文与 Agent 的可选 setup;进入两个注册表;依次宣告 `session/created` 与 `agent/created`;发出 `agent/session-start`;此后才启动驱动器。创建运行时子 Agent 的调用方显式传入父 Agent;调用方 Context 则单独拥有事务和存活 handle。Setup 抛出、commit 失败或所有者 dispose 都会回滚事务而不发布任一 id。Teardown 顺序是停止并排空、撤销作用域、detach agent、再 detach 会话,且每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。
 
 ### 轮次与步骤流程
 

+ 1 - 1
packages/core/agent-loop/src/agent.ts

@@ -101,7 +101,7 @@ export class ReactLoopAgent implements Agent {
     const lastTurn = this.loopCtx.sessionProjections.stateOf(session, 'turnBoundary')?.lastTurn ?? 0
     this.phase = { kind: 'idle', lastTurn }
     this.scope = createScope(loopCtx, this)
-    this.ctx = this.scope.ctx.extend({ agent: this })
+    this.ctx = this.scope.ctx
     this.runtimeContext = new RuntimeContextProjection(this.ctx, session)
   }
 

+ 23 - 10
packages/core/agent-loop/src/index.ts

@@ -432,7 +432,7 @@ export class AgentLoop extends Service implements AgentFactory {
           void this.resumeWith(ctx, childCtx.sessionPersistence, {
             resumeSessionId,
             agentOptions: options,
-          }).catch((error: unknown) => {
+          }, undefined).catch((error: unknown) => {
             this.reportConfiguredStartupFailure(id, 'resume', resumeSessionId, error)
           })
         })
@@ -474,7 +474,7 @@ export class AgentLoop extends Service implements AgentFactory {
     await this.waitForDrainingConfiguredIdentity(ownerCtx, sessionId)
     if (!this.ownership.isActive()) return
     try {
-      await this.resumeWith(ownerCtx, persistence, { resumeSessionId: sessionId, agentOptions })
+      await this.resumeWith(ownerCtx, persistence, { resumeSessionId: sessionId, agentOptions }, undefined)
       return
     } catch (error: unknown) {
       if (!this.ownership.isActive()) return
@@ -516,7 +516,14 @@ export class AgentLoop extends Service implements AgentFactory {
    * BEFORE publication, so a mid-setup unload rolls everything back; `signal`
    * fuses caller cancellation with lifecycle teardown for setup awaits.
    */
-  private prepare(ownerCtx: Context, id: SessionId, options: AgentOptions, session: Session, callerSignal?: AbortSignal): PreparedAgent {
+  private prepare(
+    ownerCtx: Context,
+    owner: Agent | undefined,
+    id: SessionId,
+    options: AgentOptions,
+    session: Session,
+    callerSignal?: AbortSignal,
+  ): PreparedAgent {
     assertAgentOptions(options)
     ownerCtx.fiber.assertActive()
     // Every caller reaches prepare() synchronously from a service method
@@ -616,7 +623,7 @@ export class AgentLoop extends Service implements AgentFactory {
         publish: (source) => {
           assertLive()
           detachSession = agent.ctx.sessions.enter(session)
-          detachAgent = loopCtx.agents.enter(agent, ownerCtx.agent)
+          detachAgent = loopCtx.agents.enter(agent, owner)
           agent.ctx.sessions.announce(session)
           assertLive()
           loopCtx.agents.announce(agent)
@@ -648,7 +655,7 @@ export class AgentLoop extends Service implements AgentFactory {
    */
   create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd'> = {}): Agent {
     using preparation = SessionPreparation.create(this.runtime.ctx.sessions.prepare(id, { meta }))
-    const prepared = this.prepare(this.ctx, id, options, preparation.session)
+    const prepared = this.prepare(this.ctx, undefined, id, options, preparation.session)
     try {
       return prepared.publish('startup').agent
     } catch (error: unknown) {
@@ -661,15 +668,17 @@ export class AgentLoop extends Service implements AgentFactory {
    * Create an owned agent on a caller-supplied session id.
    * @param ownerCtx - caller context that structurally owns the lifecycle.
    * @param options - identities, session seed/metadata, loop options, setup, and cancellation.
+   * @param owner - live Agent that owns the new Agent at runtime, or undefined for a root owner.
    * @returns the published handle.
    */
-  async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle> {
+  async createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle> {
     const preparation = SessionPreparation.create(this.runtime.ctx.sessions.prepare(options.sessionId, {
       ...options.seed === undefined ? {} : { seed: options.seed },
       ...options.meta === undefined ? {} : { meta: options.meta },
     }))
     const published = this.setupAndPublish(
       ownerCtx,
+      owner,
       options.sessionId,
       preparation,
       options.agentOptions ?? {},
@@ -684,6 +693,7 @@ export class AgentLoop extends Service implements AgentFactory {
   /** Prepare one Agent around an acquired Session, run setup, and publish it. */
   private async setupAndPublish(
     ownerCtx: Context,
+    owner: Agent | undefined,
     id: SessionId,
     preparation: SessionPreparation,
     agentOptions: AgentOptions,
@@ -693,9 +703,9 @@ export class AgentLoop extends Service implements AgentFactory {
   ): Promise<AgentHandle> {
     using ownedPreparation = preparation
     const session = ownedPreparation.session
-    const prepared = this.prepare(ownerCtx, id, agentOptions, session, signal)
+    const prepared = this.prepare(ownerCtx, owner, id, agentOptions, session, signal)
     try {
-      const setupCommit = await raceAbort(setup?.(prepared.agent.ctx), prepared.signal, id)
+      const setupCommit = await raceAbort(setup?.(prepared.agent.ctx, prepared.agent), prepared.signal, id)
       setupCommit?.commit()
       return prepared.publish(source)
     } catch (error: unknown) {
@@ -708,14 +718,15 @@ export class AgentLoop extends Service implements AgentFactory {
    * Resume an owned agent from the configured persistence service.
    * @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
    * @param options - persisted identity, loop options, setup, and cancellation.
+   * @param owner - live Agent that owns the resumed Agent at runtime, or undefined for a root owner.
    * @returns the published handle.
    */
-  async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle> {
+  async resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle> {
     const persistence = this.runtime.ctx.get('sessionPersistence')
     if (persistence === undefined) {
       throw new Error('cannot resume: session persistence is not configured (load a dsh-session-persistence backend)')
     }
-    return this.resumeWith(ownerCtx, persistence, options)
+    return this.resumeWith(ownerCtx, persistence, options, owner)
   }
 
   /** Resume through an explicit persistence handle used by the deferred config path. */
@@ -723,6 +734,7 @@ export class AgentLoop extends Service implements AgentFactory {
     ownerCtx: Context,
     persistence: SessionPersistence,
     options: ResumeAgentOptions,
+    owner: Agent | undefined,
   ): Promise<AgentHandle> {
     const id = options.resumeSessionId
     const published = (async () => {
@@ -754,6 +766,7 @@ export class AgentLoop extends Service implements AgentFactory {
         if (!this.ownership.isActive()) throw new Error('agent loop is not active')
         return await this.setupAndPublish(
           ownerCtx,
+          owner,
           id,
           preparation,
           options.agentOptions ?? {},

+ 3 - 3
packages/core/agent-loop/tests/agent-initiator.spec.ts

@@ -238,9 +238,9 @@ describe('AgentLoop initiator scope', () => {
         const handle = await exec.agent.ctx.agents.create({
           sessionId: SessionId('child-session'),
           agentOptions: { provider: 'mock', model: 'mock' },
-          setup: (agentCtx) => {
+          setup: (agentCtx, childAgent) => {
             parentDuringSetup = ctx.agents.requireInitiator()
-            explicitChild = agentCtx.agent
+            explicitChild = childAgent
             agentCtx.tools.register(defineContentToolFixture({
               name: 'observe-child',
               description: 'observe child execution identity',
@@ -252,7 +252,7 @@ describe('AgentLoop initiator scope', () => {
               },
             }))
           },
-        })
+        }, exec.agent)
         child = handle.agent
         parentWhileChildDriverActive = ctx.agents.requireInitiator()
         send(handle.agent, 'run child')

+ 3 - 3
packages/core/agent-loop/tests/resume.spec.ts

@@ -314,10 +314,10 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
     const resuming = ctx.agents.resume({
       resumeSessionId: sessionId,
       agentOptions: { provider: 'mock', model: 'mock' },
-      setup: async (agentCtx) => {
-        expect(agentCtx.agent?.id).toBe(sessionId)
+      setup: async (agentCtx, agent) => {
+        expect(agent.id).toBe(sessionId)
         // The two persisted events plus the end-seed marker.
-        expect(agentCtx.agent?.session.events).toHaveLength(3)
+        expect(agent.session.events).toHaveLength(3)
         agentCtx.on('session/created', () => void order.push('setup-listener:session/created'))
         agentCtx.on('agent/created', () => void order.push('setup-listener:agent/created'))
         order.push('setup:start')

+ 6 - 9
packages/core/agent-loop/tests/scope-lifecycle.spec.ts

@@ -142,17 +142,14 @@ describe('agent scope lifecycle', () => {
     await ctx.fiber.dispose()
   })
 
-  it('wires agent.ctx: tagged with the agent, DX field set, ctx.agent safe elsewhere', async () => {
+  it('tags agent.ctx with the Agent scope key', async () => {
     const ctx = await harness()
     const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
     expect(scopeOf(agent.ctx)).toBe(agent)
-    expect(agent.ctx.agent).toBe(agent)
-    // The root accessor default: a plain context answers undefined, not a throw.
-    expect(ctx.agent).toBeUndefined()
     await agent.whenIdle()
   })
 
-  it('records agents created through an agent context as non-root runtime children', async () => {
+  it('records an explicitly owned Agent as a non-root runtime child', async () => {
     const ctx = await harness()
     const root = await ctx.agents.create({
       sessionId: SessionId('runtime-root'),
@@ -161,7 +158,7 @@ describe('agent scope lifecycle', () => {
     const child = await root.agent.ctx.agents.create({
       sessionId: SessionId('runtime-child'),
       agentOptions: { model: 'mock' },
-    })
+    }, root.agent)
 
     expect(ctx.agents.list()).toEqual([root.agent, child.agent])
     expect(ctx.agents.roots()).toEqual([root.agent])
@@ -258,8 +255,8 @@ describe('agent scope lifecycle', () => {
     const creating = ctx.agents.create({
       sessionId: SessionId('atomic'),
       agentOptions: acceptedOptions,
-      setup: async (agentCtx) => {
-        expect(agentCtx.agent?.id).toBe(SessionId('atomic'))
+      setup: async (agentCtx, agent) => {
+        expect(agent.id).toBe(SessionId('atomic'))
         agentCtx.on('session/created', () => void order.push('setup-listener:session/created'))
         agentCtx.on('agent/created', () => void order.push('setup-listener:agent/created'))
         order.push('setup:start')
@@ -586,7 +583,7 @@ describe('agent scope lifecycle', () => {
 
     await expect(loop.createAgent(ctx, {
       sessionId: SessionId('factory-inactive-s'),
-    })).rejects.toThrow(/agent loop is not active|inactive context/)
+    }, undefined)).rejects.toThrow(/agent loop is not active|inactive context/)
     await ctx.fiber.dispose()
   })
 

+ 2 - 2
packages/core/agent/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/core/agent/README.md
-README.md: 906f377fe0a5272ce3a4705536ccaa6b84c4afcf
-README.zh.md: 7ae6dd766c78ca0088806c10e567ea6c389f4cf8
+README.md: de8db86c4083f0fc705ae09265b909e663fd37ff
+README.zh.md: 21379a0184c60f0f9f59b2f9557bd5d7bf539c30

+ 2 - 2
packages/core/agent/README.md

@@ -40,7 +40,7 @@ const handle = await ctx.agents.create({
 await handle.dispose()   // stops the loop, unregisters, removes the session, unwinds the scope
 ```
 
-`AgentOptions` supplies the initial provider/model route, optional adapter-owned `reasoningEffort`, and optional positive `maxTokens` output cap. The loop validates exact-model reasoning support, resolves adapter defaults, records the effective values in the request header, and applies them to each conversation request. An optional `setup(agentCtx)` callback composes the agent's scoped world before it is published — scoped tools, prompt sections, and listeners exist before any creation announcement. Setup is composition-only: drive the agent only after creation resolves.
+`AgentOptions` supplies the initial provider/model route, optional adapter-owned `reasoningEffort`, and optional positive `maxTokens` output cap. The loop validates exact-model reasoning support, resolves adapter defaults, records the effective values in the request header, and applies them to each conversation request. An optional `setup(agentCtx, agent)` callback composes the agent's scoped world before it is published: `agentCtx` owns registrations, while the explicit unpublished Agent provides its Session; the Context has no reverse Agent property. Scoped tools, prompt sections, and listeners exist before any creation announcement. Setup is composition-only: drive the agent only after creation resolves.
 
 ### Drive an agent's conversation
 
@@ -166,7 +166,7 @@ These limits define when this package needs special care. They are current packa
 
 - **Initiator scope is process-local** — workers, child processes, HTTP, durable queues, and restarts must materialize any required identity explicitly.
 - **Ambient identity may outlive liveness** — consumers still check `agent.status`, cancellation, and the owning capability contract before lifecycle-sensitive work.
-- **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead.
+- **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx, agent)` transaction instead.
 - **`cancel()` clears the inbox by default** — it aborts the in-flight turn plus queued and steering work; `cancel(cause, { keepInbox: true })` aborts only the turn and preserves pending items, and there is no step-only abort that keeps the turn running.
 - **Each additional `UserMessage` carries exactly one `MessageSource`** — contributions from several plugins merged onto one message collapse under one source, so the message cannot name several producers.
 

+ 2 - 2
packages/core/agent/README.zh.md

@@ -40,7 +40,7 @@ const handle = await ctx.agents.create({
 await handle.dispose()   // stops the loop, unregisters, removes the session, unwinds the scope
 ```
 
-`AgentOptions` 提供初始 provider/model 路由、可选的适配器所有 `reasoningEffort`,以及可选的正数 `maxTokens` 输出上限。循环会校验确切模型的推理支持、解析适配器默认值、把有效值记录在请求头中,并将它们应用到每个对话请求。可选的 `setup(agentCtx)` 回调会在 agent 发布之前组合其作用域世界——作用域工具、提示词段与监听器在任何创建公告之前就已存在。Setup 只做组合:创建完成后才能驱动 agent。
+`AgentOptions` 提供初始 provider/model 路由、可选的适配器所有 `reasoningEffort`,以及可选的正数 `maxTokens` 输出上限。循环会校验确切模型的推理支持、解析适配器默认值、把有效值记录在请求头中,并将它们应用到每个对话请求。可选的 `setup(agentCtx, agent)` 回调会在 agent 发布之前组合其作用域世界:`agentCtx` 拥有注册,显式的未发布 Agent 则提供其 Session;Context 不含反向 Agent 属性。作用域工具、提示词段与监听器在任何创建公告之前就已存在。Setup 只做组合:创建完成后才能驱动 agent。
 
 ### 驱动 agent 的对话
 
@@ -166,7 +166,7 @@ await handle.agent.whenIdle()
 
 - **发起方作用域只存在于进程内**:worker、子进程、HTTP、持久队列和重启必须显式传递所需身份。
 - **环境身份可能比存活状态更久**:消费方在生命周期敏感工作前,仍要检查 `agent.status`、取消状态和所属能力约定。
-- **`agent/session-start` 不能为启动设置门禁**:它仍是同步且不可 veto 的通知;必须在发布前完成的异步组合属于工厂的 `setup(agentCtx)` 事务。
+- **`agent/session-start` 不能为启动设置门禁**:它仍是同步且不可 veto 的通知;必须在发布前完成的异步组合属于工厂的 `setup(agentCtx, agent)` 事务。
 - **`cancel()` 默认清空收件箱**:它会中止正在处理的轮次以及排队和 steering 工作;`cancel(cause, { keepInbox: true })` 只中止轮次并保留待处理项,且不存在让轮次继续运行、只中止步骤的操作。
 - **每条附加 `UserMessage` 恰好携带一个 `MessageSource`**:多个插件合并到一条消息上的贡献会归入同一来源,因此该消息无法列出多个生产者。
 

+ 18 - 28
packages/core/agent/src/index.ts

@@ -27,16 +27,6 @@ export type { AgentEventDispatch, AgentSubjectEvent } from './dispatch.ts'
 declare module '@deepseek-ai/cordis' {
   interface Context {
     agents: AgentRegistry
-    /**
-     * The agent association installed as an own property on `Agent.ctx`, or
-     * `undefined` on a plain context. Contexts derived from `Agent.ctx` inherit
-     * the association; a deliberately nested scope may carry a nearer
-     * `dsh-scope` tag while retaining it, so this field is DX context rather
-     * than the scope resolver. {@link AgentRegistry} registers a root accessor
-     * defaulting to `undefined`, and core packages below the agent layer use
-     * `scopeOf()` for layer selection instead of reading this field.
-     */
-    agent?: Agent
   }
 }
 
@@ -55,10 +45,12 @@ export interface AgentSetupCommit {
 /**
  * Compose an unpublished Agent scope and optionally return its publication commit.
  * @param agentCtx - unpublished Agent scope.
+ * @param agent - unpublished Agent being composed.
  * @returns an optional synchronous commit invoked after setup awaits settle and immediately before publication.
  */
 export type AgentSetup = (
   agentCtx: Context,
+  agent: Agent,
 ) => AgentSetupCommit | Promise<AgentSetupCommit | void> | void
 
 /**
@@ -188,9 +180,10 @@ export interface AgentFactory {
    * ownership from the factory object's registration context.
    * @param ownerCtx - caller-bound context that owns the transaction and live handle.
    * @param options - agent/session identity, configuration, and optional setup.
+   * @param owner - explicitly supplied live runtime owner, or undefined for a root owner.
    * @returns the owned handle after setup, both announcements, and loop start complete.
    */
-  createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>
+  createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
   /**
    * Prepare a persisted session and resume an agent on it. Async because it awaits
    * both `ctx.sessionPersistence.prepare` and the optional unpublished setup
@@ -199,9 +192,10 @@ export interface AgentFactory {
    * ordered boundary as {@link createAgent}.
    * @param ownerCtx - caller-bound context that owns load, setup, and the live handle.
    * @param options - persisted identity, configuration, and optional setup.
+   * @param owner - explicitly supplied live runtime owner, or undefined for a root owner.
    * @returns the owned handle after setup, both announcements, and loop start complete.
    */
-  resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
+  resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
 }
 
 /** Thrown when create/resume is called before an agent factory is registered. */
@@ -267,17 +261,9 @@ export class AgentRegistry extends Service {
       typeCtx.typert.contexts.registerHost('agent', {
         wire: 'agentId',
         wireTypeSymbol: '@deepseek-ai/dsh-session/types#SessionId',
-        identity: candidate => candidate.agent?.id,
         resolve: sessionId => this.get(sessionId)?.ctx,
       })
     })
-    // The `ctx.agent` DX accessor: default `undefined` on every context, so a
-    // plain plugin context reads cleanly instead of hitting the Cordis
-    // unknown-property throw. Each Agent.ctx shadows it with an own property
-    // (own properties resolve before the context proxy is consulted), so the
-    // accessor body never needs to resolve a scope itself. Effect-scoped:
-    // unwinds with this service's fiber.
-    ctx.accessor('agent', { get: () => undefined })
     ctx.on('internal/status', (fiber) => {
       if (fiber.state === FiberState.UNLOADING && this.hasLifecycleAncestor(fiber)) {
         this.closeInitiators()
@@ -293,7 +279,8 @@ export class AgentRegistry extends Service {
    * Read the Agent that initiated the inherited asynchronous driver chain.
    * Use this optional form for logging, tracing, metrics, or host attribution
    * that also supports agentless calls. When a parent creates a child, setup
-   * reports the causal parent while `agentCtx.agent` identifies the child.
+   * reports the causal parent while the setup callback's Agent parameter
+   * identifies the child.
    * @returns the inherited Agent, or `undefined` outside an initiator boundary
    *   and inside an explicit clearing boundary.
    * @throws when this service instance has been disposed.
@@ -392,9 +379,10 @@ export class AgentRegistry extends Service {
    * registered or creation/setup fails. The resolved {@link AgentHandle} lets
    * the owner tear down exactly this agent.
    * @param options - shared identity, session seed/metadata, and agent options.
+   * @param owner - explicit live runtime owner, or undefined for a root Agent.
    * @returns the handle after setup, rollback-covered publication, and loop start complete.
    */
-  async create(options: CreateAgentOptions): Promise<AgentHandle> {
+  async create(options: CreateAgentOptions, owner?: Agent): Promise<AgentHandle> {
     const ownerCtx = this.ctx
     // Re-trace a Service-backed factory through the accessing context
     // explicitly. This preserves AgentLoop's dependency origin while binding
@@ -403,7 +391,7 @@ export class AgentRegistry extends Service {
     const { target } = this.requireFactory()
     const receiver = getTraceable(ownerCtx, target)
     // oxlint-disable-next-line typescript/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
-    return Reflect.apply(target.createAgent, receiver, [ownerCtx, options])
+    return Reflect.apply(target.createAgent, receiver, [ownerCtx, options, owner])
   }
 
   /**
@@ -411,14 +399,15 @@ export class AgentRegistry extends Service {
    * factory. Rejects if no factory is registered; the factory rejects if
    * session persistence is not configured or persistence/setup fails.
    * @param options - persisted identity, configuration, and optional setup.
+   * @param owner - explicit live runtime owner, or undefined for a root Agent.
    * @returns the handle after setup, rollback-covered publication, and loop start complete.
    */
-  async resume(options: ResumeAgentOptions): Promise<AgentHandle> {
+  async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle> {
     const ownerCtx = this.ctx
     const { target } = this.requireFactory()
     const receiver = getTraceable(ownerCtx, target)
     // oxlint-disable-next-line typescript/unbound-method -- Reflect.apply intentionally supplies the caller-traced receiver
-    return Reflect.apply(target.resume, receiver, [ownerCtx, options])
+    return Reflect.apply(target.resume, receiver, [ownerCtx, options, owner])
   }
 
   /**
@@ -430,6 +419,7 @@ export class AgentRegistry extends Service {
    * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
    * requires passing the carrier). Returns the disposer.
    * @param agent - the already-constructed agent to record in the store.
+   * @param owner - explicit live runtime owner, or undefined for a root Agent.
    * @returns the EXACT Cordis effect disposer (single-shot; a repeat call
    *   returns undefined without awaiting an in-flight teardown). Exact
    *   identity is load-bearing: a composite (generator) effect that owns a
@@ -439,9 +429,9 @@ export class AgentRegistry extends Service {
    *   owner unload, unregistering the agent (and emitting `agent/disposed`)
    *   while its final turn is still draining.
    */
-  register(agent: Agent): () => void {
+  register(agent: Agent, owner?: Agent): () => void {
     const dispose = this.ctx.effect(function* (this: AgentRegistry) {
-      yield this.enter(agent, this.ctx.agent)
+      yield this.enter(agent, owner)
       this.announce(agent)
     }.bind(this), 'agents.register()')
     // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
@@ -455,7 +445,7 @@ export class AgentRegistry extends Service {
    * returned detach closure into its pre-installed composite teardown before
    * calling {@link announce}. Ordinary callers use {@link register}.
    * @param agent - the prepared, unpublished agent.
-   * @param owner - live agent whose scoped context created this agent, or
+   * @param owner - explicitly supplied live runtime owner, or
    *   undefined for a top-level runtime root. This is runtime ownership, not
    *   the resumed session's durable parent lineage.
    * @returns an idempotent closure that removes this exact entry and emits

+ 22 - 9
packages/core/agent/tests/agent.spec.ts

@@ -149,7 +149,6 @@ describe('AgentRegistry', () => {
     await agentFiber
     await ctx.plugin(TypertRegistry)
     const agent = stubAgent('remote-agent')
-    Object.defineProperty(agent, 'ctx', { value: agent.ctx.extend({ agent }) })
     const disposeAgent = ctx.agents.register(agent)
 
     const lookup = ctx.typert.lookups.get('agent')
@@ -161,8 +160,6 @@ describe('AgentRegistry', () => {
     })
     expect(lookup?.resolve(agent.id)).toBe(agent)
     const context = ctx.typert.contexts.getHost('agent')
-    expect(context?.identity(agent.ctx)).toBe(agent.id)
-    expect(context?.identity(ctx)).toBeUndefined()
     expect(context?.resolve(agent.id)).toBe(agent.ctx)
 
     disposeAgent()
@@ -362,16 +359,16 @@ describe('explicit cancellation contract', () => {
 describe('AgentRegistry factory seam', () => {
   function stubFactory() {
     const calls: {
-      create: Array<{ ownerCtx: Context; options: CreateAgentOptions }>
-      resume: Array<{ ownerCtx: Context; options: ResumeAgentOptions }>
+      create: Array<{ ownerCtx: Context; options: CreateAgentOptions; owner: Agent | undefined }>
+      resume: Array<{ ownerCtx: Context; options: ResumeAgentOptions; owner: Agent | undefined }>
     } = { create: [], resume: [] }
     const factory: AgentFactory = {
-      async createAgent(ownerCtx, options) {
-        calls.create.push({ ownerCtx, options })
+      async createAgent(ownerCtx, options, owner) {
+        calls.create.push({ ownerCtx, options, owner })
         return { agent: stubAgent(options.sessionId), dispose: () => Promise.resolve() }
       },
-      async resume(ownerCtx, options) {
-        calls.resume.push({ ownerCtx, options })
+      async resume(ownerCtx, options, owner) {
+        calls.resume.push({ ownerCtx, options, owner })
         return { agent: stubAgent(options.resumeSessionId), dispose: () => Promise.resolve() }
       },
     }
@@ -393,6 +390,22 @@ describe('AgentRegistry factory seam', () => {
     }, { inject: ['agents'] }))
     expect(calls.create[0]?.ownerCtx.fiber).toBe(callerFiber)
     expect(calls.resume[0]?.ownerCtx.fiber).toBe(callerFiber)
+    expect(calls.create[0]?.owner).toBeUndefined()
+    expect(calls.resume[0]?.owner).toBeUndefined()
+  })
+
+  it('passes the explicit runtime owner separately from the caller context', async () => {
+    const ctx = new Context()
+    await ctx.plugin(AgentRegistry)
+    const { factory, calls } = stubFactory()
+    ctx.agents.setFactory(factory)
+    const parent = stubAgent('parent')
+    const unregister = ctx.agents.register(parent)
+
+    await ctx.agents.create({ sessionId: SessionId('child') }, parent)
+
+    expect(calls.create[0]?.owner).toBe(parent)
+    unregister()
   })
 
   it('rejects a second factory and clears the slot with its owner (HMR)', async () => {

+ 15 - 15
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -117,15 +117,15 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         returns: 'the published running agent.',
       },
       {
-        signature: 'async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>',
+        signature: 'async createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle>',
         description: 'Create an owned agent on a caller-supplied session id.',
-        parameters: [{ name: 'ownerCtx', description: 'caller context that structurally owns the lifecycle.' }, { name: 'options', description: 'identities, session seed/metadata, loop options, setup, and cancellation.' }],
+        parameters: [{ name: 'ownerCtx', description: 'caller context that structurally owns the lifecycle.' }, { name: 'options', description: 'identities, session seed/metadata, loop options, setup, and cancellation.' }, { name: 'owner', description: 'live Agent that owns the new Agent at runtime, or undefined for a root owner.' }],
         returns: 'the published handle.',
       },
       {
-        signature: 'async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>',
+        signature: 'async resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle>',
         description: 'Resume an owned agent from the configured persistence service.',
-        parameters: [{ name: 'ownerCtx', description: 'caller context that owns load, setup, and the live lifecycle.' }, { name: 'options', description: 'persisted identity, loop options, setup, and cancellation.' }],
+        parameters: [{ name: 'ownerCtx', description: 'caller context that owns load, setup, and the live lifecycle.' }, { name: 'options', description: 'persisted identity, loop options, setup, and cancellation.' }, { name: 'owner', description: 'live Agent that owns the resumed Agent at runtime, or undefined for a root owner.' }],
         returns: 'the published handle.',
       },
     ],
@@ -250,7 +250,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
     methods: [
       {
         signature: 'currentInitiator(): Agent | undefined',
-        description: 'Read the Agent that initiated the inherited asynchronous driver chain. Use this optional form for logging, tracing, metrics, or host attribution that also supports agentless calls. When a parent creates a child, setup reports the causal parent while `agentCtx.agent` identifies the child.',
+        description: 'Read the Agent that initiated the inherited asynchronous driver chain. Use this optional form for logging, tracing, metrics, or host attribution that also supports agentless calls. When a parent creates a child, setup reports the causal parent while the setup callback\'s Agent parameter identifies the child.',
         parameters: [],
         returns: 'the inherited Agent, or `undefined` outside an initiator boundary and inside an explicit clearing boundary.',
         throws: ['when this service instance has been disposed.'],
@@ -283,27 +283,27 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
         returns: 'the disposer that clears the factory slot. The exact Cordis effect disposer (single-shot): composite (generator) effects may yield it directly — exact identity nests the teardown in order.',
       },
       {
-        signature: 'async create(options: CreateAgentOptions): Promise<AgentHandle>',
+        signature: 'async create(options: CreateAgentOptions, owner?: Agent): Promise<AgentHandle>',
         description: 'Create and publish a new agent through the registered factory. Distinct from register (which records an already-constructed agent): this constructs the agent and its session. Rejects if no factory is registered or creation/setup fails. The resolved AgentHandle lets the owner tear down exactly this agent.',
-        parameters: [{ name: 'options', description: 'shared identity, session seed/metadata, and agent options.' }],
+        parameters: [{ name: 'options', description: 'shared identity, session seed/metadata, and agent options.' }, { name: 'owner', description: 'explicit live runtime owner, or undefined for a root Agent.' }],
         returns: 'the handle after setup, rollback-covered publication, and loop start complete.',
       },
       {
-        signature: 'async resume(options: ResumeAgentOptions): Promise<AgentHandle>',
+        signature: 'async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>',
         description: 'Load a persisted session and resume an agent on it through the registered factory. Rejects if no factory is registered; the factory rejects if session persistence is not configured or persistence/setup fails.',
-        parameters: [{ name: 'options', description: 'persisted identity, configuration, and optional setup.' }],
+        parameters: [{ name: 'options', description: 'persisted identity, configuration, and optional setup.' }, { name: 'owner', description: 'explicit live runtime owner, or undefined for a root Agent.' }],
         returns: 'the handle after setup, rollback-covered publication, and loop start complete.',
       },
       {
-        signature: 'register(agent: Agent): () => void',
+        signature: 'register(agent: Agent, owner?: Agent): () => void',
         description: 'Register a live agent. Throws if an agent with the same id is already registered. Emits `agent/created` on registration and `agent/disposed` when the calling fiber is disposed — both with the agent\'s scope carrier (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the emits are scope-filtered regardless of which context invoked `register` (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always requires passing the carrier). Returns the disposer.',
-        parameters: [{ name: 'agent', description: 'the already-constructed agent to record in the store.' }],
+        parameters: [{ name: 'agent', description: 'the already-constructed agent to record in the store.' }, { name: 'owner', description: 'explicit live runtime owner, or undefined for a root Agent.' }],
         returns: 'the EXACT Cordis effect disposer (single-shot; a repeat call returns undefined without awaiting an in-flight teardown). Exact identity is load-bearing: a composite (generator) effect that owns a teardown ORDER — the agent factory\'s lifecycle chain — must yield THIS function so Cordis nests the unregistration at that yield position; yielding a wrapper would leave it disposing as a concurrent sibling on owner unload, unregistering the agent (and emitting `agent/disposed`) while its final turn is still draining.',
       },
       {
         signature: 'enter(agent: Agent, owner: Agent | undefined): () => void',
         description: 'Insert an already-constructed agent without announcing it. This is the advanced ordered-lifecycle primitive used by the async agent factory: it first completes setup while the agent is unpublished, then assigns the returned detach closure into its pre-installed composite teardown before calling announce. Ordinary callers use register.',
-        parameters: [{ name: 'agent', description: 'the prepared, unpublished agent.' }, { name: 'owner', description: 'live agent whose scoped context created this agent, or undefined for a top-level runtime root. This is runtime ownership, not the resumed session\'s durable parent lineage.' }],
+        parameters: [{ name: 'agent', description: 'the prepared, unpublished agent.' }, { name: 'owner', description: 'explicitly supplied live runtime owner, or undefined for a top-level runtime root. This is runtime ownership, not the resumed session\'s durable parent lineage.' }],
         returns: 'an idempotent closure that removes this exact entry and emits `agent/disposed` with listener failures contained. When called from a synchronous `agent/created` listener, removal and disposal wait until that creation dispatch unwinds.',
       },
       {
@@ -3396,7 +3396,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'AgentFactory',
-    declaration: 'export interface AgentFactory {\n    createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>;\n    resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>;\n}',
+    declaration: 'export interface AgentFactory {\n    createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle>;\n    resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle>;\n}',
   },
   {
     name: 'AgentHandle',
@@ -3428,7 +3428,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'AgentSetup',
-    declaration: 'export type AgentSetup = (agentCtx: Context) => AgentSetupCommit | Promise<AgentSetupCommit | void> | void;',
+    declaration: 'export type AgentSetup = (agentCtx: Context, agent: Agent) => AgentSetupCommit | Promise<AgentSetupCommit | void> | void;',
   },
   {
     name: 'AgentSetupCommit',
@@ -5832,7 +5832,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'TypertRemoteEventContext',
-    declaration: 'export interface TypertRemoteEventContext {\n    readonly value: Context;\n    readonly subject: object;\n}',
+    declaration: 'export interface TypertRemoteEventContext {\n    readonly value: Context;\n    readonly subject: object;\n    readonly agentId: string;\n}',
   },
   {
     name: 'TypertRemoteEventDispatch',

+ 1 - 1
packages/schedule/schedule/tests/plugin.spec.ts

@@ -64,7 +64,7 @@ describe('Schedule plugin composition', () => {
     agentEvents(ctx, root.agent).emit('agent/status', { status: 'running' })
     agentEvents(ctx, root.agent).emit('agent/status', { status: 'idle' })
 
-    const child = await root.agent.ctx.agents.create({ sessionId: SessionId('schedule-child') })
+    const child = await root.agent.ctx.agents.create({ sessionId: SessionId('schedule-child') }, root.agent)
     expect(ctx.agents.roots()).toEqual([existing.agent, root.agent])
     expect(ctx.tools.get('schedule_create', child.agent)).toBeUndefined()
 

+ 1 - 1
packages/sdk/server/tests/built-scope-carrier.e2e.ts

@@ -62,7 +62,7 @@ try {
     sessionId: SessionId("built-child"),
     meta: { cwd: storageRoot, parentSession: SessionId("built-parent") },
     agentOptions: { model: "test" },
-  });
+  }, parent.agent);
   const result = Promise.withResolvers();
   const unregister = ctx.subagents.registerProvider({
     name: "built-local",

+ 8 - 8
packages/sdk/server/tests/server.spec.ts

@@ -450,7 +450,7 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('parentless-child-session'),
         meta: { cwd: storageDir },
         agentOptions: { model: 'deepseek-official' },
-      })
+      }, parentHandle.agent)
       await settleSubagent(ctx, parentHandle.agent, {
         provider: 'spawn',
         id: SessionId('child-session'),
@@ -512,7 +512,7 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('remote-run-id'),
         meta: { cwd: storageDir, parentSession: SessionId('collision-parent') },
         agentOptions: { model: 'deepseek-official' },
-      })
+      }, parentHandle.agent)
 
       await settleSubagent(ctx, parentHandle.agent, {
         provider: 'remote',
@@ -551,7 +551,7 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('continuation-child'),
         meta: { cwd: storageDir, parentSession: SessionId('continuation-parent') },
         agentOptions: { model: 'deepseek-official' },
-      })
+      }, parentHandle.agent)
 
       await settleSubagent(ctx, parentHandle.agent, {
         provider: 'continuation',
@@ -596,7 +596,7 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('reused-child'),
         meta: { cwd: storageDir, parentSession: SessionId('old-parent') },
         agentOptions: { model: 'deepseek-official' },
-      })
+      }, oldParent.agent)
       const first = Promise.withResolvers<SubagentResult>()
       const sameLifetime = Promise.withResolvers<SubagentResult>()
       const replacement = Promise.withResolvers<SubagentResult>()
@@ -637,7 +637,7 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('reused-child'),
         meta: { cwd: storageDir, parentSession: SessionId('new-parent') },
         agentOptions: { model: 'deepseek-official' },
-      })
+      }, newParent.agent)
       currentLocalAgent = newChild.agent
       const secondRun = await ctx.subagents.start('reused', {
         parent: newParent.agent,
@@ -695,7 +695,7 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('provider-reuse-child'),
         meta: { cwd: storageDir, parentSession: SessionId('provider-reuse-parent') },
         agentOptions: { model: 'deepseek-official' },
-      })
+      }, parent.agent)
       const localResult = Promise.withResolvers<SubagentResult>()
       const remoteResult = Promise.withResolvers<SubagentResult>()
       const unregisterLocal = ctx.subagents.registerProvider({
@@ -788,13 +788,13 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('fallback-child-session'),
         meta: { cwd: storageDir, parentSession: SessionId('fallback-parent') },
         agentOptions: { provider: 'deepseek-official', model: 'deepseek-official' },
-      })
+      }, parentHandle.agent)
       const fallbackChild = handle.agent
       failedHandle = await parentHandle.agent.ctx.agents.create({
         sessionId: SessionId('failed-child-session'),
         meta: { cwd: storageDir },
         agentOptions: { provider: 'deepseek-official', model: 'deepseek-official' },
-      })
+      }, parentHandle.agent)
       const missedStartResult = Promise.withResolvers<SubagentResult>()
       const disposeMissedStartProvider = ctx.subagents.registerProvider({
         name: 'fork',

+ 3 - 3
packages/subagent/subagent-in-process-driver/src/index.ts

@@ -117,8 +117,8 @@ export async function startInProcessRun(
   const inherited = captureDelegatedPolicyOverrides(parent)
 
   let structured: StructuredAttachment | undefined
-  const setup = (childCtx: Context): void => {
-    appendDelegatedPolicyOverrides((childCtx.agent as Agent).session, inherited)
+  const setup = (childCtx: Context, child: Agent): void => {
+    appendDelegatedPolicyOverrides(child.session, inherited)
     applyChildComposition(childCtx, parent, {
       persona: request.persona,
       toolFilter: request.toolFilter,
@@ -136,7 +136,7 @@ export async function startInProcessRun(
     agentOptions: resolveChildAgentOptions(parent, request.agentOptions, childDepth),
     signal: request.signal,
     setup,
-  })
+  }, parent)
   return drivePublishedRun(
     handle,
     request.signal,

+ 4 - 4
packages/subagent/subagent/src/continuation.ts

@@ -1066,12 +1066,12 @@ export class SubagentContinuationManager {
     // `AgentRegistry.enter()` is the authoritative collision boundary for an id
     // some other owner holds — a duplicate would reject there with rollback.
     inputs.signal.throwIfAborted()
-    const setup = (childCtx: Context): AgentSetupCommit => {
+    const setup = (childCtx: Context, child: Agent): AgentSetupCommit => {
       // Only fresh creation seeds the delegation policy onto the child's own
       // log (after any fork seed, so fresh policy wins stale seed state); a
       // cold resume replays those persisted events instead.
       if (create !== undefined) {
-        appendDelegatedPolicyOverrides((childCtx.agent as Agent).session, create.delegatedPolicies)
+        appendDelegatedPolicyOverrides(child.session, create.delegatedPolicies)
       }
       applyChildComposition(childCtx, parent, inputs.composition)
       return this.setupRegistry.apply(childCtx)
@@ -1085,7 +1085,7 @@ export class SubagentContinuationManager {
         agentOptions: inputs.agentOptions,
         signal: inputs.signal,
         setup,
-      })
+      }, parent)
       : await this.ownerCtx.agents.create({
         sessionId: childId,
         meta: create.meta,
@@ -1093,7 +1093,7 @@ export class SubagentContinuationManager {
         agentOptions: inputs.agentOptions,
         signal: inputs.signal,
         setup,
-      })
+      }, parent)
 
     const activation: Activation = {
       childId,

+ 5 - 9
packages/subagent/tool-subagent/src/index.ts

@@ -618,7 +618,7 @@ export function apply(ctx: Context, config: Config): void {
     throw new Error('tool-subagent: `modelSelectionSettings` requires an Agent or preset scope')
   }
 
-  const selectForAgent = (agent: NonNullable<Context['agent']>): ModelSelectionPolicy | undefined => {
+  const selectForAgent = (agent: Agent): ModelSelectionPolicy | undefined => {
     let allowedModels = subagentModelSelectionPolicy(ctx.sessionProjections, agent.session)
     if (allowedModels === undefined) {
       const parentId = agent.session.header.origin === 'subagent'
@@ -640,11 +640,6 @@ export function apply(ctx: Context, config: Config): void {
     return allowedModels === undefined ? undefined : { routes: allowedModels }
   }
 
-  const agent = ctx.agent
-  if (agent !== undefined) {
-    install(ctx, selectForAgent(agent))
-    return
-  }
   const agents = ctx.get('agents')
   /* v8 ignore next -- Agent and preset scopes are minted only by the Agent registry. */
   if (agents === undefined) throw new Error('tool-subagent: scoped model-selection settings require the Agent registry')
@@ -685,9 +680,9 @@ export function apply(ctx: Context, config: Config): void {
       else removeScoped(candidate)
     }
   }
-  // A shipped preset is mounted once in a standing scope. Its listener admits
-  // only descendant Agents and installs the sampled tool definition in each
-  // Agent's own scope, so a later settings change cannot mutate a live session.
+  // The scoped listener admits this Agent or preset's descendant Agents and
+  // installs the sampled tool definition in each Agent's own scope, so a later
+  // settings change cannot mutate a live session.
   ctx.on('agent/created', ({ agent: created }) => {
     installScoped(created)
   })
@@ -696,4 +691,5 @@ export function apply(ctx: Context, config: Config): void {
   // set and emits `tools/change`; reconcile the Agent-owned override with the
   // new ancestry. Other registry changes are idempotent no-ops here.
   ctx.on('tools/change', reconcileComposedAgents)
+  reconcileComposedAgents()
 }

+ 2 - 2
packages/typert/protocol/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/typert/protocol/README.md
-README.md: 34e685b841e3bf98d1b23306ed90fd3ca8381f79
-README.zh.md: d85cda322f9a3117a4b7bbe4405140b75d5915b8
+README.md: 6fd5390f68f148711f1b70f6e30fab99cf2868fa
+README.zh.md: 80039ac2e9be5786632a5a38ba23b68cca9777b7

+ 1 - 1
packages/typert/protocol/README.md

@@ -46,7 +46,7 @@ Generation turns the method into a wire endpoint under the service's namespace;
 
 ### Associating Host objects and Contexts with wire identities
 
-Complex Host objects cannot cross the wire directly. A business package declares the association through the merge-extensible `TypertLookupMap` and `TypertContextMap`. Host and Client Context adapters both map `Context` to a wire identity and that identity back to `Context`; the Host adapter also owns the stable wire declaration. Host composition may override its synchronous or asynchronous resolver. A policy rejection can throw `TypertLookupFailure` to carry an adapter-owned failure value to the caller.
+Complex Host objects cannot cross the wire directly. A business package declares the association through the merge-extensible `TypertLookupMap` and `TypertContextMap`. A Host Context adapter owns the stable wire declaration and resolves wire identities to live Contexts. A Client Context adapter maps in both directions because scoped calls originate from a Client Context and forwarded Host events resolve their explicit wire identity there. Host composition may override its synchronous or asynchronous resolver. A policy rejection can throw `TypertLookupFailure` to carry an adapter-owned failure value to the caller.
 
 ### Receiving forwarded Host events on the Client
 

+ 1 - 1
packages/typert/protocol/README.zh.md

@@ -46,7 +46,7 @@ export class GoalService extends TypertRemoteService {
 
 ### 把 Host 对象与 Context 关联到 wire identity
 
-复杂的 Host 对象不能直接跨 wire 传输。业务包通过可合并扩展的 `TypertLookupMap` 与 `TypertContextMap` 声明关联。Host 与 Client Context adapter 都把 `Context` 映射为 wire identity,也把该 identity 映射回 `Context`;Host adapter 还拥有稳定 wire 声明。Host 组合可以覆盖其同步或异步 resolver。策略拒绝可以抛出 `TypertLookupFailure`,把适配器拥有的失败值带给调用方。
+复杂的 Host 对象不能直接跨 wire 传输。业务包通过可合并扩展的 `TypertLookupMap` 与 `TypertContextMap` 声明关联。Host Context adapter 拥有稳定 wire 声明,并把 wire identity 解析为活跃 Context。Client Context adapter 需要双向映射,因为作用域调用从 Client Context 发起,而转发的 Host 事件要在 Client 侧解析其显式 wire identity。Host 组合可以覆盖其同步或异步 resolver。策略拒绝可以抛出 `TypertLookupFailure`,把适配器拥有的失败值带给调用方。
 
 ### 在 Client 侧接收转发的 Host 事件
 

+ 0 - 2
packages/typert/protocol/src/index.ts

@@ -64,7 +64,6 @@ export type {
   TypertClientContextAdapter,
   TypertCodec,
   TypertContext,
-  TypertContextAdapter,
   TypertContextMap,
   TypertContextRegistry,
   TypertContextWire,
@@ -72,7 +71,6 @@ export type {
   TypertForwardableEvent,
   TypertForwardableEventEntry,
   TypertHostContextAdapter,
-  TypertHostContextIdentity,
   TypertHostContextResolver,
   TypertLocalRegistry,
   TypertLookup,

+ 8 - 34
packages/typert/protocol/src/types.ts

@@ -350,31 +350,20 @@ export interface TypertLookupDefinition {
   readonly wireTypeSymbol: string
 }
 
-/** Bidirectional projection between one environment's Context and its wire identity. */
-export interface TypertContextAdapter<Wire = unknown> {
-  /**
-   * Read the identity represented by a live Context.
-   * @param ctx - Context in this adapter's environment.
-   * @returns the wire identity, or `undefined` when the Context has another kind.
-   */
-  identity(ctx: Context): Wire | undefined
+/** Host wire-to-Context resolver plus the declaration used by strict Remote methods. */
+export interface TypertHostContextAdapter<Wire = unknown> {
+  /** Wire field carrying the Context identity. */
+  readonly wire: string
+  /** Canonical wire type symbol used by strict generation. */
+  readonly wireTypeSymbol: string
   /**
-   * Resolve a wire identity to a live Context in this adapter's environment.
-   * An asynchronous Client resolver may wait for its owner to create the Context.
+   * Resolve a validated wire identity to a live Host Context.
    * @param id - validated wire identity.
    * @returns the Context, or `undefined` when it is unavailable.
    */
   resolve(id: Wire): Context | undefined | Promise<Context | undefined>
 }
 
-/** Host Context adapter plus the wire declaration used by strict Remote methods. */
-export interface TypertHostContextAdapter<Wire = unknown> extends TypertContextAdapter<Wire> {
-  /** Wire field carrying the Context identity. */
-  readonly wire: string
-  /** Canonical wire type symbol used by strict generation. */
-  readonly wireTypeSymbol: string
-}
-
 /** Composition-owned resolver replacing one Host Context adapter's default lookup policy. */
 export type TypertHostContextResolver<Wire = unknown> = (
   id: Wire,
@@ -396,14 +385,6 @@ export interface TypertClientContextAdapter<Wire = unknown> {
   resolve(id: Wire): Context | undefined
 }
 
-/** Host Context identity selected from the registered adapter set. */
-export interface TypertHostContextIdentity {
-  /** Merge-declared Context kind whose adapter recognized the Context. */
-  readonly kind: string
-  /** Wire identity returned by that adapter. */
-  readonly identity: unknown
-}
-
 /** Notification emitted after a Typert runtime registry changes. */
 export interface TypertRegistryChange {
   readonly kind: 'local' | 'remote' | 'lookup' | 'host-context' | 'client-context'
@@ -513,7 +494,7 @@ export interface TypertContextRegistry {
   /**
    * Register a Host Context adapter.
    * @param key - merge-declared Context key.
-   * @param adapter - owning package's bidirectional Host projection.
+   * @param adapter - owning package's Host resolver and wire declaration.
    * @returns disposer withdrawing the exact adapter.
    */
   registerHost<K extends StringKeyOf<TypertContextMap>>(
@@ -541,13 +522,6 @@ export interface TypertContextRegistry {
     key: K,
     adapter: TypertClientContextAdapter<TypertContextWire<TypertContextMap[K]>>,
   ): TypertDisposer
-  /**
-   * Identify a live Host Context through the sole registered adapter set.
-   * @param ctx - Context projected by a Host-to-Client scoped event.
-   * @returns its kind and wire identity, or `undefined` when no adapter recognizes it.
-   * @throws when more than one Context kind recognizes the same Context.
-   */
-  identifyHost(ctx: Context): TypertHostContextIdentity | undefined
   /**
    * Look up a Host Context adapter.
    * @param key - descriptor Context key.

+ 2 - 2
packages/typert/registry/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/typert/registry/README.md
-README.md: a19226addaa9512eaaefb621d0f5ae70653bf216
-README.zh.md: 8c909bb698f1b6658fe90f9d56f2c0d543957ec9
+README.md: 369ab26064db7c685cb961af9714a2a32a82a520
+README.zh.md: 306a917c7ffab0d670f307f7a8b5126022b3c5d3

+ 1 - 1
packages/typert/registry/README.md

@@ -45,7 +45,7 @@ Generated artifacts register through the [loader](../loader/README.md) in Loader
 
 ### Lookup and Context providers
 
-Remote calls resolve Host objects and scoped Contexts through `ctx.typert.lookups` and `ctx.typert.contexts`. `registerHost()` installs one bidirectional Host Context adapter and its wire declaration, while `configureHost()` replaces only its resolver. `registerClient()` installs the bidirectional Client adapter for the same merge-declared kind. `identifyHost(ctx)` asks the Host adapters for the single kind and identity represented by a live Context and rejects ambiguous recognition.
+Remote calls resolve Host objects and scoped Contexts through `ctx.typert.lookups` and `ctx.typert.contexts`. `registerHost()` installs the Host wire declaration and its wire-to-Context resolver, while `configureHost()` replaces only that resolver. `registerClient()` installs the bidirectional Client adapter for the same merge-declared kind. Host-to-Client event sources carry their domain identity explicitly instead of projecting it back from a Context.
 
 -----
 

+ 1 - 1
packages/typert/registry/README.zh.md

@@ -45,7 +45,7 @@ kind: "package-reference"
 
 ### Lookup 与 Context 提供方
 
-Remote 调用通过 `ctx.typert.lookups` 与 `ctx.typert.contexts` 解析 Host 对象与作用域 Context。`registerHost()` 安装一个双向 Host Context adapter 及其 wire 声明,`configureHost()` 只替换其中的 resolver。`registerClient()` 为同一个 merge-declared kind 安装双向 Client adapter。`identifyHost(ctx)` 通过 Host adapter 识别活 Context 所代表的唯一 kind 与 identity,并拒绝歧义识别
+Remote 调用通过 `ctx.typert.lookups` 与 `ctx.typert.contexts` 解析 Host 对象与作用域 Context。`registerHost()` 安装 Host wire 声明及其 wire 到 Context 的 resolver,`configureHost()` 只替换该 resolver。`registerClient()` 为同一个 merge-declared kind 安装双向 Client adapter。Host 到 Client 的事件源显式携带领域 identity,而不从 Context 反向投影
 
 -----
 

+ 0 - 17
packages/typert/registry/src/service.ts

@@ -357,7 +357,6 @@ class ContextStore {
         key: K,
         adapter: TypertClientContextAdapter<TypertContextWire<TypertContextMap[K]>>,
       ) => this.registerClient(ctx, key, adapter),
-      identifyHost: context => this.identifyHost(context),
       getHost: key => this.getHost(key),
       getClient: key => this.clients.get(key)?.provider,
       subscribe: listener => this.changes.subscribe(ctx, listener),
@@ -372,26 +371,10 @@ class ContextStore {
     return {
       wire: adapter.wire,
       wireTypeSymbol: adapter.wireTypeSymbol,
-      identity: context => adapter.identity(context),
       resolve: id => resolver.resolve(id),
     }
   }
 
-  private identifyHost(ctx: Context): ReturnType<TypertContextRegistry['identifyHost']> {
-    let match: ReturnType<TypertContextRegistry['identifyHost']>
-    for (const key of this.hosts.keys()) {
-      const identity = this.getHost(key)?.identity(ctx)
-      if (identity === undefined) continue
-      if (match !== undefined) {
-        throw new Error(
-          `typert: Host Context is recognized by both ${JSON.stringify(match.kind)} and ${JSON.stringify(key)}`,
-        )
-      }
-      match = { kind: key, identity }
-    }
-    return match
-  }
-
   private configureHost<Wire>(
     ctx: Context,
     key: string,

+ 0 - 31
packages/typert/registry/tests/typert.spec.ts

@@ -332,7 +332,6 @@ describe('TypertRegistry', () => {
     const disposeHost = ctx.typert.contexts.registerHost('registryFixture', {
       wire: 'agentId',
       wireTypeSymbol: '@fixture/session#SessionId',
-      identity: candidate => candidate === scoped ? object.id : undefined,
       resolve: id => id === object.id ? scoped : undefined,
     })
     const disposeClient = ctx.typert.contexts.registerClient('registryFixture', {
@@ -348,15 +347,9 @@ describe('TypertRegistry', () => {
       hostTypeSymbol: '@fixture/agent#Agent',
       wireTypeSymbol: '@fixture/session#SessionId',
     }])
-    expect(ctx.typert.contexts.getHost('registryFixture')?.identity(scoped)).toBe('agent-1')
     expect(ctx.typert.contexts.getHost('registryFixture')?.resolve('agent-1')).toBe(scoped)
     expect(ctx.typert.contexts.getClient('registryFixture')?.identity(scoped)).toBe('agent-1')
     expect(ctx.typert.contexts.getClient('registryFixture')?.resolve('agent-1')).toBe(scoped)
-    expect(ctx.typert.contexts.identifyHost(scoped)).toEqual({
-      kind: 'registryFixture',
-      identity: 'agent-1',
-    })
-    expect(ctx.typert.contexts.identifyHost(ctx)).toBeUndefined()
 
     await Promise.all([disposeClient(), disposeHost(), disposeLookup()])
     expect(ctx.typert.lookups.keys()).toEqual([])
@@ -365,26 +358,6 @@ describe('TypertRegistry', () => {
     expect(ctx.typert.contexts.getClient('registryFixture')).toBeUndefined()
   })
 
-  it('rejects a Host Context recognized by more than one registered kind', async () => {
-    const ctx = await makeCtx()
-    const scoped = ctx.extend()
-    ctx.typert.contexts.registerHost('registryFixture', {
-      wire: 'agentId',
-      wireTypeSymbol: '@fixture#AgentId',
-      identity: candidate => candidate === scoped ? 'first' : undefined,
-      resolve: () => undefined,
-    })
-    ctx.typert.contexts.registerHost('registryFixtureOther', {
-      wire: 'otherAgentId',
-      wireTypeSymbol: '@fixture#OtherAgentId',
-      identity: candidate => candidate === scoped ? 'second' : undefined,
-      resolve: () => undefined,
-    })
-
-    expect(() => ctx.typert.contexts.identifyHost(scoped))
-      .toThrow('recognized by both "registryFixture" and "registryFixtureOther"')
-  })
-
   it('configures an asynchronous lookup resolver independently of provider load order', async () => {
     const ctx = await makeCtx()
     const fallback = { id: 'fallback' }
@@ -430,11 +403,9 @@ describe('TypertRegistry', () => {
     const disposeProvider = ctx.typert.contexts.registerHost('registryFixture', {
       wire: 'agentId',
       wireTypeSymbol: '@fixture/session#SessionId',
-      identity: candidate => candidate === fallback ? 'fallback' : undefined,
       resolve: id => id === 'fallback' ? fallback : undefined,
     })
     await expect(ctx.typert.contexts.getHost('registryFixture')?.resolve('configured')).resolves.toBe(configured)
-    expect(ctx.typert.contexts.getHost('registryFixture')?.identity(fallback)).toBe('fallback')
     expect(() => ctx.typert.contexts.configureHost('registryFixture', () => undefined)).toThrow('already configured')
 
     await disposeProvider()
@@ -442,7 +413,6 @@ describe('TypertRegistry', () => {
     const disposeReloadedProvider = ctx.typert.contexts.registerHost('registryFixture', {
       wire: 'agentId',
       wireTypeSymbol: '@fixture/session#SessionId',
-      identity: candidate => candidate === fallback ? 'fallback' : undefined,
       resolve: id => id === 'fallback' ? fallback : undefined,
     })
     await expect(ctx.typert.contexts.getHost('registryFixture')?.resolve('configured')).resolves.toBe(configured)
@@ -471,7 +441,6 @@ describe('TypertRegistry', () => {
     const host = {
       wire: 'agentId',
       wireTypeSymbol: '@fixture#AgentId',
-      identity: (_candidate: Context) => undefined,
       resolve: () => undefined,
     }
     const client = {

+ 1 - 4
packages/webhook/webhook/src/session.ts

@@ -89,11 +89,8 @@ function reportRollbackFailure(ctx: Context, subject: string, error: unknown): v
 
 /** Apply the creation-time selection until its first durable request header exists. */
 function installInitialModelSelection(agentCtx: Context, selection: ModelSelection): void {
-  agentCtx.on('agent/request', async (_payload, next): Promise<LlmCallConfig> => {
+  agentCtx.on('agent/request', async ({ agent }, next): Promise<LlmCallConfig> => {
     const resolved = await next()
-    const agent = agentCtx.agent
-    /* v8 ignore next -- AgentRegistry setup always provides the unpublished scoped Agent. */
-    if (agent === undefined) throw new Error('webhook Session setup has no scoped Agent')
     if (agent.session.requestHeader() !== undefined
       || resolved.provider !== selection.provider
       || resolved.model !== selection.model) return resolved

+ 10 - 9
packages/webhook/webhook/tests/runtime.spec.ts

@@ -242,16 +242,17 @@ describe('WebhookRuntime', () => {
     } as never)
     ctx.provide('sessionTitle', { rename: () => ({}) } as never)
     ctx.provide('agents', {
-      create: async (options: { setup?: (agentCtx: unknown) => Promise<void> }) => {
-        await options.setup?.({ on: () => () => {} })
-        return {
-          agent: {
-            session,
-            followup: (message: unknown) => {
-              messages.push(message)
-              if (messages.length === 2) followedTwice.resolve(true)
-            },
+      create: async (options: { setup?: (agentCtx: unknown, agent: unknown) => Promise<void> }) => {
+        const agent = {
+          session,
+          followup: (message: unknown) => {
+            messages.push(message)
+            if (messages.length === 2) followedTwice.resolve(true)
           },
+        }
+        await options.setup?.({ on: () => () => {} }, agent)
+        return {
+          agent,
           dispose: async () => {},
         }
       },

+ 6 - 4
packages/webhook/webhook/tests/session.spec.ts

@@ -22,6 +22,7 @@ interface SessionHarness {
   readonly calls: string[]
   readonly messages: unknown[]
   readonly modelListeners: Map<string, unknown>
+  readonly agent: unknown
   markRequestHeader(): void
   readonly controller: AbortController
   readonly request: WebhookSessionRequest
@@ -116,16 +117,15 @@ function harness(options: HarnessOptions = {}): SessionHarness {
       },
     },
     agents: {
-      async create(createOptions: { setup?: (ctx: unknown) => Promise<void> }) {
+      async create(createOptions: { setup?: (ctx: unknown, agent: unknown) => Promise<void> }) {
         calls.push('agent-create')
         if (options.failAt === 'agent') throw new Error('agent failed')
         await createOptions.setup?.({
-          agent,
           on(event: string, listener: unknown) {
             modelListeners.set(event, listener)
             return () => {}
           },
-        })
+        }, agent)
         if (options.abortAt === 'agent') controller.abort(new Error('abort after agent'))
         return handle
       },
@@ -143,6 +143,7 @@ function harness(options: HarnessOptions = {}): SessionHarness {
     calls,
     messages,
     modelListeners,
+    agent,
     markRequestHeader() { requestHeader = {} },
     controller,
     request: {
@@ -184,10 +185,11 @@ function modelRequestListener(test: SessionHarness): (
   if (typeof listener !== 'function') {
     throw new Error('webhook Session did not install its initial model selection')
   }
-  return listener as (
+  const request = listener as (
     payload: unknown,
     next: () => Promise<LlmCallConfig>,
   ) => Promise<LlmCallConfig>
+  return (_payload, next) => request({ agent: test.agent }, next)
 }
 
 describe('webhook Session creation', () => {

+ 3 - 0
pnpm-lock.yaml

@@ -677,6 +677,9 @@ importers:
       '@deepseek-ai/cordis':
         specifier: workspace:^
         version: link:../../../vendor/cordis
+      '@deepseek-ai/dsh-agent':
+        specifier: workspace:^
+        version: link:../../core/agent
       '@deepseek-ai/dsh-agent-presets':
         specifier: workspace:^
         version: link:../../preset/agent-presets

+ 0 - 1
scripts/gen-cordis-catalog.ts

@@ -144,7 +144,6 @@ export const SERVICE_PAGE: Record<string, string> = {
  * to a model as `cordis_runtime_inspect what:"client"`).
  */
 export const SERVICE_WALK_EXEMPTIONS: Record<string, string> = {
-  agent: 'not a service: the DX accessor field on Agent.ctx (root accessor defaulting to undefined) — docs/subsystems/core.md owns the Agent handle',
   appReady: 'not a service: launcher-provided successful-startup signal — packages/boot/cmdline/README.md owns the launcher contract',
   appExit: 'not a service: launcher-provided bounded process-exit callback — packages/boot/cmdline/README.md owns the launcher contract',
   cmdlineArgs: 'not a service: launcher-provided immutable app argument accessor — packages/boot/cmdline/README.md owns the launcher contract',