Kaynağa Gözat

refactor(agent): move runtime parent into options

_Kerman 2 hafta önce
ebeveyn
işleme
0b8956aab9
27 değiştirilmiş dosya ile 142 ekleme ve 136 silme
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.i18n.yaml
  5. 4 4
      .agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md
  6. 4 4
      .agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md
  7. 2 2
      docs/subsystems/core.i18n.yaml
  8. 13 17
      docs/subsystems/core.md
  9. 13 17
      docs/subsystems/core.zh.md
  10. 2 2
      packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts
  11. 2 2
      packages/core/agent-loop/README.i18n.yaml
  12. 1 1
      packages/core/agent-loop/README.md
  13. 1 1
      packages/core/agent-loop/README.zh.md
  14. 14 17
      packages/core/agent-loop/src/index.ts
  15. 2 1
      packages/core/agent-loop/tests/agent-initiator.spec.ts
  16. 3 2
      packages/core/agent-loop/tests/scope-lifecycle.spec.ts
  17. 2 2
      packages/core/agent/README.i18n.yaml
  18. 1 1
      packages/core/agent/README.md
  19. 1 1
      packages/core/agent/README.zh.md
  20. 18 18
      packages/core/agent/src/index.ts
  21. 11 11
      packages/core/agent/tests/agent.spec.ts
  22. 14 14
      packages/extensions/tool-cordis/src/api-catalog.ts
  23. 4 1
      packages/schedule/schedule/tests/plugin.spec.ts
  24. 2 1
      packages/sdk/server/tests/built-scope-carrier.e2e.ts
  25. 16 8
      packages/sdk/server/tests/server.spec.ts
  26. 2 1
      packages/subagent/subagent-in-process-driver/src/index.ts
  27. 4 2
      packages/subagent/subagent/src/continuation.ts

+ 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: 74780e075795ede44016c451d229b5a5c3fff23a
-2026-07-12-agent-scope-runtime-design.zh.md: e3c9f050eabcc259fa2940e06c6d83e679433525
+2026-07-12-agent-scope-runtime-design.md: bb8ef6ca2257c3bf23a11df69e63a5cf94255992
+2026-07-12-agent-scope-runtime-design.zh.md: 8862f7bc3b0be1fc8c8539fd34a5d6dc32b269af

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

@@ -102,7 +102,7 @@ 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. 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. The [explicit runtime-identity decision](2026-08-31-explicit-agent-runtime-identity.md) owns this separation and the continuable-child ownership rule that follows from it.
+The factory stores concrete trace targets but invokes them through a caller-bound Cordis trace. A runtime child creator sets `parentAgent` in the create or resume options, and AgentRegistry forwards those options without deriving a parent 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. The [explicit runtime-identity decision](2026-08-31-explicit-agent-runtime-identity.md) owns this separation and the continuable-child ownership rule that follows from it.
 
 ### Setup is trusted composition inside a private world
 

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

@@ -104,7 +104,7 @@ detach 闭包捕获其确切注册表条目。它仅在映射仍指向该注册
 
 创建准备一个新 Session。恢复加载并验证持久化的 Session,然后准备相同的活跃会话标识。两条路径随后构建作用域、agent 和 driver,并调用相同的 setup/发布算法。
 
-工厂存储具体的 trace 目标,但通过调用方绑定的 Cordis trace 调用它们。运行时子 Agent 的创建方显式传入父 Agent,AgentRegistry 再将其与调用方 Context 分开转交。这既保留了依赖来源和两种所有权事实,又不堆叠 trace 代理,也不把领域对象附着到 Context。作用域 Remote 事件适配器同样从 request 接收 Agent,校验它就是 carrier key,再直接投影其 Context 与 wire identity。系统不会通过作用域索引从 Context 重建 Agent。[显式运行时身份决策](2026-08-31-explicit-agent-runtime-identity.zh.md)拥有这项分离原则及由此确定的可续跑子级归属规则。
+工厂存储具体的 trace 目标,但通过调用方绑定的 Cordis trace 调用它们。运行时子 Agent 的创建方在 create 或 resume options 中设置 `parentAgent`,AgentRegistry 转交这些 options,不从调用方 Context 推导父级。这既保留了依赖来源和两种所有权事实,又不堆叠 trace 代理,也不把领域对象附着到 Context。作用域 Remote 事件适配器同样从 request 接收 Agent,校验它就是 carrier key,再直接投影其 Context 与 wire identity。系统不会通过作用域索引从 Context 重建 Agent。[显式运行时身份决策](2026-08-31-explicit-agent-runtime-identity.zh.md)拥有这项分离原则及由此确定的可续跑子级归属规则。
 
 ### Setup 是私有世界内的可信组合
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.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-08-31-explicit-agent-runtime-identity.md
-2026-08-31-explicit-agent-runtime-identity.md: 9177688866bf6b0c9c4a50f2b9169d4bc6a66c65
-2026-08-31-explicit-agent-runtime-identity.zh.md: 0cede058cb19c7f6a55d49d58e40c795f8c32a33
+2026-08-31-explicit-agent-runtime-identity.md: 6f0fafa243ee6843981bbb78af626988c4131f80
+2026-08-31-explicit-agent-runtime-identity.zh.md: 703ccd0384003c30e4c402e96e6a04083e473024

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.md

@@ -14,11 +14,11 @@ Without an explicit owner, `SubagentContinuationManager` creates and resumes chi
 
 ## Decision
 
