@henryqw/pi-subagent 24.0.0 → 25.0.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.
- package/CONTEXT.md +1 -1
- package/README.md +4 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +24 -8
- package/dist/mcp-role.d.ts +7 -5
- package/dist/mcp-role.js +56 -8
- package/dist/schema.d.ts +30 -0
- package/dist/schema.js +26 -2
- package/docs/orchestration.md +1 -1
- package/extensions/isolated.ts +4 -4
- package/extensions/role-mcp.ts +6 -10
- package/extensions/subagent.ts +2 -2
- package/extensions/workflow.ts +15 -1
- package/package.json +7 -7
package/CONTEXT.md
CHANGED
|
@@ -62,7 +62,7 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
|
62
62
|
### Resource and launch policy
|
|
63
63
|
|
|
64
64
|
- Ambient child extensions and Skills stay disabled. Only Role/caller resources plus required internal adapters load.
|
|
65
|
-
- Every Role requires `tools`, `extensions`, and `skills` arrays. Omitted or empty `mcps` denies MCP access. Direct `pi-mcp-adapter` loading is rejected because it bypasses the allowlist.
|
|
65
|
+
- Every Role requires `tools`, `extensions`, and `skills` arrays. `extensions` may name Pi built-in extensions as `builtin:<name>`. `mcps` names servers from the global `~/.pi/agent/mcp.json`; their tools load in the child with direct exposure. Omitted or empty `mcps` denies MCP access. Direct `pi-mcp-adapter` loading is rejected because it bypasses the allowlist.
|
|
66
66
|
- Selected extensions are trusted executable bundles, not a sandbox. All tools and lifecycle behavior they register load together.
|
|
67
67
|
- Role Skill names resolve through Main's effective Pi registry. Missing Roles, Skills, tools, MCP servers, routes, models, or thinking levels fail before the first productive turn.
|
|
68
68
|
- Route precedence is call model class, then Role default, then registered Model Task assignment/default. A direct model replaces only the route model and must support its thinking level.
|
package/README.md
CHANGED
|
@@ -13,24 +13,17 @@ pi install npm:@henryqw/pi-subagent
|
|
|
13
13
|
|
|
14
14
|
Run `/task-models` and configure the `fast`, `balanced`, `frontier`, and `fav` routes you use.
|
|
15
15
|
|
|
16
|
-
Install `pi-mcp-adapter` only when a Role declares an MCP server allowlist:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
pi install npm:pi-mcp-adapter
|
|
20
|
-
```
|
|
21
|
-
|
|
22
16
|
## Works with
|
|
23
17
|
|
|
24
18
|
| Package | Relationship | Purpose |
|
|
25
19
|
| --- | --- | --- |
|
|
26
20
|
| [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) | Required | Provides the configured model routes for delegated Roles. |
|
|
27
|
-
| [`pi-mcp-adapter`](https://github.com/nicobailon/pi-mcp-adapter) | Required | Loads the MCP servers named in a Role's `mcps` allowlist, only for Roles that declare one. |
|
|
28
21
|
|
|
29
22
|
## Use
|
|
30
23
|
|
|
31
24
|
Ask Main to delegate a bounded task, such as “Have a scout trace sign-in without editing files.” Main uses `delegate_task`; you receive progress and a result or an actionable failure while Main remains available.
|
|
32
25
|
|
|
33
|
-
Commands are for you; tools and the packaged skill are for Main. You do not need to call agent tools or manage candidate identities yourself.
|
|
26
|
+
Commands are for you; tools and the packaged skill are for Main. You do not need to call agent tools or manage candidate identities yourself. Delegation and continuation tools use plain-object parameter schemas so providers that omit root-union tools can expose them; each mode and action still requires its exact arguments.
|
|
34
27
|
|
|
35
28
|
| Surface | Type | Purpose |
|
|
36
29
|
| --- | --- | --- |
|
|
@@ -181,14 +174,14 @@ Role Markdown lives in `~/.pi/agent/config/pi-subagent/` and requires frontmatte
|
|
|
181
174
|
| `name`, `description` | Required non-empty text without terminal control characters |
|
|
182
175
|
| `modelClass` | Optional `fast`, `balanced`, `frontier`, or `fav` default |
|
|
183
176
|
| `tools` | Required array of base tool names; `[]` selects none |
|
|
184
|
-
| `extensions` | Required array of trusted absolute paths
|
|
177
|
+
| `extensions` | Required array of trusted absolute paths, supported package sources, or Pi built-in extensions such as `builtin:codemode` |
|
|
185
178
|
| `skills` | Required array of effective Pi Skill names |
|
|
186
|
-
| `mcps` | Optional exact
|
|
179
|
+
| `mcps` | Optional exact server names from `~/.pi/agent/mcp.json` in Pi's native `mcpServers` format; omitted or `[]` denies MCP access |
|
|
187
180
|
| body | Required system instructions |
|
|
188
181
|
|
|
189
182
|
Roles describe responsibility and capabilities. They do not choose isolation; each request does. A same-named user Role overrides a built-in Role. The package ships `implementer`, `reviewer`, and `scout`.
|
|
190
183
|
|
|
191
|
-
Children
|
|
184
|
+
Children run with `--no-extensions`, so a Role must list the extension that registers its route model's provider, and only declared resources plus required internal policy adapters load. `builtin:<name>` entries load Pi built-in extensions (`builtin:codemode`, `builtin:tool-search`, `builtin:llama.cpp`) explicitly; `builtin:mcp` is rejected because it would connect every configured server and bypass `mcps`. The servers named in `mcps` come from the global `~/.pi/agent/mcp.json` only; their tools load in the child with direct exposure and are declared to the model as `mcp__<server>__<tool>` once the server connects. 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.
|
|
192
185
|
|
|
193
186
|
## API
|
|
194
187
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { 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
|
-
export {
|
|
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";
|
|
6
6
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
|
|
7
7
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,10 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { DefaultPackageManager, getAgentDir, parseFrontmatter, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { extensionConfigDir } from "@henryqw/pi-config-store";
|
|
6
6
|
import { hasDisplayControlCharacters } from "./display-text.js";
|
|
7
|
-
import {
|
|
7
|
+
import { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
|
|
8
8
|
import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, PROFILE_NAMES, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
|
|
9
9
|
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
|
|
10
|
-
export {
|
|
10
|
+
export { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
|
|
11
11
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
|
|
12
12
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, } from "./worktree.js";
|
|
13
13
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, } from "./review-evidence.js";
|
|
@@ -66,12 +66,25 @@ const stringList = (value, field, source) => {
|
|
|
66
66
|
}
|
|
67
67
|
return value.map((item) => item.trim());
|
|
68
68
|
};
|
|
69
|
+
// Pi 0.99 built-in extensions (`builtin:<name>`, see Pi's settings docs); Pi exports no list of them.
|
|
70
|
+
// `builtin:mcp` is excluded: it connects every server in mcp.json and would bypass the Role `mcps` allowlist.
|
|
71
|
+
const BUILTIN_EXTENSION_NAMES = ["codemode", "tool-search", "llama.cpp"];
|
|
72
|
+
const BUILTIN_EXTENSION_PREFIX = "builtin:";
|
|
69
73
|
function validateExtension(extension, source) {
|
|
70
74
|
const value = cleanText(extension, "extension", source);
|
|
75
|
+
if (value.startsWith(BUILTIN_EXTENSION_PREFIX)) {
|
|
76
|
+
if (value === `${BUILTIN_EXTENSION_PREFIX}mcp`) {
|
|
77
|
+
throw new Error(`${source} must select MCP servers with mcps instead of loading builtin:mcp directly.`);
|
|
78
|
+
}
|
|
79
|
+
if (!BUILTIN_EXTENSION_NAMES.includes(value.slice(BUILTIN_EXTENSION_PREFIX.length))) {
|
|
80
|
+
throw new Error(`${source}: unknown built-in extension ${value}; use ${BUILTIN_EXTENSION_NAMES.map((name) => `${BUILTIN_EXTENSION_PREFIX}${name}`).join(", ")}.`);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
71
84
|
const packageSource = /^(?:npm|git|github|https?|ssh):/.test(value);
|
|
72
85
|
const userPath = isAbsolute(value) || value.startsWith("~/") || value.startsWith("~\\") || value.startsWith("file://");
|
|
73
86
|
if (!packageSource && !userPath) {
|
|
74
|
-
throw new Error(`${source}: extensions entries must be absolute paths
|
|
87
|
+
throw new Error(`${source}: extensions entries must be absolute paths, package sources, or builtin:<name>.`);
|
|
75
88
|
}
|
|
76
89
|
return value;
|
|
77
90
|
}
|
|
@@ -231,7 +244,9 @@ export async function resolveRolePackageResources(role, ctx) {
|
|
|
231
244
|
const resolved = await packageManager(ctx).resolveExtensionSources([...sources]);
|
|
232
245
|
const resourceGroups = [resolved.extensions, resolved.skills, resolved.prompts, resolved.themes]
|
|
233
246
|
.map((resources) => resources.filter((resource) => resource.enabled));
|
|
234
|
-
|
|
247
|
+
// Built-in extensions resolve to their `builtin:<name>` path with the bare source `builtin`.
|
|
248
|
+
const resolvedSources = new Set(resourceGroups.flat()
|
|
249
|
+
.map((resource) => resource.metadata.source === "builtin" ? resource.path : resource.metadata.source));
|
|
235
250
|
const missing = sources.filter((source) => !resolvedSources.has(source));
|
|
236
251
|
if (missing.length)
|
|
237
252
|
throw new Error(`Role extension sources resolved no resources: ${missing.join(", ")}.`);
|
|
@@ -317,6 +332,9 @@ export function prepareRoleLaunch(pi, ctx, input) {
|
|
|
317
332
|
? prepareRoleLaunchFromSkills(ctx, input, resolveRoleSkills(pi, input.role))
|
|
318
333
|
: prepareResolvedRoleLaunch(pi, ctx, input);
|
|
319
334
|
assertNoMissingRoleSkills(input.role, prepared);
|
|
335
|
+
// Validate the mcp.json the child reads: its environment inherits Main's, with launch overrides on top.
|
|
336
|
+
if (input.role.mcps?.length)
|
|
337
|
+
loadRoleMcpConfig(prepared.env.PI_CODING_AGENT_DIR ?? getAgentDir(), input.role.mcps);
|
|
320
338
|
return { ...prepared, role };
|
|
321
339
|
}
|
|
322
340
|
/** Resolve and prepare a configured Role with its package-owned resources. */
|
|
@@ -328,10 +346,8 @@ export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
|
|
|
328
346
|
if (matches.length !== 1)
|
|
329
347
|
throw new Error(`Required configured Role ${roleName} is missing or ambiguous.`);
|
|
330
348
|
const role = matches[0];
|
|
331
|
-
if (role.mcps?.length)
|
|
332
|
-
|
|
333
|
-
selectRoleMcpConfig(loadMcpConfig(join(getAgentDir(), "mcp.json"), ctx.cwd), role.mcps);
|
|
334
|
-
}
|
|
349
|
+
if (role.mcps?.length)
|
|
350
|
+
loadRoleMcpConfig(getAgentDir(), role.mcps);
|
|
335
351
|
const resources = await resolveRolePackageResources(role, ctx);
|
|
336
352
|
const effectiveRole = { ...role, extensions: resources.extensions };
|
|
337
353
|
const namedSkills = resolveRoleSkills(pi, effectiveRole);
|
package/dist/mcp-role.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
mcpServers: Record<string, unknown>;
|
|
3
|
-
settings?: Record<string, unknown>;
|
|
4
|
-
}
|
|
1
|
+
import type { LoadedMcpConfig } from "@earendil-works/pi-coding-agent";
|
|
5
2
|
export declare function parseRoleMcpAllowlist(value: unknown): string[];
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Select the Role's allowlisted servers from the global `mcp.json` in Pi's native `mcpServers`
|
|
5
|
+
* shape; the project `.pi/mcp.json` is ignored. A `--no-extensions` child has no codemode tool, so
|
|
6
|
+
* every selected server is forced to direct exposure and codemode is never activated.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadRoleMcpConfig(agentDir: string, allowlist: readonly string[]): LoadedMcpConfig;
|
package/dist/mcp-role.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
export function parseRoleMcpAllowlist(value) {
|
|
2
4
|
if (typeof value !== "string")
|
|
3
5
|
throw new Error("The Role MCP policy flag must contain a JSON array of MCP server names.");
|
|
@@ -17,14 +19,60 @@ export function parseRoleMcpAllowlist(value) {
|
|
|
17
19
|
throw new Error("The Role MCP policy flag contains duplicate MCP server names.");
|
|
18
20
|
return names;
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
const isStringRecord = (value) => isRecord(value) && Object.values(value).every((item) => typeof item === "string");
|
|
26
|
+
/** Documented optional fields of an `mcpServers` entry and their shapes (see Pi's docs/mcp.md). */
|
|
27
|
+
const FIELD_CHECKS = {
|
|
28
|
+
args: [(value) => Array.isArray(value) && value.every((item) => typeof item === "string"), "an array of strings"],
|
|
29
|
+
env: [isStringRecord, "an object of strings"],
|
|
30
|
+
headers: [isStringRecord, "an object of strings"],
|
|
31
|
+
cwd: [(value) => typeof value === "string", "a string"],
|
|
32
|
+
timeout: [(value) => typeof value === "number" && Number.isFinite(value) && value > 0, "a positive number"],
|
|
33
|
+
enabled: [(value) => typeof value === "boolean", "a boolean"],
|
|
34
|
+
oauth: [isRecord, "an object"],
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Select the Role's allowlisted servers from the global `mcp.json` in Pi's native `mcpServers`
|
|
38
|
+
* shape; the project `.pi/mcp.json` is ignored. A `--no-extensions` child has no codemode tool, so
|
|
39
|
+
* every selected server is forced to direct exposure and codemode is never activated.
|
|
40
|
+
*/
|
|
41
|
+
export function loadRoleMcpConfig(agentDir, allowlist) {
|
|
42
|
+
const path = join(agentDir, "mcp.json");
|
|
43
|
+
let parsed = {};
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
49
|
+
throw new Error(`${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!isRecord(parsed) || (parsed.mcpServers !== undefined && !isRecord(parsed.mcpServers))) {
|
|
53
|
+
throw new Error(`${path}: expected an object with an "mcpServers" object.`);
|
|
54
|
+
}
|
|
55
|
+
const configured = parsed.mcpServers ?? {};
|
|
56
|
+
const missing = allowlist.filter((name) => !Object.hasOwn(configured, name));
|
|
22
57
|
if (missing.length)
|
|
23
58
|
throw new Error(`Role MCP servers are not configured: ${missing.join(", ")}.`);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
59
|
+
const servers = allowlist.map((name) => {
|
|
60
|
+
const value = configured[name];
|
|
61
|
+
if (!isRecord(value))
|
|
62
|
+
throw new Error(`${path}: MCP server "${name}" must be an object.`);
|
|
63
|
+
if (value.enabled === false)
|
|
64
|
+
throw new Error(`${path}: Role MCP server "${name}" is disabled.`);
|
|
65
|
+
const stdio = typeof value.command === "string" && value.command.trim() !== "" && (value.type === undefined || value.type === "stdio");
|
|
66
|
+
const http = typeof value.url === "string" && value.url.trim() !== "" && (value.type === undefined || value.type === "http" || value.type === "streamable-http");
|
|
67
|
+
if (stdio === http) {
|
|
68
|
+
throw new Error(`${path}: MCP server "${name}" needs either a "command" (stdio) or a "url" (http or streamable-http).`);
|
|
69
|
+
}
|
|
70
|
+
for (const [field, [valid, shape]] of Object.entries(FIELD_CHECKS)) {
|
|
71
|
+
if (value[field] !== undefined && !valid(value[field]))
|
|
72
|
+
throw new Error(`${path}: MCP server "${name}" field "${field}" must be ${shape}.`);
|
|
73
|
+
}
|
|
74
|
+
const { toolExposure: _toolExposure, ...config } = value;
|
|
75
|
+
return { name, config: { ...config, exposure: "direct" }, source: path, scope: "global" };
|
|
76
|
+
});
|
|
77
|
+
return { servers, autoEnableCodemode: false, errors: [] };
|
|
30
78
|
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -129,6 +129,11 @@ export declare const ResumeRequestSchema: Type.TUnion<[Type.TObject<{
|
|
|
129
129
|
id: Type.TString;
|
|
130
130
|
action: Type.TLiteral<"finalize">;
|
|
131
131
|
}>]>;
|
|
132
|
+
export declare const ResumeRequestParameters: Type.TObject<{
|
|
133
|
+
id: Type.TString;
|
|
134
|
+
action: Type.TUnion<[Type.TLiteral<"retry">, Type.TLiteral<"verify">, Type.TLiteral<"finalize">]>;
|
|
135
|
+
taskId: Type.TOptional<Type.TString>;
|
|
136
|
+
}>;
|
|
132
137
|
export type IdOnly = Static<typeof IdOnlySchema>;
|
|
133
138
|
export type CheckCommand = Static<typeof CheckCommandSchema>;
|
|
134
139
|
export type Judgment = Static<typeof JudgmentSchema>;
|
|
@@ -369,6 +374,31 @@ export declare const IntegrationActionSchema: Type.TUnion<[Type.TObject<{
|
|
|
369
374
|
taskId: Type.TOptional<Type.TString>;
|
|
370
375
|
attempt: Type.TOptional<Type.TInteger>;
|
|
371
376
|
}>]>;
|
|
377
|
+
export declare const IntegrationActionParameters: Type.TObject<{
|
|
378
|
+
id: Type.TString;
|
|
379
|
+
generation: Type.TInteger;
|
|
380
|
+
action: Type.TUnion<[Type.TLiteral<"refresh">, Type.TUnion<[Type.TLiteral<"advance">, Type.TLiteral<"validate">, Type.TLiteral<"correct">, Type.TLiteral<"promote">, Type.TLiteral<"reconcile">, Type.TLiteral<"cleanup">]>, Type.TLiteral<"release">]>;
|
|
381
|
+
expectedTip: Type.TObject<{
|
|
382
|
+
branch: Type.TString;
|
|
383
|
+
head: Type.TString;
|
|
384
|
+
index: Type.TString;
|
|
385
|
+
tree: Type.TString;
|
|
386
|
+
}>;
|
|
387
|
+
expectedMain: Type.TOptional<Type.TObject<{
|
|
388
|
+
branch: Type.TString;
|
|
389
|
+
head: Type.TString;
|
|
390
|
+
index: Type.TString;
|
|
391
|
+
tree: Type.TString;
|
|
392
|
+
}>>;
|
|
393
|
+
newMain: Type.TOptional<Type.TObject<{
|
|
394
|
+
branch: Type.TString;
|
|
395
|
+
head: Type.TString;
|
|
396
|
+
index: Type.TString;
|
|
397
|
+
tree: Type.TString;
|
|
398
|
+
}>>;
|
|
399
|
+
taskId: Type.TOptional<Type.TString>;
|
|
400
|
+
attempt: Type.TOptional<Type.TInteger>;
|
|
401
|
+
}>;
|
|
372
402
|
export type IntegrationAction = Static<typeof IntegrationActionSchema>;
|
|
373
403
|
export declare function parseIntegrationAction(value: unknown): IntegrationAction;
|
|
374
404
|
export declare function parseStageRequest(value: unknown): StageRequest;
|
package/dist/schema.js
CHANGED
|
@@ -65,7 +65,17 @@ export const ResumeRequestSchema = Type.Union([
|
|
|
65
65
|
Type.Object({ id: IdSchema, action: Type.Literal("retry"), taskId: IdSchema }, { additionalProperties: false }),
|
|
66
66
|
Type.Object({ id: IdSchema, action: Type.Literal("verify"), taskId: IdSchema }, { additionalProperties: false }),
|
|
67
67
|
Type.Object({ id: IdSchema, action: Type.Literal("finalize") }, { additionalProperties: false }),
|
|
68
|
-
]
|
|
68
|
+
]);
|
|
69
|
+
// Tool parameters need a plain object root; the action union remains the validation boundary.
|
|
70
|
+
export const ResumeRequestParameters = Type.Object({
|
|
71
|
+
id: IdSchema,
|
|
72
|
+
action: Type.Union([
|
|
73
|
+
ResumeRequestSchema.anyOf[0].properties.action,
|
|
74
|
+
ResumeRequestSchema.anyOf[1].properties.action,
|
|
75
|
+
ResumeRequestSchema.anyOf[2].properties.action,
|
|
76
|
+
]),
|
|
77
|
+
taskId: Type.Optional(IdSchema),
|
|
78
|
+
}, { additionalProperties: false });
|
|
69
79
|
const AllocationLifecycleFields = {
|
|
70
80
|
generation: Type.Integer({ minimum: 1, maximum: 2 }),
|
|
71
81
|
token: Type.String({ pattern: TOKEN_PATTERN }),
|
|
@@ -167,7 +177,21 @@ export const IntegrationActionSchema = Type.Union([
|
|
|
167
177
|
taskId: Type.Optional(IdSchema),
|
|
168
178
|
attempt: Type.Optional(Type.Integer({ minimum: 1, maximum: 2 })),
|
|
169
179
|
}, { additionalProperties: false }),
|
|
170
|
-
]
|
|
180
|
+
]);
|
|
181
|
+
export const IntegrationActionParameters = Type.Object({
|
|
182
|
+
id: IntegrationActionSchema.anyOf[0].properties.id,
|
|
183
|
+
generation: IntegrationActionSchema.anyOf[0].properties.generation,
|
|
184
|
+
action: Type.Union([
|
|
185
|
+
IntegrationActionSchema.anyOf[0].properties.action,
|
|
186
|
+
IntegrationActionSchema.anyOf[1].properties.action,
|
|
187
|
+
IntegrationActionSchema.anyOf[2].properties.action,
|
|
188
|
+
]),
|
|
189
|
+
expectedTip: IntegrationActionSchema.anyOf[0].properties.expectedTip,
|
|
190
|
+
expectedMain: Type.Optional(IntegrationActionSchema.anyOf[0].properties.expectedMain),
|
|
191
|
+
newMain: Type.Optional(IntegrationActionSchema.anyOf[0].properties.newMain),
|
|
192
|
+
taskId: IntegrationActionSchema.anyOf[2].properties.taskId,
|
|
193
|
+
attempt: IntegrationActionSchema.anyOf[2].properties.attempt,
|
|
194
|
+
}, { additionalProperties: false });
|
|
171
195
|
export function parseIntegrationAction(value) {
|
|
172
196
|
if (!Check(IntegrationActionSchema, value))
|
|
173
197
|
throw schemaValidationError("subagent_integrate requires an exact generation and tip", Errors(IntegrationActionSchema, value));
|
package/docs/orchestration.md
CHANGED
|
@@ -139,7 +139,7 @@ Every child launch:
|
|
|
139
139
|
5. excludes Main-only delegation/recovery tools and `ask_question`; and
|
|
140
140
|
6. prepends the delegated-subagent identity contract.
|
|
141
141
|
|
|
142
|
-
An empty `tools` list adds no base tools, though selected extension tools can still activate. Omitted or empty `mcps` denies MCP access. Loading `pi-mcp-adapter` directly is rejected because it bypasses the allowlist.
|
|
142
|
+
An empty `tools` list adds no base tools, though selected extension tools can still activate. Children run with `--no-extensions`, so a Role must list the extension that registers its route model's provider; `builtin:<name>` entries load Pi built-in extensions. `mcps` names servers from the global `~/.pi/agent/mcp.json` in Pi's native format, and their tools load in the child with direct exposure. Omitted or empty `mcps` denies MCP access. Loading `pi-mcp-adapter` directly is rejected because it bypasses the allowlist.
|
|
143
143
|
|
|
144
144
|
Isolated Herdr Role launches receive the same turn, token, and child-runtime budget metadata as ephemeral launches. Retained workers therefore keep one non-refilling session budget across follow-ups rather than receiving a new request budget.
|
|
145
145
|
|
package/extensions/isolated.ts
CHANGED
|
@@ -17,9 +17,9 @@ import { RoleLaunchRuntime, type LaunchRuntimeOptions } from "../dist/launch-run
|
|
|
17
17
|
import { IsolatedRunner, type OperationContext, type RunResponse } from "../dist/runner.js";
|
|
18
18
|
import {
|
|
19
19
|
IdOnlySchema,
|
|
20
|
-
|
|
20
|
+
ResumeRequestParameters,
|
|
21
21
|
StageRequestSchema,
|
|
22
|
-
|
|
22
|
+
IntegrationActionParameters,
|
|
23
23
|
parseIntegrationAction,
|
|
24
24
|
parseStageRequest,
|
|
25
25
|
parseExecuteRequest,
|
|
@@ -614,7 +614,7 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
614
614
|
name: "subagent_resume",
|
|
615
615
|
label: "Subagent resume",
|
|
616
616
|
description: "Resume one unfinished isolated request without resetting its recorded policy or correction count.",
|
|
617
|
-
parameters:
|
|
617
|
+
parameters: ResumeRequestParameters,
|
|
618
618
|
prepareArguments: parseResumeRequest,
|
|
619
619
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
620
620
|
latestCtx = ctx;
|
|
@@ -641,7 +641,7 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
641
641
|
name: "subagent_integrate",
|
|
642
642
|
label: "Subagent integrate",
|
|
643
643
|
description: "Main advances staged dependents, refreshes after clean Main drift, validates, corrects, promotes, reconciles an interrupted promotion, cleans up promoted resources, or explicitly releases rejected/superseded owned resources. Never replays an uncertain mutation.",
|
|
644
|
-
parameters:
|
|
644
|
+
parameters: IntegrationActionParameters,
|
|
645
645
|
prepareArguments: parseIntegrationAction,
|
|
646
646
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
647
647
|
latestCtx = ctx;
|
package/extensions/role-mcp.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { createMcpAdapter } from "pi-mcp-adapter";
|
|
4
|
-
import { loadMcpConfig } from "pi-mcp-adapter/config";
|
|
5
|
-
import { roleMcpAllowlistFromArgv, ROLE_MCP_POLICY_FLAG, selectRoleMcpConfig } from "@henryqw/pi-subagent";
|
|
1
|
+
import { createMcpExtension, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadRoleMcpConfig, roleMcpAllowlistFromArgv, ROLE_MCP_POLICY_FLAG } from "@henryqw/pi-subagent";
|
|
6
3
|
|
|
7
|
-
export default function roleMcp(pi: ExtensionAPI): void {
|
|
4
|
+
export default function roleMcp(pi: ExtensionAPI): void | Promise<void> {
|
|
8
5
|
pi.registerFlag(ROLE_MCP_POLICY_FLAG, {
|
|
9
6
|
description: "Internal Pi Subagent Role MCP policy",
|
|
10
7
|
type: "string",
|
|
11
8
|
});
|
|
12
|
-
// Pi binds extension flag values after factories load.
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
createMcpAdapter({ config })(pi);
|
|
9
|
+
// Pi binds extension flag values after factories load. Throwing here exits the child non-zero.
|
|
10
|
+
const config = loadRoleMcpConfig(getAgentDir(), roleMcpAllowlistFromArgv(process.argv));
|
|
11
|
+
return createMcpExtension({ loadConfig: () => config })(pi);
|
|
16
12
|
}
|
package/extensions/subagent.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { registerIsolatedExtension } from "./isolated.ts";
|
|
|
9
9
|
import { registerSubagentCommand, type DirectTask } from "./subagent-command.ts";
|
|
10
10
|
import { MODEL_CLASS_GUIDANCE } from "./model-class-policy.ts";
|
|
11
11
|
import { ENTRY_STATUS_PRESENTATION, formatWorkflowResult, type BackgroundWorkflowTransportDetails, type WorkflowTransportEntry } from "./result-transport.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { DelegateTaskParameters, identifyWorkflowEntries, parseDelegateTask, runForegroundWorkflow, type Delegation, type ParsedWorkflow, type WorkflowEntry } from "./workflow.ts";
|
|
13
13
|
import { createDirectHerdr, type DirectHandle, type DirectTab } from "../dist/direct-herdr.js";
|
|
14
14
|
import { materializeTransientLaunch } from "../dist/launch-runtime.js";
|
|
15
15
|
const WIDGET_KEY = "subagent-status";
|
|
@@ -460,7 +460,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
460
460
|
"Isolation uses typed tasks and dependencies. Keep tightly coupled changes with one owner; do not split by file count. Failures, ambiguity, limits, and conflicts retain work and never waive checks or identity guards.",
|
|
461
461
|
`For delegate_task, ${MODEL_CLASS_GUIDANCE} A direct model replaces only the selected route's model; its thinking level stays unchanged.`,
|
|
462
462
|
],
|
|
463
|
-
parameters:
|
|
463
|
+
parameters: DelegateTaskParameters,
|
|
464
464
|
prepareArguments(args) {
|
|
465
465
|
try {
|
|
466
466
|
const parsed = parseDelegateTask(args);
|
package/extensions/workflow.ts
CHANGED
|
@@ -42,7 +42,21 @@ export const DirectWorkflowSchema = Type.Object({
|
|
|
42
42
|
chain: Type.Optional(Type.Array(DelegationSchema, { minItems: 1, maxItems: MAX_WORKFLOW_ENTRIES })),
|
|
43
43
|
}, { additionalProperties: false, description: "Direct mode: exactly one compact single, tasks, or chain workflow" });
|
|
44
44
|
|
|
45
|
-
export const DelegateTaskSchema = Type.Union([DirectWorkflowSchema, ExecuteRequestSchema]
|
|
45
|
+
export const DelegateTaskSchema = Type.Union([DirectWorkflowSchema, ExecuteRequestSchema]);
|
|
46
|
+
// Keep the strict mode union for parsing; expose a plain object root to tool providers.
|
|
47
|
+
export const DelegateTaskParameters = Type.Object({
|
|
48
|
+
...DirectWorkflowSchema.properties,
|
|
49
|
+
mode: Type.Union([DirectWorkflowSchema.properties.mode, ExecuteRequestSchema.properties.mode]),
|
|
50
|
+
id: Type.Optional(ExecuteRequestSchema.properties.id),
|
|
51
|
+
goal: Type.Optional(ExecuteRequestSchema.properties.goal),
|
|
52
|
+
tasks: Type.Optional(Type.Union([
|
|
53
|
+
Type.Array(DelegationSchema, { minItems: 1, maxItems: MAX_WORKFLOW_ENTRIES }),
|
|
54
|
+
ExecuteRequestSchema.properties.tasks,
|
|
55
|
+
])),
|
|
56
|
+
finalChecks: ExecuteRequestSchema.properties.finalChecks,
|
|
57
|
+
finalJudgment: ExecuteRequestSchema.properties.finalJudgment,
|
|
58
|
+
}, { additionalProperties: false });
|
|
59
|
+
export const WorkflowSchema = DirectWorkflowSchema;
|
|
46
60
|
|
|
47
61
|
export type Delegation = Static<typeof DelegationSchema> & { kind: "text" };
|
|
48
62
|
export type WorkflowMode = "single" | "parallel" | "chain";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-subagent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "25.0.0",
|
|
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": ">=0.
|
|
44
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
45
|
-
"@earendil-works/pi-tui": ">=0.
|
|
43
|
+
"@earendil-works/pi-ai": ">=0.99.1 <0.100.0",
|
|
44
|
+
"@earendil-works/pi-coding-agent": ">=0.99.1 <0.100.0",
|
|
45
|
+
"@earendil-works/pi-tui": ">=0.99.1 <0.100.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": "0.
|
|
78
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
79
|
-
"@earendil-works/pi-tui": "0.
|
|
77
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
78
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
79
|
+
"@earendil-works/pi-tui": "0.99.1",
|
|
80
80
|
"@types/proper-lockfile": "^4.1.4"
|
|
81
81
|
}
|
|
82
82
|
}
|