# 子 Agent

> Agent 工具怎样派生子 Agent：内置 general-purpose 与 Explore、自定义 Markdown 画像与模型选择、独立的子 AgentRuntime 与借用的 MCP 连接、父子双向消息、审批回到父会话、后台完成通知、持久记忆与 TUI 观察。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.org/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.org/manual/zcode/subagents
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

ZCode 的子 Agent 是一次工具调用：父回合调用 `Agent`，父运行时在同一个进程里新建一个 `AgentRuntime`，让它在一个独立的子会话里跑一个完整回合，再把最终文本作为工具结果交回父回合。加上 `run_in_background: true` 时，工具立刻返回一个 `agentId`，子回合跑完后，父运行时收到一条 `<task-notification>`，由它叫醒父会话。子 Agent 不能再派生子 Agent，委派只有一层。

代码分三层。核心在 `apps/zcode-cli/packages/core/src/subagent/`，其中 `runner.ts` 一个文件就有 2142 行，管前台、后台、任务注册表和通知的顺序；把子运行时真正拼起来的是 `apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts`；Markdown 画像的发现在 `apps/zcode-cli/packages/bootstrap/src/subagents.ts`，桌面端设置页的读写在 `packages/services/src/subagents/`。

| 位置 | 职责 |
| --- | --- |
| `subagent/profile*.ts`、`general-purpose.ts`、`explore*.ts` | 画像类型、两个内置子 Agent、frontmatter 解析与模型选择 |
| `subagent/runner.ts` | `SubagentPort` 的实现：前台 `run`、后台 `start`、停止、`SendMessage` 投递 |
| `runtime/methods/subagent.ts` | 新建子 `AgentRuntime`：模型、权限模式、工具白名单、MCP、技能、事件镜像 |
| `subagent/context-builder.ts`、`system-prompt.ts` | 子 Agent 专用的系统提示词组装 |
| `subagent/borrowed-mcp-port.ts`、`tool-policy.ts`、`computer-use-policy.ts` | 借用父会话的 MCP、统一剔除的工具、禁用 Computer Use |
| `runtime/helpers/child-client-ports.ts`、`subagent-interaction-broker.ts` | 把审批等对外请求路由回父会话 |
| `tool/handlers/agent.ts`、`send-message.ts`、`respond-to-coordinator.ts` | 三个工具 |
| `subagent/persistent-memory*.ts` | 按画像开启的持久记忆 |

## 怎么用：Agent 工具

`Agent` 的参数只有四个（`apps/zcode-cli/packages/contracts/src/tools/agent.ts:18`）：

| 参数 | 说明 |
| --- | --- |
| `description` | 三到五个词的任务描述，也是 UI 里子 Agent 的标题 |
| `prompt` | 交给子 Agent 的任务 |
| `subagent_type` | 用哪个画像，省略时是 `general-purpose`（`apps/zcode-cli/packages/core/src/tool/handlers/agent.ts:176`） |
| `run_in_background` | 为真时后台运行，完成后通知 |

参数里刻意没有模型：注释说子 Agent 的模型统一由设置和 Markdown 画像决定，若让父模型在调用时指定，历史里的旧调用会不断覆盖当前配置（`apps/zcode-cli/packages/contracts/src/tools/agent.ts:25`）。`subagent_type` 允许模型写得不太准：先精确匹配，不中再做 NFKC 归一、转小写、去掉空白、连字符和下划线后比较，唯一命中就收敛到规范名，多个命中则报歧义（`apps/zcode-cli/packages/core/src/subagent/runner.ts:757`、`runner.ts:786`）。

工具声明里 `readOnly: true`、`concurrentSafe: true`、`needsApproval: false`，没有超时，给模型的结果最多 120000 字节（`tool/handlers/agent.ts:21`、`handlers/agent.ts:224`、`handlers/agent.ts:267`）。只读与免审批是因为子 Agent 自己的工具调用会另行受权限约束（`handlers/agent.ts:246`）。工具说明先列出可用画像和各自的工具，再给几条用法（`handlers/agent.ts:103`），其中两条：