-Runtime interfaces carry Agent identity at the point that owns it. `AgentSetup` receives `(agentCtx, agent)`; Agent creation and resume receive an explicit runtime owner; scoped events carry their Agent in the payload; Remote forwarding verifies that `request.agent` is the carrier key; and Host Typert Context resolution maps wire identity to a live Agent Context without a reverse scan. `agent.ctx` remains the registration and lifecycle owner and exposes no reverse Agent property.
+Runtime interfaces carry Agent identity at the point that owns it. `AgentSetup` receives `(agentCtx, agent)`; Agent creation and resume options carry `parentAgent` for a runtime child; scoped events carry their Agent in the payload; Remote forwarding verifies that `request.agent` is the carrier key; and Host Typert Context resolution maps wire identity to a live Agent Context without a reverse scan. `agent.ctx` remains the registration and lifecycle owner and exposes no reverse Agent property.
 
 Scope-aware registries continue to use the opaque scope key only for registration membership. Tool-subagent does not classify that key or resolve an Agent from Context. A direct `AgentSetup` passes the unpublished Session explicitly and installs through the supplied Context before publication. A settings-backed standing preset reserves one Cordis cleanup effect for each matching Agent before sampling policy: the event payload supplies the Agent, its Session supplies the policy target, its Context owns the registrations, and the preset effect joins their removal after reparenting or preset unload.
 
-`SubagentContinuationManager` passes the exact parent to both fresh creation and cold resume. A live continuable child is therefore excluded from `AgentRegistry.roots()` and satisfies `isOwnedBy(child.id, parent)`. Durable `parentSession` metadata does not substitute for this relation: a fork or resumed Session may be a runtime root when no live Agent owns it.
+`SubagentContinuationManager` puts the exact parent in both fresh-creation and cold-resume options. A live continuable child is therefore excluded from `AgentRegistry.roots()` and satisfies `isOwnedBy(child.id, parent)`. Durable `parentSession` metadata does not substitute for this relation: a fork or resumed Session may be a runtime root when no live Agent owns it.
 
 The [Agent registration-scope decision](2026-07-08-agent-scope-contexts.md), its [runtime design](2026-07-12-agent-scope-runtime-design.md), and the [initiator-scope decision](2026-07-15-agent-initiator-scope.md) retain their independent registration, lifecycle, and private-chain rationale. This decision supersedes only the reverse Context association and implicit runtime-owner derivation described there.
 
@@ -32,7 +32,7 @@ Remote-event tests reject a missing or mismatched Agent before forwarding a scop
 
 **Keep `Context.agent`.** A reverse accessor makes registration ownership look like operation identity and requires every Context derivation, adapter, and test double to preserve an association unrelated to Cordis service selection or effect cleanup.
 
-**Infer runtime ownership from the caller Context.** A private manager Context, an Agent Context, and a standing preset Context can all call the same factory. Context ancestry therefore does not state which live Agent owns the result; the creator must pass the owner it already knows.
+**Infer runtime ownership from the caller Context.** A private manager Context, an Agent Context, and a standing preset Context can all call the same factory. Context ancestry therefore does not state which live Agent owns the result; the creator must put the parent it already knows in the request options.
 
 **Classify Agent scope keys.** An opaque scope key states routing membership, not domain identity. Classifying it would make Agent the center of composition and would still couple a plugin's effect owner to the Session whose policy it needs.
 
@@ -42,6 +42,6 @@ Remote-event tests reject a missing or mismatched Agent before forwarding a scop
 
 ## Consequences
 
-Lifecycle, event, service, and transport signatures carry more explicit Agent parameters, but each boundary states the identity it uses and TypeScript checks both sides. Context remains reusable for dependency access and effect ownership without becoming an alternate domain-object locator.
+Lifecycle options, events, service requests, and transport requests carry explicit Agent identities, so each operation states the identity it uses and TypeScript checks both sides. Context remains reusable for dependency access and effect ownership without becoming an alternate domain-object locator.
 
 Continuable children have the same runtime parent relation as one-shot in-process children. Root-only consumers exclude them, parent teardown can reason from one live ownership graph, and durable lineage remains free to describe history rather than process-local lifetime.

+ 4 - 4
.agents/notes/implemented/architecture/2026-08-31-explicit-agent-runtime-identity.zh.md

@@ -14,11 +14,11 @@ Agent 的 Cordis Context 拥有注册及其清理。Agent 身份则为某项操
 
 ## 决策
 
-运行时接口在拥有身份的位置携带 Agent 身份。`AgentSetup` 接收 `(agentCtx, agent)`;Agent 创建与恢复接收显式运行时所属方;作用域事件在 payload 中携带 Agent;Remote 转发校验 `request.agent` 就是 carrier key;Host Typert Context 解析则把协议身份映射到存活 Agent Context,不执行反向扫描。`agent.ctx` 继续拥有注册和生命周期,不暴露反向 Agent 属性。
+运行时接口在拥有身份的位置携带 Agent 身份。`AgentSetup` 接收 `(agentCtx, agent)`;创建与恢复 Agent 的 options 通过 `parentAgent` 标识运行时子级;作用域事件在 payload 中携带 Agent;Remote 转发校验 `request.agent` 就是 carrier key;Host Typert Context 解析则把协议身份映射到存活 Agent Context,不执行反向扫描。`agent.ctx` 继续拥有注册和生命周期,不暴露反向 Agent 属性。
 
 感知作用域的注册表继续仅使用不透明作用域键判断注册成员关系。tool-subagent 不会分类该键,也不会从 Context 解析 Agent。直接 `AgentSetup` 显式传入尚未发布的 Session,并在发布前通过所给 Context 完成安装。由设置控制的常驻 preset 在读取策略前,会为每个匹配 Agent 预留一个 Cordis 清理 effect:事件 payload 提供 Agent,其 Session 提供策略目标,其 Context 拥有注册项,而 preset effect 会在重设父级或 preset 卸载后等待其清理完成。
 
