- agent_run gains depth / spawn_reason (0029); agent_run_event types
subagent_spawned / subagent_event / subagent_completed
- agent.spawn (propose, medium, replay never): allowlist ⊆ parent, mode not
above parent, budget carved from parent's remainder, idempotent per run+input;
settings.pi.subagents {enabled, maxConcurrent, maxDepth} (subagentsEnabled kept as alias)
- child runs claim a concurrency slot under an advisory lock (waitForFact while
queued); agent.await polls business tables without calling the model
- parent abort cascades to all descendants; parent runtime budget counts
descendant usage; child events mirrored into the parent stream by parent seq
- authorize caps every child call by all ancestors' allowlists/kinds
- session page shows parent link, inherited info and the subagent tree
- e2e E11; runbook section on subagents
Co-Authored-By: Claude Opus 5.5 <[email protected]>
329 lines
9.0 KiB
TypeScript
329 lines
9.0 KiB
TypeScript
import { z } from "zod";
|
|
import { ApiScope } from "./common.ts";
|
|
|
|
/**
|
|
* 能力调用上下文(REQ §10.3)。产品自有接口,不是 Pi SDK 原生 API。
|
|
* signal 由任务取消传播而来。
|
|
*/
|
|
export interface CapabilityContext {
|
|
actorId: string;
|
|
taskId: string;
|
|
scopeIds: string[];
|
|
signal: AbortSignal;
|
|
}
|
|
|
|
/** 变更计划:由业务端生成与保存,审批绑定 id + payloadHash + expectedRevision */
|
|
export const MutationPlan = z.object({
|
|
id: z.string(),
|
|
operation: z.string(),
|
|
targetIds: z.array(z.string()),
|
|
expectedRevision: z.number().int().nonnegative(),
|
|
evidenceIds: z.array(z.string()),
|
|
payloadHash: z.string(),
|
|
requiresApproval: z.boolean(),
|
|
});
|
|
export type MutationPlan = z.infer<typeof MutationPlan>;
|
|
|
|
/** 能力 ID:`域.动作`,如 download.submit */
|
|
export const CapabilityId = z.string().regex(/^[a-z][a-z0-9]*\.[a-z][a-z0-9_]*$/);
|
|
export type CapabilityId = z.infer<typeof CapabilityId>;
|
|
|
|
// ── 能力描述符与首批能力(任务书 §3.1-9;REQ §10.3) ──
|
|
|
|
/** 风险级别:low 只读;medium 预览/可撤回写入;high 触发下载、文件变更或策略发布 */
|
|
export const RiskLevel = z.enum(["low", "medium", "high"]);
|
|
export type RiskLevel = z.infer<typeof RiskLevel>;
|
|
|
|
/**
|
|
* 接口类别(REQ §12.3),由弱到强:
|
|
* - read:只读查询
|
|
* - preview:生成预览/方案,不落任何变更
|
|
* - propose:生成并保存变更计划(MutationPlan),等待授权
|
|
* - approve:对计划作出授权决定
|
|
* - execute:产生外部副作用(下载提交、Copy/Move、策略发布…)
|
|
*/
|
|
export const CapabilityKind = z.enum(["read", "preview", "propose", "approve", "execute"]);
|
|
export type CapabilityKind = z.infer<typeof CapabilityKind>;
|
|
|
|
/** 崩溃恢复时 harness 能否用持久化参数重跑:有副作用的一律 never,只读用 safe */
|
|
export const CapabilityReplay = z.enum(["safe", "never"]);
|
|
export type CapabilityReplay = z.infer<typeof CapabilityReplay>;
|
|
|
|
/** 首批能力 ID */
|
|
export const CAPABILITY_IDS = [
|
|
"catalog.lookup",
|
|
"release.search",
|
|
"release.inspect",
|
|
"identity.resolve",
|
|
"download.plan",
|
|
"download.submit",
|
|
"download.recheck",
|
|
"organize.plan",
|
|
"organize.execute",
|
|
"task.inspect",
|
|
"task.retry",
|
|
"task.cancel",
|
|
"policy.preview",
|
|
"policy.apply",
|
|
"inventory.query",
|
|
"agent.spawn",
|
|
"agent.await",
|
|
] as const;
|
|
export const KnownCapabilityId = z.enum(CAPABILITY_IDS);
|
|
export type KnownCapabilityId = z.infer<typeof KnownCapabilityId>;
|
|
|
|
/** schema 引用:指向 contracts 中导出的 schema 名(如 "ReleaseSearchInput"),便于序列化给 UI/MCP 列表 */
|
|
export const SchemaRef = z.string().regex(/^[A-Z][A-Za-z0-9]*$/);
|
|
|
|
/**
|
|
* 能力的可序列化描述(唯一定义;处理器在 server 侧挂载,模型只能看到白名单内的描述)。
|
|
* 运行时注册用 `CapabilitySpec`(同字段,inputSchema/outputSchema 换成真实 zod schema),
|
|
* 首批能力的 spec 用 `capabilitySpec(id)` 从 CAPABILITY_CATALOG + CAPABILITY_IO 组装。
|
|
*/
|
|
export const CapabilityDescriptor = z.object({
|
|
id: CapabilityId,
|
|
version: z.number().int().min(1),
|
|
title: z.string().min(1),
|
|
description: z.string().min(1),
|
|
kind: CapabilityKind,
|
|
inputSchema: SchemaRef,
|
|
outputSchema: SchemaRef,
|
|
/** 经 HTTP/MCP 调用时所需的 API scope */
|
|
scope: ApiScope,
|
|
riskLevel: RiskLevel,
|
|
idempotent: z.boolean(),
|
|
supportsPreview: z.boolean(),
|
|
replay: CapabilityReplay,
|
|
});
|
|
export type CapabilityDescriptor = z.infer<typeof CapabilityDescriptor>;
|
|
|
|
/** 运行时能力规格:描述符字段 + 真实的输入/输出 zod schema(注册中心、Pi 工具、HTTP 共用) */
|
|
export type CapabilitySpec<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> = Omit<
|
|
CapabilityDescriptor,
|
|
"inputSchema" | "outputSchema"
|
|
> & { inputSchema: I; outputSchema: O };
|
|
|
|
/** 需要走授权回调(automation_grant / approval)的类别 */
|
|
export const AUTHORIZED_KINDS: readonly CapabilityKind[] = ["propose", "approve", "execute"];
|
|
|
|
/** 首批能力的描述(inputSchema/outputSchema 名对应 capabilities-io.ts 中的导出) */
|
|
export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor> = {
|
|
"catalog.lookup": d(
|
|
"catalog.lookup",
|
|
"查作品",
|
|
"按标题/别名/外部 ID 查本地作品与元数据候选",
|
|
"read",
|
|
"CatalogLookup",
|
|
"read",
|
|
"low",
|
|
true,
|
|
false,
|
|
),
|
|
"release.search": d(
|
|
"release.search",
|
|
"搜资源",
|
|
"先查本地资源索引与站点快照,不足时对已开启站点实时补查",
|
|
"read",
|
|
"ReleaseSearch",
|
|
"read",
|
|
"low",
|
|
true,
|
|
false,
|
|
),
|
|
"release.inspect": d(
|
|
"release.inspect",
|
|
"看资源详情",
|
|
"读取资源详情、文件清单、促销与解析结果",
|
|
"read",
|
|
"ReleaseInspect",
|
|
"read",
|
|
"low",
|
|
true,
|
|
false,
|
|
),
|
|
"identity.resolve": d(
|
|
"identity.resolve",
|
|
"识别身份",
|
|
"为观察/资源提出身份映射;证据充分时自动接受,有歧义时转为待处理事项",
|
|
"propose",
|
|
"IdentityResolve",
|
|
"organize.execute",
|
|
"medium",
|
|
true,
|
|
true,
|
|
),
|
|
"download.plan": d(
|
|
"download.plan",
|
|
"生成下载计划",
|
|
"评估候选,生成占用版本目标(slot)与预算预留的下载计划;不提交",
|
|
"preview",
|
|
"DownloadPlan",
|
|
"organize.execute",
|
|
"medium",
|
|
true,
|
|
true,
|
|
),
|
|
"download.submit": d(
|
|
"download.submit",
|
|
"提交下载",
|
|
"按已保存的计划提交到下载器;校验授权与硬阈值",
|
|
"execute",
|
|
"DownloadSubmit",
|
|
"organize.execute",
|
|
"high",
|
|
true,
|
|
false,
|
|
),
|
|
"download.recheck": d(
|
|
"download.recheck",
|
|
"重新校验/恢复下载",
|
|
"对已纳管的下载任务强制重新校验(action=recheck),或恢复非阈值、非人工的暂停(action=resume);不换源、不删除、不改保存路径",
|
|
"execute",
|
|
"DownloadRecheck",
|
|
"organize.execute",
|
|
"medium",
|
|
true,
|
|
false,
|
|
),
|
|
"organize.plan": d(
|
|
"organize.plan",
|
|
"生成整理计划",
|
|
"生成并保存整理计划:冻结源/目标路径、整理方式与前置条件;不执行",
|
|
"preview",
|
|
"OrganizePlan",
|
|
"organize.read",
|
|
"low",
|
|
true,
|
|
true,
|
|
),
|
|
"organize.execute": d(
|
|
"organize.execute",
|
|
"执行整理",
|
|
"执行已保存的整理计划(复制/移动),生成整理记录",
|
|
"execute",
|
|
"OrganizeExecute",
|
|
"organize.execute",
|
|
"high",
|
|
true,
|
|
false,
|
|
),
|
|
"task.inspect": d(
|
|
"task.inspect",
|
|
"查看任务",
|
|
"读取任务状态、业务时间线与错误",
|
|
"read",
|
|
"TaskInspect",
|
|
"read",
|
|
"low",
|
|
true,
|
|
false,
|
|
),
|
|
"task.retry": d(
|
|
"task.retry",
|
|
"重试任务",
|
|
"对失败/死信任务重新排队;副作用经操作账本去重",
|
|
"execute",
|
|
"TaskRetry",
|
|
"organize.execute",
|
|
"medium",
|
|
true,
|
|
false,
|
|
),
|
|
"task.cancel": d(
|
|
"task.cancel",
|
|
"取消任务",
|
|
"停止未来步骤,不撤销已完成的外部动作",
|
|
"execute",
|
|
"TaskCancel",
|
|
"organize.execute",
|
|
"medium",
|
|
true,
|
|
false,
|
|
),
|
|
"policy.preview": d(
|
|
"policy.preview",
|
|
"预览策略",
|
|
"用历史数据回放策略草案,展示新增命中与误排除",
|
|
"preview",
|
|
"PolicyPreview",
|
|
"read",
|
|
"low",
|
|
true,
|
|
true,
|
|
),
|
|
"policy.apply": d(
|
|
"policy.apply",
|
|
"发布策略",
|
|
"发布策略版本(需授权);不直接执行 JavaScript",
|
|
"execute",
|
|
"PolicyApply",
|
|
"admin",
|
|
"high",
|
|
true,
|
|
true,
|
|
),
|
|
"inventory.query": d(
|
|
"inventory.query",
|
|
"查库存",
|
|
"查询本地资产、版本目标满足情况与外部库存证据",
|
|
"read",
|
|
"InventoryQuery",
|
|
"read",
|
|
"low",
|
|
true,
|
|
false,
|
|
),
|
|
// 子代理(设置 pi.subagents 开启后才提供给模型):类别 propose——只建有界子会话,不产生外部副作用,
|
|
// "先给方案"的会话也能派生(子会话同样只给方案);授权不高于父会话,由服务端校验
|
|
"agent.spawn": d(
|
|
"agent.spawn",
|
|
"派生子会话",
|
|
"把一个有界子目标交给子会话处理:能力白名单须是本会话白名单的子集,预算从本会话剩余预算里划拨,模式不高于本会话;返回子会话 runId。await=true 时等它结束并返回汇报",
|
|
"propose",
|
|
"AgentSpawn",
|
|
"organize.execute",
|
|
"medium",
|
|
true,
|
|
false,
|
|
),
|
|
"agent.await": d(
|
|
"agent.await",
|
|
"等待子会话",
|
|
"等待本会话派生的子会话结束(等待期间不调用模型),返回各自状态、用量与汇报摘要;超时仍未结束的标为 pending",
|
|
"read",
|
|
"AgentAwait",
|
|
"read",
|
|
"low",
|
|
true,
|
|
false,
|
|
),
|
|
};
|
|
|
|
function d(
|
|
id: KnownCapabilityId,
|
|
title: string,
|
|
description: string,
|
|
kind: CapabilityKind,
|
|
schemaBase: string,
|
|
scope: ApiScope,
|
|
riskLevel: RiskLevel,
|
|
idempotent: boolean,
|
|
supportsPreview: boolean,
|
|
): CapabilityDescriptor {
|
|
return {
|
|
id,
|
|
version: 1,
|
|
title,
|
|
description,
|
|
kind,
|
|
inputSchema: `${schemaBase}Input`,
|
|
outputSchema: `${schemaBase}Output`,
|
|
scope,
|
|
riskLevel,
|
|
idempotent,
|
|
supportsPreview,
|
|
// 有副作用的类别一律不可重放
|
|
replay: kind === "read" || kind === "preview" ? "safe" : "never",
|
|
};
|
|
}
|