// src/agent/pi-adapter.ts — PiAdapter:封装 Pi Agent 注入机制 // // Phase 9.6:v9 新增 — AgentAdapter 的 Pi 实现。 // - system_prompt 注入:api.on("before_agent_start") 每轮追加 segment // - 这些是 Agent Runtime 层(Pi API),本 Adapter 在该层做 session→system_prompt 映射 // // v19(issue pt-llm-tool-consolidation): // - 移除 api.on("input") handler:人类不再用 `/pt_turn_inject` 命令触发 TurnContext 注入 // (语义不符 + 与 tool 重复 + 人类有更好替代,见 issue §人类命令统一方案)。 // LLM 仍可调 `pt_inject` tool 获取手册内容(同一 renderTurnInject 内核)。 // - listManuals 返回的 name 改为纯 domain 名(不再伪装成 `/pt_turn_inject ` 命令); // flowsText 文案相应改为 "调 pt_inject tool 获取 手册"。 // // Pt 核心只调 AgentAdapter 接口,不直接调 Pi API。加新 Agent 只加 Adapter。 // // Tech Debt T6: 全用 type guard 收窄,不用 as 断言(pt-quality #1) // Tech Debt T2: 用 constants 模块名常量(pt-quality #5) // // v12.x(issue pt-session-singleton-pi-web-pollution 修复): // - handler 内部读 `args[1]?.sessionManager?.getSessionId()` 拿 per-session sessionId // - 通过 `getSessionById(sessionId)` 拿 per-session SessionState,**不再读 module-level 单例** // - 配合 `src/agent/registry.ts` 的 per-pi WeakMap 改造, // `this.ctx / this.blueprint / this.domains / this.segment / this.injectedApi` 五个实例字段 // 现在是 per-pi 隔离——其他 session 的 setContext 不会覆盖本 session 的 segment import { AGENT_PI, MOD_FLOWS, MOD_RULES } from "../constants.js"; import { isFlowTemplateArray, isRuleArray } from "../compile/type-guards.js"; import { refreshInjectionFooter } from "../manual-session.js"; import { getSessionById } from "../session.js"; import { filterDomainsByProfile, type AgentAdapter, type AgentAPI, type AgentContext, type Blueprint, type Domain, type Profile, } from "../schema.js"; import { renderSessionInject } from "../render/session-inject.js"; /** v12.x:从 handler 的 args[1] ctx 提取 sessionId。 * pi 的 `pi.on(event, handler)` 触发时传 `(event, ctx)` 两个参数;通过 AgentAPI 包装后 * handler 拿到 `(...args)`,args[0] = event, args[1] = ctx (ExtensionContext)。 */ function sessionIdFromArgs(args: unknown[]): string | undefined { const ctx = args[1] as { sessionManager?: { getSessionId?: () => string } } | undefined; return ctx?.sessionManager?.getSessionId?.(); } /** PiAdapter:封装 Pi Agent 的注入机制。 */ export class PiAdapter implements AgentAdapter { name = AGENT_PI; /** Phase term-P4.3:保留 Pi API 名 system_prompt/context_message—— * 这是 AgentAdapter 映射边界声明(Pi 支持哪些 Agent Runtime 层注入位置)。 * Blueprint 用 session/turn 语义值(聚合组 inject 字段),Adapter 内部映射到此字段声明的 Pi API 名。 */ supportedTargets = ["system_prompt", "context_message"]; private ctx: AgentContext | null = null; private blueprint: Blueprint | null = null; private domains: Domain[] = []; private segment: string | null = null; /** 与当前 Pi runtime 的 AgentAPI 绑定;同一 runtime 不重复注册 handler。 * v12.x:per-pi 实例字段——`registry.ts` 给每个 pi 一个新 PiAdapter,所以 `injectedApi` * 不会被其他 session 覆盖。 */ private injectedApi: AgentAPI | null = null; /** v13.x(issue pt-turn-inject-not-profile-scoped):当前 Profile——turn 路径 scope 过滤用。 */ private profile: Profile | null = null; /** 设置编译产物(transpile 后调)。v12.x:per-pi 实例字段——本 session 的 segment * 不会被其他 session 覆盖。 * v13.x(issue pt-turn-inject-not-profile-scoped):加 profile 参数——turn 路径 scope 过滤用。 */ setAgentContext( ctx: AgentContext, blueprint: Blueprint, domains: Domain[], profile: Profile ): void { this.ctx = ctx; this.blueprint = blueprint; this.domains = domains; this.profile = profile; this.segment = renderSessionInject(ctx, blueprint); } /** 清除当前 session 的 context;保留当前 runtime 的 handler 绑定。 * v12.x:per-pi 实例字段——只清本 session 的状态。 */ resetInjection(): void { this.ctx = null; this.blueprint = null; this.domains = []; this.profile = null; this.segment = null; } /** 启动时注册:把 AgentContext 注入到 Agent。 */ registerInject( api: AgentAPI, ctx: AgentContext, blueprint: Blueprint, domains: Domain[] = this.domains, profile: Profile | null = this.profile ): void { // 先更新状态;同一 runtime 的后续 Profile 切换不能重新注册 handler, // 但 handler 会在事件发生时读取最新的 this.segment / this.ctx。 this.ctx = ctx; this.blueprint = blueprint; this.domains = domains; this.profile = profile; this.segment = renderSessionInject(ctx, blueprint); if (this.injectedApi === api) { api.log?.debug("agent:registerInject skipped (already injected)"); return; } this.injectedApi = api; // session 注入(inject=session → Pi system_prompt 事件):每轮追加 segment // v10.x:包 try/catch,运行时异常走 api.log.error + ui.notify,不再 swallow // v11.x(issue pt-injection-status-manual-track):三分支写 session.injectionState + // 调 api.ui?.setStatus,让 footer 三态文字真实反映注入结果(自报,不检测 Pi) // v12.x:从 args[1] ctx 拿 per-session sessionId,写 per-session SessionState api.on("before_agent_start", async (...args: unknown[]) => { const t0 = Date.now(); try { const sessionId = sessionIdFromArgs(args); const sessionState = sessionId ? getSessionById(sessionId) : null; const currentSegment = this.segment; if (!currentSegment) { // 无 segment(未加载 Profile / 已被 reset)→ idle if (sessionState) { sessionState.injectionState = "idle"; sessionState.injectionError = null; // v18.x(issue pt-footer-status-stale-cache):改调 refreshInjectionFooter // ——拼 manual suffix + 同步 lastFooterText,避免 PiAdapter 直写 setStatus // 绕过缓存导致 manual suffix 丢失 / stale 缓存。 if (api.ui) refreshInjectionFooter(api.ui, sessionState); } return undefined; } const event = args[0]; if (!isSystemPromptEvent(event)) { // 事件形状异常:归类为 idle(不视为失败——Pi 可能改了事件签名) if (sessionState) { sessionState.injectionState = "idle"; sessionState.injectionError = null; if (api.ui) refreshInjectionFooter(api.ui, sessionState); } return undefined; } const final = `${event.systemPrompt}\n\n## 当前任务上下文\n\n${currentSegment}`; api.log?.debug("agent:before_agent_start ok", { originalLen: event.systemPrompt.length, injectedLen: final.length, deltaLen: final.length - event.systemPrompt.length, durationMs: Date.now() - t0, }); if (sessionState) { api.onInjected?.(final); // 成功注入 → injected sessionState.injectionState = "injected"; sessionState.injectionError = null; if (api.ui) refreshInjectionFooter(api.ui, sessionState); } else { api.onInjected?.(final); } return { systemPrompt: final }; } catch (e) { const msg = e instanceof Error ? e.message : String(e); api.log?.error("agent:before_agent_start failed", { err: msg, durationMs: Date.now() - t0, }); api.ui?.notify(`[pt] before_agent_start failed: ${msg}`, "error"); // 异常 → failed + 错误消息(footer 追加) const sessionId = sessionIdFromArgs(args); const sessionState = sessionId ? getSessionById(sessionId) : null; if (sessionState) { sessionState.injectionState = "failed"; sessionState.injectionError = msg; if (api.ui) refreshInjectionFooter(api.ui, sessionState); } return undefined; // 失败降级, 不影响主流程 } }); // v19(issue pt-llm-tool-consolidation):删除 api.on("input") handler // - 人类不再用 `/pt_turn_inject` 触发 TurnContext 注入(语义不符 + 与 tool 重复) // - LLM 调 `pt_inject` tool 仍走同一 renderTurnInject 内核(src/index.ts 调) // - renderTurnInject 仍按 `/pt_turn_inject ` 字符串前缀解析(tool execute 拼前缀传入) // 是内部耦合保留,不在本 scope(issue §边界纪律) } /** 查询可用手册(/pt flows 用)。 * v9:遍历 Blueprint 的 inject=turn 聚合组 → 引用 Domain → 找 FlowTemplate + Rules 段的 Rule。 */ listManuals( _ctx: AgentContext, blueprint: Blueprint, domains: Domain[] ): Array<{ name: string; hint?: string; domain: string }> { const flows: Array<{ name: string; hint?: string; domain: string }> = []; for (const group of blueprint.groups) { // Phase term-P4.3:inject 语义值 context_message → turn // Phase term-naming:字段名 target → inject if (group.inject !== "turn") continue; // v13.x(issue pt-turn-inject-not-profile-scoped):按 Profile scope 过滤 domains—— // adapter 内部统一过滤,调用方(commands.ts flowsText)无需预过滤。 const scoped = filterDomainsByProfile(domains, this.profile); for (const d of scoped) { // Phase term-P9.2:FlowTemplate 在 ## Flows 段;Rule[] 在 ## Rules 段;Checklist[] 在 ## Checklists 段。 const flowsContent = d.modules[MOD_FLOWS]; if (isFlowTemplateArray(flowsContent)) { for (const t of flowsContent) { flows.push({ name: t.name, hint: t.argumentHint, domain: d.name }); } } const rulesContent = d.modules[MOD_RULES]; if (isRuleArray(rulesContent) && rulesContent.length > 0) { // v19(issue pt-llm-tool-consolidation):name 改为纯 domain 名(不再伪装成人类命令); // flowsText 文案相应改为"调 pt_inject tool 获取 手册"。 flows.push({ name: d.name, hint: `${rulesContent.length} 条规范`, domain: d.name, }); } // Checklist[] 暂不单独暴露——pt_inject tool 会统一处理(renderDomainManual) } } // 去重(按 name) const seen = new Set(); return flows.filter((f) => { const key = `${f.domain}/${f.name}`; if (seen.has(key)) return false; seen.add(key); return true; }); } } // ==================== Pi ExtensionAPI 事件 type guards ==================== function isSystemPromptEvent(x: unknown): x is { systemPrompt: string } { return ( !!x && typeof x === "object" && typeof (x as { systemPrompt?: unknown }).systemPrompt === "string" ); }