billion-context-pi 0.1.25 → 0.1.27

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
@@ -2429,9 +2429,17 @@ ${argStr}` : argStr || textParts;
2429
2429
  }
2430
2430
  return [{ id, role: "assistant", contentType: "text", text: extractText(msg.content) }];
2431
2431
  }
2432
- const customText = extractText(msg.content);
2432
+ const customText = extractText(msg.content) || fallbackText(msg);
2433
2433
  return customText.length > 0 ? [{ id, role: "user", contentType: "text", text: customText }] : [];
2434
2434
  }
2435
+ function fallbackText(msg) {
2436
+ const parts = [];
2437
+ if (msg.command) parts.push(`$ ${msg.command}`);
2438
+ const out = extractText(msg.output);
2439
+ if (out) parts.push(out);
2440
+ if (msg.summary) parts.push(msg.summary);
2441
+ return parts.join("\n").trim();
2442
+ }
2435
2443
  function stringifyArgs(args) {
2436
2444
  if (!args) return "";
2437
2445
  if (typeof args === "string") return args;
@@ -2621,6 +2629,12 @@ function mergeInitialState(parsed) {
2621
2629
  }
2622
2630
 
2623
2631
  // src/runtime.ts
2632
+ function readContextEntries(sm) {
2633
+ const source = sm;
2634
+ if (typeof source.buildContextEntries === "function") return source.buildContextEntries();
2635
+ if (typeof source.getBranch === "function") return source.getBranch();
2636
+ return [];
2637
+ }
2624
2638
  function createRuntime(adapter) {
2625
2639
  const core = createCore({ countTokens: defaultCountTokens });
2626
2640
  const store = new SessionStateStore();
@@ -2652,7 +2666,7 @@ function createRuntime(adapter) {
2652
2666
  async function stateFor(ctx) {
2653
2667
  const sm = ctx.sessionManager;
2654
2668
  const state = await store.load(sm.getSessionFile() ?? void 0, sm.getSessionId());
2655
- const entries = sm.buildContextEntries();
2669
+ const entries = readContextEntries(sm);
2656
2670
  return { state, coreMessages: entriesToCoreMessages(entries), entries };
2657
2671
  }
2658
2672
  async function save(state, ctx) {
@@ -7649,15 +7663,19 @@ var MAX_DEPTH = 2;
7649
7663
  var SYNC_TIMEOUT_MS = 5 * 6e4;
7650
7664
  var RESULT_SUMMARY_CHARS = 500;
7651
7665
  var OUT_DIR = join4(tmpdir2(), "acp-delegate");
7666
+ var ACP_TOOLS = ["compress", "decompress", "search_context", "acp_status"];
7667
+ var RESTRICTED_TOOLS = "read,bash,grep,find,ls";
7652
7668
  var AGENTS = {
7653
7669
  reviewer: {
7654
- tools: "read,bash",
7670
+ tools: RESTRICTED_TOOLS,
7671
+ restricted: true,
7655
7672
  prompt: `You are a senior code reviewer with read-only access.
7656
7673
  Read the given code and report: bugs, security/safety risks, correctness issues, and concrete improvement suggestions.
7657
7674
  Be specific \u2014 cite file:line for every finding. Do NOT modify any files; only read and report.`
7658
7675
  },
7659
7676
  researcher: {
7660
- tools: "read,bash",
7677
+ tools: RESTRICTED_TOOLS,
7678
+ restricted: true,
7661
7679
  prompt: `You are a code researcher with read-only access.
7662
7680
  Investigate the codebase to answer the question thoroughly. Report findings with exact file:line references, function/type signatures, and relevant code snippets.
7663
7681
  Do NOT modify any files; only read and report.`
@@ -7669,13 +7687,15 @@ Make exactly the requested code changes \u2014 minimal, focused, following exist
7669
7687
  After editing, briefly summarize what you changed and why. Do not expand scope.`
7670
7688
  },