-`SubagentContinuationManager` 会把确切父级同时传给全新创建与冷恢复。因此,存活的可续跑子级不会出现在 `AgentRegistry.roots()` 中,并且满足 `isOwnedBy(child.id, parent)`。持久化 `parentSession` 元数据不能代替这项关系:没有存活 Agent 拥有 fork 或已恢复会话时,它仍可成为 runtime root。
+`SubagentContinuationManager` 会把确切父级放进全新创建与冷恢复的 options。因此,存活的可续跑子级不会出现在 `AgentRegistry.roots()` 中,并且满足 `isOwnedBy(child.id, parent)`。持久化 `parentSession` 元数据不能代替这项关系:没有存活 Agent 拥有 fork 或已恢复会话时,它仍可成为 runtime root。
 
 [Agent 注册作用域决策](2026-07-08-agent-scope-contexts.zh.md)、其[运行时设计](2026-07-12-agent-scope-runtime-design.zh.md)和[发起方作用域决策](2026-07-15-agent-initiator-scope.zh.md)继续拥有各自独立的注册、生命周期及私有调用链理由。本决策只取代其中描述的反向 Context 关联和隐式运行时所属方推导。
 
@@ -32,7 +32,7 @@ Remote 事件测试会在转发作用域 waterfall 前拒绝缺失或不匹配
 
 **保留 `Context.agent`。** 反向 accessor 会让注册所有权看起来等同于操作身份,还要求每个 Context 派生、适配器和测试替身保留一项与 Cordis 服务选择或 effect 清理无关的关联。
 
-**从调用方 Context 推断运行时归属。** 私有管理器 Context、Agent Context 和常驻 preset Context 都能调用同一个工厂。因此,Context 祖先关系无法说明由哪个存活 Agent 拥有结果;创建方必须传入它已经知道的所属方
+**从调用方 Context 推断运行时归属。** 私有管理器 Context、Agent Context 和常驻 preset Context 都能调用同一个工厂。因此,Context 祖先关系无法说明由哪个存活 Agent 拥有结果;创建方必须把它已知的父级放进请求 options
 
 **分类 Agent 作用域键。** 不透明作用域键表达路由成员关系,而不是领域身份。分类该键会让 Agent 成为组合中心,也仍会把插件的 effect 所有者与策略所需的 Session 耦合起来。
 
@@ -42,6 +42,6 @@ Remote 事件测试会在转发作用域 waterfall 前拒绝缺失或不匹配
 
 ## 后果
 
-生命周期、事件、服务和传输签名会携带更多显式 Agent 参数,但每个边界都会声明自身使用的身份,TypeScript 也会检查两侧。Context 可以继续复用于依赖访问与 effect 所有权,而不会成为另一种领域对象定位器。
+生命周期 options、事件、服务请求和传输请求会携带显式 Agent 身份,因此每项操作都会声明自身使用的身份,TypeScript 也会检查两侧。Context 可以继续复用于依赖访问与 effect 所有权,而不会成为另一种领域对象定位器。
 
 可续跑子级与一次性进程内子级使用同一种运行时父级关系。仅限根级的消费方会排除这些子级,父级 teardown 可以依据唯一的存活归属图推理,而持久化谱系仍可描述历史,不必承担进程内生命周期语义。

+ 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: 67831787fd2223b9508111805266853526be4d19
-core.zh.md: c46d49d40a5b1ce8d87d843697ed4ed969c8d6e5
+core.md: c88d5dad32a4425c078867bfbb922796d5141b6a
+core.zh.md: da72ca3f73adb9ee5404ef4f0188401d82236f59

+ 13 - 17
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, the `isSeeded` marker, origin classification, delegation depth, and `agentPreset`), the exact fork cut in sibling field `inheritedEventCount`, an optional `seed` replay prefix, 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.
+`CreateAgentOptions` carries the shared identity and everything a fresh agent needs before publication: an optional live `parentAgent`, session metadata (`meta` — validated `cwd`, fork lineage, the `isSeeded` marker, origin classification, delegation depth, and `agentPreset`), the exact fork cut in sibling field `inheritedEventCount`, an optional `seed` replay prefix, per-agent `AgentOptions`, a creation-only cancellation `signal`, and `setup`. `ResumeAgentOptions` is the persisted-identity counterpart: `resumeSessionId`, `parentAgent`, `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. 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.
+`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 sets `options.parentAgent`; the registry passes the options and caller Context to the factory without deriving one from the other. The exact `create`/`resume` signatures and rollback contracts are in the [generated section](#ctxagents--agentregistry) below.
 
 ## The agent handle
 
@@ -368,20 +368,18 @@ async create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader
 /**
  * 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.
+ * @param options - identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.
  * @returns the published handle.
  */
-async createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
+async createAgent(ownerCtx: Context, options: CreateAgentOptions): 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.
+ * @param options - persisted identity, optional live parent, loop options, setup, and cancellation.
  * @returns the published handle.
  */
-async resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
+async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
 ```
 
 Types: [SessionHeader](persistence.md)
@@ -709,21 +707,19 @@ setFactory(factory: AgentFactory): () => void
  * agent): this constructs the agent and its session. Rejects if no factory is
  * 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.
+ * @param options - shared identity, optional live parent, session seed/metadata, and agent options.
  * @returns the handle after setup, rollback-covered publication, and loop start complete.
  */
-async create(options: CreateAgentOptions, owner?: Agent): Promise<AgentHandle>
+async create(options: CreateAgentOptions): 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.
+ * @param options - persisted identity, optional live parent, configuration, and setup.
  * @returns the handle after setup, rollback-covered publication, and loop start complete.
  */
-async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>
+async resume(options: ResumeAgentOptions): Promise<AgentHandle>
 
 /**
  * Register a live agent. Throws if an agent with the same id is already
@@ -732,9 +728,9 @@ async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>
  * (`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.
+ * requires passing the carrier). The entry is a runtime root; factory-backed
+ * creation uses `options.parentAgent` for child ownership. 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
@@ -744,7 +740,7 @@ async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>
  *   owner unload, unregistering the agent (and emitting `agent/disposed`)
  *   while its final turn is still draining.
  */
