上一章选择了 Coding Agent SDK。本章把它放进桌面进程:
Desktop Window
→ Application Controller
→ AgentSessionRuntime
→ AgentSession
→ Pi Agent/Model/Tools
目标是不依赖 Pi TUI,同时保留 Skills、Extensions、Session 与 Compaction。
1. 最小同进程嵌入
import {
createAgentSession,
ModelRuntime,
SessionManager,
} from "@earendil-works/pi-coding-agent";
const modelRuntime = await ModelRuntime.create();
const { session } = await createAgentSession({
cwd: workspacePath,
modelRuntime,
sessionManager: SessionManager.inMemory(workspacePath),
});
const unsubscribe = session.subscribe(handleEvent);
await session.prompt("分析当前项目");
这个 Factory 创建单个 AgentSession。不传 Resource Loader 时使用
DefaultResourceLoader 和标准发现。
2. 桌面架构不要让组件直接持有 Session
flowchart TD
V["View Components"] --> ST["Desktop Store"]
ST --> CT["Agent Controller"]
CT --> RT["AgentSessionRuntime"]
RT --> S["current AgentSession"]
S --> PI["Pi Runtime"]
S --> EV["AgentSessionEvent"]
EV --> CT
Controller 负责:
- Prompt/Abort/Queue;
- Event Subscription;
- Runtime Replacement;
- Extension Binding;
- Error Mapping;
- Dispose。
View 只消费 State 和发 Intent,避免 Session Replacement 后仍引用旧对象。
3. AgentSession 管什么
- Message History;
- Current Model/Thinking;
- Prompt、Steer、Follow-up;
- Active Tool;
- Agent Event;
- Compaction;
- In-place Tree Navigation;
- Extension Runner;
- Session Persistence。
New/Resume/Fork/Import 属于 AgentSessionRuntime,不是单个 AgentSession。
4. 创建可替换 Runtime
const createRuntime: CreateAgentSessionRuntimeFactory = async ({
cwd,
agentDir,
sessionManager,
sessionStartEvent,
projectTrustContext,
}) => {
const services = await createAgentSessionServices({
cwd,
agentDir,
resourceLoaderReloadOptions: {
resolveProjectTrust: ({ extensionsResult }) =>
resolveDesktopProjectTrust({
cwd,
extensionsResult,
projectTrustContext,
}),
},
});
return {
...(await createAgentSessionFromServices({
services,
sessionManager,
sessionStartEvent,
})),
services,
diagnostics: services.diagnostics,
};
};
const runtime = await createAgentSessionRuntime(
createRuntime,
{
cwd,
agentDir,
sessionManager: SessionManager.create(cwd),
},
);
Factory 把 Process-wide Input 捕获在 Closure 中,并按 Effective CWD 重建 CWD-bound Services。
类型注解保证参数不退化成 Implicit any;agentDir 必须继续传入 Services。
resolveDesktopProjectTrust() 是宿主实现的已类型化 Helper,它用本次 Replacement 传入的
projectTrustContext 连接桌面信任 UI/Policy。
5. AgentSessionServices
Services 聚合创建 Session 所需的共享/工作区依赖,例如:
- CWD/AgentDir;
- Settings;
- Auth/Model Runtime;
- Resource Loader;
- Diagnostics;
- Package/Trust 相关服务。
它们不是全都 Process-global。切换到另一个 Session CWD 时,应重建与 CWD 绑定的 Services, 而不是只改一个 String。
6. AgentSessionRuntime 的职责
Runtime 持有:
current session
current services
current cwd
diagnostics
runtime factory
并实现:
newSession();switchSession();fork()/ Clone;importFromJsonl()。
这些操作会 Shutdown/Dispose 旧 Session,再创建并应用新 Runtime。
7. Replacement 后必须重新绑定
let unsubscribe = runtime.session.subscribe(handleEvent);
runtime.setBeforeSessionInvalidate(() => {
unsubscribe();
detachOldDesktopBridgeSynchronously();
});
runtime.setRebindSession(async (nextSession) => {
unsubscribe = nextSession.subscribe(handleEvent);
try {
await bindDesktopExtensions(nextSession);
} catch (error) {
unsubscribe();
throw error;
}
});
原因:
runtime.session指向新对象;- Subscription 绑定旧 AgentSession;
- Extension UI/Command Binding 也属于旧 Session;
- 捕获的旧 Context 会失效。
setBeforeSessionInvalidate() 在旧 Extension Context 失效前同步拆除宿主 UI/Bridge;
setRebindSession() 在新 Session 创建后负责订阅和绑定。先订阅、后 bindExtensions(),
才能捕获 Extension 处理 session_start 与 resources_discover 期间通过 pi.sendMessage()
等方式产生的公开 Session Event。这两个生命周期事件本身只发给 Extension Runner,不属于
AgentSessionEvent。若采用相反顺序,就要在绑定后从 Session Snapshot 显式补水。不要只
更新 Store 中的 Session ID。
8. Event Adapter
function handleEvent(event: AgentSessionEvent) {
switch (event.type) {
case "message_update":
store.applyAssistantDelta(event);
break;
case "tool_execution_start":
store.startTool(event);
break;
case "tool_execution_end":
store.finishTool(event);
break;
case "agent_settled":
store.setIdle();
break;
}
}
Pi 没有为所有 AgentMessage 提供统一、稳定的 Message ID;Assistant responseId 也不是
每个 Provider 都保证存在。Reducer 应为每条会话消息生成自己的 UI Key,并只在 Tool
状态中使用稳定的 toolCallId。Replacement 时先完成旧 Subscription Teardown,再连接新
Session。
9. Streaming 与 UI
prompt() 等待整个 Run,包括 Tool Loop。界面更新来自 Event:
prompt intent
→ optimistic user state
→ message_start/update/end
→ tool events
→ agent_settled
不要等待 prompt() Resolve 后才展示文字,否则失去 Streaming。
10. Steering 与 Follow-up
Streaming 中:
steer():尽快把用户消息送入正在运行的 Agent;followUp():排队到当前 Run 后续;prompt(..., { streamingBehavior }):统一入口。
桌面 UI 要显示 Queue State。直接重复调用 Idle-only 操作会得到 Busy/Invalid State。
11. 依赖注入
可注入:
- Model Runtime/Auth;
- SessionManager;
- ResourceLoader;
- Custom Tools;
- Built-in Tool Set;
- Settings;
- Inline Extensions;
- Allow/Exclude Tool Names。
测试中注入 In-memory Session、Faux Provider 和 Test Resource Loader;生产中注入 Persistent Store 与真实 Permission Backend。
12. ModelRuntime 生命周期
Model/Auth 配置可以在多个 Session 之间共享,但要明确:
- Registry Update 是否广播;
- OAuth Refresh 并发;
- Credential Storage;
- Provider Extension Reload;
- Session Model Snapshot。
共享 Runtime 不代表多个 Session 共享 Agent State。
13. 多窗口策略
每窗口一个 Runtime
简单隔离:
Window A → Runtime A → Session A
Window B → Runtime B → Session B
优点是 Event、Queue、Approval 清晰;代价是资源与 Provider Client 可能重复。
应用级 Runtime Registry
windowId → runtimeId → AgentSessionRuntime
适合窗口切换同一 Session,但 Controller 必须做 Ownership、Reference Count 和 Subscription Routing。
一个 Runtime 同时只能有一个 Current Session。
14. 多会话并发
每个 AgentSession 本身各自拥有:
- Agent Run;
- Abort Controller;
- Tool Batch;
- SessionManager;
- Event Stream。
Extension Instance/Runtime State 是否独立则取决于 ResourceLoader/Services:若要求扩展 隔离,就不能复用同一个 ResourceLoader/Extension Runtime。可以有意共享 ModelRuntime 与 认证设施。共享 Tool Backend 必须支持 Concurrency。Filesystem Mutation 应使用 Pi 的 File Mutation Queue 或宿主 Lock;Database/API 用 Transaction/Idempotency。
15. Project Trust
Default Resource Loader 可能遇到项目动态配置。简单调用 createAgentSession() 不会自动弹出
桌面信任对话,也不会替宿主解析 Trust。桌面应用要提供明确 Trust Flow:
discover pre-trust extensions
→ project_trust decision
→ load project resources
SDK 宿主应在 createAgentSessionServices() 的
resourceLoaderReloadOptions.resolveProjectTrust 中连接决策,或自行构建已带 Resolver
Reload 的 DefaultResourceLoader 再传给 createAgentSession()。使用
AgentSessionRuntime Replacement 时,还要把本次 projectTrustContext 交给 Resolver。
无交互环境默认不能凭空获得桌面 Dialog;宿主要保存 Decision 的 Scope。
Trust Project 不等于 Trust 每个 Package,Marketplace/Package 仍需审查。
16. Extension UI Binding
桌面应用可实现 ExtensionUIContext:
- Select;
- Confirm;
- Input;
- Notify;
- Status。
不支持的终端专属能力应明确 No-op/Unsupported。Approval Extension 在调用 Confirm 前检查
ctx.hasUI;无 UI 时默认拒绝。
17. 生命周期管理
创建:
services → session/runtime → subscribe → bind extensions
销毁:
abort active work
→ unsubscribe
→ await runtime.dispose()
(内部:session_shutdown → beforeSessionInvalidate → session.dispose)
→ release host resources
AgentSessionRuntime.dispose() 会先发 session_shutdown,再让 Session 失效;
AgentSession.dispose() 自身不会发这个扩展生命周期事件。拥有 Runtime 的宿主必须调用前者,
否则会跳过 Extension Shutdown Handler。宿主仍要清理自己的 Event Listener、Window Bridge
与 Shared Resource Reference。
18. Error Adapter
将错误分层:
| 层 | UI 状态 |
|---|---|
| Provider/Auth | 需要登录/重试 |
| Tool | Tool Card Error |
| Extension Load/Handler | Diagnostics/Policy Failure |
| Session File | 恢复/只读打开 |
| Runtime Replacement | 保留旧 UI 快照,提示失败 |
| App Bridge | Worker/Controller Error |
不要把所有 Error 都显示成“模型失败”。
19. Diagnostics
Controller 应聚合多个诊断来源,而不是只读取 runtime.diagnostics:
resourceLoader.getExtensions().errors;getSkills().diagnostics;getPrompts().diagnostics;getThemes().diagnostics(即使桌面不使用 Pi TUI,也可能需要展示加载问题);runtime.diagnostics;runtime.modelFallbackMessage;- Session Recovery/Replacement 的异常路径。
安全关键 Policy 缺失时,Controller 可以禁用 Prompt/高风险 Tool。
20. 一个 Controller 轮廓
class DesktopAgentController {
private runtime: AgentSessionRuntime;
private unsubscribe?: () => void;
async start() {
await this.bind(this.runtime.session);
this.runtime.setBeforeSessionInvalidate(
() => this.detachBridgeSynchronously(),
);
this.runtime.setRebindSession(
async (session) => this.bind(session),
);
}
private async bind(session: AgentSession) {
this.unsubscribe?.();
this.unsubscribe = session.subscribe(
(event) => this.store.reduce(event),
);
try {
await session.bindExtensions(this.extensionBindings);
} catch (error) {
this.unsubscribe();
this.unsubscribe = undefined;
throw error;
}
}
async dispose() {
this.unsubscribe?.();
await this.runtime.dispose();
}
}
这是宿主结构示例,不是 Pi 导出的 Class。
21. 常见误区
“runtime.session 永远是同一个对象”
错误。Replacement 后会变化。
“订阅 Runtime 一次就覆盖所有 Session”
错误。Subscription 属于具体 AgentSession。
“修改 cwd 字段就完成工作区切换”
错误。CWD-bound Services/Resources 要重建。
“prompt Resolve 后再更新 UI”
会丢掉 Streaming 体验;应消费 Event。
“多窗口可以共享一个 Current Session 而无路由”
会造成 Event/Approval 混线。
22. 本章小结
- createAgentSession 适合单 Session,AgentSessionRuntime 负责 Replacement;
- Desktop View 应通过 Store/Controller 间接访问 Pi;
- Services 中有 CWD-bound 依赖,切工作区要重建;
- Runtime Session Replacement 后必须重订阅、重绑 Extension;
- Streaming UI 来自 Event,而不是等待 prompt 完成;
- 多窗口优先每窗口 Runtime,或建立严格 Registry/Ownership;
- Shared Backend 需要并发与幂等;
- Project Trust、Extension UI、Diagnostics 都要由桌面宿主适配;
- 由 Runtime Owner 调用
runtime.dispose()才会先发 Extension Shutdown;宿主资源仍需自行清理。
23. 自测
- createAgentSession 与 AgentSessionRuntime 分别适合什么?
- 为什么 Session Replacement 后要重新订阅?
- 哪些 Service 与 CWD 绑定?
- Desktop View 为什么不应直接持有 AgentSession?
- prompt Promise 与 Streaming Event 各负责什么?
- 多窗口共享 Runtime 的主要风险是什么?
- 无 UI 时 Approval 应怎样处理?
- dispose 后宿主还要清理哪些资源?