@henryqw/pi-subagent 26.2.3 → 26.3.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/README.md CHANGED
@@ -6,7 +6,7 @@ Delegate work to configured Pi Roles with [Herdr](https://herdr.dev/). Authorize
6
6
 
7
7
  ## Install
8
8
 
9
- Requires Pi 1.0.0 or later. Development and automated checks use Pi 1.0.0. Older Pi releases are no longer supported.
9
+ Requires Pi 1.1.0 or later in the 1.x series. Development and automated checks use Pi 1.1.0. Older Pi releases are no longer supported.
10
10
 
11
11
  ```bash
12
12
  pi install npm:@henryqw/pi-task-models
@@ -229,7 +229,7 @@ Main's nested calls pass admission by their own tool names, not by script or anc
229
229
 
230
230
  To let a Role run `codemode` scripts, list `builtin:codemode` in `extensions` and `codemode` in `tools`; both are required, because the built-in registers the tool inactive and the Role tool policy activates it. `codemode` alone in `tools` fails the launch as an unavailable tool. Pi's `codemode.mode` and `codemode.inlineBudget` settings apply in the child unchanged, so the Role never forces codemode-only operation, and MCP configuration never activates `codemode` in a child. Scripts can call only the Role's active tools plus `codemode`/`deferred` extension and MCP tools; inactive base tools such as `bash` stay unreachable.
231
231
 
232
- The servers named in `mcps` come from the global `~/.pi/agent/mcp.json` only. In a Role that activates `codemode`, each selected server keeps its configured `exposure` and `toolExposure` (Pi's default `codemode` exposure makes tools callable from scripts without declaring them, `direct` declares them, `deferred` tools stay reachable from scripts, `hidden` tools stay unreachable), and an invalid exposure value fails the launch. In every other Role, selected servers are forced to `direct` exposure and `toolExposure` is ignored, because that child cannot reach undeclared tools. Declared tools appear to the model as `mcp__<server>__<tool>` once the server connects. Pi replaces `-` in server and tool names with `_` and appends a hash suffix when two tool names collide or a name exceeds 64 characters. Each selected entry is checked against Pi's documented `mcpServers` rules before launch, in Main and again in the child: a disabled, malformed, or unknown-transport entry, an invalid server name, two selected names that differ only in `-` and `_`, `auth.provider` over plain HTTP outside `localhost`, `127.0.0.1`, or `[::1]`, or a non-HTTPS `oauth.authServerMetadataUrl` outside those hosts fails the launch with the file path and field. The file is never modified; fix the entry or choose another server. Valid `auth`, `oauth`, and `description` fields are passed through so Pi's native provider auth and OAuth apply. `oauth.clientRegistration` must be `dcr` or `cimd`; `cimd` requires Pi 1.0.1 or later; Pi 1.0.0 rejects it before child launch. It cannot use `clientId` or `clientName`, and any `callbackUrl` must use localhost or 127.0.0.1 with path `/callback`. Project MCP overrides remain ignored: a project cannot change the Role's global server allowlist or exposure. Missing Skills, tools, MCP servers, Roles, or routes fail before productive work starts. Main-only delegation and recovery tools plus `ask_question` are excluded from children.
232
+ The servers named in `mcps` come from the global `~/.pi/agent/mcp.json` only. In a Role that activates `codemode`, each selected server keeps its configured `exposure` and `toolExposure` (Pi's default `codemode` exposure makes tools callable from scripts without declaring them, `direct` declares them, `deferred` tools stay reachable from scripts, `hidden` tools stay unreachable), and an invalid exposure value fails the launch. In every other Role, selected servers are forced to `direct` exposure and `toolExposure` is ignored, because that child cannot reach undeclared tools. Declared tools appear to the model as `mcp__<server>__<tool>` once the server connects. Pi replaces `-` in server and tool names with `_` and appends a hash suffix when two tool names collide or a name exceeds 64 characters. Each selected entry is checked against Pi's documented `mcpServers` rules before launch, in Main and again in the child: a disabled, malformed, or unknown-transport entry, an invalid server name, two selected names that differ only in `-` and `_`, `auth.provider` over plain HTTP outside `localhost`, `127.0.0.1`, or `[::1]`, or a non-HTTPS `oauth.authServerMetadataUrl` outside those hosts fails the launch with the file path and field. The file is never modified; fix the entry or choose another server. Valid `auth`, `oauth`, and `description` fields are passed through so Pi's native provider auth and OAuth apply. `oauth.clientRegistration` must be `dcr` or `cimd`. A `cimd` entry cannot use `clientId` or `clientName`, and any `callbackUrl` must use localhost or 127.0.0.1 with path `/callback`. Project MCP overrides remain ignored: a project cannot change the Role's global server allowlist or exposure. Missing Skills, tools, MCP servers, Roles, or routes fail before productive work starts. Main-only delegation and recovery tools plus `ask_question` are excluded from children.
233
233
 
234
234
  ## API
235
235
 
@@ -246,7 +246,9 @@ The servers named in `mcps` come from the global `~/.pi/agent/mcp.json` only. In
246
246
  | `RoleLaunchRuntime`, `HerdrHostRuntime`, `CheckedGitRuntime` | classes | Supply launch, host, and Git behavior to the runner. |
247
247
  | `ExecutionPolicySnapshot`, `RunState`, `CoordinatorRuntime` | types | Describe persisted policy, state, and runner integration. |
248
248
 
249
- `createEphemeralSubagentExecutor` accepts global concurrency, turn/token, idle, and hard-runtime policy. Queued time consumes no child timeout. A run resolves resources only after receiving its permit and accepts abort, output, token, and activity callbacks. Output and diagnostics are bounded.
249
+ Role route and launch helpers require only model routing fields and `isProjectTrusted()` from the context, plus `cwd` for configured package resources. They do not require UI methods.
250
+
251
+ `createEphemeralSubagentExecutor` accepts global concurrency, turn/token, idle, and hard-runtime policy. Queued time consumes no child timeout. A run resolves resources only after receiving its permit and accepts abort, output, token, and activity callbacks. Output and diagnostics are bounded. A child that reports `agent_settled.aborted: true` returns a failure result with `stopReason: "aborted"`, even if its process exits with code 0. Collected output, usage, and child error text are kept. `aborted: false` does not prove provider success. A parent abort or timeout still rejects with `EphemeralSubagentError`.
250
252
 
251
253
  See [Orchestration and package-author API](./docs/orchestration.md) for the detailed contracts and recovery model.
252
254
 
package/dist/ephemeral.js CHANGED
@@ -38,7 +38,7 @@ const PI_JSON_EVENTS = {
38
38
  summarization_retry_finished: true,
39
39
  bash_execution_update: true,
40
40
  };
41
- const CONSUMED_JSON_EVENTS = new Set(["message_start", "message_update", "message_end"]);
41
+ const CONSUMED_JSON_EVENTS = new Set(["message_start", "message_update", "message_end", "agent_settled"]);
42
42
  const JSON_EVENT_TYPE = /^\s*\{\s*"type"\s*:\s*"([^"\\]+)"/;
43
43
  const JSON_STRING = `"(?:[^"\\\\\u0000-\u001f]|\\\\(?:["\\\\/bfnrt]|u[0-9a-fA-F]{4}))*"`;
44
44
  const JSON_OVERSIZED_TOOL_START = new RegExp(`^\\s*\\{\\s*"type"\\s*:\\s*"tool_execution_start"\\s*,\\s*"toolCallId"\\s*:\\s*(${JSON_STRING})\\s*,\\s*"toolName"\\s*:\\s*(${JSON_STRING})(?=\\s*,)`);
@@ -429,6 +429,7 @@ async function runPi(prepared, input, budget, invocation) {
429
429
  let spawnError;
430
430
  let protocolError;
431
431
  let aborted = false;
432
+ let settlementAborted = false;
432
433
  let limit;
433
434
  let tokenBudget = "within";
434
435
  let startedTurns = 0;
@@ -528,6 +529,8 @@ async function runPi(prepared, input, budget, invocation) {
528
529
  }
529
530
  else {
530
531
  const exitCode = code ?? 1;
532
+ if (settlementAborted)
533
+ stopReason = "aborted";
531
534
  const outcome = exitCode !== 0 || stopReason === "error" || stopReason === "aborted" ? "failure" : "success";
532
535
  resolve({
533
536
  outcome,
@@ -627,6 +630,14 @@ async function runPi(prepared, input, budget, invocation) {
627
630
  const record = event;
628
631
  if (typeof record.type !== "string" || !Object.hasOwn(PI_JSON_EVENTS, record.type))
629
632
  return;
633
+ if (record.type === "agent_settled") {
634
+ if (typeof record.aborted !== "boolean") {
635
+ protocolError = new Error("Subagent agent_settled.aborted must be a boolean.");
636
+ stop(true);
637
+ return;
638
+ }
639
+ settlementAborted ||= record.aborted;
640
+ }
630
641
  observeEvent();
631
642
  if (record.type === "turn_start") {
632
643
  if (++startedTurns > budget.maxTurns) {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
2
+ import { resolveTaskModelRoute, type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
3
3
  export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.ts";
4
4
  export { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.ts";
5
5
  export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutionBudget, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
@@ -83,7 +83,9 @@ export declare function loadBuiltinRole(name: BuiltinRoleName): Role;
83
83
  * user files are an error. Missing user directory returns the built-ins.
84
84
  */
85
85
  export declare function loadRoles(agentDir?: string): Role[];
86
- export declare function resolveTaskRoute(ctx: ExtensionContext, profileName: ProfileName, agentDir?: string): ResolvedTaskRoute;
86
+ type TaskRouteContext = Parameters<typeof resolveTaskModelRoute>[0];
87
+ type RoleLaunchContext = TaskRouteContext & Pick<ExtensionContext, "isProjectTrusted">;
88
+ export declare function resolveTaskRoute(ctx: TaskRouteContext, profileName: ProfileName, agentDir?: string): ResolvedTaskRoute;
87
89
  export declare function resolveRoleSkills(pi: Pick<ExtensionAPI, "getCommands">, role: Role): ResolvedRoleSkills;
88
90
  /** Resolve all enabled package resources selected by one Role's extension sources. */
89
91
  export declare function resolveRolePackageResources(role: Role, ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">): Promise<{
@@ -93,12 +95,12 @@ export declare function resolveRolePackageResources(role: Role, ctx: Pick<Extens
93
95
  themes: string[];
94
96
  }>;
95
97
  export declare function createRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: Pick<ExtensionContext, "isProjectTrusted">, input: CreateRoleLaunchInput): ResolvedRoleLaunch;
96
- export declare function resolveRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: ResolveRoleLaunchInput): ResolvedRoleLaunch;
98
+ export declare function resolveRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: RoleLaunchContext, input: ResolveRoleLaunchInput): ResolvedRoleLaunch;
97
99
  /** Prepare a resolved or resolvable Role launch while keeping its system prompt out of argv. */
98
- export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: ResolveRoleLaunchInput): PreparedRoleLaunch;
99
- export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: CreateRoleLaunchInput): PreparedRoleLaunch;
100
+ export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: RoleLaunchContext, input: ResolveRoleLaunchInput): PreparedRoleLaunch;
101
+ export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: RoleLaunchContext, input: CreateRoleLaunchInput): PreparedRoleLaunch;
100
102
  /** Resolve and prepare a configured Role with its package-owned resources. */
101
- export declare function resolveConfiguredRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: ResolveConfiguredRoleLaunchInput): Promise<PreparedRoleLaunch>;
103
+ export declare function resolveConfiguredRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: RoleLaunchContext & Pick<ExtensionContext, "cwd">, input: ResolveConfiguredRoleLaunchInput): Promise<PreparedRoleLaunch>;
102
104
  /** Insert the system prompt pair after a caller has prepared its launch argv. */
103
105
  export declare function finalizeRoleLaunch(prepared: PreparedRoleLaunch): ResolvedRoleLaunch;
104
106
  export * from "./composition.ts";
@@ -1,4 +1,4 @@
1
- import { type LoadedMcpConfig } from "@earendil-works/pi-coding-agent";
1
+ import type { LoadedMcpConfig } from "@earendil-works/pi-coding-agent";
2
2
  export declare function parseRoleMcpAllowlist(value: unknown): string[];
3
3
  /**
4
4
  * Select the Role's allowlisted servers from the global `mcp.json` in Pi's native `mcpServers`
@@ -9,5 +9,4 @@ export declare function parseRoleMcpAllowlist(value: unknown): string[];
9
9
  */
10
10
  export declare function loadRoleMcpConfig(agentDir: string, allowlist: readonly string[], options?: {
11
11
  codemode: boolean;
12
- piVersion?: string;
13
12
  }): LoadedMcpConfig;
package/dist/mcp-role.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { VERSION } from "@earendil-works/pi-coding-agent";
4
3
  export function parseRoleMcpAllowlist(value) {
5
4
  if (typeof value !== "string")
6
5
  throw new Error("The Role MCP policy flag must contain a JSON array of MCP server names.");
@@ -72,7 +71,7 @@ const EXPOSURE_CHECKS = {
72
71
  toolExposure: [(value) => isRecord(value) && Object.values(value).every(isExposure), "an object mapping tool names to exposures"],
73
72
  };
74
73
  /** Validate one selected server against Pi's documented `mcpServers` rules; the native loader is bypassed by `loadConfig`. */
75
- function validateRoleMcpServer(path, name, value, codemode, piVersion) {
74
+ function validateRoleMcpServer(path, name, value, codemode) {
76
75
  const fail = (message) => { throw new Error(`${path}: MCP server "${name}" ${message}`); };
77
76
  if (!SERVER_NAME.test(name))
78
77
  fail("has an invalid name (use letters, digits, \"_\" and \"-\").");
@@ -90,8 +89,6 @@ function validateRoleMcpServer(path, name, value, codemode, piVersion) {
90
89
  fail(`field "${field}" must be ${shape}.`);
91
90
  }
92
91
  if (http && isRecord(value.oauth) && value.oauth.clientRegistration === "cimd") {
93
- if (piVersion === "1.0.0")
94
- fail('field "oauth.clientRegistration" "cimd" requires Pi 1.0.1 or later; current Pi is 1.0.0.');
95
92
  if (value.oauth.clientId !== undefined || value.oauth.clientName !== undefined) {
96
93
  fail('field "oauth.clientRegistration" "cimd" cannot be combined with "oauth.clientId" or "oauth.clientName".');
97
94
  }
@@ -146,7 +143,7 @@ export function loadRoleMcpConfig(agentDir, allowlist, options = { codemode: fal
146
143
  if (clash)
147
144
  throw new Error(`${path}: Role MCP server "${name}" conflicts with "${clash}": names that differ only in "-" and "_" share one tool namespace.`);
148
145
  namespaces.set(mcpNamespace(name), name);
149
- return { name, config: validateRoleMcpServer(path, name, configured[name], options.codemode, options.piVersion ?? VERSION), source: path, scope: "global" };
146
+ return { name, config: validateRoleMcpServer(path, name, configured[name], options.codemode), source: path, scope: "global" };
150
147
  });
151
148
  return { servers, autoEnableCodemode: false, errors: [] };
152
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "26.2.3",
3
+ "version": "26.3.1",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -40,9 +40,9 @@
40
40
  "pack:check": "npm pack --dry-run"
41
41
  },
42
42
  "peerDependencies": {
43
- "@earendil-works/pi-ai": "^1.0.0",
44
- "@earendil-works/pi-coding-agent": "^1.0.0",
45
- "@earendil-works/pi-tui": "^1.0.0",
43
+ "@earendil-works/pi-ai": "^1.1.0",
44
+ "@earendil-works/pi-coding-agent": "^1.1.0",
45
+ "@earendil-works/pi-tui": "^1.1.0",
46
46
  "typebox": "^1.3.15"
47
47
  },
48
48
  "repository": {
@@ -74,9 +74,9 @@
74
74
  "proper-lockfile": "^4.1.2"
75
75
  },
76
76
  "devDependencies": {
77
- "@earendil-works/pi-ai": "1.0.0",
78
- "@earendil-works/pi-coding-agent": "1.0.0",
79
- "@earendil-works/pi-tui": "1.0.0",
77
+ "@earendil-works/pi-ai": "1.1.0",
78
+ "@earendil-works/pi-coding-agent": "1.1.0",
79
+ "@earendil-works/pi-tui": "1.1.0",
80
80
  "@types/proper-lockfile": "^4.1.4"
81
81
  }
82
82
  }