外观
面向其他 Pi 扩展与宿主集成的公共接口:进程内 RPC、结构化委派 API、启动预检、能力上限、后台工作提供方契约,以及 Herdr 集成。
受信任的工作流资源
已加载的受信任 TypeScript 扩展可从 pi-subagents/workflow-resources 导入 registerWorkflowResource。该子路径不会加载主扩展,也未暴露任何解析器或许可构造器。它导出的类型为 RegisterWorkflowResourceInput、WorkflowResourceDefinition 与 WorkflowResourceRegistration:
typescript
registerWorkflowResource({
sessionId: string,
definition: {
name: string,
version: number,
resolve(args: Readonly<Record<string, unknown>>):
| { script: string; hostCommands?: readonly { key: string; command: string }[] }
| { error: string },
},
}): { dispose(): void }名称大小写敏感,最长 128 个字符,且匹配正则 [A-Za-z0-9][A-Za-z0-9._-]*;请使用扩展前缀。版本为正的安全整数。遇到非法输入、受保护的内置项(review、run-ci)或同一会话内重复的名称时,注册会抛出错误。不同会话可以注册相同的名称。替换前请先 dispose;不会发生静默覆盖。
请在 session_start 中使用 ctx.sessionManager.getSessionId()(而非会话文件路径或工具参数)进行注册,在 session_shutdown 中释放。新建/恢复/分叉的会话以及重载都需要从替代运行时的 session_start 重新注册;不要保留旧的 pi/ctx 引用。该扩展负责清理,而非自动的注册生命周期管理器。dispose 是幂等的,且不能移除较新的替代项。缺失清理可能在重载时导致重复注册失败。
resolve 必须执行同步的、有界的校验与字符串构造,不进行 I/O、SDK 调用、计时器或进程工作。核心会对纯 JSON 参数进行深拷贝:编码后最多 16 KiB,嵌套深度为 8,每个对象 16 个字段,每个数组 64 项,有限数字,最长 16 KiB 的非空字符串。该扩展必须额外地拒绝不受支持的字段,并校验资源特定的语义。抛出错误、Promise/thenable 以及格式错误的展开会在授权签发前失败;错误长度上限为 4096 个字符。
宿主授权绑定的是精确的键/已去除空白的命令对,而不是独立的键与命令集合。最多接受 32 个授权,使用唯一的安全工作流键以及不含 NUL 的、最长 16 KiB 的非空命令。省略授权则不会授予宿主权限。核心在解析时对展开和授权进行快照。dispose 会停止未来的查找,但已被接纳的工作流仍保留已捕获的授权,即使在替换之后。请使用既有的停止/截止时间控制进行取消;本接口不承诺在宿主关闭或持久化命名调度下仍然存活。既有的子接纳与能力上限仍然适用。
子智能体与有限宿主混合示例
该扩展拥有两个固定命令;scripts/finite-check.mjs 必须是工作流 cwd 中已存在的、受信任的有限辅助脚本。它先运行一个评审者,再运行一个检查。命令或标志均不来自自由形式的公共参数。
typescript
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
import { registerWorkflowResource } from 'pi-subagents/workflow-resources';
export default function (pi: ExtensionAPI) {
let registration: { dispose(): void } | undefined;
pi.on('session_start', (_event, ctx) => {
registration?.dispose();
registration = registerWorkflowResource({
sessionId: ctx.sessionManager.getSessionId(),
definition: {
name: 'acme.review-check',
version: 1,
resolve(args) {
if (Object.keys(args).some((k) => k !== 'task' && k !== 'check'))
return { error: 'Only task and check are supported.' };
if (typeof args.task !== 'string' || !args.task.trim() || args.task.length > 4000)
return { error: 'task must contain 1–4000 characters.' };
if (args.check !== 'quick' && args.check !== 'full')
return { error: 'check must be quick or full.' };
const command =
args.check === 'quick'
? 'node ./scripts/finite-check.mjs --mode quick'
: 'node ./scripts/finite-check.mjs --mode full';
const host = { kind: 'command', command, timeoutMs: 120000 };
return {
hostCommands: [{ key: 'check', command }],
script: `
const review = await runs.run("review", {
agent: "reviewer", task: ${JSON.stringify(args.task)}
});
if (!review.ok) throw new Error("Review child failed");
const check = await runs.host("check", ${JSON.stringify(host)});
return { review: review.output, check };
`
};
}
}
});
});
pi.on('session_shutdown', () => {
registration?.dispose();
registration = undefined;
});
}通过公共 subagent 工具调用(前台请使用 async: false):
json
{
"workflow": "acme.review-check",
"args": { "task": "Review the current change; return findings only.", "check": "quick" },
"async": true
}父会话评估子智能体的发现以及常规的命令日志/状态/终止回执;子智能体的成功并不代表对评审结果的批准或证明。上述超时仅约束宿主命令,而非整个工作流。
信任边界:该 API 组合的是已加载的受信任代码;它既不是身份验证,也不是沙箱。会话 ID 仅界定查找范围,不授权恶意扩展之间的访问。核心负责不透明的许可和来源;调用者提供的颁发者/信任/许可元数据不能授予权限。原始公共脚本与脚本路径不会获得宿主权限,注册也不是公共参数的任意命令入口。
现有的 shell 运行器使用工作流 cwd 与继承的环境。精确匹配不会锁定 PATH 解析、可执行字节、仓库辅助脚本或凭据。这些仍属于运维人员/扩展的信任责任。JSON.stringify 将数据嵌入 JavaScript 源码;它并不是 shell 转义。请保持命令如上所示的固定形式,或在绑定已知位置前严格校验有界的数值/十六进制 token;切勿将任意任务文本或标志拼接到 shell 命令中。本接口不提供新的运行器、cwd 隔离、CI/合并策略或 SDK 生命周期框架。
进程内事件总线 RPC
其他 Pi 扩展可以使用进程内的事件总线 RPC,而不必抓取斜杠输出或调用内部模块。监听 subagents:rpc:v1:ready,在 subagents:rpc:v1:request 上发送请求,并从 subagents:rpc:v1:reply:<requestId> 读取回复。
typescript
const requestId = crypto.randomUUID();
pi.events.on(`subagents:rpc:v1:reply:${requestId}`, (reply) => {
// { version: 1, requestId, success: true, data } 或
// { version: 1, requestId, success: false, error: { code, message } }
});
pi.events.emit('subagents:rpc:v1:request', {
version: 1,
requestId,
method: 'spawn',
params: {
workflowScript: `return runs.run("main", { agent: "reviewer", task: "Review the current diff" })`,
context: 'fresh'
}
});RPC 方法包括 ping、status、manage、spawn、steer、interrupt、stop 和 resume。status、manage、steer、interrupt 与 resume 复用了常规的、由本扩展所有的动作。
方法说明:
manage仅暴露一个范围狭窄的调度白名单:schedule.list、schedule.show、schedule.history、schedule.pause、schedule.resume、schedule.run与schedule.delete。除schedule.list外的所有动作都需要id。任务记录、智能体、配置、工作树以及任意的管理动作会在执行器派发前被拒绝。ping.capabilities.managementActions会公布这份精确的白名单。spawn接受结构化的单子执行(agent、task?)、内联的workflowScript或workflowScriptPath,并且仅异步:省略async或设为async: true,省略clarify,不要传递管理类的action值。相对脚本路径相对于请求的cwd解析。它走与subagent工具相同的执行器,因此智能体发现、校验、会话归属、已配置的派生上限、子安全深度、产物与异步状态都表现一致。steer需要一个异步运行id(加上可选的子index)和一条非空的message;其回复保留常规的"已确认送达"结果。可选的mode值有steer(默认)、follow_up与auto,回执包含deliveryStatus: "delivered" | "queued"。RPC steer 在所有模式下禁用直接工具的"暂停并复活"恢复路径,使扩展保留对所派生的精确子智能体的控制权;ping.capabilities.nonRecoveringSteer会公布此保证。resume需要一个运行目标和非空的message。它委托给既有的恢复路径,该路径会在返回新的异步运行详情前校验当前会话归属、已持久化的会话/恢复元数据、停止/存活状态、能力上限以及独占的会话租约。调用者可以请求一个仅文件的file-only输出路径以获得恢复后的结果,而不必覆盖其模型、工具或预算。ping.capabilities.resume会公布此接口。stop通过停止控制通道面向当前会话的顶层异步运行,并记录一次stopped生命周期,而非报告超时。status在执行器支持的路径上保留定向与丰富的请求。没有id、runId、dir、index、view或lines的请求可以使用已恢复的内存投影和一段简短摘要;当实时状态缺失、过时、会话不匹配或未恢复时,回退到常规的执行器状态。状态中的view、lines、index会按定向的对话记录/舰队请求转发。成功的回复保留text、details、fleet与asyncSnapshot;简短摘要有意省略规范的文件系统详情、等待订阅与预算标注。
ping 上公布的能力:
events.asyncComplete— 在 RPCspawn之后进行精确的进程本地完成关联。managementActions— RPCmanage接受的精确调度管理动作。launchResolvedExtensions— 状态详情中可选的"已启动解析的扩展"投影。runtimeAcknowledgedExtensions— 可选的子运行时确认投影与事件名。processTerminalProof— 进程终止证明状态(见 observability.md)。nonRecoveringSteer— RPC steer 永不进行暂停并复活。resume— 上述的恢复接口。statusProjection: { version: 1, untargeted: "in-memory-when-ready", targeted: "executor" }— 非定向状态可以使用已恢复的有界投影;定向或丰富的状态仍由执行器支持。fleetStatus: { version: 1 }— 成功的status回复还会包含data.fleet。
一旦前台执行分配了 runId,结构化委派的进度更新就会携带该 runId,因此即使调用者自身的工具回合在终态响应前被打断,也可以保留本扩展所有的恢复目标。前台的 details.results[] 行还包含一个在运行内唯一、且在部分进度快照与最终结果之间保持稳定的数字 index;请使用 (runId, index) 而不是行位置来关联单次、计数的并行与链式子节点。
舰队状态 DTO
当 ping.capabilities.fleetStatus 为 { version: 1 } 时,成功的 status 回复包含 data.fleet:{ version: 1, entries, totalActive, omitted }。
条目是有界的、当前会话公共展示记录,包含一个不透明的对账 key、已解析的 agent、可选的 role、model、effort、面向调用者的 goal、安全的 startedAt 以及 { input, output, total } 令牌。totalActive 与 omitted 保留了超出有界条目窗口之外的溢出信息。
该 DTO 有意永远不会暴露运行、异步或工具 ID。客户端必须忽略未知字段,并在能力缺失时回退到状态文本。
data.asyncSnapshot 是另一份独立的有界投影,在可用时包含在成功的状态回复中。其 runs[].id 包含异步运行 id;与舰队 DTO 不同,它不是不透明的展示键。舰队键仍是不透明的,绝不能被解读为运行或异步标识符。
作用域
pi.events 仅在进程内。它不能到达独立的 Pi 进程或子智能体;请使用文件生命周期产物或 pi-intercom 进行跨进程协调。
从独立扩展进行运行时智能体注册
一个独立安装的 Pi 扩展可以通过进程本地的 pi-subagents:runtime-agent-register:v1 事件将智能体注册到已安装的 pi-subagents 所有者。请在扩展初始化之后(如 session_start 期间)发出该事件。事件传递是同步的,因此所有者在 emit() 返回前将结果写入请求。
typescript
const request: {
version: 1;
name: string;
definition: {
description: string;
systemPrompt: string;
tools?: readonly string[];
};
result?: { ok: true; registration: { dispose(): void } } | { ok: false; error: Error };
} = {
version: 1,
name: 'runtime-probe-agent',
definition: {
description: 'Agent registered by an independent extension',
systemPrompt: 'Return the words runtime probe.',
tools: []
}
};
pi.events.emit('pi-subagents:runtime-agent-register:v1', request);
if (!request.result) throw new Error('pi-subagents is not installed or not ready');
if (!request.result.ok) throw request.result.error;
const registration = request.result.registration;
// 在你的扩展清理期间调用 registration.dispose()。如果 pi-subagents 是消费包的可解析依赖,则 pi-subagents/agents 会导出 RUNTIME_AGENT_REGISTER_EVENT、请求/结果类型,以及 registerAgentViaEvents() 来达成同一契约。一个单独安装的 Pi 包不会自动成为另一个包的 Node 依赖。在这种情况下,请直接使用事件契约,而非运行时导入。类型级的开发依赖是可选的。
已安装的所有者会应用既有的运行时智能体验证、冲突检查、限额、运行时源元数据以及清理。如果有多个所有者监听,则首个写入 request.result 的处理程序胜出。不支持的版本、格式错误的请求以及注册失败会返回 { ok: false, error }。没有结果意味着没有兼容的所有者处理该事件。
该契约为进程本地。它不会在子会话或其他 Pi 进程中注册智能体,也不会改变包发现或包解析。
舰队视图中的外部任务
使用 pi-subagents/external-runs 将由其他扩展拥有的、仅用于显示的当前会话任务发布出去:
ts
import {
registerExternalRun,
updateExternalRun,
unregisterExternalRun
} from 'pi-subagents/external-runs';
registerExternalRun({
id: 'dependency-review',
sessionId: ctx.sessionManager.getSessionId(),
source: 'interactive-shell',
label: 'Dependency review',
state: 'running',
startedAt: Date.now(),
currentAction: 'Inspecting package metadata'
});
updateExternalRun(ctx.sessionManager.getSessionId(), 'dependency-review', {
state: 'completed',
updatedAt: Date.now(),
endedAt: Date.now(),
preview: 'No dependency blockers found.',
reportPath: '/tmp/dependency-review.md'
});
unregisterExternalRun(ctx.sessionManager.getSessionId(), 'dependency-review');该 API 在调用者注册或更新任务时验证并缓存有界的显示字段。舰队视图仅读取该缓存,不会轮询调用者代码。snapshotExternalRuns(sessionId) 与 listExternalRuns(sessionId) 返回有界的当前会话快照。快照会在检查记录字段之前先按会话限定的缓存键进行过滤;API 写入的记录通过模块私有来源避免重复归一化,而通过进程本地注册表替换或变更的记录会按需验证。默认情况下,所请求会话的格式错误记录会抛出包含验证错误的异常。仅用于显示的舰队调用者可传入 { ignoreMalformed: true, onMalformedRecord } 以移除坏记录,并以编程诊断继续渲染。
外部任务是观察性的。调用者负责执行、持久化、取消与结果交付。舰队视图不为它们暴露 stop、steer、resume、cancel 或检查器控件。提供的报告与对话记录路径仅显示为有界文本;舰队视图不会读取任意的外部路径。
启动契约预检
当扩展需要在决定是否运行前检查已解析的子启动契约时,请使用 pi-subagents/preflight:
ts
import { resolveSubagentLaunchContract } from 'pi-subagents/preflight';
const result = await resolveSubagentLaunchContract({
agent: 'reviewer',
task: 'Review the current diff.',
context: 'fresh',
cwd: ctx.cwd,
sessionRoot: '/tmp/my-extension-preflight-session-root',
availableModels: ctx.modelRegistry.getAvailable()
});
if (!result.ok) {
// missing_agent、ambiguous_agent、missing_skill、denied_required_tool、
// invalid_artifact_dir、invalid_cwd、unsupported_mode、restricted_agent、
// thinking_ceiling、invalid_extension_bindings 或 invalid_intercom_bridge
throw new Error(result.message);
}
console.log(result.contract.digest, result.contract.tools.effectiveAllowlist);预检涵盖常规的单智能体启动解析:
- 所选智能体的身份与被遮蔽的候选。
- 已解析定义的摘要,包括系统提示词和影响启动的模型、工具、技能、扩展、输出与记忆字段。运行时覆盖(如 Intercom 桥接)永远不会改变它。
- fresh/fork 上下文、实际生效的模型与思考级别、技能与工具解析、直接的 MCP 选择、运行时/已配置的扩展。
- 已解析的 Intercom 桥接状态(
intercomBridge.mode与intercomBridge.active)。激活的桥接会将桥接指令附加到子提示词中,并将contact_supervisor添加到已声明的工具列表中,与执行时的行为完全一致。 - 产物/会话路径、异步生命周期/状态/结果/事件/进程终止路径、包/生命周期版本、能力上限审计数据与稳定摘要。
launchContractDigest 是调用者任务、实际生效的系统提示词(含激活的桥接指令)、模型候选、实际生效的工具/扩展/MCP(含继承的能力上限与桥接工具)、输出绑定以及常规前台与异步执行在结果/状态/事件和元数据中所报告的结构化输出 schema 的规范摘要。预检与每条报告摘要的执行路径都通过同一绑定组装它,因此相同输入会产生相同摘要。
桥接输入:
intercomBridge会针对本次启动替换全局的intercomBridge配置,语义与subagent工具及委派覆盖相同。请向你要比较的启动传入相同的值。预检在每次调用时从磁盘读取全局配置,而正在运行的扩展保留其在启动时加载的配置,因此当摘要不能依赖于该文件时,请传入覆盖。- 默认的桥接指令永远不命名父会话,因此大多数宿主不需要进一步的输入。当已配置的
instructionFile对{orchestratorTarget}进行插值时,除非宿主提供非空的orchestratorTarget,否则预检会报告host_required诊断;执行器通过pi-subagents/intercom-bridge中的resolveIntercomSessionTarget从父会话名与 id 派生该目标。
边界:
- 运行时的验收文本与输出任务标注被有意排除,因为无副作用的预检不会解析那些宿主/运行时的增强;启动与任务摘要明确划定了这一边界。
- 原始提示词不会暴露在公共契约输出中。
- 对于启动状态是无副作用的:它不会创建子会话、临时提示词文件、结构化输出运行时、工具诊断文件或运行产物。
- 某些宿主所拥有的事实(如精确的 fork 快照、嵌套异步根和实时模型注册表)只能由 Pi 宿主证明;这些事实会以
host_required诊断的形式出现,而不是装作精确。 - 预检会读取扩展配置,因此
defaultSubagentContext: "fresh"或"fork"会以与执行相同的方式影响被省略的上下文。显式的context仍优先。
结构化委派 API
其他 Pi 扩展可以通过结构化委派 API 让 pi-subagents 运行一个已配置的前台叶子智能体。它使用已有的 prompt-template:subagent:* 事件族,并使用与 subagent 工具相同的执行器;它不会新增另一个启动器。
ts
import {
SUBAGENT_DELEGATION_REQUEST_EVENT,
SUBAGENT_DELEGATION_RESPONSE_EVENT,
type SubagentDelegationRequest,
type SubagentDelegationResponse
} from 'pi-subagents/delegation';
const request: SubagentDelegationRequest = {
requestId: crypto.randomUUID(),
ownerRunId: workflowRunId,
nodeId: 'review-accuracy',
agent: 'reviewer',
task: 'Review the supplied evidence.',
context: 'fresh',
cwd: ctx.cwd,
thinking: 'high',
result: {
kind: 'structured',
schema: {
type: 'object',
properties: { verdict: { type: 'string' } },
required: ['verdict'],
additionalProperties: false
}
}
};
const unsubscribe = pi.events.on(SUBAGENT_DELEGATION_RESPONSE_EVENT, (payload) => {
const response = payload as SubagentDelegationResponse;
if (response.requestId !== request.requestId) return;
if (response.ownerRunId !== request.ownerRunId || response.nodeId !== request.nodeId) return;
unsubscribe();
// 检查 response.status、response.result、response.usage、model 与 thinking。
});
pi.events.emit(SUBAGENT_DELEGATION_REQUEST_EVENT, request);身份:
ownerRunId与nodeId是活跃的逻辑身份;requestId标识一次尝试。- 同一逻辑节点的第二次活跃尝试会收到
duplicate_node,而不会干扰原尝试。 - 已启动、更新、响应与取消的负载都携带完整的元组。取消仅影响完全匹配的元组,包括"取消先于启动"的竞争。
- 每次尝试最多发出一个终态响应。
结果:
- 结果模式是显式的。即使文本看起来像 JSON,它也保持为字面字符串。结构化模式返回单独捕获的、经 schema 校验的 JSON 值。
- 终态使用情况报告输入、输出、缓存读、缓存写、费用、回合、工具调用与持续时长,以及实际生效的模型与思考级别(若已知)。
实时更新事件是有界的进度快照,而非补丁,因此消费者应替换先前的快照,而不是作为增量合并。结构化委派会合并那些"委派可见进度与最近输出未变"的心跳;因此仅含持续时间的心跳不会再产生一次更新。终态响应仍是完整结果、错误与最终使用情况的权威来源。
边界:
- Schema 上限为 64 KiB;任务与返回的文本/结构化值上限为 1 MiB,身份/配置字符串的边界更小,且
timeoutMs最大为 2,147,483,647。 - 结构化委派接受
toolBudget: { hard: 0, block: "*" }来阻止首次工具调用并运行一个零工具叶子智能体;常规的面向模型/已配置的预算保持其"最少一次"的现有最低值。 intercomBridge可为单次委派替换全局桥接配置,例如在没有监督者会话应答子智能体时使用{ mode: "off" }。请将相同的值传递给resolveSubagentLaunchContract,以将launchContractDigest与终态响应进行比较。- 前台桥接为每个扩展上下文保留最多 8,192 个精确的待取消与已结算的尝试身份。如果任一历史记录填满,它会以
unavailable_context为后续的启动失败闭合,而非逐出身份事实;生命周期重置会清空有界历史记录。
约束:
- 委派需要一个活跃的扩展上下文。请从受支持的事件回调或排队的应用步骤中发出请求,不要在另一个工具的
tool_call钩子内递归地调用subagent工具。 - 调用者选择已配置的智能体,但智能体的发现与有效工具仍由本扩展所有。请求不能授予任意工具,工具限制也不是操作系统沙箱。
- 分离的 RPC 仍仅异步;该 API 仅前台。
带有 requestId、agent、task、context、model 与 cwd 的无版本提示词模板负载会被作为旧版直接委派拒绝。新的集成必须使用上述结构化的"已拥有的叶子智能体"请求。pi-subagents/delegation 是扩展集成的规范契约。
能力上限
父扩展可以在不向 subagent 添加模型可见字段的情况下,实施带会话作用域的、带外能力上限:
ts
import { registerSubagentCapabilityCeiling } from 'pi-subagents/capability-ceiling';
const restriction = registerSubagentCapabilityCeiling({
sessionId: ctx.sessionManager.getSessionId(),
source: 'plan-mode',
ceiling: {
allowedAgents: ['plan-scout', 'plan-researcher', 'plan-reviewer'],
allowedTools: ['read', 'grep', 'find', 'ls'],
denyExtensions: true
}
});
// restriction.update(...) 会以原子方式替换该提供方的策略。
// restriction.dispose() 仅移除该提供方的注册。语义:
- 活跃的注册会对其
allowedTools与allowedAgents集合取交集,并对denyExtensions取并集。 - 显式的空列表意味着该字段对调用方没有任何工具或可启动的智能体;省略列表则不限制名称。
allowedAgents条目是规范智能体名称,且大小写敏感。- 启动不在白名单中的智能体会在派生前失败;
{ action: "list" }将受限的智能体保留在一个独立的不可执行部分中,而不是静默隐藏。 - 已解析的快照以单调方式向下传递至嵌套与异步子节点,并被保留用于恢复。
- 当输出 schema 要求时,
structured_output可作为本扩展所有的内部协议工具保留;它不是调用方的能力。 - 被拒绝的惰性技能
read需求会在派生前失败,而不是扩展上限。
denyExtensions 会抑制环境、已配置的与 MCP 提供方扩展,同时保留子协议强制所需的本扩展运行时。这是同一进程内的策略边界,而非针对已在父进程中运行的恶意代码的沙箱。
在上限处于活跃状态时所创建的调度会被拒绝,直至持久的调度持久化可用;不受限的调度仍受其触发时任何活跃策略的约束。公共状态仅暴露有界的审计计数与来源,永不暴露完整的扩展路径。
后台工作提供方 API
其他 Pi 扩展可以通过进程本地的提供方契约,将其当前会话的任务暴露给 bg_wait:
ts
import { registerBackgroundWorkProvider } from 'pi-subagents/background-work';
const dispose = registerBackgroundWorkProvider({
name: 'my-background-extension',
wakeChannels: ['my-extension:job-finished'],
listActiveWork: (context) =>
jobs
.filter(
(job) => job.status === 'running' && (!context || job.ownerSessionId === context.sessionId)
)
.map((job) => ({ id: job.id, sessionId: job.ownerSessionId })),
reconcile: ({ sessionId, nowMs }) => reconcileJobs(sessionId, nowMs)
});语义:
- 每条记录都需要一个稳定的、提供方本地的 ID 以及拥有它的精确 Pi 会话 ID。
bg_wait会捕获这些身份而非计数,因此当一个任务完成而另一个启动时,仍能以"先完成"的方式满足等待而不丢失替代项。 listActiveWork在快照期间接收一个可选的{ sessionId, nowMs }上下文。提供方可以使用sessionId来避免扫描无关工作;既有的零参数() => items提供方继续工作,返回的条目仍会被校验并过滤到精确请求的会话。- 它会将快照过滤到活跃会话;如果提供方在其工作被追踪时消失则失败闭合,并以提供方上下文显示格式错误的快照或提供方错误。
- 唤醒通道仅用于缩短轮询;已校验的快照仍为权威来源。
- 提供方通过
Symbol.for("pi-subagents.background-work.v1")共享一个注册表,使独立加载的扩展模块能够在一个 Pi 进程中汇聚。 - 注册是重载安全的:同名的新提供方会替换旧回调,而旧的释放器不能移除替代项。请尽可能在扩展关闭时调用释放器。
子智能体不会自动获得提供方工具或扩展。请将 bg_wait 添加到子智能体智能体的 tools 白名单,并通过 extensions 或 subagentOnlyExtensions 加载每个提供方。父会话生效的 waitTool 设置通过其类型化的运行时配置传递给每个子智能体;PI_SUBAGENT_WAIT_TOOL_ENABLED 在父会话中保持优先级。
前台子智能体永远不加载父会话的环境扩展:它们与父会话共享同一进程,若加载它们会在其内部启动每个环境扩展的第二份拷贝,包括本扩展。需要 MCP 工具(mcpDirectTools,或来自像 pi-mcp-adapter 这类环境适配器的 MCP 工具)或来自提供方扩展的模型的智能体必须作为后台子智能体运行(async: true),它们会在分离的运行器进程内加载环境扩展,除非该智能体设置了 extensions 或能力上限拒绝了扩展。
外部任务提供方桥接
拥有长时间运行的咨询任务的扩展可以为 runner.type: external-job 智能体注册一个进程本地的提供方:
ts
import { registerExternalJobProvider } from 'pi-subagents/external-job-provider';
const dispose = registerExternalJobProvider({
name: 'surf-oracle',
start: ({ prompt, promptDigest, cwd, runId, stepIndex, agent, options }) =>
startSurfJob({ prompt, promptDigest, cwd, runId, stepIndex, agent, options }),
followUp: ({ prompt, parentProviderJobId, requestId, requestDigest, options }) =>
followUpSurfJob({ prompt, parentProviderJobId, requestId, requestDigest, options }),
status: (providerJobId) => getSurfJobStatus(providerJobId),
result: (providerJobId) => getSurfJobResult(providerJobId),
reattach: (providerJobId) => reattachSurfJob(providerJobId)
});提供方返回带有 providerJobId、state、可选的 handleUrl/conversationUrl、可选的 failureCode/failureMessage 与针对容量冲突的 blockingJobId 的句柄。result 还可以返回 output 与/或 artifactPath。
followUp(input) 是可选的。当它存在时,已完成的外部任务运行可通过 subagent({ action: "resume", id: "<run>", message: "..." }) 继续。Pi 会发送已完成的父提供方作业 id,以及一个稳定的 requestId 与 requestDigest。提供方必须延续该父会话,否则失败闭合。在父会话缺失时,它不能打开新的线程。
异步运行器进程不会导入提供方内部实现。它将操作请求写入其异步运行目录。父 Pi 进程会针对已注册的提供方处理这些请求,并写入操作响应。如果提供方未注册,桥接会失败闭合并给出可操作的错误。如果一次运行在提供方作业元数据存在后被恢复,则运行器会调用 reattach 与 result;它不会再次调用 start 或 follow-up。
检查器集成
检查器是用于现有异步运行的可移植命令与动作面。公共动作为:
ts
subagent({ action: 'inspector.command', id: '<run-id>', index: 0 });
subagent({ action: 'inspector.open', id: '<run-id>', index: 0, focus: true });
subagent({ action: 'inspector.status', id: '<run-id>', index: 0 });
subagent({ action: 'inspector.close', id: '<run-id>', index: 0 });inspector.command 返回独立的运行器命令,不联系宿主,也不写入绑定。inspector.open 选择一个可用的内置检查器插件。status 与 close 选择拥有该运行绑定的插件,并在该插件不支持所请求的生命周期动作时清楚地报告。在没有可用插件的情况下,open 会失败闭合并给出可操作的消息;普通的启动仍以无头方式进行。关闭检查器永远不会停止运行。
Herdr 检查器插件
内置的 Herdr 检查器插件支持 Herdr 0.7.5+。它打开的是原始仪表板面板,而非子会话,也不是字面意义上的 attach。它会读取生命周期、状态、输出与任务记录产物;steer 与 stop 继续通过 pi-subagents 既有的控制收件箱。仅与 inspector.open 一起使用 focus;Herdr 0.7.5 无法聚焦任意既有的原始面板 id。
Ghostty 检查器插件
macOS 上的 Ghostty 1.3+ 是第二个内置的"仅打开"插件,使用 Ghostty 的 preview AppleScript API。它分割当前聚焦的终端并启动只读的检查器命令;status 与 close 不可用,因为它不写入绑定。需要 Ghostty Automation 权限。
Herdr 集成
当 Pi 在 Herdr 内运行时,pi-subagents 会通过 Herdr 面板元数据自动报告活跃的异步运行计数。
- 仅当 Herdr 提供
HERDR_ENV=1与HERDR_PANE_ID时,桥接才会启用;在 Herdr 之外不会注册任何监听器或计时器。 - 它会在
/reload或/resume之后恢复当前会话的活跃运行,在工作处于活跃状态时刷新元数据,并在完成或关闭时清除。 - 当异步子智能体需要关注时,桥接使用 Herdr 既有的
herdr:blocked兄弟事件,并在异步工作仍然存在时发出herdr:busy。支持该兄弟事件的 Herdr 版本会保持面板的语义状态为working;较旧的版本会安全地忽略它,并仍会在 Pi 集成保持生命周期权威时显示元数据标签。 - 拥有 Pi 会话是其自身面板元数据的唯一发布者。当活跃工作流具有显式有界
label时,最新的活跃标签会出现在摘要与紧凑的title-suffix中;重叠的完成会恢复之前的活跃标签。原始的任务与目标提示词永远不会进入 Herdr 元数据。没有标签时,一个活跃运行使用其智能体名称,两个或更多则使用活跃运行计数。关注会追加⚠,当活跃工作达到 0 时,后缀会被清除。
要在展开的 Agent 侧栏中显示报告的标签,请在行布局中包含 state_text 或 $summary:
toml
[ui.sidebar.agents]
rows = [
["state_icon", "workspace", "tab"],
["agent", "state_text"],
]项目面板
对于在另一个代码库中的实质性工作,Herdr 0.7.5+ 可以打开一个项目所有的 Pi 面板,并扎根于该仓库:
ts
subagent({
action: 'project.open',
cwd: '/path/to/repo',
message: 'Own the auth refresh mission for this project.'
});
subagent({ action: 'project.status', cwd: '/path/to/repo' });
subagent({ action: 'project.close', cwd: '/path/to/repo' });项目面板在其目标目录中运行自己的 Pi 会话,因此从该面板启动的子智能体会使用该项目的配置、智能体、技能、文件、git 状态与任务记录。父会话保留协调权威,但并不拥有或控制对等面板内的子智能体。既有的无头运行不会被移入该面板。面板绑定位于 <projectRoot>/.pi/subagents/project-panes/herdr.json 下,且仅作为指向 Herdr 面板的本地指针。
其他 Pi 扩展应使用带版本的公共 TypeScript 接口,而非调用面向模型的工具或导入检查器内部:
ts
import {
PROJECT_PANES_API_VERSION,
openProjectPane,
getProjectPaneStatus,
focusProjectPane,
closeProjectPane
} from 'pi-subagents/project-panes';
const opened = await openProjectPane({ cwd: '/path/to/repo', focus: false });
const status = await getProjectPaneStatus({ cwd: '/path/to/repo' });
const focused = await focusProjectPane({ cwd: '/path/to/repo' });
const closed = await closeProjectPane({ cwd: '/path/to/repo', requireIdle: true });该 API 返回具有判别式的结构化结果,包含规范项目根、绑定路径、面板身份、有界的 Herdr 运行时字段、稳定的错误代码与 PROJECT_PANES_API_VERSION: 1。
- 除非已保存的面板 id 仍为该项目验证且 Herdr 明确报告
agent_status: "idle",否则关闭会失败闭合。requireIdle会为已传递它的调用者保留,但不能削弱该规则。 - Focus 使用已保存的面板 id,向 Herdr 请求其
tab_id或workspace_id,然后调用匹配的 Herdr focus 命令。 - API 报告
trust: "human-verification-required"。它永远不会绕过或声称证明 Pi 的项目信任提示。
宿主会话生命周期与完成唤醒
嵌入本扩展的宿主负责决定完成唤醒是否能够被送达。
常规的异步与前台完成唤醒使用 registerSubagentNotify 与 sendCompletion。它们监听完成事件,并通过 pi.sendMessage(..., { triggerTurn }) 投递。会话关闭会停止结果监视器并释放此完成通知器。createWaitSubscriptionManager 是独立的:它是针对没有原生通知的工作的显式非阻塞 bg_wait 订阅路径,而非常规的完成唤醒路径。
分离的子智能体在会话关闭时不会停止。它们是宿主进程的子进程,而非会话的子进程,因此该运行继续、完成且不通知任何人。失去的是通知,而非工作本身。
这一点很重要,因为"父会话是否忙碌?"是错误的空闲信号。父会话派生一次分离的运行并将控制权交还——这是异步启动输出告诉它做的事情——它不会进行提示、流式传输、压缩或运行 shell 命令。仅基于这些信号就回收会话的宿主会恰好释放那个本应等待被唤醒的会话。
当 pi-subagents 在兼容的 pi-web 宿主内运行时,它会查找带版本的 Symbol.for("@agegr/pi-web/session-liveness/v1") 注册表,并为当前会话注册一个提供方。该提供方报告实时的 queued/running 异步任务、活跃的嵌套后代(包括在其直接父会话结算后仍保留的前台路由)、仍具有调度所有者或活跃子节点的前台控件,以及等待其批量送达定时器的完成通知。已保留的终态历史、未来的调度与等待订阅本身并不使会话保持活跃。注册会在会话变化时被替换,并在运行时关闭或重载期间被释放;其他宿主不受影响。
如果你的宿主回收空闲会话,请在它仍有活跃的分离工作时保持会话存活:
- 从异步运行目录下的状态文件读取运行状态,而非事件流量。长时间安静的工作流几乎不向父会话发送任何内容,因此"最近活动"的启发式会得出错误的结论。
- 将
queued与running视为活跃,与isActiveAsyncState保持一致。已被中断并处于paused的运行是已结算的。一个因子智能体使用contact_supervisor而暂停的工作流仍具有一个活跃子节点;在对账写入complete或failed之前,请保持该父会话。 - 不要将
lastUpdate视为心跳。运行器在内存中每秒推进一次,但仅在活动分类变化时才重写status.json,因此在一个长时间安静的工具调用内,活跃的运行会在文件后留下陈旧记录。依据文件年龄判断活跃度会恰好回收你本想保护的那个运行。 - 优先使用已记录的运行器
pid,它在一次安静的工具调用中保持为 true,并在运行器死亡时变为 false。仅在没有记录 pid 的运行上将文件年龄作为兜底,并为其设置一个宽窗口。 - 将
status.json中的sessionId与两种形式都匹配。它被解析为getSessionFile() ?? getSessionId(),因此通常为父会话的会话文件路径,但未持久化的会话会改为记录一个裸的会话 id。
错失这一点时的症状是安静的,且容易被误归因:子智能体看似从不回报,这看起来像是本扩展的故障,而非释放了监听器的宿主的故障。
运行时文件
本仓库中主要的运行时文件:
| 文件 | 用途 |
|---|---|
src/extension/index.ts | 扩展注册、工具注册、消息/渲染连线。 |
src/integrations/pi-web-session-liveness.ts | 可选的 pi-web 空闲回收的活跃度桥接。 |
src/agents/agents.ts | 智能体与链式调用的发现、frontmatter 解析。 |
src/runs/foreground/subagent-executor.ts | single、parallel、chain、management、status、interrupt 与 doctor 动作的主执行路由。 |
src/runs/foreground/execution.ts | 核心前台 runSync 处理:为每次尝试驱动一个进程内子会话。 |
src/runs/shared/child-session.ts | 进程内子会话工厂(可注入缝后的 createAgentSession)与共享的模型运行时;被两条启动路径共用。 |
src/runs/shared/child-launch.ts | 为任一宿主进程内的子智能体构建工具计划、类型化的子运行时配置与会话启动。 |
src/runs/shared/child-tool-plan.ts | 子启动的工具、MCP 与扩展解析。 |
src/runs/shared/child-runtime-config.ts | ChildRuntimeConfig:子侧钩子所需的一切。 |
src/runs/shared/child-hooks.ts | 每个子智能体获得的行内钩子扩展(提示词运行时、快速模式、扇出)。 |
src/runs/background/subagent-runner.ts | 分离的异步运行器;将其后台子会话承载于自己的进程中。 |
src/runs/background/run-child-session.ts | 驱动一次后台子会话,并将其事件镜像到运行产物中。 |
src/runs/background/runner-aliases.ts | 将宿主页的 peer 包别名映射到为运行器安装的 pi 包(JITI_ALIAS)。 |
src/runs/background/async-execution.ts | 后台启动支持。 |
src/runs/background/async-status.ts | 异步运行的状态发现与格式化。 |
src/workflows/scripted-workflow.ts / src/runs/foreground/subagent-executor.ts | 脚本化工作流编排与子启动路由。 |
src/shared/settings.ts | 链式行为、指令与配置辅助函数。 |
src/runs/shared/worktree.ts | Git 工作树隔离。 |
src/intercom/intercom-bridge.ts | 运行时对讲机桥接指令与诊断。 |
src/extension/schemas.ts / src/shared/types.ts | 工具模式、共享类型与事件常量。 |
test/unit/ / test/integration/ | 单元测试与基于加载器的集成测试。 |
