billion-context-pi 0.1.25 → 0.1.26

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.
package/README.md CHANGED
@@ -86,17 +86,21 @@ This has two practical implications:
86
86
 
87
87
  ### acp_delegate — clean-context delegation
88
88
 
89
- Hand a self-contained task to a fresh pi process running in a clean context. Five built-in roles, each with a tailored tool whitelist and system prompt:
89
+ Hand a self-contained task to a fresh pi process running in a clean context. Five built-in roles, each with a system prompt and a **soft tool guardrail**:
90
90
 
91
91
  | Role | Tools | Best for |
92
92
  |------|-------|----------|
93
- | `reviewer` | read, bash | Read-only code review (bugs, risks, file:line) |
94
- | `researcher` | read, bash | Read-only codebase investigation |
93
+ | `reviewer` | read, bash, grep, find, ls + ACP | Read-only code review (bugs, risks, file:line) |
94
+ | `researcher` | read, bash, grep, find, ls + ACP | Read-only codebase investigation |
95
95
  | `worker` | read, edit, write, bash | Make code changes |
96
- | `planner` | read, bash | Analyze + propose a step-by-step plan |
97
- | `oracle` | read, bash | Answer questions / advise |
96
+ | `planner` | read, bash, grep, find, ls + ACP | Analyze + propose a step-by-step plan |
97
+ | `oracle` | read, bash, grep, find, ls + ACP | Answer questions / advise |
98
98
 
99
- The full delegate result is saved to a file (`/tmp/acp-delegate/<runId>.out`); the tool result and injected notification carry only the **task title + file path** (no preview) use `read` for the details. This keeps the parent context lean.
99
+ Read-only roles (reviewer, researcher, planner, oracle) receive a restricted tool allowlist (`read, bash, grep, find, ls`) plus ACP context tools (`compress, decompress, search_context, acp_status`) so they can manage their own context. This prevents accidental file modifications, but `bash` can bypass it - **it is a guardrail, not a security boundary**.
100
+
101
+ Worker runs on Pi's full default toolset - no `--tools` allowlist is applied, so any loaded extension or custom tools (e.g. ACP, LSP, MCP) remain available. This keeps primary-task delegation fully capable. The `read, edit, write, bash` listing above reflects core tools only.
102
+
103
+ The full delegate result is saved to a file (`/tmp/acp-delegate/<runId>.out`); the tool result and injected notification carry only the **task title + file path** (no preview) - use `read` for the details. This keeps the parent context lean.
100
104
 
101
105
  - **Interactive (TUI) & RPC modes**: `async:true` (default) runs the child in the background; a short completion notification is injected into the chat when it finishes.
102
106
  - **Print / JSON modes** (`pi -p`, SDK): `async:true` auto-downgrades to **synchronous** — the result returns as the tool result in the same turn (the parent exits after one turn, so background injection would be lost).
package/README.zh-CN.md CHANGED
@@ -85,17 +85,21 @@ billion-context 通过拦截 Pi 的 `context` 事件接管上下文管理。**Pi
85
85
 
86
86
  ### acp_delegate — 干净上下文委派
87
87
 
88
- 把一个自包含的任务交给一个运行在干净上下文中的新 pi 进程。五个内置角色,各自有定制的工具白名单和系统提示:
88
+ 把一个自包含的任务交给一个运行在干净上下文中的新 pi 进程。五个内置角色,各自有系统提示和**软工具护栏**:
89
89
 
90
90
  | 角色 | 工具 | 适用场景 |
91
91
  |------|------|----------|
92
- | `reviewer` | read, bash | 只读代码审查(bug、风险、file:line) |
93
- | `researcher` | read, bash | 只读代码库调研 |
92
+ | `reviewer` | read, bash, grep, find, ls + ACP | 只读代码审查(bug、风险、file:line) |
93
+ | `researcher` | read, bash, grep, find, ls + ACP | 只读代码库调研 |
94
94
  | `worker` | read, edit, write, bash | 修改代码 |
95
- | `planner` | read, bash | 分析 + 提出分步计划 |
96
- | `oracle` | read, bash | 回答问题 / 建议 |
95
+ | `planner` | read, bash, grep, find, ls + ACP | 分析 + 提出分步计划 |
96
+ | `oracle` | read, bash, grep, find, ls + ACP | 回答问题 / 建议 |
97
97
 
