DSH / Atlas
2026-10-03proposedfeature

Pi extensions bridge

Pi 扩展桥接

Pi 1.0.0 extensions are in-process TypeScript factories `(pi: ExtensionAPI) => void`. A factory subscribes to about forty lifecycle, context, tool, and provider events, registers model tools, slash commands, model providers, and MCP servers, and talks to the user through `ctx.ui` ([extensions guide](https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/extensions.md), [API types](https://github.

English

Problem

Pi 1.0.0 extensions are in-process TypeScript factories (pi: ExtensionAPI) => void. A factory subscribes to about forty lifecycle, context, tool, and provider events, registers model tools, slash commands, model providers, and MCP servers, and talks to the user through ctx.ui (extensions guide, API types). The API is published and widely used: about 11,000 npm packages carried the pi-package keyword in October 2026.

DSH has bridges for two other ecosystems: shell hooks written for Claude Code and Codex (hooks group) and Claude Code mods (compatibility page). Pi's extension API is broader than either. It lets one in-process module rewrite the messages of a single request, edit tool arguments, append its own durable records, and continue a finished run. Serving that API from DSH extension points answers two questions: how much of an extension written for another harness runs unchanged, and which DSH core mechanisms are narrower than Pi's.

Proposal

Add an experimental bridge that loads unmodified Pi extension sources and serves Pi's ExtensionAPI from existing DSH extension points. The bridge is a capability demonstration: it runs extensions as written where an existing DSH mechanism carries the behavior, leaves the rest unserved, and lists every difference on a compatibility page. Apart from the restricted plugin records that extension state needs (row 9 of the gap table), each core gap is closed by a separate change.

Packages and composition

  • @deepseek-ai/dsh-experimental-pi-extensions is the Host package and provides ctx.piExtensions. @deepseek-ai/dsh-experimental-client-ui-pi-extensions is the Web package. Neither is an optional bundle; an opt-in overlay composes them, as for the Claude Code mods bridge.
  • One cordis.yml row names one extension source: a file, a directory with an index.ts or index.js, or a Pi package directory whose package.json lists pi.extensions. The row also carries the extension's flag values. Row order is Pi's load order, which is also its handler dispatch order.
  • The bridge does not scan ~/.pi or .pi, install packages, or decide project trust. A Pi package's skills, prompt templates, and themes are not loaded.

Loading and execution

  • The bridge has its own loader and event dispatcher. @earendil-works/pi-coding-agent is not a dependency.
  • jiti imports the source, so TypeScript loads in a built installation. The loader resolves the imports Pi supplies to extensions: typebox and @earendil-works/pi-ai to the bridge's own dependencies, @earendil-works/pi-coding-agent to a module of the bridge that implements the helpers that need no Pi runtime (defineTool, the tool-event type guards, the truncation helpers, withFileMutationQueue, getAgentDir), and @earendil-works/pi-tui to the optional peer dependency when the deployment installed it. An extension that imports a module the loader cannot resolve fails to load with an error naming the module.
  • Extension code runs in the Host process with the process's full authority, as mods do.
  • Every Session with an in-process Agent gets its own runtime: the module is evaluated once per row, and its factory runs once per Session, as Pi re-runs factories for each Session. State an extension keeps at module scope is shared by all Sessions of the Host process; Pi also keeps one module instance across the Sessions of a process, but runs one Session at a time. A subagent's Session gets a separate runtime in Pi's print mode with hasUI: false, so a tool call a subagent makes passes the same extension handlers as the root's.

Serving the API

Pi APIDSH mechanism
session_start, session_shutdownagent/created (awaited before the first turn), agent/disposed
input (after inbox admission; see row 7), before_agent_startsystem-prompt/assemble and agent/pre-step: claimed user messages are transformed or dropped, a returned systemPrompt replaces the assembled prompt for that turn, and returned messages join the step
tool_calltools/pre-execute: block becomes a denial; a handler that throws denies the call
tool_resulttools/post-execute: replaced content is installed as the result's content
agent_before_settleagent/turn-stopping: returned custom entries are recorded, and a returned message with continue is steered into the turn
agent_start, agent_end, agent_settled, turn_start, turn_end, message_start, message_end, tool_execution_start, tool_execution_end, model_select, session_info_changed, session_compactSession events and agent/status, delivered in order as notifications
registerTool, ctx.executeToolAgent-scoped ctx.tools registrations; nested calls run the full tool pipeline; built-in names follow the translation below
registerCommand, getCommandsAgent-scoped ctx.commands
sendUserMessage, sendMessageagent.followup(), agent.steer(), agent.inject()
appendEntry, setLabel, tool details, ctx.sessionManagerIgnorable Session records and tool/result metadata, read back as Pi entries
getActiveTools, setActiveTools, getAllToolsctx.tools.schemas(); setActiveTools applies a scoped ctx.tools.restrict() to global tools and adds or removes the bridge's own Agent-scoped registrations, because a restriction does not hide scoped tools
setModel, getThinkingLevel, setThinkingLevelAn agent/request override
execctx.subprocess, argv without a shell
ctx.ui.select, confirm, input, editorctx.userQuestions
ctx.ui.notify, setStatus, setWidget, setTitle, setEditorTextA Remote stream the Web package renders
registerFlag, getFlag, eventsRow configuration; an in-process event bus per Session runtime

All Pi handlers for one Session run through one ordered queue. A notification never overtakes an earlier one, and an awaited hook such as tool_call runs after every notification queued before it, which keeps the order Pi's awaited dispatch gives handlers.

The Web and Desktop GUI appear to extensions as Pi's rpc mode with hasUI: true; headless, SDK, and ACP Sessions appear as print mode. Extensions already handle both: terminal-only calls such as ctx.ui.custom() return what Pi's rpc mode returns.

Built-in tool calls as Pi sees them

DSH and Pi share the names bash, read, edit, write, and grep, but not their arguments, and Pi's find is DSH's glob. A Pi safety extension reads event.input.path or event.input.command. The bridge therefore presents each call of these six tools to handlers with Pi's name and Pi's argument names, translated from the DSH call. Other tools pass through unchanged. A test loads the registered DSH tool schemas and fails when a mapped parameter is renamed or removed, or when a tool gains a parameter the table has not classified.

The opposite direction uses the same table. getActiveTools() and getAllTools() report Pi names for the six tools and DSH names for every other tool, and setActiveTools() accepts the same names. ctx.executeTool() given a Pi name translates the arguments into the DSH call; an edit call with more than one entry in edits is refused, because one DSH edit call makes one replacement. An extension tool registered under a Pi built-in name replaces the corresponding DSH tool for that Agent, as it replaces Pi's own tool (find replaces glob).

Unserved features

  • An event DSH cannot raise is accepted at registration, never raised, and named in a warning when the extension loads.
  • A pi or ctx member DSH cannot serve throws an error naming the member.
  • Where Pi fails closed, the bridge does too. A tool_call handler that edits event.input gets the call denied with a reason, because the logged call would otherwise differ from the executed one.

Extension state

Pi extensions rebuild their state on session_start from what they wrote earlier. The bridge keeps that state in the Session log so that it follows resume and fork:

  • appendEntry and setLabel write ignorable records whose type starts with plugin:. A restricted core operation, callable only from experimental packages, appends them. They are not SessionEventMap members, so the persistence catalog and its type history are unchanged. A build without the bridge retains and skips them when it reads the current format, as the ignorable events decision requires. A later format edge carries them on a best-effort basis: it keeps them under their names where it can and otherwise states which it drops, while the alpha historical edges refuse every unknown event (decision).
  • A tool's details is stored as the tool/result metadata the tool's output declaration projects.
  • sendMessage content is logged as an ordinary user-sourced message with the extension's content unchanged. A companion ignorable record carries customType, display, and details.

Core mechanisms narrower than Pi's

Ranked by how much of Pi's API each blocks, with the GitHub issue that tracks each. Rows 1 and 3 follow from DSH rules: a request is derived from the Session log, and the log is linear. The other rows are missing extension points.

#MechanismPi API blockedDSH todayIssue
1Per-request rewrite of model inputcontext, context_with_system, before_provider_request, prepareLoadout hidden declarationsThe loop derives each request from the log and freezes it; llm/stream listeners read it. A rewrite needs an awaited hook and a core event that every log reader applies to that request.#5661
2Tool-argument rewritetool_call editing event.inputPreToolDecision excludes input rewriting (proposal).#5662
3Session tree and assistant-message replacementnavigateTree, session_before_tree, session_tree, message_end replacementThe log is linear, a fork creates a new Session, and an assistant/message cannot replace another.#5663
4Awaited step-end and pre-commit hooksturn_end entries and continue, message_endstep/end and assistant/message are committed events; a Session-event listener cannot append.#5664
5Tool progress and result replacementonUpdate, tool_execution_update, tool_result turning an error into a success, tool usageToolRunContext has no progress channel, and tools/post-execute cannot replace the value of a failed result.#5665
6Per-tool exposurecodemode, deferred, and hidden exposurePresentation is chosen per Agent, and a tool the model cannot see refuses execution.#5666
7Input admissioninput before the prompt is storedA prompt is durable in the inbox before agent/pre-step; a swallowed prompt leaves a turn without a step.#5667
8Logged model call by a pluginctx.modelRegistry.complete, stream, streamSimpleNo Session event records a plugin's own model request, which also blocks $.model.complete for mods.#5668
9Plugin-owned durable recordsappendEntrySession.append() accepts declared event types only. The restricted plugin: records close this for experimental packages.#5669
10Compaction customizationsession_before_compactOne CompactionEngine owns summarization, and /compact takes no instructions.#5670
11Lifecycle reasonssession_start reasons new, fork, and reload; session_shutdown reasonsagent/created reports startup or resume; SessionStartSource also declares clear and compact, which nothing emits yet; agent/disposed carries no reason.#5671
12Host-plugin display in the GUIctx.ui.notify, setStatus, setWidgetNo shared mechanism; each bridge ships its own Client package.#5672
13Command capabilitiesArgument completion; newSession, fork, and switchSession moving the user to another Sessionctx.commands takes unstructured text and has no Client-side effects.#5673
14Provider request hooksbefore_provider_headers, after_provider_response, provider_stream_eventHeaders, responses, and raw stream events stay inside each adapter.#5674
15Fault isolationAn extension bug stops one Pi processAn uncaught exception in the Host disposes the application for every Session.#5675

Terminal rendering (ctx.ui.custom, custom editors, renderers, shortcuts) is outside this list: DSH's GUI is not a terminal, and Pi's own rpc mode does not serve these either.

Alternatives considered

Depend on @earendil-works/pi-coding-agent and reuse its loader and runner. It would reproduce Pi's handler ordering and error rules exactly and supply every helper extensions import. It was rejected because the exercise is to carry Pi's API on DSH mechanisms, not to embed Pi's harness: the runner is constructed over Pi's SessionManager and ModelRegistry, the package brings Pi's whole dependency closure, and its built-in tool factories would run Pi's own file and shell implementations outside DSH's filesystem and sandbox policy.

A wrapper module per extension, like defineMod. A Pi extension is already a default-exported factory with no manifest to replace, so a wrapper adds a step for every extension and prevents loading a published Pi package directory.

Discover Pi's own locations automatically. Loading ~/.pi/agent/extensions and project .pi/extensions executes code a DSH user did not compose, which needs a project-trust decision DSH does not have. The mods bridge removed its directory loader for the same composition reason. Discovery can be a later opt-in layer.

Run extensions in a separate process per Session. An uncaught extension error would then reach the Host as an event. Every awaited hook would become an inter-process round trip, and Pi's edit-in-place event objects would have to be reproduced by sending changes back. Deferred until real extensions show the isolation is needed.

Evaluate each extension module once per Session. Concurrent Sessions would then not share module-scope state. Rejected because Pi keeps one module instance across the Sessions of a process, so an extension may rely on module state surviving a Session switch, and per-Session evaluation repeats the transpilation for every Session and subagent.

Run extensions for root Sessions only. It needs an explicit filter, since agent/created fires for every Agent, and it lets a subagent's tool calls bypass a Pi safety extension.

Pass DSH tool names and arguments to handlers unchanged. Pi's own protected-paths example reads event.input.path; with DSH's file_path it throws, and a handler written slightly differently would allow the write.

Declare pi/* Session events and a pi-extension message source. Correct attribution, but each is a required-on-read persistence type: the catalog and type history change, and a build without the bridge refuses the Session.

Keep extension state in a storage domain outside the log. State would not follow a fork, and ctx.sessionManager.getBranch() would not show what the extension wrote.

Raise context and before_provider_request without logging their effect. The model would see input that the Session log cannot reconstruct.

Refuse to load an extension that subscribes to an unserved event. Most extensions use several features; the mods bridge already warns and keeps the rest working.

Build a shared GUI notification mechanism first. It would also serve the mods bridge, but it is a core addition; the Web package of this bridge renders its own stream until that exists.

Acceptance criteria

  • Pi's MIT-licensed example extensions are committed unchanged as fixtures, and only those a test exercises. Through a real agent loop with a mock model: a permission extension denies a tool call and the model sees the reason; an extension tool is called by the model and its details survive a resumed Session; an extension command runs; an extension replaces the system prompt for a turn; appendEntry state is rebuilt on session_start after resume.
  • A subagent's tool call reaches the handlers of its own runtime.
  • The tool translation test fails when a mapped DSH tool parameter changes.
  • A Loader composition test boots the bridge from a cordis.yml, and a disposal test shows every registration removed when the bridge unloads.
  • A recorded-Session snapshot covers an extension-denied tool call and an extension tool call.
  • docs/subsystems/pi-extensions.md lists every event and member with its status and every behavioral difference.
  • The new packages' src/ have 100% coverage, and doc-sync passes.

Risks

  • Extension code has the Host's full authority, and one uncaught error stops every Session. Mount only extensions you would run as a plugin.
  • Fidelity rests on the bridge's reading of Pi 1.0.0 dispatch rules, checked against Pi's examples but not executed on Pi's code. A later Pi release can change an event or helper the bridge serves.
  • An extension that imports a helper the bridge does not implement fails to load, even if it would have used that helper on one code path only.
  • A future Session format edge that cannot carry plugin records loses extension state for Sessions written before it.
  • Extensions that keep per-Session state at module scope mix the state of concurrent Sessions, including subagents.
  • A message an extension sends is attributed to the user everywhere outside the Web package that reads the companion record.

中文

问题

Pi 1.0.0 的扩展是进程内的 TypeScript 工厂函数 (pi: ExtensionAPI) => void。工厂函数订阅约四十个生命周期、上下文、工具与提供方事件,注册模型工具、斜杠命令、模型提供商与 MCP server,并通过 ctx.ui 与用户交互(扩展指南、API 类型)。该 API 已发布并被广泛使用:截至 2026 年 10 月,npm 上约有 11,000 个包带有 pi-package 关键字。

DSH 已为另外两个生态提供桥接:为 Claude Code 与 Codex 编写的 shell 钩子(hooks 组),以及 Claude Code mods(兼容性页面)。Pi 的扩展 API 比两者都宽。它允许一个进程内模块改写单次请求的消息、编辑工具参数、追加自己的持久记录,并让已经结束的运行继续。用 DSH 扩展点提供这套 API 可以回答两个问题:为另一个 harness 编写的扩展有多少能原样运行,以及 DSH 的哪些核心机制比 Pi 窄。

提案

新增一个实验性桥接,加载未修改的 Pi 扩展源码,并用现有的 DSH 扩展点提供 Pi 的 ExtensionAPI。该桥接是一次能力演示:凡现有 DSH 机制能承载的行为,就让扩展原样运行;其余部分不提供,并在兼容性页面列出每一处差异。除扩展状态所需的受限插件记录(缺口表第 9 行)外,每项核心缺口都由单独的变更弥补。

包与组合

  • @deepseek-ai/dsh-experimental-pi-extensions 是 Host 包,提供 ctx.piExtensions。@deepseek-ai/dsh-experimental-client-ui-pi-extensions 是 Web 包。两者都不是可选 bundle;与 Claude Code mods 桥接一样,由一个按需启用的 overlay 组合它们。
  • cordis.yml 的一行指定一个扩展来源:一个文件、一个含 index.ts 或 index.js 的目录,或一个在 package.json 中列出 pi.extensions 的 Pi 包目录。该行同时携带扩展的 flag 值。行的顺序就是 Pi 的加载顺序,也是其 handler 的分发顺序。
  • 桥接不扫描 ~/.pi 或 .pi,不安装包,也不判定项目信任。Pi 包中的 skill、提示词模板与主题不会被加载。

加载与执行

  • 桥接有自己的 loader 与事件分发器。@earendil-works/pi-coding-agent 不是依赖。
  • 源码由 jiti 导入,因此 TypeScript 在已构建的安装中也能加载。loader 解析 Pi 提供给扩展的那些导入:typebox 与 @earendil-works/pi-ai 解析到桥接自己的依赖;@earendil-works/pi-coding-agent 解析到桥接的一个模块,它实现了不需要 Pi 运行时的辅助函数(defineTool、工具事件类型守卫、截断辅助函数、withFileMutationQueue、getAgentDir);@earendil-works/pi-tui 在部署安装了该可选对等依赖(peer dependency)时解析到它。扩展导入了 loader 无法解析的模块时加载失败,错误信息会指出该模块。
  • 与 mods 一样,扩展代码在 Host 进程中以进程的全部权限运行。
  • 每个拥有进程内 Agent 的 Session 都有自己的运行时:每行的模块只求值一次,其工厂函数对每个 Session 运行一次,正如 Pi 为每个 Session 重新运行工厂函数。扩展保存在模块作用域的状态由 Host 进程中的所有 Session 共享;Pi 同样在一个进程的多个 Session 之间保留同一个模块实例,但同一时间只运行一个 Session。subagent 的 Session 有独立的运行时,处于 Pi 的 print 模式且 hasUI: false,因此 subagent 发起的工具调用与根 Agent 的调用经过同样的扩展 handler。

提供 API

Pi APIDSH 机制
session_start、session_shutdownagent/created(在首个轮次之前等待完成)、agent/disposed
input(在 inbox 准入之后,见第 7 行)、before_agent_startsystem-prompt/assemble 与 agent/pre-step:被认领的用户消息可被改写或丢弃,返回的 systemPrompt 替换该轮次已组装的提示词,返回的消息加入该步骤
tool_calltools/pre-execute:block 成为拒绝;handler 抛出异常时拒绝该调用
tool_resulttools/post-execute:被替换的内容安装为结果内容
agent_before_settleagent/turn-stopping:返回的自定义条目被记录,带 continue 返回的消息被 steer 进该轮次
agent_start、agent_end、agent_settled、turn_start、turn_end、message_start、message_end、tool_execution_start、tool_execution_end、model_select、session_info_changed、session_compact会话事件与 agent/status,作为通知按顺序送达
registerTool、ctx.executeToolAgent 作用域的 ctx.tools 注册;嵌套调用走完整的工具管线;内置名称遵循下文的转换
registerCommand、getCommandsAgent 作用域的 ctx.commands
sendUserMessage、sendMessageagent.followup()、agent.steer()、agent.inject()
appendEntry、setLabel、工具 details、ctx.sessionManagerignorable Session 记录与 tool/result 元数据,读回时还原为 Pi 条目
getActiveTools、setActiveTools、getAllToolsctx.tools.schemas();setActiveTools 对全局工具使用作用域内的 ctx.tools.restrict(),并增删桥接自己在 Agent 作用域的注册,因为限制不会隐藏作用域内的工具
setModel、getThinkingLevel、setThinkingLevelagent/request 覆盖
execctx.subprocess,不经 shell 的 argv
ctx.ui.select、confirm、input、editorctx.userQuestions
ctx.ui.notify、setStatus、setWidget、setTitle、setEditorText由 Web 包渲染的 Remote 流
registerFlag、getFlag、events行配置;每个 Session 运行时一个进程内事件总线

同一个 Session 的所有 Pi handler 都经过一个有序队列。通知不会越过更早的通知;tool_call 这类需要等待的钩子,在排在它之前的所有通知之后运行。这样就保持了 Pi 的等待式分发给 handler 的顺序。

Web 与 Desktop GUI 对扩展呈现为 Pi 的 rpc 模式且 hasUI: true;headless、SDK 与 ACP Session 呈现为 print 模式。扩展本来就处理这两种模式:ctx.ui.custom() 这类仅限终端的调用返回 Pi 的 rpc 模式所返回的值。

Pi 视角下的内置工具调用

DSH 与 Pi 共有 bash、read、edit、write 与 grep 这些名称,但参数不同,而 Pi 的 find 是 DSH 的 glob。Pi 的安全类扩展会读取 event.input.path 或 event.input.command。因此,桥接把这六个工具的每次调用以 Pi 的名称和 Pi 的参数名呈现给 handler,由 DSH 调用转换而来。其他工具原样传递。有一项测试加载已注册的 DSH 工具 schema,在被映射的参数改名或被移除时失败,在工具新增了映射表尚未归类的参数时也失败。

反方向使用同一张映射表。getActiveTools() 与 getAllTools() 对这六个工具报告 Pi 名称,对其他工具报告 DSH 名称;setActiveTools() 接受同样的名称。ctx.executeTool() 收到 Pi 名称时,把参数转换为 DSH 调用;edits 中多于一项的 edit 调用会被拒绝,因为一次 DSH edit 调用只做一处替换。以 Pi 内置名称注册的扩展工具会替换该 Agent 中对应的 DSH 工具,正如它在 Pi 中替换 Pi 自己的工具(find 替换 glob)。

不提供的功能

  • DSH 无法触发的事件:注册时接受,永不触发,并在扩展加载时的警告中列出名称。
  • DSH 无法提供的 pi 或 ctx 成员:抛出指明该成员的错误。
  • Pi 在哪里失败关闭,桥接也在哪里失败关闭。编辑 event.input 的 tool_call handler 会使该调用被拒绝并附带原因,否则已记录的调用会与实际执行的调用不一致。

扩展状态

Pi 扩展在 session_start 时根据自己此前写入的内容重建状态。桥接把这些状态保存在 Session 日志中,使其跟随 resume 与 fork:

  • appendEntry 与 setLabel 写入类型以 plugin: 开头的 ignorable 记录。追加它们的是一项受限的核心操作,只能由实验性包调用。它们不是 SessionEventMap 成员,因此持久化目录及其类型历史不变。不带该桥接的构建在读取当前格式时保留并跳过它们,这是 ignorable 事件决策的要求。之后的格式迁移边以尽力而为的方式携带它们:能保留时以原名称保留,否则说明丢弃哪些;Alpha 历史格式迁移边则拒绝所有未知事件(决策)。
  • 工具的 details 保存为 tool/result 元数据,由工具的输出声明投影得出。
  • sendMessage 的内容记录为普通的用户来源消息,扩展的内容保持不变。一条伴随的 ignorable 记录携带 customType、display 与 details。

比 Pi 窄的核心机制

按各自阻塞的 Pi API 数量排序,并列出跟踪每一项的 GitHub issue。第 1、3 行源自 DSH 的规则:请求由 Session 日志派生,而日志是线性的。其余各行是缺失的扩展点。

#机制被阻塞的 Pi APIDSH 现状Issue
1按请求改写模型输入context、context_with_system、before_provider_request、prepareLoadout 隐藏声明循环从日志派生每个请求并将其冻结;llm/stream 监听器只读取它。改写需要一个可等待的钩子,以及一个每个日志读取方都会应用到该请求上的核心事件。#5661
2改写工具参数编辑 event.input 的 tool_callPreToolDecision 不包含输入改写(提案)。#5662
3Session 树与 assistant 消息替换navigateTree、session_before_tree、session_tree、message_end 替换日志是线性的,fork 会创建新的 Session,assistant/message 不能替换另一条。#5663
4可等待的步骤结束钩子与提交前钩子turn_end 的条目与 continue、message_endstep/end 与 assistant/message 是已提交的事件;会话事件监听器不能追加事件。#5664
5工具进度与结果替换onUpdate、tool_execution_update、把错误变为成功的 tool_result、工具 usageToolRunContext 没有进度通道,tools/post-execute 不能替换失败结果的值。#5665
6按工具设置可见性codemode、deferred 与 hidden exposure呈现方式按 Agent 选择,模型看不到的工具拒绝执行。#5666
7输入准入在提示词被保存之前的 input提示词在 agent/pre-step 之前就已持久保存在 inbox 中;被吞掉的提示词会留下一个没有步骤的轮次。#5667
8插件发起且被记录的模型调用ctx.modelRegistry.complete、stream、streamSimple没有会话事件记录插件自己的模型请求,这同样阻塞了 mods 的 $.model.complete。#5668
9插件拥有的持久记录appendEntrySession.append() 只接受已声明的事件类型。受限的 plugin: 记录为实验性包补上了这一点。#5669
10定制压缩session_before_compact摘要由唯一的 CompactionEngine 负责,/compact 不接受指令。#5670
11生命周期原因session_start 的 new、fork 与 reload 原因;session_shutdown 的原因agent/created 报告 startup 或 resume;SessionStartSource 还声明了尚无生产者的 clear 与 compact;agent/disposed 不带原因。#5671
12Host 插件在 GUI 中的显示ctx.ui.notify、setStatus、setWidget没有共享机制;每个桥接各自附带 Client 包。#5672
13命令能力参数补全;把用户带到另一个 Session 的 newSession、fork 与 switchSessionctx.commands 接受非结构化文本,没有 Client 侧效果。#5673
14提供方请求钩子before_provider_headers、after_provider_response、provider_stream_event请求头、响应与原始流事件都留在各适配器内部。#5674
15故障隔离扩展的缺陷只中止一个 Pi 进程Host 中未捕获的异常会为所有 Session 释放整个应用。#5675

终端渲染(ctx.ui.custom、自定义编辑器、渲染器、快捷键)不在此列:DSH 的 GUI 不是终端,Pi 自己的 rpc 模式也不提供这些。

考虑过的替代方案

依赖 @earendil-works/pi-coding-agent 并复用其 loader 与 runner。 这样可以精确复现 Pi 的 handler 顺序与错误规则,并提供扩展导入的全部辅助函数。否决的原因是:这项工作要用 DSH 的机制承载 Pi 的 API,而不是嵌入 Pi 的 harness。该 runner 构造于 Pi 的 SessionManager 与 ModelRegistry 之上,这个包会带入 Pi 的全部依赖闭包,而且它的内置工具工厂会在 DSH 的文件系统与沙箱策略之外运行 Pi 自己的文件与 shell 实现。

像 defineMod 那样为每个扩展写一个包装模块。 Pi 扩展本身就是默认导出的工厂函数,没有需要替代的 manifest,因此包装只会给每个扩展多加一步,并且无法直接加载已发布的 Pi 包目录。

自动发现 Pi 自己的位置。 加载 ~/.pi/agent/extensions 与项目的 .pi/extensions 会执行 DSH 用户并未组合进来的代码,这需要 DSH 尚不具备的项目信任判定。mods 桥接出于同样的组合原因移除了它的目录 loader。发现机制可以作为之后按需启用的一层。

为每个 Session 在独立进程中运行扩展。 这样未捕获的扩展错误会以事件的形式到达 Host。但每个需要等待的钩子都会变成一次跨进程往返,Pi 的就地编辑事件对象也必须通过回传变更来复现。待真实扩展表明确实需要这种隔离时再做。

为每个 Session 各求值一次扩展模块。 这样并发的 Session 不会共享模块作用域的状态。否决的原因是:Pi 在一个进程的多个 Session 之间保留同一个模块实例,扩展可能依赖模块状态在切换 Session 后仍然存在;而且按 Session 求值会为每个 Session 与 subagent 重复转译。

只为根 Session 运行扩展。 这需要显式过滤,因为 agent/created 对每个 Agent 都会触发;它还会让 subagent 的工具调用绕过 Pi 的安全类扩展。

把 DSH 的工具名称与参数原样传给 handler。 Pi 自己的 protected-paths 示例读取 event.input.path;遇到 DSH 的 file_path 时它会抛出异常,而写法稍有不同的 handler 会放行这次写入。

声明 pi/* 会话事件与 pi-extension 消息来源。 归属是正确的,但两者都是读取时必需的持久化类型:目录与类型历史会改变,没有该桥接的构建会拒绝这个 Session。

把扩展状态保存在日志之外的 storage domain 中。 状态不会跟随 fork,ctx.sessionManager.getBranch() 也看不到扩展写入的内容。

触发 context 与 before_provider_request 但不记录其效果。 模型会看到 Session 日志无法重建的输入。

拒绝加载订阅了不提供的事件的扩展。 多数扩展同时使用多项功能;mods 桥接已经采用“发出警告并让其余部分继续工作”的做法。

先构建共享的 GUI 通知机制。 它也能服务 mods 桥接,但属于核心新增;在它出现之前,本桥接的 Web 包渲染自己的流。

验收标准

  • Pi 以 MIT 许可发布的示例扩展原样提交为 fixture,且只提交有测试覆盖的那些。在使用 mock 模型的真实 agent loop 中:权限扩展拒绝一次工具调用,模型看到原因;模型调用扩展工具,其 details 在恢复的 Session 中仍然存在;扩展命令可以运行;扩展替换某个轮次的系统提示词;resume 之后,appendEntry 的状态在 session_start 时被重建。
  • subagent 的工具调用到达它自己运行时的 handler。
  • 被映射的 DSH 工具参数变化时,工具转换测试失败。
  • Loader 组合测试从 cordis.yml 启动桥接,释放测试表明桥接卸载时每项注册都被移除。
  • 一个录制的 Session 快照覆盖一次被扩展拒绝的工具调用和一次扩展工具调用。
  • docs/subsystems/pi-extensions.md 列出每个事件与成员及其状态,以及每一处行为差异。
  • 新包的 src/ 覆盖率为 100%,doc-sync 通过。

风险

  • 扩展代码拥有 Host 的全部权限,一个未捕获的错误会中止所有 Session。只挂载你愿意作为插件运行的扩展。
  • 保真度取决于桥接对 Pi 1.0.0 分发规则的理解;它经过 Pi 示例的检验,但没有在 Pi 的代码上执行。之后的 Pi 版本可能改变桥接所提供的事件或辅助函数。
  • 扩展导入了桥接未实现的辅助函数时会加载失败,即使它只在某一条代码路径上用到该函数。
  • 无法携带插件记录的未来 Session 格式迁移边,会使迁移之前写入的 Session 丢失扩展状态。
  • 把按 Session 区分的状态放在模块作用域的扩展,会混用并发 Session(包括 subagent)的状态。
  • 扩展发送的消息,在读取伴随记录的 Web 包之外的所有地方,都归属于用户。