# AgentRuntime：端口、依赖与方法装配

> 一个会话对应一个 AgentRuntime：它身上的八十多个状态字段、构造时注入的四十九项依赖，近百个方法文件怎样用声明合并加原型安装装到同一个类上，AgentRuntimeInternal 的作用，trace 上下文如何从根一路传到模型请求，以及 core 包对外导出了什么。

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

ZCode 的 Agent 运行时住在 `apps/zcode-cli/packages/core/src/runtime`：163 个文件、约 3.7 万行，全部围绕一个类 `AgentRuntime`。终端 TUI、`-p` 无头模式、桌面端与 Web 背后的协议服务，最终都是拿到一个 `AgentRuntime` 实例，调它的方法、订阅它的事件。它自己不碰具体实现：文件、子进程、网络、存储与模型大多经“端口”注入，端口的接口声明在 `apps/zcode-cli/packages/contracts`（`src/interfaces` 下有 25 个 `*.port.ts`），实现在 `adapters`，由 `bootstrap` 组装。

这一篇只讲这个类本身：一个实例对应什么、身上挂着哪些状态、要哪些依赖、近百个方法文件怎样装到同一个类上，以及 trace 上下文怎么一路传下去。回合怎么跑、输入怎么受理、事件怎么落库，分别见[回合循环](https://daiw.org/manual/zcode/turn-loop)、[输入受理](https://daiw.org/manual/zcode/prompt-admission)与[会话事件流](https://daiw.org/manual/zcode/session-events)。

## 目录分工

| 位置（相对 `core/src/runtime`） | 内容 |
| --- | --- |
| `agent-runtime.ts` | 类本体：82 个私有字段、构造器、两个关闭方法，以及同名 `interface` 声明的公开方法 |
| `methods/` | 97 个文件、约 2.56 万行；每个文件导出若干以 `this` 为首参的函数，由 `methods/index.ts` 安装到原型上 |
| `helpers/` | 53 个文件、约 7900 行；显式接收 `runtime` 参数或完全无状态的辅助函数 |
| `internal.ts`、`internal-methods.ts`、`internal-turn-methods.ts`、`internal-hook-methods.ts` | `AgentRuntimeInternal`：给方法文件用的内部视图 |
| `types.ts` | `AgentRuntimeConfig`、`AgentRuntimeDeps` 与各方法的入参、出参类型 |
| `deps.ts` | 统一依赖出口：从 `@zcode/contracts` 与 core 其他目录再导出，外加两个 trace 包装函数 |
| `execution-state.ts`、`session-mode-port.ts`、`model-selection.ts` | 权限模式与 Plan 开关、给工具用的模式端口、模型选择的防御性拷贝 |
| `command-queue.ts` | 运行时命令队列，见[输入受理](https://daiw.org/manual/zcode/prompt-admission) |
| `permission-full-access.ts`、`permission-grant-recovery.ts` | 审批时一键切换完全访问，见[权限模式与规则](https://daiw.org/manual/zcode/permission) |

## 一个会话，一个实例

构造器签名是 `constructor(sessionId, config, deps)`（`apps/zcode-cli/packages/core/src/runtime/agent-runtime.ts:228`）。`sessionId` 只在构造时赋值一次（`agent-runtime.ts:230`），类上没有任何改写它的方法，`getSessionId()` 原样返回它（`apps/zcode-cli/packages/core/src/runtime/methods/config.ts:255`），所以一个实例从生到死只服务一个会话。bootstrap 里有一段注释把这层关系写得很直白：`ZCodeApp`、`AgentRuntime`、`sessionId` 两两 1:1（`apps/zcode-cli/packages/bootstrap/src/app/dynamic-workflow-run-progress-sink.ts:27`）。

整个仓库里 `new AgentRuntime(` 只出现在四处：

| 位置 | 创建的是 | 事件存储 |
| --- | --- | --- |
| `apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:726` | 每个 `ZCodeApp` 的主会话 | 宿主传入，缺省新建内存存储（`create-app.ts:730`） |
| `apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts:239` | 子 Agent 的子会话 | 与父共用（`subagent.ts:296`） |
| `apps/zcode-cli/packages/bootstrap/src/app/script-workflow-child-runtime.ts:111` | 动态工作流 actor 与旧脚本工作流的子会话 | 与父共用（`script-workflow-child-runtime.ts:191`） |
| `apps/zcode-cli/packages/bootstrap/src/app/workflow-facade.ts:283` | 专家工作流每个活动的子会话 | 另建一个内存存储（`workflow-facade.ts:299`） |

一个进程里可以同时存在多个实例。TUI 同一时刻只持有一个 `ZCodeApp`，换会话时新建、再关掉旧的（`apps/zcode-cli/packages/cli/src/tui-prompt-handler.ts:63`、`tui-prompt-handler.ts:123`）；桌面端与 Web 拉起的协议服务则用一张 `Map` 管着多条会话记录（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server.ts:260`），每条记录各有自己的事件存储、`ZCodeApp` 与 runtime（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server-operations.ts:3300`、`server-operations.ts:3323`），空闲的由常驻池回收，见[会话事件流](https://daiw.org/manual/zcode/session-events)的“会话驻留”一节。

## 它持有什么

82 个字段全部声明为 `private`（`agent-runtime.ts:133` 到 `agent-runtime.ts:226`），都只在内存里；需要跨进程存活的部分写进 `SessionStorePort`，冷启动时由 `resumeFromStore` 读回，见 [SQLite 会话库](https://daiw.org/manual/zcode/session-store)。按用途归纳：

| 类别 | 代表字段 |
| --- | --- |
| 身份与配置 | `sessionId`、`turnNumber`、`config`、`appVersion`、`workingDirectory`（Bash `cd` 会改）、`workspaceRoot`（不随 `cd` 漂移）、`sessionModelSelection` |
| 端口与协作对象 | `permissionService`、`permissionBroker`、`toolScheduler`、`registry`、`executor`、`hookRunner`、`modelFactory`、`executionPort`、`fileSystemPort`、`mcpPort`、`subagentPort` 等二十余个 |
| 对话与上下文 | `messageHistory`、`readFileState`、`cachedTools`、`contextBuilder`、`contextInitialized`、`contextSourceSnapshot`、`memoryRoot` |
| 事件与观测 | `eventReducer`、`eventStore`、`eventSinks`（一个 `Set`）、`rootTraceContext`、`logger`、`agentTelemetry` |
| 调度与回合 | `runtimeCommandQueue`、`runtimeCommandDrainActive`、`activeForegroundExecution`、`foregroundPromotionLease`、`activeTurn`、`activeTurnStartReservation`、`pendingInputReservations`、`queueAutoDrain` |
| 持久化与投影标记 | `sessionPersisted`、`latestConversationMessageId`、`latestAssistantTurnId`、`pendingModelChangeTimeline`、`sessionTitleGenerationAttempted` |
| 计数与保护 | `mainTurnCacheHitAggregate`、`currentTurnFileChanges`、`autoCompactConsecutiveFailures`、`branchGeneration`（回退后丢弃旧分支的后台结果） |
| 生命周期 | `mcpStartupPromise`、`residencyBlockingWorkCount`、`shuttingDown`、`backgroundTaskNotificationsSealed` |

## 依赖哪些 Port

`AgentRuntimeDeps` 共 49 项（`apps/zcode-cli/packages/core/src/runtime/types.ts:309`），只有事件存储 `eventStore` 与模型工厂 `modelFactory` 必填（`types.ts:314`、`types.ts:317`），其余缺席就意味着对应能力不存在。下表中端口接口所在文件都在 `apps/zcode-cli/packages/contracts/src/interfaces/` 下，另注明的除外：

| 类别 | 依赖 | 接口定义 |
| --- | --- | --- |
| 会话与事件 | `eventStore`、`eventSink`、`sessionStore`、`sessionMailboxPort` | `session.port.ts:57`、`session.port.ts:76`、`session-store.port.ts:1089`、`session-mailbox.port.ts:12` |
| 模型 | `modelFactory`、`providerRuntimeHeadersPort`、`modelRequestAdmission`、`modelCatalogPort`、`resolveEffectiveModelSelection`、`modelIoDir` | `types.ts:386`、`types.ts:395`（core）；`apps/zcode-cli/packages/contracts/src/model/index.ts:74`；`model-catalog.port.ts:29` |
| 本地 I/O | `executionPort`、`fileSystemPort`、`httpClientPort`、`imageProcessorPort`、`pdfDocumentPort`、`artifactStore`、`browserControlPort` | `execution.port.ts:278`、`file-system.port.ts:280`、`http-client.port.ts:89`、`image-processor.port.ts:78`、`pdf-document.port.ts:41`、`tool-artifact-store.port.ts:106`、`browser-control.port.ts:537` |
| 上下文与扩展 | `contextSourcePort`、`skillPort`、`mcpPort`、`contextBuilder`、`memoryRoot` | `context-source.port.ts:92`、`apps/zcode-cli/packages/contracts/src/skills/index.ts:108`、`mcp.port.ts:290` |
| 权限与钩子 | `permissionService`、`permissionBroker`、`hookRunner`、`workspaceHookAdmission`、`workspaceHookSnapshot` | `permission.port.ts:95`；其余是 core 自己的类型 |
| 工具装配 | `toolRegistry`、`toolExecutor`、`toolScheduler`、`runtimeTaskRegistry` | core 内部类型，缺省在构造器里新建 |
| 多 Agent 与工作流 | `subagentPort`、`coordinatorResponsePort`、`workflowPort`、`workflowSubmitPort`、`workflowSubmitSchema`、`workflowEscalatePort`、`dynamicWorkflowRunPort`、`dynamicWorkflowSnippetPort` | `subagent.port.ts:113`、`coordinator-response.port.ts:18`、`workflow.port.ts:36`、`workflow-submit.port.ts:45`、`workflow-escalate.port.ts:47`、`dynamic-workflow-run.port.ts:600`、`dynamic-workflow-snippet.port.ts:49` |
| 自动化 | `automationPort`、`offPeakPort` | `automation.port.ts:43`、`off-peak.port.ts:47` |
| 观测与环境 | `agentTelemetry` 及其两项因果配置、`logger`、`traceContext`、`appVersion`、`now`、`isRemoteWorkspace` | `apps/zcode-cli/packages/contracts/src/telemetry/agent-execution.ts:277` |

有一部分端口 runtime 自己并不保存，只在构造时转交给工具执行器，例如 `httpClientPort`、`automationPort`、`offPeakPort`、三个工作流端口（`apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts:154`）。端口在不在场，也直接决定注册哪些内置工具（`runtime-tools.ts:49`）：

```ts
  registerBuiltInTools(runtime.registry, {
    bashTimeoutPolicy: runtime.config.bashTimeoutPolicy,
    includeSkill: Boolean(runtime.skillPort),
    includeAgent: Boolean(runtime.subagentPort),
    includeSendMessage: runtime.subagentPort?.sendMessage !== undefined,
    includeRespondToCoordinator:
      runtime.config.taskType === "subagent_child" && Boolean(deps.coordinatorResponsePort),
    // submit_result 只在注入了 workflowSubmitPort 的 workflow actor 会话注册。以端口存在为门，
    // 与 taskType 无关：workflow actor 是 workflow_child，其 runtimeScope 目前是 "main"。
    includeSubmitResult: Boolean(deps.workflowSubmitPort),
    // ...
    includeWorkflow: Boolean(deps.workflowPort),
    includeAutomation: Boolean(deps.automationPort) && runtime.config.taskType !== "subagent_child",
```

例外写在注释里：十个动态工作流工具的端口在任何 CLI 里都装配齐全，灰度由宿主决定，所以不看端口，看 `config.dynamicWorkflowEnabled`（`runtime-tools.ts:70`）；`node_repl` 与浏览器控制也不因宿主给了 `browserControlPort` 就暴露，而是看官方插件推导出的 `runtimeFeatures`（`runtime-tools.ts:74`）。

端口的边界并不绝对。`apps/zcode-cli/AGENTS.md` 要求业务模块不直接调用 `fs`、`child_process`、`process.env` 等底层 API（`apps/zcode-cli/AGENTS.md:53`），但 `core/src` 里仍有 13 个文件直接 import 了 `node:fs`（直接起子进程的一个也没有），例如 `apps/zcode-cli/packages/core/src/runtime/methods/bash-shell-snapshot.ts:1`，多数是 Bash、任务输出与工作流相关的工具，另有浏览器客户端、钩子与子 Agent 的几处。端口的实现从哪来、怎样按配置组装，见下一篇 [bootstrap](https://daiw.org/manual/zcode/bootstrap-assembly)。

## 近百个方法文件怎样装到一个类上

类文件只有 664 行，因为方法体都不在里面。`agent-runtime.ts` 先声明一个只有字段和构造器的 `class`，再声明一个同名的 `interface` 列出公开方法，两者被 TypeScript 合并成同一个类型；文件末尾一行把实现装上原型（`agent-runtime.ts:131`）：

```ts
// oxlint-disable typescript-eslint/no-unsafe-declaration-merging
export class AgentRuntime {
  private sessionId: SessionId;
  // ...
}

export interface AgentRuntime {
  lastPermissionGrantId?: string;
  beginShutdown(): void;
  // ...
  getMode(): CollaborationMode;
  // ...
}

installAgentRuntimeMethods(AgentRuntime);
```

安装函数就是一长串赋值（`apps/zcode-cli/packages/core/src/runtime/methods/index.ts:197`）：

```ts
type AgentRuntimeConstructor = { prototype: object };

export function installAgentRuntimeMethods(ctor: AgentRuntimeConstructor): void {
  const proto = ctor.prototype as Record<string, unknown>;
  proto.updateConfig = updateConfig;
  proto.setExecutionState = setExecutionState;
  proto.grantPermissionFullAccess = grantPermissionFullAccess;
  proto.initializeSessionShellEnvironmentIfNeeded = initializeSessionShellEnvironmentIfNeeded;
  proto.getSessionShellSelection = getSessionShellSelection;
  proto.getMode = getMode;
  // ...
  proto.isProjectMemoryEnabled = isProjectMemoryEnabled;
}
```

被安装的都是普通函数，用 TypeScript 的 `this` 参数声明调用者类型，例如 `export function getMode(this: AgentRuntimeInternal)`（`config.ts:86`）。一共装了 187 个方法，来自 46 个方法模块和另外两个文件（`permission-full-access.ts` 与 `helpers/project-memory-extraction.ts`）。其中 87 个出现在公开 `interface` 里，再加上类体里的 `beginShutdown` 与 `closeBrowserSession`，公开方法共 89 个；另外 100 个只给内部互相调用。方法文件里还有两种不安装的写法：只在模块内用的 `this` 函数经 `.call(this, …)` 调用，如 `runRuntimeCommand.call(this, firstCommand)`（`apps/zcode-cli/packages/core/src/runtime/methods/runtime-command-queue.ts:61`）；显式收 `runtime` 参数的函数，如 `applyRuntimeExecutionState(runtime, input, cause)`（`apps/zcode-cli/packages/core/src/runtime/execution-state.ts:43`）。

为什么这么拆？CLI 的 `AGENTS.md` 规定单个源文件默认不超过 400 行（`apps/zcode-cli/AGENTS.md:12`）；`internal-hook-methods.ts` 开头的注释说，它是为了让 `internal-methods.ts` 不越过 400 行、通过一个名为 runtime-module-boundary 的测试而单独拆出来的（`apps/zcode-cli/packages/core/src/runtime/internal-hook-methods.ts:5`）。这个测试不在开源仓库里，全仓只有 4 个测试文件，都不在 `apps/zcode-cli`。规则也没有被严格执行：97 个方法文件里有 22 个超过 400 行，最大的 `session-fork.ts` 有 1487 行、`steering.ts` 有 1403 行，`types.ts` 则干脆关掉了 `max-lines` 检查（`types.ts:2`）。

代价是编译器不再替你核对“原型上真的有这个方法”，这正是被关掉的那条 lint 规则要防的事。从代码看，新增一个方法要改三处：方法文件、`methods/index.ts` 的安装行、内部或公开的方法声明，漏掉安装行只会在运行时以“不是函数”的形式暴露。

## AgentRuntimeInternal：内部视图

字段是 `private` 的，类体之外的函数按理读不到。`AgentRuntimeInternal`（`apps/zcode-cli/packages/core/src/runtime/internal.ts:60`）把同样的字段以公开属性重新声明一遍，并继承三组方法声明：`AgentRuntimeCoreMethods`、`AgentRuntimeTurnMethods`、`AgentRuntimeHookMethods`，分别在 `internal-methods.ts`、`internal-turn-methods.ts`、`internal-hook-methods.ts`。方法文件一律把 `this` 声明成它，于是可以随意读写状态、互相调用内部方法；bootstrap 与协议层拿到的仍是合并后的 `AgentRuntime` 类型，看不到私有字段，也看不到那 100 个内部方法。

由于同名字段一边是 `private`、一边是公开属性，两个类型互不兼容，构造器只能先做一次 `this as unknown as AgentRuntimeInternal` 的双重断言（`agent-runtime.ts:229`），再调用 `createDefaultSubagentPort`、`startMcpStartup` 等内部方法。内部视图比类多出 4 个字段：`sessionMailboxPort`、`permissionFullAccessPending`、`pendingInputDrains`、`lastPermissionGrantId`（`internal.ts:117`、`internal.ts:136`）。后三个是运行中动态挂上的，例如 `permissionFullAccessPending` 在 `apps/zcode-cli/packages/core/src/runtime/permission-full-access.ts:34` 置位；`sessionMailboxPort` 则只有声明、从未被赋值，邮箱端口实际直接从 `deps` 读（`runtime-tools.ts:109`）。还有些状态干脆不挂在实例上，而是放进以实例为键的 `WeakMap`，例如“事务已提交、事件尚未发布”的权限授予（`apps/zcode-cli/packages/core/src/runtime/permission-grant-recovery.ts:4`）。

## 构造时做了什么

构造器依次做配置投影、建缺省对象、派生根 trace 与子 logger、保存端口、新建消息历史与命令队列、装配工具，最后提前启动 MCP，主干如下（`agent-runtime.ts:228`）：

```ts
  constructor(sessionId: SessionId, config: AgentRuntimeConfig, deps: AgentRuntimeDeps) {
    const runtime = this as unknown as AgentRuntimeInternal;
    this.sessionId = sessionId;
    this.turnNumber = 0;
    // ...
    this.permissionBroker = deps.permissionBroker ?? createDenyPermissionBroker();
    // ...
    this.eventReducer = new EventReducer();
    this.eventStore = deps.eventStore;
    this.sessionStore = deps.sessionStore;
    this.rootTraceContext = deps.traceContext ?? createRootTraceContext({ sessionId });
    // ...
    this.subagentPort = deps.subagentPort ?? runtime.createDefaultSubagentPort(deps);
    // ...
    const tooling = initializeRuntimeTooling(runtime, deps, sessionId);
    this.hookRunner = tooling.hookRunner;
    this.workspaceHookAdmission = deps.workspaceHookAdmission;
    this.executor = tooling.executor;

    this.contextBuilder = deps.contextBuilder ?? null;
    if (this.contextBuilder) {
      runtime.initializeMessageHistoryFromContext(this.contextBuilder, this.rootTraceContext);
      this.contextInitialized = true;
    }
    runtime.startMcpStartup(this.rootTraceContext);
  }
```

几个值得注意的缺省：

- **审批缺省拒绝**。宿主不注入 `permissionBroker` 时用 `DenyPermissionBroker`，任何需要问人的工具调用都会以“No permission client configured”被拒（`apps/zcode-cli/packages/core/src/permission/broker.ts:24`）。
- **上下文预算策略被强制改写**。`AgentRuntimeConfig` 允许传 `"legacy"` 或 `"preflight-v1"`（`types.ts:130`），但构造器无论传什么都覆盖成共享默认值（`agent-runtime.ts:232`），即 `"preflight-v1"`（`packages/shared/src/zcode-protocol/index.ts:1696`）；注释说 3.12.2 仍接受旧宿主传入 `legacy`，但运行时、日志与子 Agent 只用 preflight。
- **模型选择可以缺席**。旧会话恢复时可能没有完整选择，构造器不替它挑默认模型（`agent-runtime.ts:274`），真正开跑时才由 `createRuntimeModel` 拒绝（`apps/zcode-cli/packages/core/src/runtime/methods/runtime-model.ts:21`）。
- **MCP 在构造时就开始连**。`startMcpStartup` 立即发起配置里各服务器的连接，并登记为阻止会话回收的在飞工作（`apps/zcode-cli/packages/core/src/runtime/methods/mcp.ts:111`），首轮回合开始时多半已经连好，细节见 [MCP](https://daiw.org/manual/zcode/mcp)。

## trace 上下文怎么传下去

CLI 的 `AGENTS.md` 对可观测性的要求很硬：所有任务执行都携带可传播的 `traceId`，它默认对应一次顶层会话的完整任务链，子会话、子 Agent、重试与后台任务都归属同一个 `traceId`；`sessionId`、`turnId`、`spanId` 等是它之下的结构化子标识（`apps/zcode-cli/AGENTS.md:74`、`apps/zcode-cli/AGENTS.md:75`）。上下文对象定义在 `apps/zcode-cli/packages/contracts/src/tracing/tracer.ts:14`，字段是 `traceId`、`queryId`、`spanId`、`parentSpanId`、`parentId`、`sessionId`、`turnId` 与自由属性 `attributes`。派生子上下文沿用 `traceId`、生成新的 `spanId`、把父 `spanId` 记成 `parentSpanId`，并合并属性（`tracer.ts:212`）：

```ts
export function createChildTraceContext(
  parent: TraceContext,
  options: {
    queryId?: QueryId;
    sessionId?: SessionId;
    turnId?: TurnId;
    attributes?: Record<string, string | number | boolean>;
  } = {},
): TraceContext {
  return {
    traceId: parent.traceId,
    queryId: options.queryId ?? parent.queryId,
    spanId: generateSpanId(),
    parentSpanId: parent.spanId,
    parentId: parent.spanId,
    sessionId: options.sessionId ?? parent.sessionId,
    turnId: options.turnId ?? parent.turnId,
    attributes: {
      ...parent.attributes,
      ...options.attributes,
    },
  };
}
```

`deps.ts` 在这个函数和 `traceContextToLogContext` 外面又各包了一层，显式把 `queryId` 带上（`apps/zcode-cli/packages/core/src/runtime/deps.ts:18`、`deps.ts:34`），runtime 内部一律用包装后的版本。一条消息的 trace 链路是这样串起来的：

```mermaid
flowchart TD
  H["宿主请求携带的 traceId 与 spanId，可缺省"] --> R["协议层根上下文"]
  R --> A["ZCodeApp.traceContext"]
  A --> RT["AgentRuntime.rootTraceContext"]
  RT --> L["子 logger 绑定 trace 字段"]
  RT --> T["回合上下文：turnId、queryId、turnNumber"]
  T --> ALS["AsyncLocalStorage"]
  T --> M["模型步上下文：providerId、modelId、iteration"]
  T --> G["guide 注入后切换 queryId"]
  ALS --> E["工具与钩子发事件时取当前上下文"]
  T --> S["子 Agent 的 runtime 沿用同一 traceId"]
```

- **根**。协议层建会话记录时，如果宿主请求带了 `traceId` 与 `spanId` 就沿用，否则新生成一个 UUID（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server-types.ts:274`）；进程内的 TUI 由 `createZCodeApp` 自己新建（`create-app.ts:153`）。runtime 拿到后存进 `rootTraceContext`，并用它派生带 `module: "core.runtime"` 的子 logger（`agent-runtime.ts:258`、`agent-runtime.ts:260`）。
- **回合**。受理输入时派生回合上下文，写入新的 `turnId`、`queryId` 与属性 `turnNumber`（`apps/zcode-cli/packages/core/src/runtime/methods/prompt-admission.ts:88`），整个回合在 `runWithContextAsync` 里执行（`apps/zcode-cli/packages/core/src/runtime/methods/turn.ts:184`），底层是 `AsyncLocalStorage`（`tracer.ts:166`）。
- **模型步**。每次模型请求再派生一层，带上 `providerId`、`modelId`、`iteration` 与 `querySource`（`apps/zcode-cli/packages/core/src/runtime/methods/turn-model-step.ts:158`）；标题生成这类旁路请求同样从回合上下文派生。回合中途注入一条 guide 后，后续请求改挂在那条输入的 `queryId` 上（`apps/zcode-cli/packages/core/src/runtime/methods/turn-guide-drain.ts:52`）。
- **隐式取用**。工具执行器、钩子运行器发事件时没有显式上下文，一律取 `getCurrentTraceContext() ?? runtime.rootTraceContext`（`runtime-tools.ts:99`、`runtime-tools.ts:161`）。
- **落到事件与子会话**。事件本身只记 `traceId` 与 `turnId`（`apps/zcode-cli/packages/core/src/runtime/methods/events.ts:69`），`spanId` 这一层只进日志；子 Agent 的 runtime 直接拿请求里的 trace 作为自己的根（`subagent.ts:366`），与父会话共享 `traceId`。

## 执行状态、模式端口与模型选择

**执行状态**只有两个字段：权限模式 `mode`（`build`、`edit`、`yolo` 与内部用的 `auto`；`plan` 只在读旧格式时接受）和 Plan 开关 `planEnabled`（`packages/shared/src/execution-state.ts:3`）。修改统一走 core 的 `applyRuntimeExecutionState`（`apps/zcode-cli/packages/core/src/runtime/execution-state.ts:43`）：完全访问的切换进行中则拒绝；Plan 与进行中的 Goal 不能同时生效；先把状态写成会话条目 `runtime/execution_state`，成功后才改内存，最后发 `SessionModeChanged` 事件，注释写明“保存失败不发布成功快照，也不提前改内存”（`runtime/execution-state.ts:42`）。

**模式端口** `createRuntimeSessionModePort`（`apps/zcode-cli/packages/core/src/runtime/session-mode-port.ts:5`）把进出 Plan 模式包装成端口交给工具执行器（`runtime-tools.ts:193`），Plan 相关工具经它切换状态，不直接碰 runtime 字段；工具一侧的行为见 [Todo、提问与 Plan 模式](https://daiw.org/manual/zcode/interaction-tools)。

**模型选择** `cloneModelSelection` 只做一件事：逐字段拷贝 `providerId`、`modelId` 与 `options`（`apps/zcode-cli/packages/core/src/runtime/model-selection.ts:3`）。读写 `sessionModelSelection` 时两头都拷贝（`config.ts:94`），外部拿到的对象改了也不会串回 runtime。模型怎样按选择创建，见[模型适配层](https://daiw.org/manual/zcode/model-adapters)。

## 对外公开的方法

89 个公开方法按用途归纳：

| 类别 | 个数 | 主要方法 |
| --- | --- | --- |
| 输入与回合 | 10 | `admitPrompt`、`executeTurn`、`steerTurn`、`enqueueDeferredInput`、`stopActiveForegroundExecution`、`recordExternalUserPrompt` |
| 队列管理 | 10 | `removePendingInputById`、`reservePendingInputById`、`editPendingInputById`、`reorderPendingInput`、`setQueueAutoDrain`、`setFollowupMode` |
| 配置与模式 | 12 | `updateConfig`、`setExecutionState`、`getMode`、`grantPermissionFullAccess`、`setSessionModelSelection`、`emitModelSelected` |
| 事件与投影 | 7 | `subscribeEvents`、`appendEvent`、`getSessionEventStore`、`getProjection`、`isSessionPersisted` |
| 会话标题 | 3 | `setCustomSessionTitle`、`maybeStartSessionTitleGenerationFromExternalInput` |
| 工具与权限 | 9 | `getToolRegistry`、`invalidateToolCache`、`scheduleTools`、`executeTools`、`resolvePermission`、`createChildClientPorts` |
| 目标 | 5 | `recordTargetChanged`、`continueActiveTargetIfIdle`、`continueActiveTargetLoop` |
| 后台任务与工作流 | 10 | `readBackgroundBashOutput`、`stopBackgroundTask`、`sealBackgroundTaskNotifications`、`startSavedWorkflowRun`、`amendWorkflowRunSettings` |
| 恢复、回退与分叉 | 9 | `resumeFromStore`、`rewindConversationToMessage`、`forkStableConversationAtMessage`、`previewWorkspaceFileRewind` |
| 驻留与生命周期 | 5 | `hasActiveOrQueuedTurnWork`、`hasResidencyBlockingWork`、`trackResidencyBlockingWork`、`beginShutdown`、`closeBrowserSession` |
| 其他 | 9 | `getSessionId`、`getProjectId`、`getSkillCatalog`、`generateWorkspaceText`、`testModelConnectivity`、`drainMemoryExtractions` |

`createChildClientPorts`、`getSessionEventStore`、`notifyExternalChildSessionEvent` 三个方法是给“在类外构造子 runtime”的 bootstrap 留的接缝：子会话必须共用父的事件存储，否则 transcript 会是一片空白；审批与请求头端口必须路由回父会话，注释记录了曾经因为身份错位让子代理在首个模型请求前挂起 80 分钟的事故（`config.ts:177`、`config.ts:191`、`config.ts:216`）。

## RuntimeFactory 与 core 的导出面

`@zcode/core` 的 `package.json` 声明了四个入口：主入口、`./repl`、`./browser-client` 与 `./create-workflow-graph-bounds`（`apps/zcode-cli/packages/core/package.json:8`）。主入口把 agent、context、compact、tool、hooks、mcp、subagent、runtime-task、workflow、permission 等子目录整体再导出，运行时部分在 `apps/zcode-cli/packages/core/src/index.ts:132` 导出 `AgentRuntime`，另外点名导出几个 bootstrap 也要用的函数，例如 fork 与 dwf 截断共用的消息克隆器，注释解释了为什么不让 bootstrap 再抄一份（`core/src/index.ts:135`）。

`core/src/runtime.ts` 是运行时的二级出口，除了 `AgentRuntime` 与一批类型，还声明了一个工厂接口 `RuntimeFactory`，只有一个方法 `create(config)`，返回 `Promise<AgentRuntime>`（`apps/zcode-cli/packages/core/src/runtime.ts:38`）。它经 `index.ts` 以类型导出，但在整个仓库里找不到任何实现或使用方，四处创建实例都直接 `new AgentRuntime(...)`。从签名看它也装不下今天的构造器：只收 `config`，既没有 `sessionId` 也没有 `deps`，更像早期设计留下的契约。

下一篇：[bootstrap：把运行时拼起来](https://daiw.org/manual/zcode/bootstrap-assembly)——这四十九项依赖从哪里来：配置分层、数据目录、模型工厂，以及会话怎样创建与恢复。
