@narumitw/pi-subagents 3.1.1 → 3.2.1

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/docs/tools.md CHANGED
@@ -5,7 +5,7 @@
5
5
  | Parameter | Type | Required | Constraint / default |
6
6
  | --- | --- | --- | --- |
7
7
  | `task` | `string` | Yes | Self-contained task, up to 50 KiB of UTF-8 text. |
8
- | `tools` | `string[]` | No | Pi core tools: `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, `ls`; defaults to `read`, `grep`, `find`, `ls`. |
8
+ | `tools` | `string[]` | No | Pi tools: `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, `ls`, `codemode`; defaults to `read`, `grep`, `find`, `ls`. |
9
9
  | `skills` | `string[]` | No | Up to 16 local skill files or directories; Pi uses progressive disclosure. |
10
10
  | `extensions` | `{ path: string; tools: string[] }[]` | No | Up to 16 trusted local extensions and their initially selected tools. |
11
11
  | `thinkingLevel` | `string` | No | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; defaults to the main agent's level. |
@@ -13,6 +13,11 @@
13
13
 
14
14
  Starts one job and returns its `jobId` immediately. The child inherits the main agent's effective model and always receives `subagent_send` and `subagent_wait`.
15
15
 
16
+ Select `codemode` explicitly with the other required tools, for example `tools: ["read", "grep", "codemode"]`.
17
+ Pi's built-in codemode extension runs scripts against the child's callable tools, including `subagent_send` and `subagent_wait`; automatic resource discovery stays disabled.
18
+ Codemode also exposes Pi's classifier and image model API using child-visible credentials; these calls can send data to providers and incur costs.
19
+ The default tools do not include codemode.
20
+
16
21
  Skills do not add tools or inject their entire bodies. An extension's `tools` list selects its initial tools; use an empty list for provider or lifecycle behavior only. An attached extension runs trusted code with full child-process permissions, not in a sandbox.
17
22
 
18
23
  Attachments must be existing local paths. Every non-ignored declared skill must load successfully, each skill name must be unique, and each skill path must contain at least one loadable Pi skill. Missing or misattributed extension tools fail before the child model receives the task. See [Attachment behavior](./attachments.md) for validation, trust, limits, provider credentials, and startup failures.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "3.1.1",
3
+ "version": "3.2.1",
4
4
  "description": "Subagent jobs with asynchronous main-agent messaging for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,12 +39,12 @@
39
39
  "typebox": "*"
40
40
  },
41
41
  "devDependencies": {
42
- "@earendil-works/pi-ai": "1.0.0",
43
- "@earendil-works/pi-coding-agent": "1.0.0",
44
- "@earendil-works/pi-tui": "1.0.0",
45
- "@types/node": "26.6.4",
42
+ "@earendil-works/pi-ai": "1.2.0",
43
+ "@earendil-works/pi-coding-agent": "1.2.0",
44
+ "@earendil-works/pi-tui": "1.2.0",
45
+ "@types/node": "26.6.5",
46
46
  "esbuild": "0.28.2",
47
- "typebox": "1.3.34",
47
+ "typebox": "1.3.37",
48
48
  "typescript": "7.0.2"
49
49
  },
50
50
  "repository": {
@@ -53,6 +53,6 @@
53
53
  "directory": "packages/pi-subagents"
54
54
  },
55
55
  "dependencies": {
56
- "ignore": "^7.0.11"
56
+ "ignore": "^7.0.12"
57
57
  }
58
58
  }
package/src/process.ts CHANGED
@@ -100,6 +100,7 @@ export function buildPiArgs(request: ChildCommandRequest): string[] {
100
100
  "-e",
101
101
  childCommunicationBridgePath(),
102
102
  ];
103
+ if (request.tools.includes("codemode")) args.push("-e", "builtin:codemode");
103
104
  for (const extension of request.extensions) args.push("-e", extension.path);