98
- 委派的完整结果保存到文件(`/tmp/acp-delegate/<runId>.out`);工具结果和注入通知只携带**任务标题 + 文件路径**(无预览)— 需要细节时用 `read` 读取。这让父上下文保持精简。
98
+ 只读角色(reviewer、researcher、planner、oracle)获得受限工具白名单(`read, bash, grep, find, ls`)+ ACP 上下文工具(`compress, decompress, search_context, acp_status`),以便管理自己的上下文。这能防止意外修改文件,但 `bash` 可绕过 - **这是护栏,不是安全边界**。
99
+
100
+ Worker 运行在 Pi 的完整默认工具集上 - 不应用 `--tools` 白名单,因此任何已加载的扩展或自定义工具(如 ACP、LSP、MCP)保持可用。这确保主任务委派能力完整。上表中的 `read, edit, write, bash` 仅反映核心工具。
101
+
102
+ 委派的完整结果保存到文件(`/tmp/acp-delegate/<runId>.out`);工具结果和注入通知只携带**任务标题 + 文件路径**(无预览)- 需要细节时用 `read` 读取。这让父上下文保持精简。
99
103
 
100
104
  - **交互(TUI)与 RPC 模式**:`async:true`(默认)在后台运行子进程;完成时一条简短通知注入到聊天框。
101
105
  - **Print / JSON 模式**(`pi -p`、SDK):`async:true` 自动降级为**同步** — 结果在同一轮作为工具结果返回(父进程一轮后即退出,后台注入会丢失)。
@@ -1,5 +1,5 @@
1
- import { Type } from "typebox";
2
- import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
1
+ import { Type, type Static } from "typebox";
2
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
3
3
  /** Snapshot of currently-running delegate runs, for the TUI status widget. */
