@springbrand/agent-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/package.json +28 -0
  2. package/src/db/approval.repo.ts +291 -0
  3. package/src/db/ext-context.repo.ts +34 -0
  4. package/src/db/index.ts +83 -0
  5. package/src/db/message-ui.repo.ts +39 -0
  6. package/src/db/milestone.repo.ts +96 -0
  7. package/src/db/runtime-event-outbox.repo.ts +89 -0
  8. package/src/db/schema.ts +164 -0
  9. package/src/db/settlement.repo.ts +104 -0
  10. package/src/db/steer.repo.ts +73 -0
  11. package/src/db/submission.repo.ts +323 -0
  12. package/src/index.ts +133 -0
  13. package/src/kernel/approval-lifecycle.ts +552 -0
  14. package/src/kernel/bindings.ts +898 -0
  15. package/src/kernel/degradation.ts +15 -0
  16. package/src/kernel/extensions.ts +108 -0
  17. package/src/kernel/profile.ts +116 -0
  18. package/src/kernel/public-contracts.ts +17 -0
  19. package/src/kernel/receipts.ts +124 -0
  20. package/src/kernel/recoverable-chat-agent.ts +899 -0
  21. package/src/kernel/state.ts +76 -0
  22. package/src/kernel/submission-lifecycle.ts +600 -0
  23. package/src/layers/context/budget/gate.ts +88 -0
  24. package/src/layers/orchestration/subagents/agent-types/contract.ts +78 -0
  25. package/src/layers/orchestration/subagents/agent-types/extract/index.ts +47 -0
  26. package/src/layers/orchestration/subagents/agent-types/fanout/index.ts +53 -0
  27. package/src/layers/orchestration/subagents/agent-types/registry.ts +16 -0
  28. package/src/layers/orchestration/temporary-agent/core.ts +152 -0
  29. package/src/layers/orchestration/temporary-agent/runner.ts +133 -0
  30. package/src/layers/orchestration/temporary-agent/workspace.ts +154 -0
  31. package/src/lib/artifacts.ts +54 -0
  32. package/src/lib/egress.ts +44 -0
  33. package/src/lib/execution-level.ts +27 -0
  34. package/src/lib/extension-name.ts +18 -0
  35. package/src/lib/host-actions.ts +57 -0
  36. package/src/lib/mcp.ts +86 -0
  37. package/src/lib/model-catalog.ts +7 -0
  38. package/src/lib/prompt.ts +139 -0
  39. package/src/lib/telemetry-dev.ts +44 -0
  40. package/src/pi/assembly/context.ts +510 -0
  41. package/src/pi/assembly/extensions.ts +661 -0
  42. package/src/pi/assembly/index.ts +19 -0
  43. package/src/pi/assembly/snapshot.ts +200 -0
  44. package/src/pi/message/contract.ts +8 -0
  45. package/src/pi/message/conversion.ts +73 -0
  46. package/src/pi/message/index.ts +3 -0
  47. package/src/pi/message/projection.ts +604 -0
  48. package/src/pi/runtime-adapter/assembly.ts +552 -0
  49. package/src/pi/runtime-adapter/execution.ts +683 -0
  50. package/src/pi/runtime-adapter/index.ts +232 -0
  51. package/src/pi/runtime-adapter/models.ts +243 -0
  52. package/src/pi/runtime-adapter/recovery.ts +805 -0
  53. package/src/pi/runtime-adapter/transcript.ts +825 -0
  54. package/src/pi/session/index.ts +24 -0
  55. package/src/pi/session/storage.ts +353 -0
  56. package/src/pi/tool/ai-adapter.ts +100 -0
  57. package/src/pi/tool/base.ts +110 -0
  58. package/src/pi/tool/compiler.ts +444 -0
  59. package/src/pi/tool/core-host.ts +48 -0
  60. package/src/pi/tool/core.ts +251 -0
  61. package/src/pi/tool/index.ts +32 -0
  62. package/src/pi/tool/mcp.ts +319 -0
  63. package/src/pi/tool/schedule.ts +198 -0
  64. package/src/pi/tool/skill.ts +455 -0
  65. package/src/pi/tool/subagent.ts +148 -0
  66. package/src/pi/tool/web-search/api.ts +1292 -0
  67. package/src/pi/tool/web-search/index.ts +2 -0
  68. package/src/pi/tool/web-search/web-search.ts +127 -0
  69. package/src/pi/tool/workspace-sandbox.ts +664 -0
  70. package/src/pi/turn/approval.ts +181 -0
  71. package/src/pi/turn/index.ts +62 -0
  72. package/src/pi/turn/tool-recovery.ts +792 -0
  73. package/src/plugins.ts +1024 -0
  74. package/src/runtime-agent.ts +654 -0
  75. package/src/runtime.ts +2880 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Durable history needs bounded values so one Tool response cannot make a