104
105
  if (requiresReadinessAttestation(request)) args.push("-e", childReadinessProbePath());
105
106
  for (const skill of request.skills) args.push("--skill", skill);
@@ -153,8 +154,8 @@ export function childReadinessProbePath(): string {
153
154
  return fileURLToPath(new URL("./child-readiness-probe.ts", import.meta.url));
154
155
  }
155
156
 
156
- function requiresReadinessAttestation(request: Pick<ChildRequest, "extensions">): boolean {
157
- return request.extensions.length > 0;
157
+ function requiresReadinessAttestation(request: Pick<ChildRequest, "tools" | "extensions">): boolean {
158
+ return request.tools.includes("codemode") || request.extensions.length > 0;
158
159
  }
159
160
 
160
161
  function selectedChildTools(request: Pick<ChildRequest, "tools" | "extensions">): string[] {
package/src/runtime.ts CHANGED
@@ -28,6 +28,7 @@ interface InternalJob extends JobSummary {
28
28
  terminal: Promise<void>;
29
29
  resolveTerminal: () => void;
30
30
  task?: Promise<void>;
31
+ taskText: string;
31
32
  stopRequest?: StopRequest;
32
33
  control?: ChildControl;
33
34
  controlReady: Promise<ChildControl>;
@@ -48,6 +49,7 @@ export interface RuntimeDependencies {
48
49
 
49
50
  export interface ActiveJobDisplay {
50
51
  jobId: string;
52
+ task: string;
51
53
  state: Extract<SubagentJobState, "queued" | "running">;
52
54
  elapsedMs: number;
53
55
  timeout?: number;
@@ -113,6 +115,7 @@ export class SubagentRuntime {
113
115
  .sort((left, right) => left.createdAt - right.createdAt)
114
116
  .map((job) => ({
115
117
  jobId: job.jobId,
118
+ task: job.taskText,
116
119
  state: job.state,
117
120
  elapsedMs: Math.max(0, now - (job.startedAt ?? job.createdAt)),
118
121
  ...(job.timeout !== undefined ? { timeout: job.timeout } : {}),
@@ -155,6 +158,7 @@ export class SubagentRuntime {
155
158
  ...(input.timeout !== undefined ? { timeout: input.timeout } : {}),
156
159
  controller,
157
160
  tools: effectiveTools,
161
+ taskText: input.task,
158
162
  terminal,
159
163
  resolveTerminal,
160
164
  controlReady,
package/src/tools.ts CHANGED
@@ -209,7 +209,7 @@ export function registerSubagentTools(
209
209
  if (ctx.cwd !== cwd || ctx.isProjectTrusted() !== projectTrusted) {
210
210
  throw new Error("Subagent spawn context changed during attachment validation; retry the request.");
211
211
  }
212
- if (attachments.extensions.length > 0) {
212
+ if (tools.includes("codemode") || attachments.extensions.length > 0) {
213
213
  assertChildBootstrapCapacity([...new Set([...attachments.effectiveTools, ...CHILD_COMMUNICATION_TOOL_NAMES])]);
214
214
  }
215
215
  const model = resolveChildModel(ctx, attachments.extensions.length > 0);
package/src/types.ts CHANGED
@@ -13,7 +13,17 @@ export const TERMINAL_JOB_STATES = new Set<SubagentJobState>([
13
13
  "cancelled",
14
14
  ]);
15
15
 
16
- export const CHILD_CORE_TOOL_NAMES = ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"] as const;
16
+ export const CHILD_CORE_TOOL_NAMES = [
17
+ "read",
18
+ "bash",
19
+ "powershell",
20
+ "edit",
21
+ "write",
22
+ "grep",
23
+ "find",
24
+ "ls",
25
+ "codemode",
26
+ ] as const;
17
27
  export const DEFAULT_SUBAGENT_TOOLS = ["read", "grep", "find", "ls"] as const;
18
28
  export const SUBAGENT_THINKING_LEVELS = [
19
29
  "off",
package/src/widget.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
- import { truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import { sanitizeTerminalText } from "./message-broker.js";
4
4
  import type { ActiveJobDisplay, SubagentRuntime } from "./runtime.js";
5
5
 
@@ -7,6 +7,7 @@ export const SUBAGENT_WIDGET_KEY = "subagents";
7
7
  export const SUBAGENT_WIDGET_REFRESH_INTERVAL_MS = 1_000;
8
8
 
9
9
  const WIDGET_OPTIONS = { placement: "aboveEditor" } as const;
10
+ const TASK_PREVIEW_WIDTH = 40;
10
11
 
11
12
  export interface SubagentWidgetController {
12
13
  start(ctx: ExtensionContext): void;
@@ -86,17 +87,21 @@ function renderJob(job: ActiveJobDisplay, theme: Theme): string {
86
87
  const symbol = theme.fg(running ? "accent" : "dim", running ? "▶ " : "○ ");
87
88
  const state = theme.fg(running ? "accent" : "muted", job.state);
88
89
  const jobId = sanitizeLabel(job.jobId);
90
+ const taskText = sanitizeTask(job.task);
91
+ // Pi's truncation adds ANSI resets; remove them before applying the callback theme.
92
+ const task = stripTerminalSequences(truncateToWidth(taskText, TASK_PREVIEW_WIDTH, "…"));
93
+ const preview = visibleWidth(task) > 0 ? ` · ${theme.fg("text", task)}` : "";
89
94
  const tools = job.tools.length > 0 ? job.tools.map(sanitizeLabel).join(", ") : "none";
90
95
  const timeout = job.timeout === undefined ? "no timeout" : formatSeconds(job.timeout);
91
96
  const detail = ` · ${formatSeconds(Math.floor(job.elapsedMs / 1_000))} / ${timeout} · tools: ${tools}`;
92
- return `${symbol}${theme.fg("text", jobId)} · ${state}${theme.fg("muted", detail)}`;
97
+ return `${symbol}${theme.fg("text", jobId)}${preview} · ${state}${theme.fg("muted", detail)}`;
93
98
  }
94
99
 
95
100
  function widgetValue(jobs: readonly ActiveJobDisplay[]): string {
96
101
  return jobs
97
102
  .map(
98
103
  (job) =>
99
- `${job.jobId}\0${job.state}\0${Math.floor(job.elapsedMs / 1_000)}\0${job.timeout ?? ""}\0${job.tools.join(",")}`,
104
+ `${job.jobId}\0${job.task}\0${job.state}\0${Math.floor(job.elapsedMs / 1_000)}\0${job.timeout ?? ""}\0${job.tools.join(",")}`,
100
105
  )
101
106
  .join("\n");
102
107
  }
@@ -105,6 +110,17 @@ function cloneDisplayJob(job: ActiveJobDisplay): ActiveJobDisplay {
105
110
  return { ...job, tools: [...job.tools] };
106
111
  }
107
112
 
113
+ function sanitizeTask(value: string): string {
114
+ // Stop sequence bodies at another control introducer so malformed input cannot
115
+ // repeatedly scan the rest of a maximum-size task, as Pi's escape stripper can.
116
+ const text = value.replace(
117
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Remove untrusted terminal escape sequences before display.
118
+ /(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]|(?:\u001b[\]PX^_]|[\u0090\u0098\u009d-\u009f])[^\u0007\u001b\u0080-\u009f]*(?:\u0007|\u001b\\|\u009c)|\u001b[ -/]*[0-~]/gu,
119
+ "",
120
+ );
121
+ return sanitizeLabel(text.replace(/\p{Bidi_Control}/gu, ""));
122
+ }
123
+
108
124
  function sanitizeLabel(value: string): string {
109
125
  return sanitizeTerminalText(value).replace(/\s+/gu, " ").trim();
110
126
  }