> A new Agent call starts fresh, so the prompt must be self-contained.
> When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.

另有一个 `Task` 别名，对模型不可见，只为插件里照 Claude Code 写的“调用 Task 工具”这类说明准备（`handlers/agent.ts:287`）。前台结果交回模型时，正文后面固定附一行 `agentId` 和一段 `<usage>`，提示可以用 `SendMessage` 接着找这个子 Agent（`handlers/agent.ts:130`）。

## 两个内置子 Agent

画像表里总有两个内置项，同名的用户或项目画像可以覆盖它们（`apps/zcode-cli/packages/core/src/subagent/profile.ts:89`）：

| | `general-purpose` | `Explore` |
| --- | --- | --- |
| 工具 | `*`，继承父会话的工具 | `Bash`、`Glob`、`Grep`、`Read`、`WebFetch`、`WebSearch`、`TodoWrite` |
| AGENTS.md | 注入 | 不注入 |
| 权限 | 继承父会话的模式与权限服务 | 固定 `yolo`，另配一份默认权限配置 |
| 出处 | `profile.ts:115` | `profile.ts:66` |

`general-purpose` 的提示词第一句是“You are an agent for ZCode CLI.”，随后要求把任务做完整、不镀金，结束时给出精简报告（`apps/zcode-cli/packages/core/src/subagent/general-purpose.ts:9`）。`Explore` 的提示词开头是一段只读禁令（`apps/zcode-cli/packages/core/src/subagent/explore.ts:35`）：

```ts
    "You are ZCode Explore, a file search and codebase research specialist for ZCode CLI. You excel at thoroughly navigating and exploring codebases.",
    "",
    "=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===",
    "This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:",
    "- Creating new files (no Write, touch, or file creation of any kind)",
    "- Modifying existing files (no Edit operations)",
    "- Deleting files (no rm or deletion)",
```

