Przeglądaj źródła

Merge PR #340 into codex/website-api-jsdoc

Tianyi Cui 2 miesięcy temu
rodzic
commit
2397059fe3

+ 5 - 1
docs/architecture.md

@@ -16,7 +16,7 @@ A harness is one [Cordis](cordis-primer.md) context. Packages contribute service
 | `ctx.sessions` | `dsh-session` | in-memory event-sourced sessions |
 | `ctx.systemPrompt` | `dsh-system-prompt` | ordered prompt sections, tool schemas, and prompt variables |
 | `ctx.tools` | `dsh-tools` | tool registry and [execution pipeline](tool-execution-pipeline.md) |
-| `ctx.agents` | `dsh-agent` | live agent registry, public `Agent` handle, `agent/*` events |
+| `ctx.agents` | `dsh-agent` | live agents, creation delegation, `agent/*` events, and process-local initiating Agent scope |
 | `ctx.agentLoop` | `dsh-agent-loop` | concrete `Agent` driver |
 
 ### Capability Services
@@ -118,6 +118,10 @@ Every session event is turn-enclosed. Reloading preserves an interrupted tail an
 
 Every live agent owns a scoped `agent.ctx`. Its registrations shadow globals, receive only that agent's dispatches, and unwind with it; async effects such as background-task cleanup are awaited. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. Typed resolvers derive carrier checks from merged `Events` signatures and `scopeTarget` ([semantic-gates RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md)). See the [agent-scope RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md) and [subagent composition controls](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md).
 
+### Initiating Agent Scope
+
+`AgentLoop` runs each process-local driver inside `ctx.agents.withInitiator()`; the [decision](rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns boundary and explicit-identity rules.
+
 ## State
 
 ### Session Log

+ 2 - 2
docs/capability-seams.md

@@ -49,7 +49,7 @@ flowchart LR
   pkg_skill["skill"]
   svc_skills["ctx.skills<br/>Skill provider registry"]
   pkg_skill_local["skill-local"]
-  svc_agents["ctx.agents<br/>Agent registry"]
+  svc_agents["ctx.agents<br/>Agent service"]
   svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
   pkg_agent_spine_demo["agent-spine-demo"]
   pkg_bash["bash"]
@@ -215,7 +215,7 @@ flowchart LR
 | `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web), [`acp`](../packages/ui/acp) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. |
 | `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`stdio-demo`](../packages/examples/stdio-demo), [`acp`](../packages/ui/acp) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`stdio-demo`](../packages/examples/stdio-demo), [`acp`](../packages/ui/acp) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
 | `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
-| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-demo`](../packages/examples/stdio-demo), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles and the create/resume factory seam. |
+| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-demo`](../packages/examples/stdio-demo), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
 | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
 | `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them. |
 | `ctx.bashEnv` | `core` | [`tool-bash`](../packages/bash/tool-bash) | - | - | - | Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace. |

+ 39 - 2
docs/cordis-catalog/services.md