-register(agent: Agent, owner?: Agent): () => void
+register(agent: Agent): () => void
 
 /**
  * Insert an already-constructed agent without announcing it. This is the

+ 13 - 17
docs/subsystems/core.zh.md

@@ -48,9 +48,9 @@ interface AgentHandle {
 }
 ```
 
-`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:会话元数据(`meta`——已校验的 `cwd`、fork 谱系、`isSeeded` 标记、来源分类、委派深度与 `agentPreset`)、同级字段 `inheritedEventCount` 所表示的精确 fork cut、可选的 `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 均不发布。
+`CreateAgentOptions` 携带共享标识以及新 agent 发布前所需的一切:可选的存活 `parentAgent`、会话元数据(`meta`——已校验的 `cwd`、fork 谱系、`isSeeded` 标记、来源分类、委派深度与 `agentPreset`)、同级字段 `inheritedEventCount` 所表示的精确 fork cut、可选的 `seed` 回放前缀、按 agent 的 `AgentOptions`、仅创建期有效的取消 `signal`,以及 `setup`。`ResumeAgentOptions` 是持久标识的对应项:`resumeSessionId`、`parentAgent`、`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` 时无需依赖具体循环包。运行时子 Agent 的创建方显式传入父 Agent;注册表将该值与调用方 Context 分开传给工厂。确切的 `create`/`resume` 签名及回滚约定见下方[生成区块](#ctxagents--agentregistry)。
+`AgentFactory` 是注册表背后的创建接口:循环经 `ctx.agents.setFactory()` 注册其工厂,因此消费方使用 `ctx.agents` 时无需依赖具体循环包。运行时子 Agent 的创建方设置 `options.parentAgent`;注册表把 options 与调用方 Context 传给工厂,不从其中一项推导另一项。确切的 `create`/`resume` 签名及回滚约定见下方[生成区块](#ctxagents--agentregistry)。
 
 <a id="the-agent-handle"></a>
 
@@ -378,20 +378,18 @@ async create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader
 /**
  * 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.
+ * @param options - identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.
  * @returns the published handle.
  */
-async createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
+async createAgent(ownerCtx: Context, options: CreateAgentOptions): 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.
+ * @param options - persisted identity, optional live parent, loop options, setup, and cancellation.
  * @returns the published handle.
  */
-async resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
+async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
 ```
 
 Types: [SessionHeader](persistence.zh.md)
@@ -719,21 +717,19 @@ setFactory(factory: AgentFactory): () => void
  * agent): this constructs the agent and its session. Rejects if no factory is
  * 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.
+ * @param options - shared identity, optional live parent, session seed/metadata, and agent options.
  * @returns the handle after setup, rollback-covered publication, and loop start complete.
  */
-async create(options: CreateAgentOptions, owner?: Agent): Promise<AgentHandle>
+async create(options: CreateAgentOptions): 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.
+ * @param options - persisted identity, optional live parent, configuration, and setup.
  * @returns the handle after setup, rollback-covered publication, and loop start complete.
  */
-async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>
+async resume(options: ResumeAgentOptions): Promise<AgentHandle>
 
 /**
  * Register a live agent. Throws if an agent with the same id is already
@@ -742,9 +738,9 @@ async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>
  * (`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.
+ * requires passing the carrier). The entry is a runtime root; factory-backed
+ * creation uses `options.parentAgent` for child ownership. 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
@@ -754,7 +750,7 @@ async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>
  *   owner unload, unregistering the agent (and emitting `agent/disposed`)
  *   while its final turn is still draining.
  */