这份“只读”有多硬？工具白名单里去掉了所有写文件的工具，但留着 `Bash`，注释直说只读语义靠提示词约束（`apps/zcode-cli/packages/core/src/subagent/explore-tools.ts:1`）；而它的权限模式固定为 `yolo`（`apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts:484`、`subagent.ts:316`），各模式的语义见[权限模式与规则](https://daiw.org/manual/zcode/permission)。启用内嵌搜索时，`Glob`、`Grep` 换成经 `Bash` 的 `find`、`grep`（`explore-tools.ts:16`）。

所有子 Agent 都拿不到 `EnterPlanMode`、`ExitPlanMode`：子 Agent 没有独立的计划审批恢复面，`ExitPlanMode` 会等用户确认而卡住父回合（`apps/zcode-cli/packages/core/src/subagent/tool-policy.ts:19`）。`Task` 别名的说明直接写着“Claude Code-compatible”（`handlers/agent.ts:289`），画像的 `model` 字段又把 `sonnet`、`opus`、`haiku` 当作继承，从这两处看，是为了兼容照 Claude Code 写的画像和插件；Claude Code 自己的子代理见站内 [Claude Code 手册的 Agents 一篇](https://daiw.org/manual/claude-code/agents)。

## 自定义子 Agent：Markdown 画像

画像是带 frontmatter 的 Markdown 文件，正文就是系统提示词。bootstrap 先后递归扫描用户级和项目级两个目录（`apps/zcode-cli/packages/bootstrap/src/subagents.ts:54`），插件里的画像另行加载：

- 用户级：`~/.zcode/agents/` 下的 `.md`、`.markdown`，存储根默认 `~/.zcode`（`apps/zcode-cli/packages/contracts/src/config/index.ts:302`）。
- 项目级：工作目录下的 `.zcode/agents/`。
- 插件：插件根的 `agents/<名字>.md`，规范名是 `<插件名>:<名字>`；裸名在全局唯一、又不与内置名和已有画像重名时，额外登记一个别名（`bootstrap/src/subagents.ts:140`、`subagents.ts:160`）。

后读到的同名画像覆盖先读到的，所以项目级覆盖用户级，二者都覆盖内置。字段（`profile.ts:154`）：

| 字段 | 说明 |
| --- | --- |
| `name`、`description` | 必填，缺一个就报诊断并跳过 |
| `tools`、`disallowedTools` | 逗号或空白分隔，也可写成列表；`Bash(git:*)` 这类写法只取括号前的工具名（`profile.ts:346`） |
| `model`、`thoughtLevel` | 见下文 |
| `skills` | 允许的技能名；写了就自动补上 `Skill` 工具（`runner.ts:2133`） |
| `mcpServers` | 父会话里 MCP 服务器名的列表，写成映射会被拒 |
| `permissionMode` | 只认 `auto`、`plan`；项目级画像里的这一项被丢弃 |
| `memory` | `user`、`project`、`local`，见“持久记忆” |
| `maxTurns`、`background`、`injectAgentsMd`、`color` | 正整数、两个布尔值、八种颜色之一 |

项目级画像不能设 `permissionMode`，理由写在注释里：仓库内容不能借 frontmatter 改子运行时的权限，这一条在解析时和装配时各拦一次（`profile.ts:183`、`bootstrap/src/subagents.ts:115`）。一个示意：

```markdown
---
name: reviewer
description: Review the current diff and report correctness bugs with file paths
tools: Read, Grep, Glob, Bash
model: inherit
skills: code-review
color: purple
---
You are a careful code reviewer. Report findings, do not edit files.
```

几处以代码为准的细节：

- **`tools`**：省略、写 `*`、写成空列表，结果一样，都是继承父会话当前注册的全部工具（`profile.ts:276`、`subagent.ts:499`）；继承时去掉 `Agent`、`Task` 和各级禁用的工具，MCP 工具取自父会话启动时的快照（`subagent.ts:506`）。
- **`skills`**：从代码看，只写 `skills`、不写 `tools` 时，工具列表只剩自动补上的 `Skill`，不再继承父会话的工具（`runner.ts:2131`、`subagent.ts:520`），需要继承时要显式写 `tools: *`。
- **`model`**：写成 `providerId/modelId`，末尾可带 `$` 加推理档位，也可用 `custom:` 编码；`thoughtLevel` 单独给档位。值是 `inherit`、`main`、`sonnet`、`opus`、`haiku` 时按继承处理（`packages/shared/src/subagent-markdown-selection.ts:8`）。
- **`maxTurns`**：解析后作为子运行时的 `maxTurns` 传入，缺省是 4（`subagent.ts:269`），但在整个仓库里找不到读取这个配置的代码。从代码看，它目前不限制子 Agent 的轮数，这与站内 Claude Code 手册描述的“撞到上限返回部分完成”不同。
- **内置画像换模型**：写在 `~/.zcode/v2/agents-state.json` 的 `builtInModelSelectionOverrides` 里，只认 `general-purpose` 与 `Explore`；同一文件的 `disabledAgentIds` 只能停用用户级画像（`bootstrap/src/subagents.ts:256`、`subagents.ts:304`）。

子 Agent 最终用哪个模型，按这个顺序决定（`subagent.ts:100`，解析函数在 `apps/zcode-cli/packages/core/src/runtime/helpers/subagent-selection.ts:15`）：闲时轮等场景下发的单次覆盖；画像里显式写的模型，经宿主解析，解析失败直接报错，注释强调不能悄悄落回父模型；都没有时，继承父回合正在用的模型，包括推理档位；再退一步用父会话的模型选择。

桌面端设置页通过 `packages/services/src/subagents/subagentMarkdown.ts:105` 把表单写回同样格式的 Markdown，用户级目录只在桌面进程里可写（`packages/services/src/subagents/subagentsService.ts:275`）。表单没有 `memory` 字段，持久记忆只能手写。

## 子会话：一个新的 AgentRuntime

`runExploreAgent` 是子 Agent 的执行体。名字里的 Explore 是历史遗留，默认子 Agent 早已换成 `general-purpose`（`subagent.ts:277`）。它为子 Agent 新建一个 `AgentRuntime`，会话 ID 是 `sess_subagent_agent_` 加 UUID（`runner.ts:806`），配置的关键部分（`subagent.ts:264`）：

```ts
          subagentContext: {
            agentPrompt: agentPrompt ?? "",
            ...(agentsMdInstructions ? { userInstructions: agentsMdInstructions } : {}),
          },
          agentName: `zcode-${request.agentType}`,
          maxTurns: request.maxTurns ?? this.config.subagents?.maxTurns ?? 4,
          parentSessionId: this.sessionId,
          taskType: "subagent_child",
          // ...
          toolset: builtInExplore ? "explore" : "main",
          toolAllowlist: childToolAllowlist,
          toolDisallowlist: this.config.toolDisallowlist,
          embeddedSearchBackend: this.config.embeddedSearchBackend,
          nativeSearchEnhancementsEnabled: this.config.nativeSearchEnhancementsEnabled,
          subagents: {
            backgroundBashMaxMs: this.config.subagents?.backgroundBashMaxMs,
            enabled: false,
          },
          mcp: childMcpAccess.config,
```

`subagents.enabled: false` 让子运行时根本不注册 `Agent`，委派因此只有一层；`taskType` 为 `subagent_child` 时，定时任务与闲时任务的工具也不注册（`apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts:66`、`runtime-tools.ts:69`）。子与父的关系归纳如下：

| 方面 | 子运行时的做法 |
| --- | --- |
| 历史 | 从空开始，第一条输入就是 `prompt`，来源记为 `subagent`（`subagent.ts:399`） |
| 系统提示词 | 走 `SubagentContextBuilder`：CLI 前缀、画像正文与持久记忆、通用注意事项、环境信息，再加可选的 AGENTS.md、日期和技能清单（`apps/zcode-cli/packages/core/src/subagent/context-builder.ts:106`）；主会话的项目上下文不继承（`subagent.ts:262`） |
| 存储 | 与父共用事件库和会话库，子会话以 `parentID` 挂在父会话下 |
| 执行与网络 | 共用执行、文件系统、HTTP、产物存储端口和模型请求准入 |
| 权限 | 见上表；对外交互改道父会话，见下文 |
| MCP | 借用父会话的连接，不自己连 |
| 技能 | 过滤后的技能端口，只露出画像允许的技能，官方 Computer Use 技能一律拒绝（`subagent.ts:709`） |
| 钩子 | 从代码看，子运行时的配置和依赖里都没有传入钩子，`createRuntimeHookRunner` 返回空（`runtime-tools.ts:93`），子 Agent 内的工具调用不触发生命周期钩子 |

注意事项里有一条是给父 Agent 省事的：“Do NOT Write report/summary/findings/analysis .md files.”（`apps/zcode-cli/packages/core/src/subagent/system-prompt.ts:17`）。

子会话先落库，再对父会话发 `SubagentSpawned`。注释解释过，以前先发事件后落库，并发派生时目录查询会少读一个子会话（`subagent.ts:376`）。

MCP 的借用写在 `apps/zcode-cli/packages/core/src/subagent/borrowed-mcp-port.ts:9`：子端只看得到父会话启动快照里已连接、又在 `mcpServers` 范围内的服务器，官方 Computer Use 服务器被排除；`callTool` 转给父端口，连接、断开之类改变生命周期的调用一律抛错，`close` 什么也不做。画像点名的服务器没连上时，启动直接报 `Required MCP server is not connected`（`subagent.ts:593`）。MCP 本身见 [MCP](https://daiw.org/manual/zcode/mcp)。

## 前台、后台与转后台

`launch` 在请求带 `run_in_background` 或画像写了 `background: true` 时走 `start`，否则走 `run`（`runner.ts:147`）。两条路都先往运行时任务注册表登记一条 `local_agent` 任务，再写 `metadata.json`；输出目录是 `~/.zcode/cli/agents/<父会话>/<agentId>/`，里面有 `metadata.json`、`output.txt`、`task.output`（`runner.ts:808`，根目录见 `apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:256`）。

- **前台**：父回合的中止信号连到子任务，另有一个活动看门狗，子运行时持续没有任何事件超过 600000 毫秒就中止，缺省值取自模型流的空闲超时（`runner.ts:208`、`contracts/src/config/index.ts:284`）。结束后写输出、更新注册表、发 `SubagentStopped`，最终文本成为工具结果。
- **后台**：等子会话就绪就返回 `async_launched`，此后子任务与父回合脱钩，父回合结束或被中止都不会连带取消它。
- **转后台**：前台运行时同时在等“转后台”请求和一个 `autoBackgroundMs` 计时器，谁先到就把它转成后台（`runner.ts:293`）。但仓库里找不到调用 `backgroundTask` 的地方，也没有地方设置 `autoBackgroundMs`（`runner.ts:532`），从代码看这条路目前没有触发方。
- **闲时轮**：单次执行的模型与鉴权不能脱离父回合进入后台，后台请求会被拒，提示改用前台（`runner.ts:150`），详见[定时任务与闲时任务](https://daiw.org/manual/zcode/cron-offpeak)。

一次前台调用里父子的往来如下：

```mermaid
sequenceDiagram
  participant M as 父回合
  participant P as SubagentPort
  participant C as 子 AgentRuntime
  participant U as 用户界面
  M->>P: 调用 Agent 工具
  P->>C: 新建运行时，子会话先落库
  P-->>M: SubagentSpawned 事件
  C->>C: executeTurn，输入为 prompt
  C-->>M: 工具事件镜像，source 为 subagent
  C->>U: 审批或问卷，改写为父会话
  U-->>C: 用户的回答
  M->>C: SendMessage，作为引导插入子回合
  C->>M: RespondToCoordinator，进父命令队列
  C-->>P: 子回合结束
  alt 前台
    P-->>M: 最终文本作为工具结果
  else 后台
    P-->>M: 启动后已返回 async_launched，完成后发 task-notification
  end
```

后台子 Agent 与后台 Bash 登记在同一张运行时任务注册表里，查看和停止走统一的后台任务接口，见[后台任务与通知](https://daiw.org/manual/zcode/background-tasks)。

## 父子之间的消息

四个名字相近的工具分两组：

| 工具 | 方向 | 谁能用 | 要点 |
| --- | --- | --- | --- |
| `SendMessage` | 父到子 | 有子 Agent 端口的会话 | `to` 填 `agentId`；超时 10000 毫秒；闲时轮拒绝（`apps/zcode-cli/packages/core/src/tool/handlers/send-message.ts:50`） |
| `RespondToCoordinator` | 子到父 | 只有 `subagent_child` | 自动补进子 Agent 的工具白名单，不受画像工具列表约束（`subagent.ts:541`） |
| `submit_result` | 工作流 actor 到引擎 | 注入了工作流提交端口的会话 | 提交本次 ask 的结构化结果，成功即结束 actor 的回合 |
| `escalate` | 工作流 actor 到主代理 | 注入了升级端口的会话 | 真卡住时提问并阻塞等待，每个 ask 最多 3 次 |

后两个只属于动态工作流的 actor，普通子 Agent 拿不到，注册门是端口存在与否（`runtime-tools.ts:58`、`runtime-tools.ts:64`），细节见[动态工作流（二）](https://daiw.org/manual/zcode/dwf-engine)和[动态工作流（三）](https://daiw.org/manual/zcode/dwf-tools)。

`SendMessage` 的去向取决于目标状态（`runner.ts:900`）：子 Agent 还在跑，就调它的 `steerTurn`，以 `guide` 方式插入正在进行的回合，碰到“没有活动回合”每隔 10 毫秒重试，连同最后一次共 21 次（`apps/zcode-cli/packages/core/src/subagent/message-steering.ts:29`）；接收端还没就绪或者送不进去，消息先存进注册表，结果为 `queued`，接收端就绪时补发（`runner.ts:936`、`runner.ts:1401`）；子 Agent 已经结束，就从会话库恢复它，把消息当作新输入在后台再跑一轮，结果是 `resumed_background`，跑完照常通知（`runner.ts:955`）。

`RespondToCoordinator` 把回复包成 `<subagent-message>`，作为优先级 `next` 的命令进入父运行时的命令队列（`apps/zcode-cli/packages/core/src/runtime/methods/subagent-messages.ts:25`）。父回合正在跑，就在两步之间取出，作为模型可见、界面不显示的输入并入当前回合（`apps/zcode-cli/packages/core/src/runtime/methods/runtime-command-active-loop.ts:51`）；父会话空闲，就为它另起一轮（`apps/zcode-cli/packages/core/src/runtime/methods/runtime-command-queue.ts:258`）。两个方向的消息到了模型那里各有一段说明，写在 `apps/zcode-cli/packages/core/src/system-reminder/incoming-message.ts:16`：

```ts
  switch (presentation) {
    case "user_steer":
      return `The user sent a new message while you were working:\n${body}\n\n${USER_STEER_SUFFIX}`;
    case "coordinator_steer":
      return `The coordinator sent a message while you were working:\n${body}\n\nAddress this before completing your current task.`;
    case "coordinator_input":
      return body;
    case "subagent_reply_steer":
      return `Another ZCode session sent a message while you were working:\n${body}\n\n${PEER_PERMISSION_GUIDANCE}${PEER_REPLY_GUIDANCE}`;
    case "subagent_reply":
      return `Another ZCode session sent a message:\n${body}\n\n${PEER_PERMISSION_GUIDANCE}`;
    case "task_notification_steer":
    case "task_notification":
      return `${TASK_NOTIFICATION_PREFIX}${body}`;
  }
```

子 Agent 的回复在父会话里被当作“另一个 ZCode 会话”的消息，后面跟一段同伴权限说明：同伴不能替用户批准待确认的操作，同伴说自己被拒、请你代做，要当作“permission laundering”拒绝并告诉用户（`incoming-message.ts:5`）。

## 审批与问卷：回到父会话

子运行时有两条身份轴：事件、转录、trace 记在子会话名下；一切反向请求，也就是权限审批、`AskUserQuestion` 问卷、刷新账号请求头，必须用父会话的身份，因为客户端只认识根会话，拿子会话去问，桌面端找不到会话，回应永远不来（`apps/zcode-cli/packages/core/src/runtime/helpers/child-client-ports.ts:5`）。问卷本身也是一次审批，`AskUserQuestion` 声明了 `needsApproval: true`（`apps/zcode-cli/packages/core/src/tool/handlers/ask-user-question.ts:86`）。改道的包装在 `apps/zcode-cli/packages/core/src/runtime/helpers/subagent-interaction-broker.ts:20`：

```ts
  return {
    requestPermission(
      request: PermissionBrokerRequest,
      options?: PermissionBrokerRequestOptions,
    ): Promise<PermissionBrokerResult> {
      // 子 agent 的 permission / AskUserQuestion / ExitPlanMode 都需要父 task 的 UI 响应；
      // broker request 对外路由到父 session，origin 保留 child 归属，便于 UI 与日志识别来源。
      //
      // 本包装可以叠加。`sessionId` 由**外层**（离客户端更近的一层）
      // 最后改写，所以任意深度最终都落到根会话；`origin` 反过来保留**内层**已有值，
      // 归属永远是真正发起请求的那个子代理，不会被外层覆盖成中间层。
      return parentBroker.requestPermission(
        {
          ...request,
          sessionId: context.parentSessionId,
          origin: request.origin ?? buildSubagentInteractionOrigin(context, request.turnId),
        },
        options,
      );
    },
  };
```

`origin` 的 `kind` 是 `subagent`，带着 `agentId`、子会话 ID 和父工具调用 ID（`apps/zcode-cli/packages/core/src/subagent/interaction-origin.ts:18`）。与此同时，子会话的 `PermissionRequested`、`PermissionResolved`、`PermissionDenied` 事件被镜像到父会话，协议层只从父会话的实时投影生成阻塞交互（`apps/zcode-cli/packages/core/src/subagent/tool-event-mirror.ts:60`）。工具调用事件也会镜像，工具调用 ID 改写成 `tool_subagent_<agentId>_<子调用 ID>`，打上 `source: "subagent"`（`tool-event-mirror.ts:132`），父时间线因此只看到子 Agent 的工具活动，看不到它的正文。审批规则本身见[权限模式与规则](https://daiw.org/manual/zcode/permission)。

## 完成通知

后台子 Agent 跑完、失败或被停止，`runner.ts` 写好输出文件、更新注册表，再生成一条通知交给父运行时（`runner.ts:1494`、`runner.ts:1830`）。通知是 XML 片段，含 `task-id`、`tool-use-id`、`output-file`、`status`、`summary`、`result` 或 `error` 以及用量，整条超过 120000 个字符就截断（`apps/zcode-cli/packages/core/src/runtime-task/notification.ts:144`、`notification.ts:18`）。它以 `task-notification` 命令、优先级 `next` 进入父会话的命令队列（`apps/zcode-cli/packages/core/src/runtime/methods/background-notifications.ts:45`），同一批通知合并成一个模型轮（`runtime-command-queue.ts:43`），送到模型时前面加一段 `[SYSTEM NOTIFICATION - NOT USER INPUT]` 的声明，提醒它这不是用户的回复或确认（`incoming-message.ts:9`）。每个任务只通知一次，注册表上记 `notified`；会话回退到别的分支后，迟到的通知按分支代数丢弃（`background-notifications.ts:33`）。

## 持久记忆

画像写了 `memory`，而会话开启了记忆（`memory.enabled` 为真、`memory.use` 不为假）时，子 Agent 就有一个自己的记忆目录（`apps/zcode-cli/packages/core/src/subagent/persistent-memory.ts:17`）：

| `memory` | 目录 |
| --- | --- |
| `user` | `~/.zcode/agent-memory/<画像名>` |
| `project` | `<工作区>/.zcode/agent-memory/<画像名>`，随版本库共享 |
| `local` | `<工作区>/.zcode/agent-memory-local/<画像名>` |

目录启动时自动建好，其中的 `MEMORY.md` 读进系统提示词，排在画像正文之后（`subagent.ts:141`）；画像列了 `tools` 时，自动补上 `Write`、`Edit`，好让它写记忆（`persistent-memory.ts:39`）。提示词定义了 user、feedback、project、reference 四类记忆，要求一条记忆一个文件、`MEMORY.md` 只做一行一条的索引，并说明索引 200 行之后会被截断（`apps/zcode-cli/packages/core/src/subagent/persistent-memory-prompt.ts:110`）。主会话的项目记忆是另一套机制，见[项目记忆](https://daiw.org/manual/zcode/memory)。

## TUI 里观察子 Agent

`apps/zcode-cli/packages/tui/SUBAGENTS.md` 写了约定，代码与之相符：右侧栏列出当前主会话的子 Agent，含已结束的；选中后左栏换成它的只读转录，主运行时照常跑，`Esc` 返回并恢复草稿和滚动位置（`SUBAGENTS.md:3`）。主转录拒收带 `source: subagent` 的镜像事件（`SUBAGENTS.md:6`）；目录只在生命周期事件后刷新，不轮询，也不为每个 token 查历史（`apps/zcode-cli/packages/tui/src/app-subagent-events.ts:19`）。目录由 bootstrap 从会话库投影，只收 `parentID` 指向当前会话、`taskType` 为 `subagent_child` 的会话，已结束的分页返回，默认 20 条、最多 100 条（`apps/zcode-cli/packages/bootstrap/src/app/subagent-observation.ts:48`、`subagent-observation.ts:82`）。界面细节见[终端界面](https://daiw.org/manual/zcode/tui)。

下一篇：[目标模式：让 Agent 做到完成为止](https://daiw.org/manual/zcode/goal-target)——给会话立一个目标，运行时怎样一轮接一轮地续跑，又由谁来判定“做完了”。