@@ -48,9 +48,46 @@ Source: [`packages/core/agent-loop/src/index.ts:407`](../../packages/core/agent-
 
 ## `ctx.agents` — `AgentRegistry`
 
-Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator plugins can find them without depending on the concrete loop package. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.
+Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.
 
 ```ts cordis-catalog
+/**
+ * Read the Agent that initiated the inherited asynchronous driver chain.
+ * @returns the inherited Agent, or `undefined` outside a driver and inside an explicit clearing boundary.
+ * @throws when this service instance has been disposed.
+ */
+currentInitiator(): Agent | undefined
+
+/**
+ * Read the initiating Agent and fail when no driver boundary is active.
+ * @returns the inherited Agent.
+ * @throws when no initiator is active or this service instance has been disposed.
+ */
+requireInitiator(): Agent
+
+/**
+ * Run an operation with one exact Agent as its process-local initiator. The
+ * exact synchronous value or Promise returned by the operation is preserved.
+ * If its inherited async chain starts an owning-fiber unload, the nested
+ * boundary lineage is excluded from the drain so teardown cannot wait on itself.
+ * @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
+ * @param operation - synchronous or asynchronous operation to invoke.
+ * @returns the exact value returned by `operation`.
+ * @throws when the initiator scope is closing/disposed, or when `operation` throws.
+ */
+withInitiator<T>(agent: Agent, operation: () => T): T
+
+/**
+ * Run an operation inside a boundary that hides any inherited initiating
+ * Agent. The exact synchronous value or Promise is preserved.
+ * If its inherited async chain starts an owning-fiber unload, the nested
+ * boundary lineage is excluded from the drain so teardown cannot wait on itself.
+ * @param operation - synchronous or asynchronous operation to invoke without an initiator.
+ * @returns the exact value returned by `operation`.
+ * @throws when the initiator scope is closing/disposed, or when `operation` throws.
+ */
+withoutInitiator<T>(operation: () => T): T
+
 /**
  * Register the agent-creation factory (the loop calls this on construction,
  * effect-scoped). A traced Cordis service is canonicalized to its concrete
@@ -165,7 +202,7 @@ roots(): Agent[]
 
 Types: [Agent](../core-data-structures/core.md)
 
-Source: [`packages/core/agent/src/index.ts:201`](../../packages/core/agent/src/index.ts)
+Source: [`packages/core/agent/src/index.ts:211`](../../packages/core/agent/src/index.ts)
 
 ## `ctx.approval` — `ApprovalService`
 

+ 4 - 0
docs/core-data-structures/core.md

@@ -403,6 +403,10 @@ interface Agent {
 
 The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits.
 
+## Initiating Agent
+
+The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns its lifetime and boundary rules.
+
 ## Interception decisions
 
 Each `agent/*` interception waterfall returns a small, seam-specific typed union — the unified Decision idiom (the tool seams' `PreToolDecision`/`PostToolDecision` in [tools.md](tools.md) follow the same shape). A CC/Codex hook bridge maps its `permissionDecision`/`decision`/`continue`/`additionalContext` fields onto these; a native plugin returns them directly. Prompt and post-tool decisions share one model-facing context shape, `HookContext`, which is `inject()`ed as a `context/message` and therefore carries a REQUIRED `source` (a missing source would default to `{kind:'user'}` and mislabel plugin context as a user prompt). Its optional `envelope` selects the canonical context tag or caller-owned raw framing, while JSON `meta` persists plugin state without exposing it to the model. Both decisions carry `additionalContexts[]` so every entry preserves its own provenance, framing, and metadata. Continuation reasons are steering messages instead and deliberately use the narrower content/source shape.

+ 1 - 0
docs/event-producer-consumer.md

@@ -53,5 +53,6 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | Event string | Dispatchers | Listeners |
 | --- | --- | --- |
 | `internal/dispatch` | - | [`invariants`](../packages/support/invariants) |
+| `internal/status` | - | [`agent`](../packages/core/agent) |
 
 Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program.

+ 1 - 0
docs/rfc/INDEX.md

@@ -163,6 +163,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand;
 | [Single-file executable SDK runtime distribution (single-exe)](implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) | 2026-07-10 |
 | [Agent-scope runtime design and correctness](implemented/architecture/2026-07-12-agent-scope-runtime-design.md) | 2026-07-12 |
 | [Provider-routed LLM adapters and a generic pi-ai backend](implemented/architecture/2026-07-14-provider-routed-llm-adapters.md) | 2026-07-14 |
+| [Initiating Agent scope over AsyncLocalStorage](implemented/architecture/2026-07-15-agent-initiator-scope.md) | 2026-07-15 |
 | [Advisory LLM catalogs and per-session ACP model selection](implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md) | 2026-07-15 |
 | [Replay token meter service](implemented/architecture/2026-07-15-replay-token-meter-service.md) | 2026-07-15 |
 

+ 6 - 0
docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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
+2026-07-15-agent-initiator-scope.md: b3c9be0be1dea29568dfcdeb0578e643734486e8
+2026-07-15-agent-initiator-scope.zh.md: 55494977b8ade0d380fa21b25171bce65a46a9fb

+ 63 - 0
docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md

@@ -0,0 +1,63 @@
+# RFC: Initiating Agent scope over AsyncLocalStorage
+
+Status: implemented
+
+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.
+
+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.
+
+## Decision
+
+The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the initiating Agent. It stores the exact `Agent` directly rather than introducing a one-field frame; a separate private run token records nested boundary lineage only for teardown bookkeeping and carries no identity. The [core-data catalog](../../../core-data-structures/core.md#initiating-agent) identifies the carried type.
+
+`currentInitiator()` reads optionally, `requireInitiator()` throws `no initiating agent is active`, and `withInitiator(agent, operation)` preserves the operation's exact synchronous value or Promise. `withoutInitiator(operation)` establishes a clearing boundary for work that must not inherit an Agent. Session remains derived as `agent.session`; turn, step, tool call, `signal`, model, `cwd`, sandbox, and authorization stay with their existing owners.
+
+`AgentLoop` already injects `ctx.agents` and wraps each concrete driver's complete `runLoop` lifetime in `agents.withInitiator(agent, ...)`. Concurrent drivers therefore 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.
+
+Ambient identity does not replace explicit contracts. `ToolExecution.agent`, `AssembleContext.agent`, `GenerateOptions.sessionId`, task 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.
+
+`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.
+
+Initiator scope does not own detached work: registry drain tracks only the Promise returned by `withInitiator()` or `withoutInitiator()`. Asynchronous resources created inside a boundary inherit its store until they settle or ALS is disabled, so their owning seam must stop unreturned work explicitly. Agent-owned foreground work returns its lifetime and keeps its cancellation contract. Unrelated timers, queues, and deployment infrastructure start under `withoutInitiator(operation)`; queue, worker, process, and wire boundaries serialize identity rather than expecting ALS propagation.
+
+A host-aware transport may derive a deployment-owned header such as `X-Harness-Session-Id` from `ctx.agents.requireInitiator().session.id`; the header is absent from model-visible schema and arguments. No production MCP or Web transport adopts such a header in this decision. A test-double transport proves the trusted boundary without assigning host routing policy to an existing provider-neutral seam.
+
+This decision extends the [Agent registration-scope contract](2026-07-08-agent-scope-contexts.md) and its [runtime design](2026-07-12-agent-scope-runtime-design.md); it does not change their static `agent.ctx` meaning.
+
+## Verification
+
+Agent service tests pin optional and required reads, exact synchronous and cross-realm Promise identity, intrinsic Promise settlement observation, overlapping, nested, and cleared boundaries, restoration after throws or rejection, ordinary and reentrant drain ordering, and retained-reference errors. AgentLoop integration pins concurrent and nested drivers, agentless calls, AgentRegistry restart, and root teardown. Composition, module-graph, build, and runtime-closure checks keep `ctx.agents` wired through the default bundle, SDK spine, Python runtime closure, and direct AgentLoop harnesses without another provider.
+
+Only a test-double host-aware transport consumes ambient identity; it derives `X-Harness-Session-Id` internally and verifies that tool schema and logged arguments contain no identity field. The service deliberately does not drain async work omitted from the Promise returned by the boundary operation; that work remains subject to its owner's explicit stop contract.
+
+## Alternatives considered
+
+**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.
+
+**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.
+
+**Store a named or complete runtime frame.** A one-field `{ agent }` frame only wraps the value, while Agent, Session, inbox, cancellation, turn, step, tool execution, and persistence already have authoritative owners. Adding more fields would create stale snapshots and another lifecycle; carrying `Agent` directly keeps the boundary named by its methods without duplicating state.
+
+**Include a step `AbortSignal`, `cwd`, sandbox, or authorization.** Their lifetimes and authority do not match the driver boundary, and their existing seams already pass them explicitly. Adding a control capability requires a separate decision and nested lifecycle contract.
+
+**Use a process-global `currentAgent`.** Concurrent Agents and subagents overwrite one another across awaited continuations, so a mutable global is correct only under a serialization guarantee the harness does not make.
+
+**Derive identity from model-visible arguments.** Model or user input cannot be trusted to select Session, tenant, or sandbox routing.
+
+**Add routing identity to every capability seam.** That spreads hosting concerns through provider-neutral APIs. A host-aware implementation owns its transport header while public boundaries remain explicit.
+
+## Consequences
+
+Deep infrastructure gains one trusted process-local initiating Agent without widening existing tool and capability requests. Concurrent and nested drivers isolate automatically, AgentLoop gains no additional mandatory service, and HMR/root disposal reaches quiescence before ALS is disabled.
+
+The dependency is implicit in function signatures and carries a capability-bearing Agent object. Consumers must restrict it to cross-cutting infrastructure, treat ambient presence as neither liveness nor authorization, and retain explicit cancellation and ownership checks. ALS also has an always-on propagation cost and does not cross worker, process, HTTP, or durable queue boundaries.
+
+The teardown design deliberately accepts Node's [Stability 1 (Experimental)](https://nodejs.org/api/async_context.html#asynclocalstoragedisable) `AsyncLocalStorage.disable()` dependency. Node requires `disable()` before an ALS instance can be garbage-collected, which matters when HMR replaces AgentRegistry-owned instances; the service state guard prevents a later boundary from re-entering the instance after disposal.
+
+The scope deliberately carries only the Agent, omitting turn, step, `signal`, `cwd`, sandbox, and authorization. A real consumer that cannot use existing explicit fields must justify any refinement separately; a stale copied field may at most mislabel telemetry, never grant control.

+ 63 - 0
docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md

@@ -0,0 +1,63 @@
+# RFC: 基于 AsyncLocalStorage 的发起 Agent 作用域
+
+Status: implemented
+
+[English](2026-07-15-agent-initiator-scope.md) | 中文
+
+## 问题
+
+Harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负责选择服务、注册归属和生命周期;`agent.ctx` 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若把根 `ctx.agent` 改成「当前正在运行的 Agent」,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。
+
+进程内深层基础设施有时需要在显式传递的循环、工具及请求参数之下获取可信的发起 Agent,例如宿主感知传输层、追踪辅助函数、日志器或网关客户端。要求每个私有辅助函数都转发 `agent` 会造成重复,而进程级可变槽会在跨 `await` 时发生并发错误。模型可见参数也不适用,因为模型不得选择可信的会话或路由请求头。该载体归 Agent 服务所有,而非模型可见的可选上下文。
+
+## 决策
+
+必需的 `ctx.agents` 服务使用 Node `AsyncLocalStorage` 携带发起 Agent。它直接存储同一个 `Agent`,不引入只有一个字段的帧;另一个私有运行标记只记录嵌套边界的谱系,供 teardown 记账使用,不携带身份。[核心数据目录](../../../core-data-structures/core.md#initiating-agent)标明了所携带的类型。
+
+`currentInitiator()` 用于可选读取,`requireInitiator()` 抛出 `no initiating agent is active`,`withInitiator(agent, operation)` 保留操作返回的同步值或 Promise 本身。`withoutInitiator(operation)` 会建立清空边界,供不得继承 Agent 的工作使用。会话仍通过 `agent.session` 推导;轮次、步骤、工具调用、`signal`、模型、`cwd`、沙箱和授权继续由现有归属方管理。
+
+`AgentLoop` 已经注入 `ctx.agents`,并用 `agents.withInitiator(agent, ...)` 包裹每个具体驱动的完整 `runLoop` 生命周期。因此,并发驱动使用彼此独立的存储。子驱动的异步延续携带子 Agent;`withInitiator()` 返回后,调用方立即恢复之前的存储,而活动运行计数仍持续跟踪返回的 Promise,直到其结束。创建、持久化加载和尚未发布的 `setup(agentCtx)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而 `agentCtx.agent` 显式标识子 Agent。
+
+隐式身份不会取代显式契约。`ToolExecution.agent`、`AssembleContext.agent`、`GenerateOptions.sessionId`、任务归属、父子请求、`ctx.agent`、`agentCtx.agent`、审批与 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 依赖顺序外仍必须统计活动边界。
+
+发起方作用域不负责管理脱离返回链的工作:注册表排空只跟踪 `withInitiator()` 或 `withoutInitiator()` 返回的 Promise。边界内创建的异步资源会继承其存储,直到自身结束或 ALS 被禁用;所属 seam 必须显式停止未纳入返回 Promise 的工作。Agent 所有前台工作会把完整生命周期纳入返回值,并保留显式取消契约。无关的定时器、队列和部署基础设施在 `withoutInitiator(operation)` 下启动;队列、worker、进程和协议边界必须序列化身份,不能期待 ALS 传播。
+
+宿主感知的传输层可以从 `ctx.agents.requireInitiator().session.id` 推导由部署方拥有的 `X-Harness-Session-Id` 等请求头;模型可见 schema 和参数中不包含该请求头。本决策不让现有生产 MCP 或 Web 传输层采用此请求头。测试替身传输层用于证明可信边界,而不会把宿主路由策略分配给现有的提供方无关 seam。
+
+本决策扩展 [Agent 注册作用域契约](2026-07-08-agent-scope-contexts.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.md),不会改变其中 `agent.ctx` 的静态含义。
+
+## 验证
+
+Agent 服务测试锁定可选与必需读取、同步值和跨 realm Promise 的引用身份、内建 Promise 结束状态观察、并发、嵌套及清空边界、同步抛错或 Promise 拒绝后的恢复、普通与重入排空顺序及保留引用的错误。AgentLoop 集成测试锁定并发与嵌套驱动、无 Agent 调用、AgentRegistry 重启及根 Context 销毁。组合、模块图、构建及运行时闭包检查确保默认组合包、SDK 主干、Python 运行时闭包及直接 AgentLoop harness 通过 `ctx.agents` 完成接线,无需其他提供方。
+
+只有测试替身形式的宿主感知传输层消费隐式身份;它在内部推导 `X-Harness-Session-Id`,并验证工具 schema 与记录参数都不包含身份字段。服务有意不排空边界操作所返回 Promise 之外的异步工作;这类工作仍由所属方的显式停止契约管理。
+
+## 考虑过的替代方案
+
+**在每个函数中传递 Agent。** 公开、worker、进程、持久化和协议边界继续显式传递,但要求每个进程内私有辅助函数都携带 Agent 只会造成重复转发,不会提高可信度。ALS 仅限于这些显式边界内部的异步调用链。
+
+**让 `ctx.agent` 变成动态值。** `ctx.agent` 已经表示与 Agent 作用域 Cordis 上下文静态关联的 Agent。改变根上下文的含义会混合注册作用域与执行作用域,并让并发行为变得意外。
+
+**新增独立的 `ctx.agentExecution` 服务。** 该载体没有独立后端、配置或身份类型:它存储的是 `ctx.agents` 已经管理的同一个 `Agent`,而 AgentLoop 本就依赖该服务。第二个必需提供方会增加包、组合、生命周期、生成目录及测试 harness 接线,却没有拆出真实能力。
+
+**保存命名帧或完整运行时帧。** 只有一个字段的 `{ agent }` 帧只是包装该值,而 Agent、会话、inbox、取消、轮次、步骤、工具执行和持久化已经有各自的真源。增加更多字段会产生陈旧快照和另一套生命周期;直接携带 `Agent`,由方法名标识边界,无需重复保存状态。
+
+**包含步骤级 `AbortSignal`、`cwd`、沙箱或授权。** 它们的生命周期及权限范围与驱动边界不一致,而且现有 seam 已经显式传递这些值。新增控制能力需要独立决策和嵌套生命周期契约。
+
+**使用进程级 `currentAgent`。** 并发 Agent 和 subagent 会在异步延续执行之间相互覆盖,因此可变全局值只在 Harness 不具备的串行保证下才正确。
+
+**从模型可见参数推导身份。** 不能信任模型或用户输入来选择会话、租户或沙箱路由。
+
+**给每个能力 seam 增加路由身份。** 这会把宿主关注点扩散到提供方无关 API。宿主感知实现拥有其传输请求头,而公开边界继续显式传递身份。
+
+## 后果
+
+深层基础设施可以获得一个可信的进程内发起 Agent,而无需加宽现有工具和能力请求。并发及嵌套驱动会自动隔离,AgentLoop 不增加新的必需服务,HMR 或根 Context dispose 会在禁用 ALS 前完成排空。
+
+该依赖不会出现在函数签名中,并且携带一个具有控制能力的 Agent 对象。消费方必须将其限制在横切基础设施中,把隐式存在视为既不证明存活、也不授予权限,并保留显式取消和归属检查。ALS 还有常驻传播成本,也无法跨越 worker、进程、HTTP 或持久化队列边界。
+
+该销毁设计有意依赖 Node 的 [Stability 1(实验性)](https://nodejs.org/api/async_context.html#asynclocalstoragedisable) API `AsyncLocalStorage.disable()`。Node 要求在 ALS 实例可被垃圾回收前调用 `disable()`,这对 HMR 替换 AgentRegistry 所拥有的实例尤为重要;服务状态守卫会阻止 dispose 后通过后续边界重新进入该实例。
+
+该作用域有意只携带 Agent,省略轮次、步骤、`signal`、`cwd`、沙箱和授权。若真实消费方无法使用现有显式字段,必须另行论证扩展;陈旧字段最多只能误标遥测数据,绝不能授予控制权。

+ 1 - 1
packages/README.md

@@ -8,7 +8,7 @@ Packages live at `packages/<group>/<pkg>/`; groups are containers, while names r
 
 | Group | Role | Release expectation |
 |---|---|---|
-| [`core/`](core/README.md) | Product API spine: session, system-prompt, tools, agent, and the concrete loop | Product — stable surface |
+| [`core/`](core/README.md) | Product API spine: sessions, prompts, tools, agent services, and the concrete loop | Product — stable surface |
 | [`llm/`](llm/README.md) | LLM capability family: the abstract service + provider adapters | Product — stable surface |
 | [`bash/`](bash/README.md) | Bash capability family: the executor seam, a local impl, and the model-facing tool | Product — stable surface |
 | [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: the abstract runtime seam for model-written programs + a worker-thread backend | Product — stable surface |

+ 5 - 1
packages/cordis/tool-cordis/src/api-catalog.ts

@@ -63,8 +63,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
   },
   {
     key: 'agents',
-    summary: 'Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator plugins can find them without depending on the concrete loop package.',
+    summary: 'Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain.',
     methods: [
+      'currentInitiator(): Agent | undefined',
+      'requireInitiator(): Agent',
+      'withInitiator<T>(agent: Agent, operation: () => T): T',
+      'withoutInitiator<T>(operation: () => T): T',
       'setFactory(factory: AgentFactory): () => void',
       'async create(options: CreateAgentOptions): Promise<AgentHandle>',
       'async resume(options: ResumeAgentOptions): Promise<AgentHandle>',

+ 2 - 2
packages/core/README.md

@@ -8,11 +8,11 @@ The session log, system-prompt assembly, tool registry, agent vocabulary, and co
 | `session/` | Event-sourced session log + in-memory store | `ctx.sessions` |
 | `system-prompt/` | Prompt-section + tool-schema assembly registry | `ctx.systemPrompt` |
 | `tools/` | Scoped tool registry + pre-policy, guards, around-dispatch, post-policy, and final-result observation | `ctx.tools` |
-| `agent/` | Agent interface, registry, `agent/*` event vocabulary | `ctx.agents` |
+| `agent/` | Agent interface, live registry, process-local initiator scope, `agent/*` event vocabulary | `ctx.agents` |
 | `agent-loop/` | Concrete plugin implementing the public `Agent` contract and owning the loop driver | `ctx.agentLoop` |
 
 `scope/` is the one non-service package here: a dependency-free library (`createScope`/`scopeOf`/`scopeTarget`) the registries and the loop build per-agent scoping on — it sits below `session/` and `system-prompt/` in the module graph precisely so they can consume it without a cycle.
 
-`agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop; everything else in `core/` is interface/vocabulary. Plugins depend on the `agent` vocabulary, never on `agent-loop` directly, so the loop stays swappable.
+`agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop. It runs each driver inside `ctx.agents.withInitiator()`. Extension plugins depend on `agent`, including when they need the initiating Agent, and never on `agent-loop` directly, so the loop stays swappable.
 
 The default composition that wires this spine into a runnable agent lives in [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md): one bundle plugin that loads the control spine plus selected default capabilities (`timer` + `llm` + sessions + system-prompt + tools + agents + invariants + the local [skill family](../skill/README.md) + `tool-bash` + workspace-context + `agent-loop`) and forwards `agent-loop`'s `agents` list as its own config. It sits in `examples/` — ready-to-run demo/reference bundles — not in `core/`: `core/` ships the swappable spine pieces, while a demo bundle picks one concrete composition of them and adds a front door.

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

@@ -50,7 +50,7 @@ The concrete `Agent` class, its `Inbox`, `runLoop`, and instance-bound publicati
 
 ### Loop lifecycle (`loop.ts`)
 
-The internal loop driver runs one agent for its whole lifetime:
+The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`, so process-local asynchronous continuations can recover the initiating Agent. Creation, persistence load, and unpublished setup stay outside the driver boundary; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. The [agent service](../agent/README.md#initiating-agent-scope) owns propagation, teardown, and detached-work rules.
 
 Every provider call that reaches a successful finish appends exactly one `assistant/message` completion anchor, including content-less calls and `max-tokens` finishes. A successful `agent/step-result` stores its transformed content; a rejected result records empty content before the original failure continues. The anchor retains exact chunk provenance (`[]` for a stream with no chunks) and usage when available, while empty content stays out of derived message history.
 

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

@@ -387,7 +387,7 @@ export class ReactLoopAgent implements Agent {
   [startDriver](): void {
     if (this._status === 'disposed') return
     this.driverStarted = true
-    this.done = runLoop(this.loopCtx, this, {
+    this.done = this.loopCtx.agents.withInitiator(this, () => runLoop(this.loopCtx, this, {
       inbox: this.#inbox,
       maxParallelToolCalls: this.maxParallelToolCalls,
       setStatus: (status) => { this.setStatus(status) },
@@ -400,7 +400,7 @@ export class ReactLoopAgent implements Agent {
       withToolBatch: run => this.withToolBatch(run),
       // Pre-step cancellation re-parks without emitting a status transition.
       settleIdle: () => { this.settleIdleWaiters() },
-    })
+    }))
   }
 
   /**

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

@@ -0,0 +1,335 @@
+import { describe, expect, it } from 'vitest'
+import { Context, type Fiber } from 'cordis'
+import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
+import AgentLoop from '@deepseek-ai/dsh-agent-loop'
+import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
+import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
+import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
+import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
+import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
+import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
+
+interface Harness {
+  ctx: Context
+  agentsFiber: Fiber
+  loopFiber: Fiber
+}
+
+async function harness(adapter: LlmAdapter): Promise<Harness> {
+  const ctx = new Context()
+  await ctx.plugin(LlmService)
+  await ctx.plugin(SessionStore)
+  await ctx.plugin(SystemPrompt)
+  await ctx.plugin(ToolRegistry)
+  const agentsFiber = await ctx.plugin(AgentRegistry)
+  const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
+  ctx.llm.registerAdapter(['mock'], adapter)
+  return { ctx, agentsFiber, loopFiber }
+}
+
+function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
+  return new Promise((resolve) => {
+    const dispose = ctx.on('agent/status', (subject, status) => {
+      if (subject === agent && status === 'idle') {
+        dispose()
+        resolve()
+      }
+    })
+  })
+}
+
+function send(agent: Agent, text: string): void {
+  agent.send([{ type: 'text', text }])
+}
+
+/** Adapter that holds both drivers at the same awaited continuation. */
+class OverlapAdapter extends LlmAdapter {
+  private readonly bothStarted = Promise.withResolvers<boolean>()
+  private starts = 0
+  readonly observations: { sessionId: SessionId | undefined; before: Agent; after: Agent }[] = []
+
+  constructor(private readonly ctx: Context) {
+    super()
+  }
+
+  async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
+    const before = this.ctx.agents.requireInitiator()
+    this.starts += 1
+    if (this.starts === 2) this.bothStarted.resolve(true)
+    await this.bothStarted.promise
+    await Promise.resolve()
+    const after = this.ctx.agents.requireInitiator()
+    this.observations.push({ sessionId: options.sessionId, before, after })
+    yield* textResponse('done')
+  }
+}
+
+/** Test-only transport that materializes ambient identity at its request boundary. */
+class TestCapabilityTransport {
+  readonly requests: { path: string; headers: Record<string, string> }[] = []
+
+  constructor(private readonly agents: AgentRegistry) {}
+
+  async request(path: string): Promise<Record<string, string>> {
+    await Promise.resolve()
+    const headers = {
+      'X-Harness-Session-Id': this.agents.requireInitiator().session.id,
+    }
+    this.requests.push({ path, headers })
+    return headers
+  }
+}
+
+/** Adapter whose first call waits for cancellation and whose later calls complete. */
+class ReloadAdapter extends LlmAdapter {
+  readonly firstStarted = Promise.withResolvers<boolean>()
+  firstAgentDuringAbort: Agent | undefined
+  laterAgent: Agent | undefined
+  calls = 0
+  agents: AgentRegistry | undefined
+
+  async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
+    const agents = this.agents
+    if (agents === undefined) throw new Error('agent service missing')
+    this.calls += 1
+    if (this.calls === 1) {
+      this.firstStarted.resolve(true)
+      try {
+        await new Promise<void>((_resolve, reject) => {
+          const abort = (): void => { reject(new Error('aborted')) }
+          if (options.signal?.aborted === true) abort()
+          else options.signal?.addEventListener('abort', abort, { once: true })
+        })
+      } catch (error: unknown) {
+        await Promise.resolve()
+        this.firstAgentDuringAbort = agents.requireInitiator()
+        throw error
+      }
+      return
+    }
+    await Promise.resolve()
+    this.laterAgent = agents.requireInitiator()
+    yield* textResponse('reloaded')
+  }
+}
+
+describe('AgentLoop initiator scope', () => {
+  it('keeps overlapping driver continuations bound to their exact Agents', async () => {
+    const ctx = new Context()
+    const adapter = new OverlapAdapter(ctx)
+    await ctx.plugin(LlmService)
+    await ctx.plugin(SessionStore)
+    await ctx.plugin(SystemPrompt)
+    await ctx.plugin(ToolRegistry)
+    await ctx.plugin(AgentRegistry)
+    await ctx.plugin(AgentLoop, { agents: [] })
+    ctx.llm.registerAdapter(['mock'], adapter)
+
+    const a = ctx.agentLoop.create(SessionId('a'), { provider: 'mock', model: 'mock' })
+    const b = ctx.agentLoop.create(SessionId('b'), { provider: 'mock', model: 'mock' })
+    const idleA = waitForIdle(ctx, a)
+    const idleB = waitForIdle(ctx, b)
+    send(a, 'a')
+    send(b, 'b')
+    await Promise.all([idleA, idleB])
+
+    expect(adapter.observations).toHaveLength(2)
+    expect(adapter.observations).toEqual(expect.arrayContaining([
+      { sessionId: a.session.id, before: a, after: a },
+      { sessionId: b.session.id, before: b, after: b },
+    ]))
+    expect(ctx.agents.currentInitiator()).toBeUndefined()
+    await ctx.fiber.dispose()
+  })
+
+  it('keeps child setup under the parent boundary and restores the parent while the child driver remains active', async () => {
+    const adapter = new MockAdapter([
+      toolCallResponse('spawn', 'spawn-child', {}),
+      toolCallResponse('observe', 'observe-child', {}),
+      textResponse('child done'),
+      textResponse('parent done'),
+    ])
+    const { ctx } = await harness(adapter)
+    let parentDuringSetup: Agent | undefined
+    let explicitChild: Agent | undefined
+    let childDuringDriver: Agent | undefined
+    let parentWhileChildDriverActive: Agent | undefined
+    let child: Agent | undefined
+
+    ctx.tools.register(defineTool({
+      name: 'spawn-child',
+      description: 'create one child agent',
+      parameters: {},
+      execute: async (_args, exec) => {
+        if (exec.agent === undefined) throw new Error('parent agent missing')
+        const handle = await exec.agent.ctx.agents.create({
+          sessionId: SessionId('child-session'),
+          agentOptions: { provider: 'mock', model: 'mock' },
+          setup: (agentCtx) => {
+            parentDuringSetup = ctx.agents.requireInitiator()
+            explicitChild = agentCtx.agent
+            agentCtx.tools.register(defineTool({
+              name: 'observe-child',
+              description: 'observe child execution identity',
+              parameters: {},
+              execute: async () => {
+                await Promise.resolve()
+                childDuringDriver = ctx.agents.requireInitiator()
+                return [{ type: 'text', text: 'observed' }]
+              },
+            }))
+          },
+        })
+        child = handle.agent
+        parentWhileChildDriverActive = ctx.agents.requireInitiator()
+        send(handle.agent, 'run child')
+        await handle.agent.whenIdle()
+        await handle.dispose()
+        return [{ type: 'text', text: 'child completed' }]
+      },
+    }))
+
+    const parentHandle = await ctx.agents.create({
+      sessionId: SessionId('parent-session'),
+      agentOptions: { provider: 'mock', model: 'mock' },
+    })
+    const idle = waitForIdle(ctx, parentHandle.agent)
+    send(parentHandle.agent, 'spawn')
+    await idle
+
+    expect(parentDuringSetup).toBe(parentHandle.agent)
+    expect(explicitChild).toBe(child)
+    expect(childDuringDriver).toBe(child)
+    expect(parentWhileChildDriverActive).toBe(parentHandle.agent)
+    expect(ctx.agents.currentInitiator()).toBeUndefined()
+    await parentHandle.dispose()
+    await ctx.fiber.dispose()
+  })
+
+  it('keeps agentless direct tools ambient-free and builds trusted transport headers internally', async () => {
+    const adapter = new MockAdapter([
+      toolCallResponse('capability', 'capability-request', { path: '/v1/capability' }),
+      textResponse('done'),
+    ])
+    const { ctx } = await harness(adapter)
+    const transport = new TestCapabilityTransport(ctx.agents)
+    let directAmbient: Agent | undefined
+    let captured: Agent | undefined
+
+    ctx.tools.register(defineTool({
+      name: 'agentless-probe',
+      description: 'observe an agentless call',
+      parameters: {},
+      execute: async () => {
+        await Promise.resolve()
+        directAmbient = ctx.agents.currentInitiator()
+        return [{ type: 'text', text: 'ok' }]
+      },
+    }))
+    ctx.tools.register(defineTool({
+      name: 'capability-request',
+      description: 'call the test capability transport',
+      parameters: { path: { type: 'string' } },
+      execute: async (args) => {
+        captured = ctx.agents.requireInitiator()
+        const path = (args as { path: string }).path
+        const headers = await transport.request(path)
+        return [{ type: 'text', text: JSON.stringify(headers) }]
+      },
+    }))
+
+    const direct = await ctx.tools.execute({
+      callId: CallId('direct'),
+      name: 'agentless-probe',
+      arguments: {},
+    })
+    expect(direct.isError).toBe(false)
+    expect(directAmbient).toBeUndefined()
+
+    const handle = await ctx.agents.create({
+      sessionId: SessionId('transport-session'),
+      agentOptions: { provider: 'mock', model: 'mock' },
+    })
+    const idle = waitForIdle(ctx, handle.agent)
+    send(handle.agent, 'call transport')
+    await idle
+
+    expect(transport.requests).toEqual([{
+      path: '/v1/capability',
+      headers: { 'X-Harness-Session-Id': 'transport-session' },
+    }])
+    const schema = adapter.requests[0]?.tools?.find(tool => tool.name === 'capability-request')
+    expect(JSON.stringify(schema?.parameters)).not.toMatch(/session|harness/i)
+    const call = handle.agent.session.events.find(event => event.type === 'tool/call')
+    expect(call?.type === 'tool/call' ? call.data.arguments : undefined)
+      .toBe(JSON.stringify({ path: '/v1/capability' }))
+    expect(captured).toBe(handle.agent)
+
+    await handle.dispose()
+    expect(captured?.status).toBe('disposed')
+    expect(ctx.agents.currentInitiator()).toBeUndefined()
+    await ctx.fiber.dispose()
+  })
+
+  it('drains the old driver before disabling ALS during agent-service restart', async () => {
+    const adapter = new ReloadAdapter()
+    const { ctx, agentsFiber, loopFiber } = await harness(adapter)
+    const oldService = ctx.agents
+    adapter.agents = oldService
+    const oldHandle = await ctx.agents.create({
+      sessionId: SessionId('before-restart-session'),
+      agentOptions: { provider: 'mock', model: 'mock' },
+    })
+    const oldAgent = oldHandle.agent
+    send(oldAgent, 'block')
+    await adapter.firstStarted.promise
+
+    await agentsFiber.restart()
+    await loopFiber.await()
+    expect(adapter.firstAgentDuringAbort?.id).toBe(oldAgent.id)
+    expect(adapter.firstAgentDuringAbort?.session).toBe(oldAgent.session)
+    expect(oldAgent.status).toBe('disposed')
+    expect(() => oldService.currentInitiator()).toThrow('agent initiator scope is disposed')
+    expect(ctx.agents).not.toBe(oldService)
+    adapter.agents = ctx.agents
+
+    const newHandle = await ctx.agents.create({
+      sessionId: SessionId('after-restart-session'),
+      agentOptions: { provider: 'mock', model: 'mock' },
+    })
+    const newAgent = newHandle.agent
+    const idle = waitForIdle(ctx, newAgent)
+    send(newAgent, 'continue')
+    await idle
+    expect(adapter.laterAgent?.id).toBe(newAgent.id)
+    expect(adapter.laterAgent?.session).toBe(newAgent.session)
+    await ctx.fiber.dispose()
+  })
+
+  it('keeps ALS readable while root disposal drains sibling AgentLoop fibers', async () => {
+    const ctx = new Context()
+    const adapter = new ReloadAdapter()
+    await ctx.plugin(LlmService)
+    await ctx.plugin(SessionStore)
+    await ctx.plugin(SystemPrompt)
+    await ctx.plugin(ToolRegistry)
+    await ctx.plugin(AgentRegistry)
+    await ctx.plugin(AgentLoop, { agents: [] })
+    ctx.llm.registerAdapter(['mock'], adapter)
+    const service = ctx.agents
+    adapter.agents = service
+    const handle = await ctx.agents.create({
+      sessionId: SessionId('root-dispose-session'),
+      agentOptions: { provider: 'mock', model: 'mock' },
+    })
+    const agent = handle.agent
+    send(agent, 'block')
+    await adapter.firstStarted.promise
+
+    await ctx.fiber.dispose()
+    expect(adapter.firstAgentDuringAbort?.id).toBe(agent.id)
+    expect(adapter.firstAgentDuringAbort?.session).toBe(agent.session)
+    expect(agent.status).toBe('disposed')
+    expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed')
+  })
+})

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