7671
7689
  planner: {
7672
- tools: "read,bash",
7690
+ tools: RESTRICTED_TOOLS,
7691
+ restricted: true,
7673
7692
  prompt: `You are a technical planner with read-only access.
7674
7693
  Analyze the task and produce a concrete, ordered step-by-step implementation plan with rationale for each step.
7675
7694
  Cite file:line for code you reference. Do NOT modify any files; only read and propose.`
7676
7695
  },
7677
7696
  oracle: {
7678
- tools: "read,bash",
7697
+ tools: RESTRICTED_TOOLS,
7698
+ restricted: true,
7679
7699
  prompt: `You are an expert advisor with read-only access.
7680
7700
  Answer the question concisely with clear reasoning. Cite file:line when referencing code. Do NOT modify any files.`
7681
7701
  }
@@ -7731,7 +7751,7 @@ var agentListLine = (name) => {
7731
7751
  planner: "analyze + propose step-by-step plan (read-only)",
7732
7752
  oracle: "answer questions / advise (read-only)"
7733
7753
  };
7734
- return ` \u2022 ${name} \u2014 ${blurb[name]} [tools: ${def.tools}]`;
7754
+ return ` \u2022 ${name} - ${blurb[name]} [tools: ${def.tools}${def.restricted ? " + ACP context tools" : ""}]`;
7735
7755
  };