3
+ * session impossible to restore.
4
+ *
5
+ * Leaf truncation is intentionally field-agnostic. Short control values such as
6
+ * status, executionId and booleans survive while current and future payload
7
+ * fields are bounded. If a protocol introduces a long opaque control token, it
8
+ * must be given an explicit protected path here.
9
+ */
10
+
11
+ /** Maximum string leaf retained in durable tool output. */
12
+ const STORAGE_LEAF_MAX_CHARS = 32 * 1024;
13
+
14
+ const ELISION_RESERVE = 40;
15
+
16
+ // 作用:把过长文本从中间截短,同时保留开头和结尾。
17
+ // 调用:`truncateStringLeaves` 遇到字符串叶子时调用,传入该叶子的上限。
18
+ // 原因:开头常有响应头,结尾常有错误细节,只保留一端会丢掉调试信息。
19
+ function truncateMiddle(text: string, maxChars: number): string {
20
+ if (text.length <= maxChars) return text;
21
+ const keep = Math.max(0, maxChars - ELISION_RESERVE);
22
+ const head = Math.ceil(keep * 0.6);
23
+ const tail = keep - head;
24
+ const elided = text.length - head - tail;
25
+ return `${text.slice(0, head)}\n…[${elided} chars elided]…\n${tail > 0 ? text.slice(-tail) : ""}`;
26
+ }
27
+
28
+ // 作用:识别不能当普通文本截断的媒体内容块。
29
+ // 调用:`truncateStringLeaves` 遍历每个对象前调用,命中后整块原样保留。
30
+ // 原因:截断编码后的二进制数据只会损坏内容,因此媒体块不应走字符串预算。
31
+ function isBinaryContentLike(value: object): boolean {
32
+ const v = value as { type?: unknown; data?: unknown; mediaType?: unknown };
33
+ if (typeof v.type === "string" && (v.type === "image" || v.type === "file" || v.type === "media")) {
34
+ return true;
35
+ }
36
+ return typeof v.data === "string" && typeof v.mediaType === "string";
37
+ }
38
+
39
+ /**
40
+ * 把任意值里过长的普通字符串截短。
41
+ *
42
+ * @remarks
43
+ * 调用方在值将进入持久化边界时调用,并明确给出单个字符串叶子的上限。
44
+ *
45
+ * 实现保留对象形状、键和非字符串值,以免破坏 Tool 结果协议。
46
+ *
47
+ * 媒体块不截断,未改变的小值返回原引用,因为无意义的深拷贝会增加内存和身份变化。
48
+ *
49
+ * Runtime 和 Tool 的术语见 `src/index.ts`。
50
+ */
51
+ export function truncateStringLeaves(value: unknown, maxChars: number): unknown {
52
+ if (typeof value === "string") return truncateMiddle(value, maxChars);
53
+ if (Array.isArray(value)) {
54
+ let changed = false;
55
+ const next = value.map((item) => {
56
+ const out = truncateStringLeaves(item, maxChars);
57
+ if (out !== item) changed = true;
58
+ return out;
59
+ });
60
+ return changed ? next : value;
61
+ }
62
+ if (value !== null && typeof value === "object") {
63
+ if (isBinaryContentLike(value)) return value;
64
+ let changed = false;
65
+ const next: Record<string, unknown> = {};
66
+ for (const [key, item] of Object.entries(value)) {
67
+ const out = truncateStringLeaves(item, maxChars);
68
+ if (out !== item) changed = true;
69
+ next[key] = out;
70
+ }
71
+ return changed ? next : value;
72
+ }
73
+ return value;
74
+ }
75
+
76
+ /**
77
+ * 用 Runtime 的固定预算收紧一份可持久化的 Tool 结果。
78
+ *
79
+ * @remarks
80
+ * Pi Tool 编译器在记录成功结果或失败结果前统一调用它;普通调用方不应自己复制这个上限。
81
+ *
82
+ * 上限只收口在这里,可避免成功和错误分支漂移成不同的持久化规则。
83
+ *
84
+ * Runtime 和 Tool 的术语见 `src/index.ts`。
85
+ */
86
+ export function boundDurableToolOutput(value: unknown): unknown {
87
+ return truncateStringLeaves(value, STORAGE_LEAF_MAX_CHARS);
88
+ }
@@ -0,0 +1,78 @@
1
+ import type { z } from "zod";
2
+
3
+ /**
4
+ * 说明一种子代理怎么接收任务、怎么运行和怎么返回结果。
5
+ *
6
+ * @remarks
7
+ * 宿主在创建子代理 Tool 和 SubAgent Durable Object 时,通过 `AGENT_TYPES`
8
+ * 按 `name` 取出这份契约。
9
+ *
10
+ * 类型按 `fanout` 或 `extract` 这类处理机制命名,避免把「研究」之类宽泛业务词
11
+ * 固定到某一套模型、Tool 和输出形式上。
12
+ *
13
+ * 子代理只承接搜索范围已知、输入输出结构化的有界任务;需要改变方向的探索
14
+ * 仍由主 Agent 处理。
15
+ *
16
+ * Agent、Plugin 和 Runtime 的术语见 `src/index.ts`。
17
+ */
18
+ export interface AgentType {
19
+ /** Tool 面和注册表共用的机制名。 */
20
+ name: string;
21
+ /** UI 展示名。 */
22
+ displayName: string;
23
+ /** 模型看到的 Tool 说明,包含适用范围和不适用的情况。 */
24
+ description: string;
25
+ /** 选用主模型或子代理模型。 */
26
+ model: "main" | "sub";
27
+ /** 传给 `assembleSubagentPrompt` 的子代理角色说明。 */
28
+ persona: string;
29
+ /** 校验 Tool 输入的结构化 schema。 */
30
+ inputSchema: z.ZodType;
31
+ /** 校验子代理最终结果的结构化 schema。 */
32
+ outputSchema: z.ZodType;
33
+ /**
34
+ * 子代理的 Tool 档位。
35
+ *
36
+ * `capable` 包含 execute、Workspace 和网络能力。
37
+ *
38
+ * `readonly` 只包含只读网络和文件能力。
39
+ */
40
+ tools: "capable" | "readonly";
41
+ }
42
+
43
+ /**
44
+ * 按原样返回一份类型完整的子代理契约。
45
+ *
46
+ * @remarks
47
+ * `fanout` 和 `extract` 在声明自己时调用它,让 TypeScript 在声明处校验字段。
48
+ *
49
+ * 它故意不做拷贝、默认值或注册;这些行为会隐藏调用方提供的真实契约,注册责任已在 `AGENT_TYPES`。
50
+ *
51
+ * Agent 术语见 `src/index.ts`。
52
+ */
53
+ export function defineAgentType(t: AgentType): AgentType {
54
+ return t;
55
+ }
56
+
57
+ /**
58
+ * 把子代理类型列表整理成按名字查找的表。
59
+ *
60
+ * @remarks
61
+ * `registry.ts` 在模块加载时调用,Tool 编译器和 SubAgent Durable Object 之后共用这张表。
62
+ *
63
+ * 重复名字会立即抛错,因为直接覆盖会让一个类型从 Tool 面无声消失。
64
+ *
65
+ * Agent 和 Runtime 术语见 `src/index.ts`。
66
+ */
67
+ export function foldAgentTypes(types: AgentType[]): { byName: Record<string, AgentType> } {
68
+ const byName: Record<string, AgentType> = {};
69
+ for (const t of types) {
70
+ if (t.name in byName) {
71
+ throw new Error(`duplicate agent type name "${t.name}"`);
72
+ }
73
+ byName[t.name] = t;
74
+ }
75
+ return { byName };
76
+ }
77
+
78
+ // 待确认:历史注释称注册表拆分是为了消除循环依赖,当前代码确实保持单向依赖,但未在现有架构决策中找到该历史结论。
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ import { defineAgentType } from "../contract";
3
+
4
+ /**
5
+ * 从一个已知的大型数据源中抽取指定字段。
6
+ *
7
+ * @remarks
8
+ * 子代理注册表在加载时收录它,主 Agent 只应在数据源和目标字段都已知时调用。
9
+ *
10
+ * 返回值只保留结构化记录,让大段原文和中间步骤留在子代理的上下文中。
11
+ *
12
+ * Workspace、Agent 和 Runtime 的术语见 `src/index.ts`。
13
+ */
14
+ export const extract = defineAgentType({
15
+ name: "extract",
16
+ displayName: "Extract",
17
+ model: "sub",
18
+ tools: "capable",
19
+ description:
20
+ "Hand a KNOWN large source (a workspace file path or a URL) to a sub-agent and get back a small " +
21
+ "structured set of records — the fields you asked for, extracted — without pulling the whole " +
22
+ "source into your context. This is a context-economy tool: the sub-agent reads the big thing, " +
23
+ "you receive only the extracted structure. Use it for bounded, well-specified extraction where " +
24
+ "you already know the source and exactly which fields you want. Do NOT use it for open-ended " +
25
+ "research, for anything you must reason over step by step, or when you don't yet know where the " +
26
+ "answer lives — in those cases read the source yourself in execute so you see everything.",
27
+ persona: [
28
+ "You are an extract sub-agent running on the universal-agent runtime.",
29
+ "You receive one KNOWN source (a workspace file path or a URL) and a list of fields to pull.",
30
+ "Read the source — for a file path use the file tools or the execute sandbox's state.* to read it;",
31
+ "for a URL fetch it in the execute sandbox. Then extract the requested fields into structured",
32
+ "records. Do not summarize into prose: emit records whose keys are the requested field names.",
33
+ "If a field is absent for a record, use an empty string rather than inventing a value.",
34
+ "Your final answer is a single structured object matching the required schema.",
35
+ ].join(" "),
36
+ inputSchema: z.object({
37
+ target: z.string().describe("a workspace file path or a URL — the known source to extract from"),
38
+ fields: z.array(z.string()).describe("the field names to extract into each record"),
39
+ instruction: z.string().optional().describe("optional guidance on what/how to extract"),
40
+ }),
41
+ outputSchema: z.object({
42
+ records: z
43
+ .array(z.record(z.string(), z.string()))
44
+ .describe("extracted records; each record's keys are the requested field names"),
45
+ note: z.string().optional().describe("optional note about coverage/gaps in the source"),
46
+ }),
47
+ });
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { defineAgentType } from "../contract";
3
+
4
+ /**
5
+ * 把一份已知清单分项处理,并返回按项对应的结构化结果。
6
+ *
7
+ * @remarks
8
+ * 子代理注册表在加载时收录它,主 Agent 只应在项目清单固定、每项都是独立有界查找时调用。
9
+ *
10
+ * 返回值只保留 findings,避免把每项的网络请求和解析步骤带回主 Agent 上下文。
11
+ *
12
+ * Agent 和 Runtime 的术语见 `src/index.ts`。
13
+ */
14
+ export const fanout = defineAgentType({
15
+ name: "fanout",
16
+ displayName: "Fanout",
17
+ model: "sub",
18
+ tools: "capable",
19
+ description:
20
+ "Fan a KNOWN list of items out to a sub-agent for bounded parallel lookups, and get back only " +
21
+ "the structured findings (one record per item) — not the intermediate fetches. Each item is " +
22
+ "processed with real network access, so the sub-agent can set its own headers, check status " +
23
+ "codes, and retry within its bounded scope. Use this ONLY when you already have the exact list " +
24
+ "and each item is a self-contained, bounded lookup (e.g. 'for each of these 40 domains, find X'). " +
25
+ "Do NOT use it for open-ended research you will reason over, for a single item (just use execute " +
26
+ "yourself), or for anything where the right next step is unknown and might need a change of angle " +
27
+ "— that reasoning lives in your loop, not in a leaf that returns a fixed schema.",
28
+ persona: [
29
+ "You are a fanout sub-agent running on the universal-agent runtime.",
30
+ "You are given a KNOWN list of items and one instruction to apply to each.",
31
+ "Process every item — use the execute sandbox to fetch and parse (set your own headers, check",
32
+ "res.status, retry or try another endpoint in the same snippet when an item is blocked).",
33
+ "Do NOT ask questions or reason about scope: the list is fixed and the instruction is fixed.",
34
+ "Return one finding per item. If an item genuinely cannot be resolved, still emit its record",
35
+ "with a finding that states what was tried and why it failed — never drop an item, never invent",
36
+ "a value. Your final answer is a single structured object matching the required schema.",
37
+ ].join(" "),
38
+ inputSchema: z.object({
39
+ items: z.array(z.string()).describe("the known list of items to process, one lookup each"),
40
+ instruction: z.string().describe("what to find/do for each item"),
41
+ }),
42
+ outputSchema: z.object({
43
+ results: z
44
+ .array(
45
+ z.object({
46
+ item: z.string().describe("the input item this record is for"),
47
+ finding: z.string().describe("the result for this item, or what was tried if it failed"),
48
+ sourceUrl: z.string().optional().describe("source URL when the finding came from a fetch"),
49
+ }),
50
+ )
51
+ .describe("one record per input item, same order"),
52
+ }),
53
+ });
@@ -0,0 +1,16 @@
1
+ import { foldAgentTypes } from "./contract";
2
+ import { fanout } from "./fanout";
3
+ import { extract } from "./extract";
4
+
5
+ /**
6
+ * 提供 Runtime 当前可用的子代理类型查找表。
7
+ *
8
+ * @remarks
9
+ * Tool 编译器、配置装配和 SubAgent Durable Object 都在需要解析 `agentType`
10
+ * 时读取它;新类型必须在这个数组中显式注册。
11
+ *
12
+ * 注册表与契约分开,让 `contract.ts` 不反向导入具体类型;当前只收录已有实际调用方的类型。
13
+ *
14
+ * Agent 和 Runtime 的术语见 `src/index.ts`。
15
+ */
16
+ export const AGENT_TYPES = foldAgentTypes([fanout, extract]);
@@ -0,0 +1,152 @@
1
+ export interface TemporaryAgentRequest {
2
+ subagentName: string;
3
+ instructions: string;
4
+ task: string;
5
+ }
6
+
7
+ export interface TemporaryAgentRunContext {
8
+ signal: AbortSignal;
9
+ requestId: string;
10
+ toolCallId: string;
11
+ }
12
+
13
+ /**
14
+ * 启动一个临时 Agent,并在它完整收尾后返回最终文本。
15
+ *
16
+ * @remarks
17
+ * `TemporaryAgentCoordinator.run` 在获得并发槽位后调用;实现方应把子运行和外部资源清理收口到同一个 Promise。
18
+ *
19
+ * 协调器只依赖这个函数而不依赖 Durable Object、UI 或具体 Agent SDK,避免把基础设施生命周期写进并发规则。
20
+ *
21
+ * Agent 和 Runtime 的术语见 `src/index.ts`。
22
+ */
23
+ export type TemporaryAgentExecutor = (
24
+ request: TemporaryAgentRequest,
25
+ ) => Promise<string>;
26
+
27
+ export interface TemporaryAgentApprovalRequest {
28
+ executionId: string;
29
+ subagentName: string;
30
+ requestId: string;
31
+ toolCallId: string;
32
+ toolName: string;
33
+ executionLevel: ExecutionLevel;
34
+ requiredExecutionLevel: ExecutionLevel;
35
+ input: unknown;
36
+ }
37
+
38
+ export interface TemporaryAgentApprovalDecision {
39
+ approved: boolean;
40
+ reason?: string;
41
+ }
42
+
43
+ // 作用:去掉临时 Agent 请求三个字段的首尾空白,并拒绝空值。
44
+ // 调用:协调器在排队前调用,所以无效请求不会占用并发槽位。
45
+ // 原因:这三个字段是公开输入边界,统一归一化可避免执行器各自处理空白。
46
+ const normalizeRequest = (
47
+ request: TemporaryAgentRequest,
48
+ ): TemporaryAgentRequest => {
49
+ const normalized = {
50
+ subagentName: request.subagentName.trim(),
51
+ instructions: request.instructions.trim(),
52
+ task: request.task.trim(),
53
+ };
54
+ for (const [key, value] of Object.entries(normalized)) {
55
+ if (!value) throw new Error(`${key} must not be blank`);
56
+ }
57
+ return normalized;
58
+ };
59
+
60
+ /**
61
+ * 在一个 Session 内为一次性临时 Agent 排队,并限制同时运行数。
62
+ *
63
+ * @remarks
64
+ * Runtime 每个 Session 持有一个协调器;主 Agent 每次调用临时 Agent 时都经过 `run`。
65
+ *
66
+ * 四个槽位和 FIFO 等待是已确认的产品规则,不是可配置项;调整它会改变同一 Session 的并发行为。
67
+ *
68
+ * Agent、Session 和 Runtime 的术语见 `src/index.ts`。
69
+ */
70
+ export class TemporaryAgentCoordinator {
71
+ private active = 0;
72
+ private readonly waiting: Array<{ grant(): void }> = [];
73
+
74
+ /**
75
+ * 排队执行一个临时 Agent,并返回它的非空最终文本。
76
+ *
77
+ * @remarks
78
+ * Runtime 的 `runTemporaryAgent` 边界为每次 Tool 调用进入这里;调用方可传入取消信号,让尚在队列中的请求退出。
79
+ *
80
+ * 槽位在执行器 Promise 真正结束后才释放。历史修复曾移除「取消后提前拒绝等待」的竞速,因为那会在子运行和清理仍未结束时提前放行下一个任务。
81
+ */
82
+ async run(
83
+ request: TemporaryAgentRequest,
84
+ execute: TemporaryAgentExecutor,
85
+ signal?: AbortSignal,
86
+ ): Promise<string> {
87
+ const normalized = normalizeRequest(request);
88
+ await this.acquire(signal);
89
+ try {
90
+ const text = (await execute(normalized)).trim();
91
+ if (!text) throw new Error("temporary agent final text must not be blank");
92
+ return text;
93
+ } finally {
94
+ this.release();
95
+ }
96
+ }
97
+
98
+ // 作用:立即取得一个空闲槽位,或按到达顺序等待。
99
+ // 调用:`run` 在启动执行器前调用;等待期间取消会从队列中删除该请求。
100
+ // 原因:原生的并发上限会直接拒绝额外任务,产品规则要求第五个起 FIFO 等待。
101
+ private async acquire(signal?: AbortSignal): Promise<void> {
102
+ if (signal?.aborted) throw this.cancelled(signal);
103
+ if (this.active < 4) {
104
+ this.active += 1;
105
+ return;
106
+ }
107
+ await new Promise<void>((resolve, reject) => {
108
+ const waiter = {
109
+ // 作用:把队首等待者转成正在运行的任务。
110
+ // 调用:`release` 取出队首后调用,调用前已有一个槽位被释放。
111
+ // 原因:槽位计数在真正放行时增加,可避免队列中的已取消请求占用并发数。
112
+ grant: () => {
113
+ signal?.removeEventListener("abort", cancel);
114
+ this.active += 1;
115
+ resolve();
116
+ },
117
+ };
118
+ // 作用:取消一个仍在排队的请求。
119
+ // 调用:`AbortSignal` 发出 abort 时调用,入队后的立即复查也会共用它。
120
+ // 原因:先确认等待者仍在队列中,可避免已获准任务被二次拒绝。
121
+ const cancel = () => {
122
+ const index = this.waiting.indexOf(waiter);
123
+ if (index < 0) return;
124
+ this.waiting.splice(index, 1);
125
+ reject(this.cancelled(signal));
126
+ };
127
+ this.waiting.push(waiter);
128
+ signal?.addEventListener("abort", cancel, { once: true });
129
+ if (signal?.aborted) cancel();
130
+ });
131
+ }
132
+
133
+ // 作用:释放当前任务的槽位,并立即放行队首。
134
+ // 调用:`run` 的 `finally` 在成功和所有失败路径上调用。
135
+ // 原因:释放收口到一处,避免模型失败、审批拒绝或取消泄漏槽位。
136
+ private release(): void {
137
+ this.active -= 1;
138
+ const next = this.waiting.shift();
139
+ if (!next) return;
140
+ next.grant();
141
+ }
142
+
143
+ // 作用:生成一个保留原始 abort 原因的统一取消错误。
144
+ // 调用:`acquire` 在入队前或等待中发现取消时调用。
145
+ // 原因:对外保持稳定错误文本,同时用 `cause` 留下超时或上游取消的真实原因。
146
+ private cancelled(signal?: AbortSignal): Error {
147
+ return new Error("temporary agent was cancelled", {
148
+ cause: signal?.reason,
149
+ });
150
+ }
151
+ }
152
+ import type { ExecutionLevel } from "../../../lib/execution-level";
@@ -0,0 +1,133 @@
1
+ import type { RuntimeExtensionConfig } from "../../../kernel/extensions";
2
+ import {
3
+ requiresExecutionApproval,
4
+ type ExecutionLevel,
5
+ } from "../../../lib/execution-level";
6
+ import type { PiToolCandidate } from "../../../pi/tool/compiler";
7
+
8
+ const BLOCKED_TOOLS = new Set([
9
+ "dispatch_background",
10
+ "run_temporary_agent",
11
+ "schedule",
12
+ "list_schedules",
13
+ "update_schedule",
14
+ "pause_schedule",
15
+ "resume_schedule",
16
+ "change_schedule_agent",
17
+ "cancel_schedule",
18
+ "execute",
19
+ "set_context",
20
+ "load_context",
21
+ "search_context",
22
+ "activate_skill",
23
+ "read_skill_resource",
24
+ "run_skill_script",
25
+ ]);
26
+
27
+ /**
28
+ * 判断一个 Tool 能不能交给临时 Agent。
29
+ *
30
+ * @remarks
31
+ * 宿主在装配临时 Agent 的平台、Workspace、Sandbox 和其他 Tool 时调用;调用方可再加名字或前缀黑名单。
32
+ *
33
+ * 固定黑名单排除再次委派、调度、主 Session 上下文和绕过隔离边界的 execute,以保持单层临时 Agent 和已确认的继承范围。
34
+ *
35
+ * Agent、Session、Workspace 和 Tool 的术语见 `src/index.ts`。
36
+ */
37
+ export function temporaryAgentToolAllowed(
38
+ name: string,
39
+ forbiddenNames: readonly string[] = [],
40
+ forbiddenPrefixes: readonly string[] = [],
41
+ ): boolean {
42
+ return (
43
+ !BLOCKED_TOOLS.has(name) &&
44
+ !forbiddenNames.includes(name) &&
45
+ !forbiddenPrefixes.some((prefix) => name.startsWith(prefix))
46
+ );
47
+ }
48
+
49
+ /**
50
+ * 给临时 Agent 里需要审批的 Tool 包上父 Session 审批。
51
+ *
52
+ * @remarks
53
+ * 宿主在筛完 Tool 候选项后调用;返回的候选项可直接交给临时 Agent 的 Pi Tool 编译流程。
54
+ *
55
+ * 包装层先按父 Agent 的执行档位请求批准,然后才调原 `execute`;审批完成后把包装候选降为 `safe`,避免临时 Agent 重复审批。
56
+ *
57
+ * Runtime、Session 和 Tool 的术语见 `src/index.ts`。
58
+ */
59
+ export function bridgeTemporaryAgentToolApprovals(
60
+ candidates: readonly PiToolCandidate[],
61
+ executionLevel: ExecutionLevel,
62
+ requestApproval: (request: {
63
+ toolName: string;
64
+ executionLevel: ExecutionLevel;
65
+ requiredExecutionLevel: ExecutionLevel;
66
+ input: unknown;
67
+ toolCallId: string;
68
+ signal: AbortSignal;
69
+ }) => Promise<void>,
70
+ ): PiToolCandidate[] {
71
+ return candidates.map((candidate) => {
72
+ if (!requiresExecutionApproval(
73
+ executionLevel,
74
+ candidate.requiredExecutionLevel,
75
+ )) {
76
+ return candidate;
77
+ }
78
+ const execute = candidate.tool.execute;
79
+ return {
80
+ ...candidate,
81
+ requiredExecutionLevel: "safe",
82
+ tool: {
83
+ ...candidate.tool,
84
+ // 作用:在执行原 Tool 前向父 Session 请求审批。
85
+ // 调用:Pi Tool 执行器调用,参数和进度回调原样传给原 Tool。
86
+ // 原因:某些调用方不传 signal,但审批契约需要一个 `AbortSignal`,因此只在缺失时创建一个不会自行取消的信号。
87
+ async execute(toolCallId, input, signal, onUpdate) {
88
+ const activeSignal = signal ?? new AbortController().signal;
89
+ await requestApproval({
90
+ toolName: candidate.tool.name,
91
+ executionLevel,
92
+ requiredExecutionLevel: candidate.requiredExecutionLevel,
93
+ input,
94
+ toolCallId,
95
+ signal: activeSignal,
96
+ });
97
+ return execute(
98
+ toolCallId,
99
+ input,
100
+ activeSignal,
101
+ onUpdate,
102
+ );
103
+ },
104
+ },
105
+ };
106
+ });
107
+ }
108
+
109
+ /**
110
+ * 判断一个 Extension 是否安全到可以交给临时 Agent。
111
+ *
112
+ * @remarks
113
+ * 宿主在加载临时 Agent 的 Extension 前调用;只有返回 `true` 的配置才会进入候选集。
114
+ *
115
+ * 当前只允许无 Workspace、Context、Message 和 Session 权限的 Extension,因为临时 Agent 不继承这些父会话状态。
116
+ *
117
+ * 修改判断必须同步核对宿主的临时 Agent 装配边界,否则 Extension 可能成为隔离绕路。
118
+ *
119
+ * Agent、Session 和 Runtime 的术语见 `src/index.ts`。
120
+ */
121
+ export function temporaryAgentExtensionIsSafe(
122
+ extension: RuntimeExtensionConfig,
123
+ ): boolean {
124
+ const permissions = extension.manifest.permissions ?? {};
125
+ return (
126
+ (permissions.workspace ?? "none") === "none" &&
127
+ permissions.context?.read === undefined &&
128
+ permissions.context?.write === undefined &&
129
+ (permissions.messages ?? "none") === "none" &&
130
+ permissions.session?.sendMessage !== true &&
131
+ permissions.session?.metadata !== true
132
+ );
133
+ }