@@ -264,6 +264,7 @@ describe('Agent', () => {
     // early-return branch.
     const ctx = new Context()
     await ctx.plugin(SessionStore)
+    await ctx.plugin(AgentRegistry)
     const session = ctx.sessions.create(SessionId('test'))
     const prepared = prepareReactLoopAgent(
       ctx, SessionId('bare'), { provider: 'mock', model: 'mock' }, session, DEFAULT_MAX_PARALLEL_TOOL_CALLS,

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

@@ -1,10 +1,10 @@
 # dsh-agent
 
-Agent interface, registry, and `agent/*` event vocabulary. Every plugin (UI, hooks, orchestrators) programs against the `Agent` handle defined here — it has zero loop dependency, so the loop is swappable.
+Agent interface, registry, process-local initiator scope, and `agent/*` event vocabulary. Every plugin (UI, hooks, orchestrators) programs against the `Agent` handle defined here — it has zero loop dependency, so the loop is swappable.
 
 ## Service: `AgentRegistry` (ctx key: `agents`)
 
-Tracks live agents so UI, hook, and orchestrator plugins can find them without importing the concrete loop package.
+Tracks live agents and carries the initiating Agent through asynchronous driver work without importing the concrete loop package.
 
 ### Public API
 
@@ -17,6 +17,17 @@ The scoped-registration surface: `Agent.ctx` is the agent's scope context (`dsh-
 - `ctx.agents.list(): Agent[]`
 - `ctx.agents.roots(): Agent[]` — live agents created without an owning agent context; a resumed lineage-bearing session can still be a runtime root.
 
+#### Initiating Agent scope
+
+`AgentLoop` runs each concrete driver's complete lifetime inside an initiator boundary. Concurrent drivers remain isolated: a child driver's continuations carry the child, while the parent continuation regains the parent as soon as `withInitiator()` returns; drain tracking continues until the child driver's Promise settles. Creation, persistence load, and unpublished setup remain outside the child's boundary, so setup initiated by a parent inherits the parent while `agentCtx.agent` identifies the child explicitly.
+
+- `ctx.agents.currentInitiator(): Agent | undefined` — read the inherited initiator without requiring one.
+- `ctx.agents.requireInitiator(): Agent` — read it or throw `no initiating agent is active`.
+- `ctx.agents.withInitiator(agent, operation)` — run with one exact Agent and preserve the operation's exact synchronous value or Promise.
+- `ctx.agents.withoutInitiator(operation)` — hide an inherited initiator for unrelated process-local work.
+
+The scope carries the `Agent` itself and is process-local. Ambient presence is neither liveness proof nor authorization; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. Teardown rejects new boundaries, lets injected dependents and returned-Promise boundaries drain, then disables the underlying `AsyncLocalStorage`; unreturned work remains owned by the subsystem that detached it. If a boundary's inherited async chain starts an owning Cordis fiber's unload, that nested boundary chain is released from the drain so the unload cannot wait on itself; its continuations observe the disposed service after teardown. The [initiator-scope decision](../../../docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns the detailed boundary and teardown contract.
+
 #### Factory seam (creation)
 
 Agent *creation* is provided by the plugin implementing `AgentFactory` (`dsh-agent-loop`), registered via `setFactory`. This keeps creation on the `dsh-agent` interface so consumers (UI, the ACP bridge) program against `ctx.agents` without depending on the concrete loop package. The registry canonicalizes an already traced Service to its concrete target and re-traces each call through the caller's context; this avoids nested Cordis shadows while passing an explicit caller-bound `ownerCtx` to plain factories.
@@ -72,6 +83,8 @@ The handle every plugin programs against:
 
 ## Known Limitations and Deferred Work
 
+- **Initiator scope is process-local** — workers, child processes, HTTP, durable queues, and restarts 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.
 - **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam.
 - **`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.
 - **No public step-only abort** — `cancel()` clears ALL pending work (queued + steering + in-flight); an abort that preserves queued prompts returns only with a named consumer ([stop-surface RFC](../../../docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md)).

+ 1 - 1
packages/core/agent/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@deepseek-ai/dsh-agent",
-  "description": "Agent interface, registry, and event vocabulary for the DeepSeek Harness",
+  "description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness",
   "version": "0.0.1",
   "private": true,
   "type": "module",

+ 167 - 8
packages/core/agent/src/index.ts

@@ -1,11 +1,14 @@
 /**
- * Agent registry service. Tracks live agents so plugins can find them without
- * depending on the concrete loop package. Agent creation belongs to the loop.
+ * Agent service: live registry, factory delegation, and process-local
+ * initiator scope. Concrete creation and driving belong to the loop.
  *
  * @module @deepseek-ai/dsh-agent
  */
 
-import { Context, getTraceable, Service, symbols } from 'cordis'
+import { Context, FiberState, getTraceable, Service, symbols } from 'cordis'
+import type { Fiber } from 'cordis'
+import { AsyncLocalStorage } from 'node:async_hooks'
+import { isPromise } from 'node:util/types'
 import { scopeTarget } from '@deepseek-ai/dsh-scope'
 import type { Scoped } from '@deepseek-ai/dsh-scope'
 import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
@@ -173,6 +176,8 @@ export interface AgentFactory {
 
 /** Thrown when create/resume is called before an agent factory is registered. */
 const NO_FACTORY_MESSAGE = 'no agent factory registered (load an agent-loop plugin)'
+const NO_INITIATOR_MESSAGE = 'no initiating agent is active'
+const DISPOSED_INITIATOR_MESSAGE = 'agent initiator scope is disposed'
 
 /** All mutable lifecycle state for one exact registry entry. */
 interface AgentEntry {
@@ -186,21 +191,32 @@ interface AgentEntry {
   detachRequested: boolean
 }
 
+/** One tracked boundary plus its inherited nesting chain. */
+interface InitiatorRun {
+  active: boolean
+  readonly parent: InitiatorRun | undefined
+}
+
 /** Plain holder prevents Cordis from tracing the factory field before the caller context is known. */
 interface FactorySlot {
   readonly target: AgentFactory
 }
 
 /**
- * Agent registry (`ctx.agents`): tracks live agents so UI, hook, and
- * orchestrator plugins can find them without depending on the concrete loop
- * package. Agent *creation* is provided by whichever plugin implements the
- * {@link AgentFactory} (`@deepseek-ai/dsh-agent-loop`), registered via
- * {@link setFactory}.
+ * Agent service (`ctx.agents`): tracks live agents and carries the initiating
+ * Agent through one process-local asynchronous driver chain. Agent *creation*
+ * is provided by whichever plugin implements the {@link AgentFactory}
+ * (`@deepseek-ai/dsh-agent-loop`), registered via {@link setFactory}.
  */
 export class AgentRegistry extends Service {
   private store = new Map<SessionId, AgentEntry>()
   private factory: FactorySlot | undefined
+  private readonly initiators = new AsyncLocalStorage<Agent | undefined>()
+  private readonly initiatorRuns = new AsyncLocalStorage<InitiatorRun>()
+  private initiatorState: 'active' | 'closing' | 'disposed' = 'active'
+  private activeInitiatorRuns = 0
+  private initiatorDrain: PromiseWithResolvers<void> | undefined
+  private initiatorDisposal: Promise<void> | undefined
 
   constructor(ctx: Context) {
     super(ctx, 'agents')
@@ -211,6 +227,63 @@ export class AgentRegistry extends Service {
     // 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()
+      }
+    })
+    ctx.effect(function* (this: AgentRegistry) {
+      yield () => this.disposeInitiators()
+      yield () => { this.closeInitiators() }
+    }.bind(this), 'agents.initiatorLifecycle()')
+  }
+
+  /**
+   * Read the Agent that initiated the inherited asynchronous driver chain.
+   * @returns the inherited Agent, or `undefined` outside a driver and inside an explicit clearing boundary.
+   * @throws when this service instance has been disposed.
+   */
+  currentInitiator(): Agent | undefined {
+    this.assertInitiatorsReadable()
+    return this.initiators.getStore()
+  }
+
+  /**
+   * Read the initiating Agent and fail when no driver boundary is active.
+   * @returns the inherited Agent.
+   * @throws when no initiator is active or this service instance has been disposed.
+   */
+  requireInitiator(): Agent {
+    const agent = this.currentInitiator()
+    if (agent === undefined) throw new Error(NO_INITIATOR_MESSAGE)
+    return agent
+  }
+
+  /**
+   * Run an operation with one exact Agent as its process-local initiator. The
+   * exact synchronous value or Promise returned by the operation is preserved.
+   * If its inherited async chain starts an owning-fiber unload, the nested
+   * boundary lineage is excluded from the drain so teardown cannot wait on itself.
+   * @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
+   * @param operation - synchronous or asynchronous operation to invoke.
+   * @returns the exact value returned by `operation`.
+   * @throws when the initiator scope is closing/disposed, or when `operation` throws.
+   */
+  withInitiator<T>(agent: Agent, operation: () => T): T {
+    return this.runWithInitiator(agent, operation)
+  }
+
+  /**
+   * Run an operation inside a boundary that hides any inherited initiating
+   * Agent. The exact synchronous value or Promise is preserved.
+   * If its inherited async chain starts an owning-fiber unload, the nested
+   * boundary lineage is excluded from the drain so teardown cannot wait on itself.
+   * @param operation - synchronous or asynchronous operation to invoke without an initiator.
+   * @returns the exact value returned by `operation`.
+   * @throws when the initiator scope is closing/disposed, or when `operation` throws.
+   */
+  withoutInitiator<T>(operation: () => T): T {
+    return this.runWithInitiator(undefined, operation)
   }
 
   /**
@@ -471,6 +544,92 @@ export class AgentRegistry extends Service {
       .filter(entry => entry.owner === undefined)
       .map(entry => entry.agent)
   }
+
+  /** Reject new initiator boundaries while inherited continuations drain. */
+  private closeInitiators(): void {
+    if (this.initiatorState === 'active') this.initiatorState = 'closing'
+  }
+
+  /** Wait for returned-Promise boundaries, then invalidate retained references. */
+  private disposeInitiators(): Promise<void> {
+    return (this.initiatorDisposal ??= (async () => {
+      this.closeInitiators()
+      this.releaseReentrantInitiatorRuns()
+      if (this.activeInitiatorRuns !== 0) {
+        this.initiatorDrain ??= Promise.withResolvers<void>()
+        await this.initiatorDrain.promise
+      }
+      this.initiatorState = 'disposed'
+      this.initiators.disable()
+      this.initiatorRuns.disable()
+    })())
+  }
+
+  /** Establish one tracked initiator or clearing boundary. */
+  private runWithInitiator<T>(agent: Agent | undefined, operation: () => T): T {
+    if (this.initiatorState !== 'active') throw new Error(DISPOSED_INITIATOR_MESSAGE)
+    const run: InitiatorRun = {
+      active: true,
+      parent: this.initiatorRuns.getStore(),
+    }
+    this.activeInitiatorRuns += 1
+    let result: T
+    try {
+      result = this.initiatorRuns.run(run, () => this.initiators.run(agent, operation))
+    } catch (error: unknown) {
+      this.releaseInitiatorRun(run)
+      throw error
+    }
+    if (isPromise(result)) {
+      try {
+        void Promise.prototype.then.call(
+          result,
+          () => { this.releaseInitiatorRun(run) },
+          () => { this.releaseInitiatorRun(run) },
+        )
+      } catch {
+        // A branded Promise may expose a failing @@species. Observer setup did
+        // not attach, so preserve the exact return without leaking the run.
+        this.releaseInitiatorRun(run)
+      }
+    } else {
+      this.releaseInitiatorRun(run)
+    }
+    return result
+  }
+
+  /** Whether one unloading fiber owns this service's lifecycle. */
+  private hasLifecycleAncestor(candidate: Fiber): boolean {
+    let fiber = this.ctx.fiber
+    while (true) {
+      if (fiber === candidate) return true
+      const parent = fiber.parent.fiber
+      if (parent === fiber) return false
+      fiber = parent
+    }
+  }
+
+  private assertInitiatorsReadable(): void {
+    if (this.initiatorState === 'disposed') throw new Error(DISPOSED_INITIATOR_MESSAGE)
+  }
+
+  /** Exclude the boundary chain that initiated this teardown from its own drain. */
+  private releaseReentrantInitiatorRuns(): void {
+    let run = this.initiatorRuns.getStore()
+    while (run !== undefined) {
+      this.releaseInitiatorRun(run)
+      run = run.parent
+    }
+  }
+
+  private releaseInitiatorRun(run: InitiatorRun): void {
+    if (!run.active) return
+    run.active = false
+    this.activeInitiatorRuns -= 1
+    if (this.activeInitiatorRuns !== 0) return
+    this.initiatorDrain?.resolve()
+    this.initiatorDrain = undefined
+  }
 }
 
 export default AgentRegistry

+ 265 - 0
packages/core/agent/tests/agent-initiator.spec.ts

@@ -0,0 +1,265 @@
+import { describe, expect, it } from 'vitest'
+import { Context } from 'cordis'
+import { runInNewContext } from 'node:vm'
+import AgentRegistry from '@deepseek-ai/dsh-agent'
+import type { Agent } from '@deepseek-ai/dsh-agent'
+import { SessionId } from '@deepseek-ai/dsh-session'
+
+function agent(id: string): Agent {
+  return { id: SessionId(id) } as Agent
+}
+
+async function harness(): Promise<{
+  ctx: Context
+  service: AgentRegistry
+  dispose: () => Promise<void>
+}> {
+  const ctx = new Context()
+  const fiber = await ctx.plugin(AgentRegistry)
+  return {
+    ctx,
+    service: ctx.agents,
+    dispose: fiber.dispose,
+  }
+}
+
+/** Fail a lifecycle regression promptly instead of waiting for Vitest's suite timeout. */
+async function promptly<T>(task: Promise<T>): Promise<T> {
+  const timeout = Promise.withResolvers<never>()
+  const timer = setTimeout(() => { timeout.reject(new Error('initiator teardown did not settle promptly')) }, 1000)
+  try {
+    return await Promise.race([task, timeout.promise])
+  } finally {
+    clearTimeout(timer)
+  }
+}
+
+describe('AgentRegistry initiator scope', () => {
+  it('reports an absent initiator and requires an active boundary', async () => {
+    const { service, dispose } = await harness()
+    expect(service.currentInitiator()).toBeUndefined()
+    expect(() => service.requireInitiator()).toThrow('no initiating agent is active')
+    await dispose()
+  })
+
+  it('preserves exact synchronous and Promise return identities across await', async () => {
+    const { service, dispose } = await harness()
+    const initiator = agent('identity')
+    const value = { result: true }
+    expect(service.withInitiator(initiator, () => {
+      expect(service.requireInitiator()).toBe(initiator)
+      return value
+    })).toBe(value)
+
+    const promise = service.withInitiator(initiator, async () => {
+      expect(service.requireInitiator()).toBe(initiator)
+      await Promise.resolve()
+      expect(service.requireInitiator()).toBe(initiator)
+      return value
+    })
+    expect(service.withInitiator(initiator, () => promise)).toBe(promise)
+    await expect(promise).resolves.toBe(value)
+    expect(service.currentInitiator()).toBeUndefined()
+    await dispose()
+  })
+
+  it('tracks a branded Promise without calling its overridable then property', async () => {
+    const { service, dispose } = await harness()
+    const initiator = agent('overridden-then')
+    const release = Promise.withResolvers<boolean>()
+    void Object.defineProperty(release.promise, 'then', {
+      value: () => { throw new Error('overridden then called') },
+    })
+
+    const pending = service.withInitiator(initiator, () => release.promise)
+    expect(pending).toBe(release.promise)
+
+    let disposed = false
+    const disposal = dispose().then(() => { disposed = true })
+    await Promise.resolve()
+    expect(disposed).toBe(false)
+
+    release.resolve(true)
+    await new Promise<void>((resolve, reject) => {
+      void Promise.prototype.then.call(pending, resolve, reject)
+    })
+    await disposal
+    expect(disposed).toBe(true)
+  })
+
+  it('preserves a settled branded Promise when its species blocks observer construction', async () => {
+    const { service, dispose } = await harness()
+    const initiator = agent('invalid-species')
+    const promise = Promise.resolve()
+    const constructor = {}
+    Object.defineProperty(constructor, Symbol.species, {
+      get: () => { throw new Error('invalid species') },
+    })
+    void Object.defineProperty(promise, 'constructor', { value: constructor })
+
+    expect(service.withInitiator(initiator, () => promise)).toBe(promise)
+    await dispose()
+  })
+
+  it('isolates overlapping initiators', async () => {
+    const { service, dispose } = await harness()
+    const a = agent('a')
+    const b = agent('b')
+    const bothStarted = Promise.withResolvers<boolean>()
+    const release = Promise.withResolvers<boolean>()
+    let starts = 0
+    const run = (initiator: Agent): Promise<void> => service.withInitiator(initiator, async () => {
+      expect(service.requireInitiator()).toBe(initiator)
+      starts += 1
+      if (starts === 2) bothStarted.resolve(true)
+      await release.promise
+      expect(service.requireInitiator()).toBe(initiator)
+    })
+
+    const pending = [run(a), run(b)]
+    await bothStarted.promise
+    expect(service.currentInitiator()).toBeUndefined()
+    release.resolve(true)
+    await Promise.all(pending)
+    await dispose()
+  })
+
+  it('restores nested and explicitly cleared boundaries', async () => {
+    const { service, dispose } = await harness()
+    const parent = agent('parent')
+    const child = agent('child')
+
+    service.withInitiator(parent, () => {
+      expect(service.requireInitiator()).toBe(parent)
+      service.withInitiator(child, () => { expect(service.requireInitiator()).toBe(child) })
+      expect(service.requireInitiator()).toBe(parent)
+      service.withoutInitiator(() => {
+        expect(service.currentInitiator()).toBeUndefined()
+        expect(() => service.requireInitiator()).toThrow('no initiating agent is active')
+      })
+      expect(service.requireInitiator()).toBe(parent)
+    })
+    expect(service.currentInitiator()).toBeUndefined()
+    await dispose()
+  })
+
+  it('restores the parent after synchronous throws and rejected operations', async () => {
+    const { service, dispose } = await harness()
+    const parent = agent('parent')
+    const child = agent('child')
+    const syncError = new Error('sync failure')
+    const asyncError = new Error('async failure')
+
+    service.withInitiator(parent, () => {
+      expect(() => service.withInitiator(child, () => { throw syncError })).toThrow(syncError)
+      expect(service.requireInitiator()).toBe(parent)
+    })
+    await expect(service.withInitiator(child, async () => {
+      await Promise.resolve()
+      throw asyncError
+    })).rejects.toBe(asyncError)
+    expect(service.currentInitiator()).toBeUndefined()
+    await dispose()
+  })
+
+  it('stops new boundaries, drains active Promises, and invalidates retained references', async () => {
+    const { ctx, service, dispose } = await harness()
+    const initiator = agent('draining')
+    const release = Promise.withResolvers<boolean>()
+    const pending = service.withInitiator(initiator, async () => {
+      await release.promise
+      expect(service.requireInitiator()).toBe(initiator)
+    })
+    let disposed = false
+    const disposal = dispose().then(() => { disposed = true })
+    await Promise.resolve()
+
+    expect(() => service.withInitiator(initiator, () => 1)).toThrow('agent initiator scope is disposed')
+    expect(() => service.withoutInitiator(() => 1)).toThrow('agent initiator scope is disposed')
+    expect(disposed).toBe(false)
+    expect(ctx.get('agents')).toBeUndefined()
+    release.resolve(true)
+    await pending
+    await disposal
+    expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed')
+    expect(() => service.requireInitiator()).toThrow('agent initiator scope is disposed')
+  })
+
+  it('drains cross-realm Promise boundaries before disposal', async () => {
+    const { service, dispose } = await harness()
+    const initiator = agent('cross-realm')
+    const release = Promise.withResolvers<boolean>()
+    const operation = runInNewContext(
+      '(async () => { await release; inspect() })',
+      {
+        release: release.promise,
+        inspect: () => { expect(service.requireInitiator()).toBe(initiator) },
+      },
+    ) as () => Promise<void>
+    const pending = service.withInitiator(initiator, operation)
+    expect(pending).not.toBeInstanceOf(Promise)
+
+    let disposed = false
+    const disposal = dispose().then(() => { disposed = true })
+    await Promise.resolve()
+    expect(disposed).toBe(false)
+
+    release.resolve(true)
+    await pending
+    await disposal
+    expect(disposed).toBe(true)
+  })
+
+  it('does not self-deadlock when a boundary returns service disposal', async () => {
+    const { service, dispose } = await harness()
+    const initiator = agent('service-disposer')
+
+    const returned = service.withInitiator(initiator, dispose)
+    await promptly(returned)
+
+    expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed')
+  })
+
+  it('does not self-deadlock when nested boundaries return ancestor disposal', async () => {
+    const { ctx, service } = await harness()
+    const parent = agent('parent-disposer')
+    const child = agent('child-disposer')
+    let disposal: Promise<void> | undefined
+
+    const returned = service.withInitiator(parent, () => service.withInitiator(child, () => {
+      disposal = ctx.fiber.dispose()
+      return disposal
+    }))
+    expect(returned).toBe(disposal)
+
+    await promptly(returned)
+    expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed')
+  })
+
+  it('excludes an asynchronous teardown initiator while draining unrelated boundaries', async () => {
+    const { ctx, service } = await harness()
+    const initiator = agent('async-disposer')
+    const unrelated = agent('unrelated')
+    const release = Promise.withResolvers<boolean>()
+    const pending = service.withInitiator(unrelated, async () => {
+      await release.promise
+      expect(service.requireInitiator()).toBe(unrelated)
+    })
+
+    const returned = service.withInitiator(initiator, async () => {
+      await Promise.resolve()
+      await ctx.fiber.dispose()
+    })
+    let disposed = false
+    void returned.then(() => { disposed = true })
+    await Promise.resolve()
+    await Promise.resolve()
+    expect(disposed).toBe(false)
+
+    release.resolve(true)
+    await pending
+    await promptly(returned)
+
+    expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed')
+  })
+})

+ 1 - 1
packages/examples/agent-spine-demo/README.md

@@ -16,7 +16,7 @@ Read this package for the whole plugin tree and its composition order.
 @deepseek-ai/dsh-tools            registry + guarded pre/around/post/final-result pipeline
 @deepseek-ai/dsh-skill            skill provider registry
 @deepseek-ai/dsh-skill-local      local filesystem skill provider
-@deepseek-ai/dsh-agent            agent registry + agent/* event vocabulary
+@deepseek-ai/dsh-agent            agent registry + initiator scope + agent/* events
 @deepseek-ai/dsh-tasks            generic background-task registry
 @deepseek-ai/dsh-invariants       dev-mode event-contract assertions
 @deepseek-ai/dsh-tool-bash        the model-facing bash schema

+ 2 - 2
scripts/gen-doc-graphs.ts

@@ -156,10 +156,10 @@ const SERVICE_ROLES: ServiceRole[] = [
   {
     key: 'agents',
     pkg: 'agent',
-    title: 'Agent registry',
+    title: 'Agent service',
     mode: 'core',
     consumers: ['agent-loop', 'acp', 'cli-demo', 'subagent-inprocess', 'stdio-demo', 'invariants'],
-    note: 'Owns live Agent handles and the create/resume factory seam.',
+    note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
   },
   {
     key: 'agentLoop',

+ 65 - 12
website/zh-CN/api/harness/agents.md

@@ -4,9 +4,62 @@
 
 `AgentRegistry` — provided by `@deepseek-ai/dsh-agent`.
 
-Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator plugins can find them without depending on the concrete loop package. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.
+Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L201)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L211)
+
+### ctx.agents.currentInitiator()
+
+```ts website-api
+currentInitiator(): Agent | undefined
+```
+
+Read the Agent that initiated the inherited asynchronous driver chain.
+
+**Returns** the inherited Agent, or `undefined` outside a driver and inside an explicit clearing boundary.
+
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L246)
+
+### ctx.agents.requireInitiator()
+
+```ts website-api
+requireInitiator(): Agent
+```
+
+Read the initiating Agent and fail when no driver boundary is active.
+
+**Returns** the inherited Agent.
+
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L256)
+
+### ctx.agents.withInitiator(agent, operation)
+
+```ts website-api
+withInitiator<T>(agent: Agent, operation: () => T): T
+```
+
+Run an operation with one exact Agent as its process-local initiator. The exact synchronous value or Promise returned by the operation is preserved. If its inherited async chain starts an owning-fiber unload, the nested boundary lineage is excluded from the drain so teardown cannot wait on itself.
+
+- `agent` — initiating Agent to inherit; presence is neither liveness proof nor authorization.
+- `operation` — synchronous or asynchronous operation to invoke.
+
+**Returns** the exact value returned by `operation`.
+
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L272)
+
+### ctx.agents.withoutInitiator(operation)
+
+```ts website-api
+withoutInitiator<T>(operation: () => T): T
+```
+
+Run an operation inside a boundary that hides any inherited initiating Agent. The exact synchronous value or Promise is preserved. If its inherited async chain starts an owning-fiber unload, the nested boundary lineage is excluded from the drain so teardown cannot wait on itself.
+
+- `operation` — synchronous or asynchronous operation to invoke without an initiator.
+
+**Returns** the exact value returned by `operation`.
+
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L285)
 
 ### ctx.agents.setFactory(factory)
 
@@ -32,7 +85,7 @@ Register the agent-creation factory (the loop calls this on construction, effect
 
 **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.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L228)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L301)
 
 ### ctx.agents.create(options)
 
@@ -55,7 +108,7 @@ Create and publish a new agent through the registered factory. Distinct from reg
 
 **Returns** the handle after setup, rollback-covered publication, and loop start complete.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L261)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L334)
 
 ### ctx.agents.resume(options)
 
@@ -76,7 +129,7 @@ Load a persisted session and resume an agent on it through the registered factor
 
 **Returns** the handle after setup, rollback-covered publication, and loop start complete.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L280)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L353)
 
 ### ctx.agents.register(agent)
 
@@ -108,7 +161,7 @@ Register a live agent. Throws if an agent with the same id is already registered
 
 **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.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L306)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L379)
 
 ### ctx.agents.enter(agent, owner)
 
@@ -138,7 +191,7 @@ Insert an already-constructed agent without announcing it. This is the advanced
 
 **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.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L330)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L403)
 
 ### ctx.agents.announce(agent)
 
@@ -157,7 +210,7 @@ Announce an agent previously inserted with enter.
 
 - `agent` — the live inserted agent to announce.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L405)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L478)
 
 ### ctx.agents.get(id)
 
@@ -176,7 +229,7 @@ Look up a live agent.
 
 **Returns** the agent, or undefined when no live agent has that id.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L439)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L512)
 
 ### ctx.agents.isOwnedBy(id, owner)
 
@@ -199,7 +252,7 @@ Test whether a live agent was created through one exact parent agent's scoped co
 
 **Returns** true only while the exact child entry is live under that owner.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L451)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L524)
 
 ### ctx.agents.list()
 
@@ -215,7 +268,7 @@ All live agents, in registration order.
 
 **Returns** a fresh array; mutating it does not affect the registry.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L459)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L532)
 
 ### ctx.agents.roots()
 
@@ -233,4 +286,4 @@ All live top-level agents in registration order. A top-level agent was created w
 
 **Returns** a fresh array; mutating it does not affect the registry.
 
-[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L469)
+[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L542)