-register(agent: Agent, owner?: Agent): () => void
+register(agent: Agent): () => void
 
 /**
  * Insert an already-constructed agent without announcing it. This is the

+ 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: bf5136159782c9e880871565bf7bce5730c4b575
-README.zh.md: 66dfa2fbc66ae58c2984218561fc294697b3230d
+README.md: d646f5ad8a3d12feb4942026a288cbc89d2995c2
+README.zh.md: 6ad383671fce1aab60d40c6d49116cc7f7931ed4

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

@@ -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 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, closes the session's write path, unwinds the scope, detaches the agent, then detaches 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 sets `options.parentAgent`; 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, closes the session's write path, unwinds the scope, detaches the agent, then detaches the session, and every detach is bound to the exact entered object so a stale disposer cannot remove a later same-id replacement.
 
 ### Persistence integration
 

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

@@ -103,7 +103,7 @@ const handle = await ctx.agents.create({
 
 ### 创建与拆除
 
-创建是同一个受回滚保护的事务:构造私有会话、具象 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 替代项。
+创建是同一个受回滚保护的事务:构造私有会话、具象 agent 与带作用域上下文;等待分别传入上下文与 Agent 的可选 setup;进入两个注册表;依次宣告 `session/created` 与 `agent/created`;发出 `agent/session-start`;此后才启动驱动器。创建运行时子 Agent 的调用方设置 `options.parentAgent`;调用方 Context 则单独拥有事务和存活 handle。Setup 抛出、commit 失败或所有者 dispose 都会回滚事务而不发布任一 id。Teardown 顺序是停止并排空、关闭会话的写路径、撤销作用域、detach agent、再 detach 会话,且每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。
 
 ### 持久化集成
 

+ 14 - 17
packages/core/agent-loop/src/index.ts

@@ -445,7 +445,7 @@ export class AgentLoop extends Service implements AgentFactory {
           void this.resumeWith(ctx, childCtx.sessionPersistence, {
             resumeSessionId,
             agentOptions: options,
-          }, undefined).catch((error: unknown) => {
+          }).catch((error: unknown) => {
             this.reportConfiguredStartupFailure(id, 'resume', resumeSessionId, error)
           })
         })
@@ -487,7 +487,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 }, undefined)
+      await this.resumeWith(ownerCtx, persistence, { resumeSessionId: sessionId, agentOptions })
       return
     } catch (error: unknown) {
       if (!this.ownership.isActive()) return
@@ -529,12 +529,12 @@ export class AgentLoop extends Service implements AgentFactory {
    */
   private prepare(
     ownerCtx: Context,
-    owner: Agent | undefined,
     id: SessionId,
     options: AgentOptions,
     session: Session,
     callerSignal?: AbortSignal,
     handle?: SessionHandle,
+    parentAgent?: Agent,
   ): PreparedAgent {
     assertAgentOptions(options)
     ownerCtx.fiber.assertActive()
@@ -655,7 +655,7 @@ export class AgentLoop extends Service implements AgentFactory {
           detachSession = agent.ctx.sessions.enter(session)
           // The mounted backend routes announced live events into the active
           // write handle by session id; the loop only owns the handle itself.
-          detachAgent = loopCtx.agents.enter(agent, owner)
+          detachAgent = loopCtx.agents.enter(agent, parentAgent)
           agent.ctx.sessions.announce(session)
           assertLive()
           loopCtx.agents.announce(agent)
@@ -692,7 +692,7 @@ export class AgentLoop extends Service implements AgentFactory {
     const stored = await this.createStoredSession(preparation.session)
     let prepared: PreparedAgent
     try {
-      prepared = this.prepare(this.ctx, undefined, id, options, preparation.session, undefined, stored?.handle)
+      prepared = this.prepare(this.ctx, id, options, preparation.session, undefined, stored?.handle)
     } catch (error: unknown) {
       await stored?.handle.close().catch(() => {})
       throw error
@@ -749,11 +749,10 @@ 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.
+   * @param options - identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.
    * @returns the published handle.
    */
-  async createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle> {
+  async createAgent(ownerCtx: Context, options: CreateAgentOptions): 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 },
@@ -778,7 +777,6 @@ export class AgentLoop extends Service implements AgentFactory {
       }
       return this.setupAndPublish(
         ownerCtx,
-        owner,
         options.sessionId,
         preparation,
         options.agentOptions ?? {},
@@ -786,6 +784,7 @@ export class AgentLoop extends Service implements AgentFactory {
         options.signal,
         'startup',
         stored,
+        options.parentAgent,
       )
     })()
     this.ownership.trackWrapper(published)
@@ -795,7 +794,6 @@ 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,
@@ -803,12 +801,13 @@ export class AgentLoop extends Service implements AgentFactory {
     signal: AbortSignal | undefined,
     source: SessionStartSource,
     stored?: StoredSession,
+    parentAgent?: Agent,
   ): Promise<AgentHandle> {
     using ownedPreparation = preparation
     const session = ownedPreparation.session
     let prepared: PreparedAgent
     try {
-      prepared = this.prepare(ownerCtx, owner, id, agentOptions, session, signal, stored?.handle)
+      prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, stored?.handle, parentAgent)
     } catch (error: unknown) {
       await stored?.handle.close().catch(() => {})
       throw error
@@ -829,16 +828,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.
+   * @param options - persisted identity, optional live parent, loop options, setup, and cancellation.
    * @returns the published handle.
    */
-  async resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle> {
+  async resume(ownerCtx: Context, options: ResumeAgentOptions): 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, owner)
+    return this.resumeWith(ownerCtx, persistence, options)
   }
 
   /** Resume through an explicit persistence handle used by the deferred config path. */
@@ -846,7 +844,6 @@ 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 () => {
@@ -900,7 +897,6 @@ export class AgentLoop extends Service implements AgentFactory {
         handle = undefined // ownership passes to setupAndPublish/prepare
         return await this.setupAndPublish(
           ownerCtx,
-          owner,
           id,
           preparation,
           options.agentOptions ?? {},
@@ -908,6 +904,7 @@ export class AgentLoop extends Service implements AgentFactory {
           options.signal,
           'resume',
           owned,
+          options.parentAgent,
         )
       } finally {
         preparation?.[Symbol.dispose]()

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

@@ -238,6 +238,7 @@ describe('AgentLoop initiator scope', () => {
         const handle = await exec.agent.ctx.agents.create({
           sessionId: SessionId('child-session'),
           agentOptions: { provider: 'mock', model: 'mock' },
+          parentAgent: exec.agent,
           setup: (agentCtx, childAgent) => {
             parentDuringSetup = ctx.agents.requireInitiator()
             explicitChild = childAgent
@@ -252,7 +253,7 @@ describe('AgentLoop initiator scope', () => {
               },
             }))
           },
-        }, exec.agent)
+        })
         child = handle.agent
         parentWhileChildDriverActive = ctx.agents.requireInitiator()
         send(handle.agent, 'run child')

+ 3 - 2
packages/core/agent-loop/tests/scope-lifecycle.spec.ts

@@ -158,7 +158,8 @@ describe('agent scope lifecycle', () => {
     const child = await root.agent.ctx.agents.create({
       sessionId: SessionId('runtime-child'),
       agentOptions: { model: 'mock' },
-    }, root.agent)
+      parentAgent: root.agent,
+    })
 
     expect(ctx.agents.list()).toEqual([root.agent, child.agent])
     expect(ctx.agents.roots()).toEqual([root.agent])
@@ -586,7 +587,7 @@ describe('agent scope lifecycle', () => {
 
     await expect(loop.createAgent(ctx, {
       sessionId: SessionId('factory-inactive-s'),
-    }, undefined)).rejects.toThrow(/agent loop is not active|inactive context/)
+    })).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: de8db86c4083f0fc705ae09265b909e663fd37ff
-README.zh.md: 21379a0184c60f0f9f59b2f9557bd5d7bf539c30
+README.md: 08855877d9e37e1c8fd8124d981047be7147a5d3
+README.zh.md: 80d02bafdfe199a4367e93f4c4b32b5a7d63413a

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

@@ -29,7 +29,7 @@ Mount `dsh-agent` wherever live agents exist: it provides `ctx.agents` and the `
 
 ### Create or resume an agent
 
-`ctx.agents.create()` builds a fresh agent and session under one identity; `ctx.agents.resume()` loads a persisted session and rebuilds the agent on it. Both delegate to the registered factory and return an `AgentHandle` — the only object that can tear that agent down. `get(id)`, `list()`, and `roots()` find live agents, and `isOwnedBy(id, owner)` tells whether one agent was created through another's scoped context.
+`ctx.agents.create()` builds a fresh agent and session under one identity; `ctx.agents.resume()` loads a persisted session and rebuilds the agent on it. Both delegate to the registered factory and return an `AgentHandle` — the only object that can tear that agent down. Set `parentAgent` in either operation's options to make the result a runtime child; omit it for a runtime root. `get(id)`, `list()`, and `roots()` find live agents, and `isOwnedBy(id, parent)` tests that exact live relation.
 
 ```text
 const handle = await ctx.agents.create({

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

@@ -29,7 +29,7 @@ kind: "package-reference"
 
 ### 创建或恢复 agent
 
-`ctx.agents.create()` 在一个身份下构建全新 agent 与会话;`ctx.agents.resume()` 加载持久化会话并在此基础上重建 agent。两者都委托给已注册工厂,并返回 `AgentHandle`——唯一能拆除该 agent 的对象。`get(id)`、`list()` 与 `roots()` 用于查找实时 agent;`isOwnedBy(id, owner)` 用于判断一个 agent 是否通过另一个 agent 的作用域上下文创建
+`ctx.agents.create()` 在一个身份下构建全新 agent 与会话;`ctx.agents.resume()` 加载持久化会话并在此基础上重建 agent。两者都委托给已注册工厂,并返回 `AgentHandle`——唯一能拆除该 agent 的对象。在任一操作的 options 中设置 `parentAgent`,可使结果成为运行时子级;省略它则得到运行时根级。`get(id)`、`list()` 与 `roots()` 用于查找实时 agent;`isOwnedBy(id, parent)` 用于检验这项确切的存活关系
 
 ```text
 const handle = await ctx.agents.create({

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

@@ -63,6 +63,8 @@ export type AgentSetup = (
 export interface CreateAgentOptions {
   /** The live agent/session identity. */
   readonly sessionId: SessionId
+  /** Live parent Agent for runtime ownership; omit for a root Agent. */
+  readonly parentAgent?: Agent
   /**
    * Session creation metadata: validated absolute `cwd`, `parentSession`
    * fork lineage, the `isSeeded` fork marker, the coarse `origin`
@@ -124,6 +126,8 @@ export interface CreateAgentOptions {
 export interface ResumeAgentOptions {
   /** The persisted session id to load and use as the live agent/session identity. */
   readonly resumeSessionId: SessionId
+  /** Live parent Agent for runtime ownership; omit for a root Agent. */
+  readonly parentAgent?: Agent
   /** Per-agent options (model, …). */
   readonly agentOptions?: AgentOptions
   /** Optional creation-only cancellation signal for persistence load/setup; detached before return. */
@@ -181,11 +185,10 @@ export interface AgentFactory {
    * transaction and resulting lifecycle to that owner; it must not infer
    * 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.
+   * @param options - agent/session identity, configuration, optional live parent, and setup.
    * @returns the owned handle after setup, both announcements, and loop start complete.
    */
-  createAgent(ownerCtx: Context, options: CreateAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
+  createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>
   /**
    * Resume an agent on a persisted session. Async because it opens the
    * persisted session for write, reads and repairs the log, publishes it, and
@@ -194,11 +197,10 @@ export interface AgentFactory {
    * Publication follows the same setup-commit and 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.
+   * @param options - persisted identity, configuration, optional live parent, and setup.
    * @returns the owned handle after setup, both announcements, and loop start complete.
    */
-  resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle>
+  resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
 }
 
 /** Thrown when create/resume is called before an agent factory is registered. */
@@ -381,11 +383,10 @@ export class AgentRegistry extends Service {
    * agent): this constructs the agent and its session. Rejects if no factory is
    * 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.
+   * @param options - shared identity, optional live parent, session seed/metadata, and agent options.
    * @returns the handle after setup, rollback-covered publication, and loop start complete.
    */
-  async create(options: CreateAgentOptions, owner?: Agent): Promise<AgentHandle> {
+  async create(options: CreateAgentOptions): 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
@@ -394,23 +395,22 @@ 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, owner])
+    return Reflect.apply(target.createAgent, receiver, [ownerCtx, options])
   }
 
   /**
    * 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.
+   * @param options - persisted identity, optional live parent, configuration, and setup.
    * @returns the handle after setup, rollback-covered publication, and loop start complete.
    */
-  async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle> {
+  async resume(options: ResumeAgentOptions): 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, owner])
+    return Reflect.apply(target.resume, receiver, [ownerCtx, options])
   }
 
   /**
@@ -420,9 +420,9 @@ export class AgentRegistry extends Service {
    * (`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.
+   * requires passing the carrier). The entry is a runtime root; factory-backed
+   * creation uses `options.parentAgent` for child ownership. 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
@@ -432,9 +432,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, owner?: Agent): () => void {
+  register(agent: Agent): () => void {
     const dispose = this.ctx.effect(function* (this: AgentRegistry) {
-      yield this.enter(agent, owner)
+      yield this.enter(agent, undefined)
       this.announce(agent)
     }.bind(this), 'agents.register()')
     // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity

+ 11 - 11
packages/core/agent/tests/agent.spec.ts

@@ -359,16 +359,16 @@ describe('explicit cancellation contract', () => {
 describe('AgentRegistry factory seam', () => {
   function stubFactory() {
     const calls: {
-      create: Array<{ ownerCtx: Context; options: CreateAgentOptions; owner: Agent | undefined }>
-      resume: Array<{ ownerCtx: Context; options: ResumeAgentOptions; owner: Agent | undefined }>
+      create: Array<{ ownerCtx: Context; options: CreateAgentOptions }>
+      resume: Array<{ ownerCtx: Context; options: ResumeAgentOptions }>
     } = { create: [], resume: [] }
     const factory: AgentFactory = {
-      async createAgent(ownerCtx, options, owner) {
-        calls.create.push({ ownerCtx, options, owner })
+      async createAgent(ownerCtx, options) {
+        calls.create.push({ ownerCtx, options })
         return { agent: stubAgent(options.sessionId), dispose: () => Promise.resolve() }
       },
-      async resume(ownerCtx, options, owner) {
-        calls.resume.push({ ownerCtx, options, owner })
+      async resume(ownerCtx, options) {
+        calls.resume.push({ ownerCtx, options })
         return { agent: stubAgent(options.resumeSessionId), dispose: () => Promise.resolve() }
       },
     }
@@ -390,11 +390,11 @@ 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()
+    expect(calls.create[0]?.options.parentAgent).toBeUndefined()
+    expect(calls.resume[0]?.options.parentAgent).toBeUndefined()
   })
 
-  it('passes the explicit runtime owner separately from the caller context', async () => {
+  it('keeps the runtime parent in options separately from the caller context', async () => {
     const ctx = new Context()
     await ctx.plugin(AgentRegistry)
     const { factory, calls } = stubFactory()
@@ -402,9 +402,9 @@ describe('AgentRegistry factory seam', () => {
     const parent = stubAgent('parent')
     const unregister = ctx.agents.register(parent)
 
-    await ctx.agents.create({ sessionId: SessionId('child') }, parent)
+    await ctx.agents.create({ sessionId: SessionId('child'), parentAgent: parent })
 
-    expect(calls.create[0]?.owner).toBe(parent)
+    expect(calls.create[0]?.options.parentAgent).toBe(parent)
     unregister()
   })
 

+ 14 - 14
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, owner: Agent | undefined): Promise<AgentHandle>',
+        signature: 'async createAgent(ownerCtx: Context, options: CreateAgentOptions): 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.' }, { name: 'owner', description: 'live Agent that owns the new Agent at runtime, or undefined for a root owner.' }],
+        parameters: [{ name: 'ownerCtx', description: 'caller context that structurally owns the lifecycle.' }, { name: 'options', description: 'identities, optional live parent, session seed/metadata, loop options, setup, and cancellation.' }],
         returns: 'the published handle.',
       },
       {
-        signature: 'async resume(ownerCtx: Context, options: ResumeAgentOptions, owner: Agent | undefined): Promise<AgentHandle>',
+        signature: 'async resume(ownerCtx: Context, options: ResumeAgentOptions): 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.' }, { name: 'owner', description: 'live Agent that owns the resumed Agent at runtime, or undefined for a root owner.' }],
+        parameters: [{ name: 'ownerCtx', description: 'caller context that owns load, setup, and the live lifecycle.' }, { name: 'options', description: 'persisted identity, optional live parent, loop options, setup, and cancellation.' }],
         returns: 'the published handle.',
       },
     ],
@@ -289,21 +289,21 @@ 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, owner?: Agent): Promise<AgentHandle>',
+        signature: 'async create(options: CreateAgentOptions): 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.' }, { name: 'owner', description: 'explicit live runtime owner, or undefined for a root Agent.' }],
+        parameters: [{ name: 'options', description: 'shared identity, optional live parent, session seed/metadata, and agent options.' }],
         returns: 'the handle after setup, rollback-covered publication, and loop start complete.',
       },
       {
-        signature: 'async resume(options: ResumeAgentOptions, owner?: Agent): Promise<AgentHandle>',
+        signature: 'async resume(options: ResumeAgentOptions): 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.' }, { name: 'owner', description: 'explicit live runtime owner, or undefined for a root Agent.' }],
+        parameters: [{ name: 'options', description: 'persisted identity, optional live parent, configuration, and setup.' }],
         returns: 'the handle after setup, rollback-covered publication, and loop start complete.',
       },
       {
-        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.' }, { name: 'owner', description: 'explicit live runtime owner, or undefined for a root Agent.' }],
+        signature: 'register(agent: 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). The entry is a runtime root; factory-backed creation uses `options.parentAgent` for child ownership. Returns the disposer.',
+        parameters: [{ name: 'agent', description: 'the already-constructed agent to record in the store.' }],
         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.',
       },
       {
@@ -3370,7 +3370,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'AgentFactory',
-    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}',
+    declaration: 'export interface AgentFactory {\n    createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>;\n    resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>;\n}',
   },
   {
     name: 'AgentHandle',
@@ -3778,7 +3778,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'CreateAgentOptions',
-    declaration: 'export interface CreateAgentOptions {\n    readonly sessionId: SessionId;\n    readonly meta?: {\n        readonly cwd?: string;\n        readonly parentSession?: SessionId;\n        readonly isSeeded?: boolean;\n        readonly origin?: \'subagent\';\n        readonly delegationDepth?: number;\n        readonly agentPreset?: string;\n    };\n    readonly inheritedEventCount?: SessionLogOffset;\n    readonly seed?: readonly SessionEvent[];\n    readonly agentOptions?: AgentOptions;\n    readonly signal?: AbortSignal;\n    readonly setup?: AgentSetup;\n}',
+    declaration: 'export interface CreateAgentOptions {\n    readonly sessionId: SessionId;\n    readonly parentAgent?: Agent;\n    readonly meta?: {\n        readonly cwd?: string;\n        readonly parentSession?: SessionId;\n        readonly isSeeded?: boolean;\n        readonly origin?: \'subagent\';\n        readonly delegationDepth?: number;\n        readonly agentPreset?: string;\n    };\n    readonly inheritedEventCount?: SessionLogOffset;\n    readonly seed?: readonly SessionEvent[];\n    readonly agentOptions?: AgentOptions;\n    readonly signal?: AbortSignal;\n    readonly setup?: AgentSetup;\n}',
   },
   {
     name: 'CreateGoalRequest',
@@ -4670,7 +4670,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'ResumeAgentOptions',
-    declaration: 'export interface ResumeAgentOptions {\n    readonly resumeSessionId: SessionId;\n    readonly agentOptions?: AgentOptions;\n    readonly signal?: AbortSignal;\n    readonly setup?: AgentSetup;\n}',
+    declaration: 'export interface ResumeAgentOptions {\n    readonly resumeSessionId: SessionId;\n    readonly parentAgent?: Agent;\n    readonly agentOptions?: AgentOptions;\n    readonly signal?: AbortSignal;\n    readonly setup?: AgentSetup;\n}',
   },
   {
     name: 'RunnerFailureRule',

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

@@ -122,7 +122,10 @@ 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') }, root.agent)
+    const child = await root.agent.ctx.agents.create({
+      sessionId: SessionId('schedule-child'),
+      parentAgent: root.agent,
+    })
     expect(ctx.agents.roots()).toEqual([existing.agent, root.agent])
     expect(ctx.tools.get('schedule_create', child.agent)).toBeUndefined()
 

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

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

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

@@ -452,7 +452,8 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('parentless-child-session'),
         meta: { cwd: storageDir },
         agentOptions: { model: 'deepseek-official' },
-      }, parentHandle.agent)
+        parentAgent: parentHandle.agent,
+      })
       await settleSubagent(ctx, parentHandle.agent, {
         provider: 'spawn',
         id: SessionId('child-session'),
@@ -514,7 +515,8 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('remote-run-id'),
         meta: { cwd: storageDir, parentSession: SessionId('collision-parent') },
         agentOptions: { model: 'deepseek-official' },
-      }, parentHandle.agent)
+        parentAgent: parentHandle.agent,
+      })
 
       await settleSubagent(ctx, parentHandle.agent, {
         provider: 'remote',
@@ -553,7 +555,8 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('continuation-child'),
         meta: { cwd: storageDir, parentSession: SessionId('continuation-parent') },
         agentOptions: { model: 'deepseek-official' },
-      }, parentHandle.agent)
+        parentAgent: parentHandle.agent,
+      })
 
       await settleSubagent(ctx, parentHandle.agent, {
         provider: 'continuation',
@@ -598,7 +601,8 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('reused-child'),
         meta: { cwd: storageDir, parentSession: SessionId('old-parent') },
         agentOptions: { model: 'deepseek-official' },
-      }, oldParent.agent)
+        parentAgent: oldParent.agent,
+      })
       const first = Promise.withResolvers<SubagentResult>()
       const sameLifetime = Promise.withResolvers<SubagentResult>()
       const replacement = Promise.withResolvers<SubagentResult>()