7736
7756
  function makeDelegateTool(pi) {
7737
7757
  return {
@@ -7772,6 +7792,12 @@ function remainingLineForWait(selfRunId) {
7772
7792
  const remaining = Array.from(runs.values()).filter((r) => r.status === "running" && r.runId !== selfRunId).length;
7773
7793
  return remaining > 0 ? ` ${remaining} delegate${remaining === 1 ? " is" : "s are"} still running.` : "";
7774
7794
  }
7795
+ function injectedWaitMessage(run, runId, remainingLine) {
7796
+ if (!run.injected) return null;
7797
+ const file = run.result?.file;
7798
+ const fileLine = file ? ` If you need details, read the result file: \`${file}\`.` : "";
7799
+ return `Delegate \`${runId}\` already delivered its result via a system notification when it finished \u2014 no need to wait on it again.${remainingLine}${fileLine}`;
7800
+ }
7775
7801
  function makeDelegateWaitTool(_pi) {
7776
7802
  return {
7777
7803
  name: "acp_delegate_wait",
@@ -7794,6 +7820,11 @@ function makeDelegateWaitTool(_pi) {
7794
7820
  return { details: void 0, content: [{ type: "text", text: `Delegate \`${args.runId}\` was cancelled (no result).${remainingLineForWait(args.runId)}` }] };
7795
7821
  }
7796
7822
  if (run.status !== "running") {
7823
+ const dedup = injectedWaitMessage(run, args.runId, remainingLineForWait(args.runId));
7824
+ if (dedup) {
7825
+ run.consumed = true;
7826
+ return { details: void 0, content: [{ type: "text", text: dedup }] };
7827
+ }
7797
7828
  run.consumed = true;
7798
7829
  if (!run.result) {
7799
7830
  return { details: void 0, content: [{ type: "text", text: `Delegate \`${args.runId}\` finished but no result is available (persist error).` }] };
@@ -7958,6 +7989,7 @@ async function runDelegate(pi, args, ctx, signal) {
7958
7989
  return;
7959
7990
  }
7960
7991
  const injected = injectResult(pi, args.agent, runId, args.task, code, file2);
7992
+ run.injected = injected;
7961
7993
  debug.event("delegate-done", { runId, code, status: run.status, injected, outLen: output.length, file: file2 });
7962
7994
  delegateStatusWidget.poke();
7963
7995
  }).catch((err) => {
@@ -8003,6 +8035,11 @@ async function buildChildArgs(args, rolePrompt, ctx) {
8003
8035
 
8004
8036
  Complete the task below.`, "utf8");
8005
8037
  const cliArgs = ["-p", "--no-session", "--append-system-prompt", promptFile];
8038
+ const agentDef = AGENTS[args.agent];
8039
+ if (agentDef?.restricted) {
8040
+ const merged = [.../* @__PURE__ */ new Set([...agentDef.tools.split(",").map((s) => s.trim()), ...ACP_TOOLS])];
8041
+ cliArgs.push("--tools", merged.join(","));
8042
+ }
8006
8043
  if (args.model && args.model.includes("/")) {
8007
8044
  const [providerId, ...rest] = args.model.split("/");
8008
8045
  const modelId = rest.join("/");
@@ -8209,7 +8246,7 @@ async function statusReport(runtime, ctx) {
8209
8246
  const activeBlocksList = state.blocks.filter((b) => b.active);
8210
8247
  const totalBlocksList = state.blocks;
8211
8248
  const lines = [];
8212
- const versionStr = "0.1.25" ? `billion-context-pi@${"0.1.25"}` : "";
8249
+ const versionStr = "0.1.27" ? `billion-context-pi@${"0.1.27"}` : "";
8213
8250
  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
8251
  lines.push("\u2502 ACP Context Analysis \u2502");
8215
8252
  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");
@@ -8353,9 +8390,11 @@ When a background delegate finishes, an automated completion notification is inj
8353
8390
 
8354
8391
  // src/tool-guardrails.ts
8355
8392
  import {
8356
- isBashToolResult,
8357
8393
  isToolCallEventType
8358
8394
  } from "@earendil-works/pi-coding-agent";
8395
+ function isBashToolResult(e) {
8396
+ return e.toolName === "bash";
8397
+ }
8359
8398
  function resolveBashTimeout(input, defaultTimeout) {
8360
8399
  if (input.timeout !== void 0) return void 0;
8361
8400
  const d = defaultTimeout ?? DEFAULT_TOOL_BASH_TIMEOUT;
@@ -8567,7 +8606,7 @@ async function checkForUpdate(autoUpdate, notify) {
8567
8606
  const data = await res.json();
8568
8607
  const latest = data.version;
8569
8608
  if (!latest) return;
8570
- const current = runtimeVersion ?? "0.1.25";
8609
+ const current = runtimeVersion ?? "0.1.27";
8571
8610
  debug.event("update-check", {
8572
8611
  current,
8573
8612
  latest,
@@ -8602,7 +8641,7 @@ import { readFile as readFile2, writeFile as writeFile4, stat, copyFile, rename
8602
8641
  import { existsSync } from "fs";
8603
8642
  import { homedir as homedir4 } from "os";
8604
8643
  import { join as join6 } from "path";
8605
- var ACP_TOOLS = ["compress", "decompress", "search_context", "acp_status"];
8644
+ var ACP_TOOLS2 = ["compress", "decompress", "search_context", "acp_status"];
8606
8645
  var BUILTIN_DEFAULT_TOOLS = {
8607
8646
  advisor: ["read", "grep", "find", "ls", "bash", "intercom"],
8608
8647
  "context-builder": ["read", "grep", "find", "ls", "bash", "write", "web_search", "intercom"],
@@ -8622,9 +8661,9 @@ function resolveAgentDir() {
8622
8661
  }
8623
8662
  function desiredTools(existing, name) {
8624
8663
  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));
8664
+ const hasAll = ACP_TOOLS2.every((t) => base.includes(t));
8626
8665
  if (hasAll) return { tools: base, changed: false };
8627
- for (const t of ACP_TOOLS) if (!base.includes(t)) base.push(t);
8666
+ for (const t of ACP_TOOLS2) if (!base.includes(t)) base.push(t);
8628
8667
  return { tools: base, changed: true };
8629
8668
  }
8630
8669
  async function ensureSubagentAcpTools(settingsPath) {
@@ -8695,7 +8734,7 @@ async function ensureSubagentAcpTools(settingsPath) {
8695
8734
  if (typeof v !== "object" || v === null) throw new Error("agentOverrides missing after write");
8696
8735
  for (const name of Object.keys(BUILTIN_DEFAULT_TOOLS)) {
8697
8736
  const tools = v[name]?.tools;
8698
- if (!Array.isArray(tools) || !ACP_TOOLS.every((t) => tools.includes(t))) {
8737
+ if (!Array.isArray(tools) || !ACP_TOOLS2.every((t) => tools.includes(t))) {
8699
8738
  throw new Error(`${name} missing ACP tools after write`);
8700
8739
  }
8701
8740
  }