From f66cde41d181cf2b6fcd80ee7426854889e8e655 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:16:07 +0800 Subject: [PATCH] docs(core): clarify initiator restoration timing --- .../2026-07-15-agent-initiator-scope.i18n.yaml | 4 ++-- .../architecture/2026-07-15-agent-initiator-scope.md | 2 +- .../architecture/2026-07-15-agent-initiator-scope.zh.md | 2 +- packages/core/agent-loop/tests/agent-initiator.spec.ts | 8 ++++---- packages/core/agent/README.md | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml index 9d9e11522f..696b119797 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-15-agent-initiator-scope.md: 480bb56f811567406e964ca94fca04fd3d4ace62 -2026-07-15-agent-initiator-scope.zh.md: 29ea3a47f42d607a4584b888ace232de124e7598 +2026-07-15-agent-initiator-scope.md: 08c9eca50ed51925fec7c09eda7a82d0c61cb55b +2026-07-15-agent-initiator-scope.zh.md: 284d52126e02e44d512b88a5d64e35ba80552d9b diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md index 480bb56f81..08c9eca50e 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md +++ b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md @@ -16,7 +16,7 @@ The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the in `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 shadows its parent, and the parent store returns when the child boundary 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. +`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. diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md index 29ea3a47f4..284d52126e 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md @@ -16,7 +16,7 @@ Harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负 `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` 生命周期。因此,并发驱动使用彼此独立的存储,子驱动会遮蔽父驱动,子边界结束后父存储得到恢复。创建、持久化加载和尚未发布的 `setup(agentCtx)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而 `agentCtx.agent` 显式标识子 Agent。 +`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 只在进程内有效。 diff --git a/packages/core/agent-loop/tests/agent-initiator.spec.ts b/packages/core/agent-loop/tests/agent-initiator.spec.ts index 7f0903fc00..8e9b951fa1 100644 --- a/packages/core/agent-loop/tests/agent-initiator.spec.ts +++ b/packages/core/agent-loop/tests/agent-initiator.spec.ts @@ -142,7 +142,7 @@ describe('AgentLoop initiator scope', () => { await ctx.fiber.dispose() }) - it('keeps child setup under the parent boundary, switches for the child driver, then restores the parent', async () => { + 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', {}), @@ -153,7 +153,7 @@ describe('AgentLoop initiator scope', () => { let parentDuringSetup: Agent | undefined let explicitChild: Agent | undefined let childDuringDriver: Agent | undefined - let parentAfterChild: Agent | undefined + let parentWhileChildDriverActive: Agent | undefined let child: Agent | undefined ctx.tools.register(defineTool({ @@ -181,9 +181,9 @@ describe('AgentLoop initiator scope', () => { }, }) child = handle.agent + parentWhileChildDriverActive = ctx.agents.requireInitiator() send(handle.agent, 'run child') await handle.agent.whenIdle() - parentAfterChild = ctx.agents.requireInitiator() await handle.dispose() return [{ type: 'text', text: 'child completed' }] }, @@ -200,7 +200,7 @@ describe('AgentLoop initiator scope', () => { expect(parentDuringSetup).toBe(parentHandle.agent) expect(explicitChild).toBe(child) expect(childDuringDriver).toBe(child) - expect(parentAfterChild).toBe(parentHandle.agent) + expect(parentWhileChildDriverActive).toBe(parentHandle.agent) expect(ctx.agents.currentInitiator()).toBeUndefined() await parentHandle.dispose() await ctx.fiber.dispose() diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 23d9d998c6..839907bf62 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -19,7 +19,7 @@ The scoped-registration surface: `Agent.ctx` is the agent's scope context (`dsh- #### Initiating Agent scope -`AgentLoop` runs each concrete driver's complete lifetime inside an initiator boundary. Concurrent drivers remain isolated, a child driver shadows its parent, and the parent returns after the child 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. +`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`.