4
4
  export declare function runningRunsSnapshot(): {
5
5
  runId: string;
@@ -14,6 +14,7 @@ declare const DelegateParams: Type.TObject<{
14
14
  model: Type.TOptional<Type.TString>;
15
15
  async: Type.TOptional<Type.TBoolean>;
16
16
  }>;
17
+ type DelegateArgs = Static<typeof DelegateParams>;
17
18
  declare const CancelParams: Type.TObject<{
18
19
  runId: Type.TString;
19
20
  }>;
@@ -22,6 +23,22 @@ declare const WaitParams: Type.TObject<{
22
23
  timeout: Type.TOptional<Type.TInteger>;
23
24
  }>;
24
25
  export declare function makeDelegateTool(pi: ExtensionAPI): ToolDefinition<typeof DelegateParams>;
26
+ /** If the delegate already delivered its result via a system notification
27
+ * (the close handler injected before this wait was called), return a short
28
+ * "already delivered" message pointing at the result file, so the model
29
+ * never sees the same result twice (once via the injected notification,
30
+ * once via this tool result). Returns null when the run was NOT injected,
31
+ * in which case the caller delivers the full payload via formatRunResult(). */
32
+ export declare function injectedWaitMessage(run: {
33
+ injected?: boolean;
34
+ result?: {
35
+ file: string;
36
+ };
37
+ }, runId: string, remainingLine: string): string | null;
25
38
  export declare function makeDelegateWaitTool(_pi: ExtensionAPI): ToolDefinition<typeof WaitParams>;
26
39
  export declare function makeDelegateCancelTool(_pi: ExtensionAPI): ToolDefinition<typeof CancelParams>;
40
+ export declare function buildChildArgs(args: DelegateArgs, rolePrompt: string, ctx: ExtensionContext): Promise<{
41
+ cliArgs: string[];
42
+ tmpDir: string;
43
+ }>;
27
44
  export {};
package/dist/index.js CHANGED
@@ -7649,15 +7649,19 @@ var MAX_DEPTH = 2;
7649
7649
  var SYNC_TIMEOUT_MS = 5 * 6e4;
7650
7650
  var RESULT_SUMMARY_CHARS = 500;
7651
7651
  var OUT_DIR = join4(tmpdir2(), "acp-delegate");
7652
+ var ACP_TOOLS = ["compress", "decompress", "search_context", "acp_status"];
7653
+ var RESTRICTED_TOOLS = "read,bash,grep,find,ls";
7652
7654
  var AGENTS = {
7653
7655
  reviewer: {
7654
- tools: "read,bash",
7656
+ tools: RESTRICTED_TOOLS,
7657
+ restricted: true,
7655
7658
  prompt: `You are a senior code reviewer with read-only access.
7656
7659
  Read the given code and report: bugs, security/safety risks, correctness issues, and concrete improvement suggestions.
7657
7660
  Be specific \u2014 cite file:line for every finding. Do NOT modify any files; only read and report.`
7658
7661
  },
7659
7662
  researcher: {
7660
- tools: "read,bash",
7663
+ tools: RESTRICTED_TOOLS,
7664
+ restricted: true,
7661
7665
  prompt: `You are a code researcher with read-only access.
7662
7666
  Investigate the codebase to answer the question thoroughly. Report findings with exact file:line references, function/type signatures, and relevant code snippets.
7663
7667
  Do NOT modify any files; only read and report.`
@@ -7669,13 +7673,15 @@ Make exactly the requested code changes \u2014 minimal, focused, following exist
7669
7673
  After editing, briefly summarize what you changed and why. Do not expand scope.`
7670
7674
  },
7671
7675
  planner: {
7672
- tools: "read,bash",
7676
+ tools: RESTRICTED_TOOLS,
7677
+ restricted: true,
7673
7678
  prompt: `You are a technical planner with read-only access.
7674
7679
  Analyze the task and produce a concrete, ordered step-by-step implementation plan with rationale for each step.
7675
7680
  Cite file:line for code you reference. Do NOT modify any files; only read and propose.`
7676
7681
  },
7677
7682
  oracle: {
7678
- tools: "read,bash",
7683
+ tools: RESTRICTED_TOOLS,
7684
+ restricted: true,
7679
7685
  prompt: `You are an expert advisor with read-only access.
7680
7686
  Answer the question concisely with clear reasoning. Cite file:line when referencing code. Do NOT modify any files.`
7681
7687
  }
@@ -7731,7 +7737,7 @@ var agentListLine = (name) => {
7731
7737
  planner: "analyze + propose step-by-step plan (read-only)",
7732
7738
  oracle: "answer questions / advise (read-only)"
7733
7739
  };
7734
- return ` \u2022 ${name} \u2014 ${blurb[name]} [tools: ${def.tools}]`;
7740
+ return ` \u2022 ${name} - ${blurb[name]} [tools: ${def.tools}${def.restricted ? " + ACP context tools" : ""}]`;
7735
7741
  };
7736
7742
  function makeDelegateTool(pi) {
7737
7743
  return {
@@ -7772,6 +7778,12 @@ function remainingLineForWait(selfRunId) {
7772
7778
  const remaining = Array.from(runs.values()).filter((r) => r.status === "running" && r.runId !== selfRunId).length;
7773
7779
  return remaining > 0 ? ` ${remaining} delegate${remaining === 1 ? " is" : "s are"} still running.` : "";
7774
7780
  }
7781
+ function injectedWaitMessage(run, runId, remainingLine) {
7782
+ if (!run.injected) return null;
7783
+ const file = run.result?.file;
7784
+ const fileLine = file ? ` If you need details, read the result file: \`${file}\`.` : "";
7785
+ return `Delegate \`${runId}\` already delivered its result via a system notification when it finished \u2014 no need to wait on it again.${remainingLine}${fileLine}`;
7786
+ }
7775
7787
  function makeDelegateWaitTool(_pi) {
7776
7788
  return {
7777
7789
  name: "acp_delegate_wait",
@@ -7794,6 +7806,11 @@ function makeDelegateWaitTool(_pi) {
7794
7806
  return { details: void 0, content: [{ type: "text", text: `Delegate \`${args.runId}\` was cancelled (no result).${remainingLineForWait(args.runId)}` }] };
7795
7807
  }
7796
7808
  if (run.status !== "running") {
7809
+ const dedup = injectedWaitMessage(run, args.runId, remainingLineForWait(args.runId));
7810
+ if (dedup) {
7811
+ run.consumed = true;
7812
+ return { details: void 0, content: [{ type: "text", text: dedup }] };
7813
+ }
7797
7814
  run.consumed = true;
7798
7815
  if (!run.result) {
7799
7816
  return { details: void 0, content: [{ type: "text", text: `Delegate \`${args.runId}\` finished but no result is available (persist error).` }] };
@@ -7958,6 +7975,7 @@ async function runDelegate(pi, args, ctx, signal) {
7958
7975
  return;
7959
7976
  }
7960
7977
  const injected = injectResult(pi, args.agent, runId, args.task, code, file2);
7978
+ run.injected = injected;
7961
7979
  debug.event("delegate-done", { runId, code, status: run.status, injected, outLen: output.length, file: file2 });
7962
7980
  delegateStatusWidget.poke();
7963
7981
  }).catch((err) => {
@@ -8003,6 +8021,11 @@ async function buildChildArgs(args, rolePrompt, ctx) {
8003
8021
 
8004
8022
  Complete the task below.`, "utf8");
8005
8023
  const cliArgs = ["-p", "--no-session", "--append-system-prompt", promptFile];
8024
+ const agentDef = AGENTS[args.agent];
8025
+ if (agentDef?.restricted) {
8026
+ const merged = [.../* @__PURE__ */ new Set([...agentDef.tools.split(",").map((s) => s.trim()), ...ACP_TOOLS])];
8027
+ cliArgs.push("--tools", merged.join(","));
8028
+ }
8006
8029
  if (args.model && args.model.includes("/")) {
8007
8030
  const [providerId, ...rest] = args.model.split("/");
8008
8031
  const modelId = rest.join("/");
@@ -8209,7 +8232,7 @@ async function statusReport(runtime, ctx) {
8209
8232
  const activeBlocksList = state.blocks.filter((b) => b.active);
8210
8233
  const totalBlocksList = state.blocks;
8211
8234
  const lines = [];
8212
- const versionStr = "0.1.25" ? `billion-context-pi@${"0.1.25"}` : "";
8235
+ const versionStr = "0.1.26" ? `billion-context-pi@${"0.1.26"}` : "";
8213
8236
  lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
8214
8237
  lines.push("\u2502 ACP Context Analysis \u2502");
8215
8238
  lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
@@ -8567,7 +8590,7 @@ async function checkForUpdate(autoUpdate, notify) {
8567
8590
  const data = await res.json();
8568
8591
  const latest = data.version;
8569
8592
  if (!latest) return;
8570
- const current = runtimeVersion ?? "0.1.25";
8593
+ const current = runtimeVersion ?? "0.1.26";
8571
8594
  debug.event("update-check", {
8572
8595
  current,
8573
8596
  latest,
@@ -8602,7 +8625,7 @@ import { readFile as readFile2, writeFile as writeFile4, stat, copyFile, rename
8602
8625
  import { existsSync } from "fs";
8603
8626
  import { homedir as homedir4 } from "os";
8604
8627
  import { join as join6 } from "path";
8605
- var ACP_TOOLS = ["compress", "decompress", "search_context", "acp_status"];
8628
+ var ACP_TOOLS2 = ["compress", "decompress", "search_context", "acp_status"];
8606
8629
  var BUILTIN_DEFAULT_TOOLS = {
8607
8630
  advisor: ["read", "grep", "find", "ls", "bash", "intercom"],
8608
8631
  "context-builder": ["read", "grep", "find", "ls", "bash", "write", "web_search", "intercom"],
@@ -8622,9 +8645,9 @@ function resolveAgentDir() {
8622
8645
  }
8623
8646
  function desiredTools(existing, name) {
8624
8647
  const base = Array.isArray(existing?.tools) && existing.tools.length > 0 ? [...existing.tools] : [...BUILTIN_DEFAULT_TOOLS[name] ?? []];
8625
- const hasAll = ACP_TOOLS.every((t) => base.includes(t));
8648
+ const hasAll = ACP_TOOLS2.every((t) => base.includes(t));
8626
8649
  if (hasAll) return { tools: base, changed: false };
8627
- for (const t of ACP_TOOLS) if (!base.includes(t)) base.push(t);
8650
+ for (const t of ACP_TOOLS2) if (!base.includes(t)) base.push(t);
8628
8651
  return { tools: base, changed: true };
8629
8652
  }
8630
8653
  async function ensureSubagentAcpTools(settingsPath) {
@@ -8695,7 +8718,7 @@ async function ensureSubagentAcpTools(settingsPath) {
8695
8718
  if (typeof v !== "object" || v === null) throw new Error("agentOverrides missing after write");
8696
8719
  for (const name of Object.keys(BUILTIN_DEFAULT_TOOLS)) {
8697
8720
  const tools = v[name]?.tools;
8698
- if (!Array.isArray(tools) || !ACP_TOOLS.every((t) => tools.includes(t))) {
8721
+ if (!Array.isArray(tools) || !ACP_TOOLS2.every((t) => tools.includes(t))) {
8699
8722
  throw new Error(`${name} missing ACP tools after write`);
8700
8723
  }
8701
8724
  }