@@ -639,7 +643,8 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('reused-child'),
         meta: { cwd: storageDir, parentSession: SessionId('new-parent') },
         agentOptions: { model: 'deepseek-official' },
-      }, newParent.agent)
+        parentAgent: newParent.agent,
+      })
       currentLocalAgent = newChild.agent
       const secondRun = await ctx.subagents.start('reused', {
         parent: newParent.agent,
@@ -697,7 +702,8 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('provider-reuse-child'),
         meta: { cwd: storageDir, parentSession: SessionId('provider-reuse-parent') },
         agentOptions: { model: 'deepseek-official' },
-      }, parent.agent)
+        parentAgent: parent.agent,
+      })
       const localResult = Promise.withResolvers<SubagentResult>()
       const remoteResult = Promise.withResolvers<SubagentResult>()
       const unregisterLocal = ctx.subagents.registerProvider({
@@ -790,13 +796,15 @@ describe('HarnessSdkJsonRpcServer', () => {
         sessionId: SessionId('fallback-child-session'),
         meta: { cwd: storageDir, parentSession: SessionId('fallback-parent') },
         agentOptions: { provider: 'deepseek-official', model: 'deepseek-official' },
-      }, parentHandle.agent)
+        parentAgent: 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)
+        parentAgent: parentHandle.agent,
+      })
       const missedStartResult = Promise.withResolvers<SubagentResult>()
       const disposeMissedStartProvider = ctx.subagents.registerProvider({
         name: 'fork',

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

@@ -133,13 +133,14 @@ export async function startInProcessRun(
 
   const handle = await parent.ctx.agents.create({
     sessionId: childId,
+    parentAgent: parent,
     meta: childSessionMeta(parent, childDepth, seed !== undefined),
     ...seed !== undefined ? { seed } : {},
     ...seed === undefined ? {} : { inheritedEventCount: activationBoundary },
     agentOptions: resolveChildAgentOptions(parent, request.agentOptions, childDepth),
     signal: request.signal,
     setup,
-  }, parent)
+  })
   return drivePublishedRun(
     handle,
     request.signal,

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

@@ -1202,19 +1202,21 @@ export class SubagentContinuationManager {
     const handle: AgentHandle = create === undefined
       ? await this.ownerCtx.agents.resume({
         resumeSessionId: childId,
+        parentAgent: parent,
         agentOptions: inputs.agentOptions,
         signal: inputs.signal,
         setup,
-      }, parent)
+      })
       : await this.ownerCtx.agents.create({
         sessionId: childId,
+        parentAgent: parent,
         meta: create.meta,
         seed: create.seed,
         inheritedEventCount: create.inheritedEventCount,
         agentOptions: inputs.agentOptions,
         signal: inputs.signal,
         setup,
-      }, parent)
+      })
 
     const activation: Activation = {
       childId,