@narumitw/pi-subagents 1.0.1 → 2.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/README.md +198 -188
- package/package.json +8 -8
- package/src/agents/built-ins.ts +13 -66
- package/src/agents/catalog.ts +19 -2
- package/src/agents/discovery.ts +31 -15
- package/src/auto-transport.ts +7 -1
- package/src/child-peer-bridge.ts +124 -0
- package/src/child-peer-tools.ts +132 -0
- package/src/completion-delivery.ts +19 -5
- package/src/completion-render.ts +189 -0
- package/src/completion-routing.ts +24 -0
- package/src/config-registration.ts +29 -4
- package/src/config-ui.ts +11 -17
- package/src/consult-registration.ts +3 -2
- package/src/consult-render.ts +1 -1
- package/src/create-stateful-transport.ts +15 -2
- package/src/execution-ui.ts +0 -72
- package/src/in-process-transport.ts +39 -7
- package/src/inspect-tool.ts +3 -1
- package/src/panel-planning.ts +2 -2
- package/src/panel-presets.ts +3 -0
- package/src/params.ts +1 -1
- package/src/peer-communication.ts +352 -0
- package/src/peer-transport.ts +49 -0
- package/src/persistence.ts +26 -1
- package/src/pi-args.ts +2 -0
- package/src/registry-types.ts +7 -0
- package/src/registry.ts +240 -41
- package/src/render.ts +2 -41
- package/src/result-contract.ts +20 -5
- package/src/rpc-transport.ts +56 -26
- package/src/runner.ts +13 -1
- package/src/settings.ts +4 -1
- package/src/spawn-idempotency.ts +2 -0
- package/src/stateful-agent-view.ts +3 -1
- package/src/stateful-guidance.ts +11 -11
- package/src/stateful-safety.ts +0 -45
- package/src/stateful-tool-params.ts +11 -3
- package/src/stateful.ts +359 -136
- package/src/subagents.ts +7 -9
- package/src/subprocess-transport.ts +49 -28
- package/src/task-path.ts +65 -0
- package/src/transport-ui.ts +0 -6
- package/src/transport.ts +2 -1
- package/src/usage-format.ts +42 -0
- package/src/workflow-ui.ts +4 -4
- package/src/automation-contract.ts +0 -709
- package/src/automation-planner.ts +0 -65
- package/src/automation-registration.ts +0 -137
- package/src/automation-tool.ts +0 -40
- package/src/automation.ts +0 -435
- package/src/execution-profiles.ts +0 -95
- package/src/workflow-plan-compiler.ts +0 -618
- package/src/workflow-plan-patch.ts +0 -636
- package/src/workflow-planning-benchmark.ts +0 -95
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-subagents",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Pi extension for delegating work to specialized isolated subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,16 +39,16 @@
|
|
|
39
39
|
"typebox": "*"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@biomejs/biome": "2.5.
|
|
43
|
-
"@earendil-works/pi-agent-core": "0.84.
|
|
44
|
-
"@earendil-works/pi-ai": "0.84.
|
|
45
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
46
|
-
"@earendil-works/pi-tui": "0.84.
|
|
47
|
-
"typebox": "1.3.
|
|
42
|
+
"@biomejs/biome": "2.5.8",
|
|
43
|
+
"@earendil-works/pi-agent-core": "0.84.2",
|
|
44
|
+
"@earendil-works/pi-ai": "0.84.2",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
46
|
+
"@earendil-works/pi-tui": "0.84.2",
|
|
47
|
+
"typebox": "1.3.14",
|
|
48
48
|
"typescript": "7.0.2"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@narumitw/pi-tui-kit": "^0.
|
|
51
|
+
"@narumitw/pi-tui-kit": "^0.55.0",
|
|
52
52
|
"proper-lockfile": "^4.1.2"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|
package/src/agents/built-ins.ts
CHANGED
|
@@ -7,58 +7,26 @@ import type { AgentConfig } from "./types.js";
|
|
|
7
7
|
|
|
8
8
|
export const BUILT_IN_AGENTS: AgentConfig[] = [
|
|
9
9
|
{
|
|
10
|
-
name: "
|
|
10
|
+
name: "explorer",
|
|
11
11
|
description:
|
|
12
|
-
"Read-only codebase
|
|
13
|
-
tools: ["read", "grep", "find", "ls"
|
|
12
|
+
"Read-only codebase exploration for specific questions; returns concise findings with paths and evidence.",
|
|
13
|
+
tools: ["read", "grep", "find", "ls"],
|
|
14
14
|
capabilityManifest: builtInManifest(["repository-search", "code-evidence"], "read", [
|
|
15
15
|
"evidence-gathering",
|
|
16
16
|
]),
|
|
17
|
+
thinkingLevel: "low",
|
|
17
18
|
source: "built-in",
|
|
18
|
-
filePath: "built-in:
|
|
19
|
+
filePath: "built-in:explorer",
|
|
19
20
|
systemPrompt: [
|
|
20
|
-
"You are
|
|
21
|
-
"Do not edit files
|
|
21
|
+
"You are an explorer subagent. Explore the codebase for specific, well-scoped questions and report grounded findings.",
|
|
22
|
+
"Do not edit files, run shell commands, or use any tool outside the read-only inspection set.",
|
|
23
|
+
"Do not install dependencies, run formatters, start servers, run tests, or execute long-running commands.",
|
|
22
24
|
"Return concise bullets with exact file paths, symbols, and open questions.",
|
|
23
25
|
].join("\n"),
|
|
24
26
|
},
|
|
25
|
-
{
|
|
26
|
-
name: "planner",
|
|
27
|
-
description: "Turns reconnaissance into a lean implementation or migration plan.",
|
|
28
|
-
tools: ["read", "grep", "find", "ls"],
|
|
29
|
-
capabilityManifest: builtInManifest(
|
|
30
|
-
["task-decomposition", "implementation-planning", "migration-planning"],
|
|
31
|
-
"read",
|
|
32
|
-
),
|
|
33
|
-
source: "built-in",
|
|
34
|
-
filePath: "built-in:planner",
|
|
35
|
-
systemPrompt: [
|
|
36
|
-
"You are a planner subagent. Produce executable, verifiable plans only.",
|
|
37
|
-
"Do not modify files. Ground the plan in the repository's actual structure.",
|
|
38
|
-
"Call out assumptions, risks, sequencing, and verification commands.",
|
|
39
|
-
].join("\n"),
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
name: "reviewer",
|
|
43
|
-
description: "Independent code review agent that inspects existing verification evidence.",
|
|
44
|
-
tools: ["read", "grep", "find", "ls", "bash"],
|
|
45
|
-
capabilityManifest: builtInManifest(
|
|
46
|
-
["code-review", "evidence-review", "security-baseline"],
|
|
47
|
-
"read",
|
|
48
|
-
["independent-review"],
|
|
49
|
-
),
|
|
50
|
-
source: "built-in",
|
|
51
|
-
filePath: "built-in:reviewer",
|
|
52
|
-
systemPrompt: [
|
|
53
|
-
"You are a reviewer subagent. Review changes adversarially and assess claims against the code and existing evidence.",
|
|
54
|
-
"Do not edit files or run tests, builds, benchmarks, formatters, or other long-running verification commands.",
|
|
55
|
-
"Inspect code, diffs, test definitions, and existing verification evidence. Recommend any additional commands for the main agent to run.",
|
|
56
|
-
"Report PASS, FAIL, or PARTIAL with evidence, commands inspected, and specific follow-ups.",
|
|
57
|
-
].join("\n"),
|
|
58
|
-
},
|
|
59
27
|
{
|
|
60
28
|
name: "worker",
|
|
61
|
-
description: "
|
|
29
|
+
description: "Bounded implementation and command-execution worker with clear ownership.",
|
|
62
30
|
capabilityManifest: builtInManifest(
|
|
63
31
|
["implementation", "command-execution", "repository-modification"],
|
|
64
32
|
"write",
|
|
@@ -67,28 +35,6 @@ export const BUILT_IN_AGENTS: AgentConfig[] = [
|
|
|
67
35
|
filePath: "built-in:worker",
|
|
68
36
|
systemPrompt: workerSystemPrompt(),
|
|
69
37
|
},
|
|
70
|
-
{
|
|
71
|
-
name: "general",
|
|
72
|
-
description: "Alias for worker; kept for model-generated subagent names.",
|
|
73
|
-
capabilityManifest: builtInManifest(
|
|
74
|
-
["implementation", "command-execution", "repository-modification"],
|
|
75
|
-
"write",
|
|
76
|
-
),
|
|
77
|
-
source: "built-in",
|
|
78
|
-
filePath: "built-in:general",
|
|
79
|
-
systemPrompt: workerSystemPrompt(),
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: "general-purpose",
|
|
83
|
-
description: "Alias for worker; compatible with common subagent naming conventions.",
|
|
84
|
-
capabilityManifest: builtInManifest(
|
|
85
|
-
["implementation", "command-execution", "repository-modification"],
|
|
86
|
-
"write",
|
|
87
|
-
),
|
|
88
|
-
source: "built-in",
|
|
89
|
-
filePath: "built-in:general-purpose",
|
|
90
|
-
systemPrompt: workerSystemPrompt(),
|
|
91
|
-
},
|
|
92
38
|
];
|
|
93
39
|
|
|
94
40
|
export function getBuiltInAgent(name: string): AgentConfig | undefined {
|
|
@@ -117,8 +63,9 @@ function builtInManifest(
|
|
|
117
63
|
|
|
118
64
|
function workerSystemPrompt(): string {
|
|
119
65
|
return [
|
|
120
|
-
"You are a focused worker subagent running in an isolated Pi
|
|
121
|
-
"Complete the delegated
|
|
122
|
-
"
|
|
66
|
+
"You are a focused worker subagent running in an isolated Pi child context.",
|
|
67
|
+
"Complete only the bounded delegated implementation slice and respect its assigned file or responsibility ownership.",
|
|
68
|
+
"Keep scope tight, avoid unrelated changes, and do not overwrite concurrent work outside your ownership.",
|
|
69
|
+
"The main agent owns integration and final verification; report changed files, commands run, and remaining risks for that handoff.",
|
|
123
70
|
].join("\n");
|
|
124
71
|
}
|
package/src/agents/catalog.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type AgentDiscoveryResult,
|
|
9
9
|
discoverAgents,
|
|
10
10
|
} from "./discovery.js";
|
|
11
|
-
import type
|
|
11
|
+
import { type AgentConfig, resolveAgentToolNames, type SubagentSettings } from "./types.js";
|
|
12
12
|
|
|
13
13
|
export function formatAgentList(
|
|
14
14
|
agents: AgentConfig[],
|
|
@@ -70,6 +70,21 @@ function normalizeCatalogDescription(description: string, maxLength: number): st
|
|
|
70
70
|
|
|
71
71
|
type CatalogScope = "user" | "project" | "project-fallback";
|
|
72
72
|
|
|
73
|
+
function formatDeclaredItems(items: readonly string[] | undefined): string {
|
|
74
|
+
if (items === undefined) return "undeclared";
|
|
75
|
+
return items.length > 0 ? items.join(", ") : "none";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function catalogAgentContractMetadata(agent: AgentConfig): string {
|
|
79
|
+
const manifest = agent.capabilityManifest;
|
|
80
|
+
return [
|
|
81
|
+
`capabilities: ${formatDeclaredItems(manifest?.capabilities)}`,
|
|
82
|
+
`tools: ${formatDeclaredItems(resolveAgentToolNames(agent.tools))}`,
|
|
83
|
+
`filesystem: ${manifest?.authority?.filesystem ?? "undeclared"}`,
|
|
84
|
+
`result formats: ${formatDeclaredItems(manifest?.resultFormats)}`,
|
|
85
|
+
].join("; ");
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
function catalogAgentLine(
|
|
74
89
|
agent: AgentConfig,
|
|
75
90
|
scope: CatalogScope,
|
|
@@ -88,7 +103,7 @@ function catalogAgentLine(
|
|
|
88
103
|
? "; overrides the default user definition for project/both"
|
|
89
104
|
: "; scope-specific fallback for the default user override"
|
|
90
105
|
: "";
|
|
91
|
-
return `- ${agent.name} [source: ${agent.source}; ${scopeLabel}${collision}] — ${normalizeCatalogDescription(agent.description, maxDescriptionLength)}`;
|
|
106
|
+
return `- ${agent.name} [source: ${agent.source}; ${scopeLabel}${collision}] [${catalogAgentContractMetadata(agent)}] — ${normalizeCatalogDescription(agent.description, maxDescriptionLength)}`;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
/**
|
|
@@ -153,6 +168,8 @@ export function formatAgentCatalog(
|
|
|
153
168
|
const render = (entries: typeof allEntries, omitted: number): string => {
|
|
154
169
|
const lines = [
|
|
155
170
|
"Available agent definitions (metadata only; runtime validation and trust remain authoritative).",
|
|
171
|
+
"Use capability and tool identifiers exactly as shown when authoring enforced contracts.",
|
|
172
|
+
"Enforced contract readPaths, writePaths, network, and secrets guarantees are unsupported.",
|
|
156
173
|
];
|
|
157
174
|
const userLines = entries
|
|
158
175
|
.filter((entry) => entry.scope === "user")
|
package/src/agents/discovery.ts
CHANGED
|
@@ -179,6 +179,24 @@ function hasOwn(obj: object, key: PropertyKey): boolean {
|
|
|
179
179
|
return Object.hasOwn(obj, key);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
function applyAgentOverride(
|
|
183
|
+
agent: AgentConfig,
|
|
184
|
+
override: NonNullable<SubagentSettings["agents"]>[string],
|
|
185
|
+
): AgentConfig {
|
|
186
|
+
const nextAgent: AgentConfig = { ...agent };
|
|
187
|
+
if (hasOwn(override, "tools")) nextAgent.tools = override.tools;
|
|
188
|
+
if (hasOwn(override, "model")) {
|
|
189
|
+
nextAgent.model = override.model === null ? undefined : override.model;
|
|
190
|
+
}
|
|
191
|
+
if (hasOwn(override, "thinkingLevel")) {
|
|
192
|
+
nextAgent.thinkingLevel = override.thinkingLevel === null ? undefined : override.thinkingLevel;
|
|
193
|
+
}
|
|
194
|
+
if (hasOwn(override, "timeoutMs")) {
|
|
195
|
+
nextAgent.timeoutMs = override.timeoutMs === null ? undefined : override.timeoutMs;
|
|
196
|
+
}
|
|
197
|
+
return nextAgent;
|
|
198
|
+
}
|
|
199
|
+
|
|
182
200
|
export function discoverAgents(
|
|
183
201
|
cwd: string,
|
|
184
202
|
scope: AgentScope,
|
|
@@ -217,23 +235,21 @@ export function discoverAgents(
|
|
|
217
235
|
|
|
218
236
|
// Apply user-configured overrides (from /subagents → Agent tool settings) on top of
|
|
219
237
|
// the final resolved agent map, regardless of agent source.
|
|
220
|
-
|
|
238
|
+
const configuredAgents = config?.agents ?? {};
|
|
239
|
+
for (const [name, override] of Object.entries(configuredAgents)) {
|
|
221
240
|
const agent = agentMap.get(name);
|
|
222
241
|
if (!agent) continue;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
nextAgent.timeoutMs = override.timeoutMs === null ? undefined : override.timeoutMs;
|
|
235
|
-
}
|
|
236
|
-
agentMap.set(name, nextAgent);
|
|
242
|
+
agentMap.set(name, applyAgentOverride(agent, override));
|
|
243
|
+
}
|
|
244
|
+
const legacyScoutOverride = configuredAgents.scout;
|
|
245
|
+
const explorerAgent = agentMap.get("explorer");
|
|
246
|
+
if (
|
|
247
|
+
legacyScoutOverride &&
|
|
248
|
+
explorerAgent &&
|
|
249
|
+
!hasOwn(configuredAgents, "explorer") &&
|
|
250
|
+
!agentMap.has("scout")
|
|
251
|
+
) {
|
|
252
|
+
agentMap.set("explorer", applyAgentOverride(explorerAgent, legacyScoutOverride));
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
const omittedAgentDefinitions =
|
package/src/auto-transport.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { discoverAgents } from "./agents/discovery.js";
|
|
2
2
|
import type { SubagentSettings } from "./agents/types.js";
|
|
3
|
-
import type { ManagedAgent, TurnOutcome } from "./registry.js";
|
|
3
|
+
import type { AgentMailboxMessage, ManagedAgent, TurnOutcome } from "./registry.js";
|
|
4
4
|
import { isWriteCapable } from "./stateful-safety.js";
|
|
5
5
|
import type { SubagentTransport } from "./transport.js";
|
|
6
6
|
import type {
|
|
@@ -48,6 +48,12 @@ export class AutoTransport implements SubagentTransport {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
async deliverMessage(agent: ManagedAgent, message: AgentMailboxMessage): Promise<boolean> {
|
|
52
|
+
const selection = this.selections.get(agent.id);
|
|
53
|
+
if (!selection) return false;
|
|
54
|
+
return this.transport(selection.kind).deliverMessage?.(agent, message) ?? false;
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
async release(agent: ManagedAgent): Promise<void> {
|
|
52
58
|
const selection = this.selections.get(agent.id);
|
|
53
59
|
this.selections.delete(agent.id);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type ChildPeerClient, createChildPeerExtension } from "./child-peer-tools.js";
|
|
4
|
+
import type { AgentMailboxMessage } from "./registry.js";
|
|
5
|
+
|
|
6
|
+
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 2_000;
|
|
8
|
+
|
|
9
|
+
const captured = captureBridgeEnvironment();
|
|
10
|
+
|
|
11
|
+
const childPeerBridge: ExtensionFactory = captured
|
|
12
|
+
? createChildPeerExtension(createProcessPeerClient(captured))
|
|
13
|
+
: () => undefined;
|
|
14
|
+
|
|
15
|
+
export default childPeerBridge;
|
|
16
|
+
|
|
17
|
+
interface CapturedBridgeEnvironment {
|
|
18
|
+
host: "127.0.0.1";
|
|
19
|
+
port: number;
|
|
20
|
+
token: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function captureBridgeEnvironment(): CapturedBridgeEnvironment | undefined {
|
|
24
|
+
const host = process.env.PI_SUBAGENT_PEER_HOST;
|
|
25
|
+
const rawPort = process.env.PI_SUBAGENT_PEER_PORT;
|
|
26
|
+
const token = process.env.PI_SUBAGENT_PEER_TOKEN;
|
|
27
|
+
delete process.env.PI_SUBAGENT_PEER_HOST;
|
|
28
|
+
delete process.env.PI_SUBAGENT_PEER_PORT;
|
|
29
|
+
delete process.env.PI_SUBAGENT_PEER_TOKEN;
|
|
30
|
+
if (!host && !rawPort && !token) return undefined;
|
|
31
|
+
const port = Number(rawPort);
|
|
32
|
+
if (host !== "127.0.0.1" || !Number.isSafeInteger(port) || port < 1 || port > 65_535 || !token) {
|
|
33
|
+
throw new Error("Invalid pi-subagents peer bridge environment");
|
|
34
|
+
}
|
|
35
|
+
return { host, port, token };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createProcessPeerClient(environment: CapturedBridgeEnvironment): ChildPeerClient {
|
|
39
|
+
const request = async (input: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
|
40
|
+
const response = await requestBridge(environment, { ...input, token: environment.token });
|
|
41
|
+
if (response.ok !== true) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
typeof response.error === "string"
|
|
44
|
+
? response.error
|
|
45
|
+
: "Subagent peer bridge rejected the request",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return response;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
async send(target, message, deduplicationKey) {
|
|
52
|
+
const response = await request({
|
|
53
|
+
action: "send",
|
|
54
|
+
target,
|
|
55
|
+
message,
|
|
56
|
+
...(deduplicationKey ? { deduplicationKey } : {}),
|
|
57
|
+
});
|
|
58
|
+
if (!isMailboxMessage(response.message)) {
|
|
59
|
+
throw new Error("Subagent peer bridge returned an invalid message receipt");
|
|
60
|
+
}
|
|
61
|
+
return response.message;
|
|
62
|
+
},
|
|
63
|
+
async list() {
|
|
64
|
+
return (await request({ action: "list" })).peers ?? [];
|
|
65
|
+
},
|
|
66
|
+
async acknowledge(messageIds, completionIds) {
|
|
67
|
+
await request({ action: "acknowledge", messageIds, completionIds });
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function requestBridge(
|
|
73
|
+
environment: CapturedBridgeEnvironment,
|
|
74
|
+
request: Record<string, unknown>,
|
|
75
|
+
): Promise<Record<string, unknown>> {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const socket = net.createConnection({ host: environment.host, port: environment.port });
|
|
78
|
+
let response = Buffer.alloc(0);
|
|
79
|
+
let settled = false;
|
|
80
|
+
const finish = (error?: Error, value?: Record<string, unknown>) => {
|
|
81
|
+
if (settled) return;
|
|
82
|
+
settled = true;
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
socket.destroy();
|
|
85
|
+
if (error) reject(error);
|
|
86
|
+
else resolve(value ?? {});
|
|
87
|
+
};
|
|
88
|
+
const timer = setTimeout(
|
|
89
|
+
() => finish(new Error("Subagent peer bridge request timed out")),
|
|
90
|
+
REQUEST_TIMEOUT_MS,
|
|
91
|
+
);
|
|
92
|
+
timer.unref();
|
|
93
|
+
socket.once("connect", () => socket.end(`${JSON.stringify(request)}\n`));
|
|
94
|
+
socket.on("data", (chunk: Buffer) => {
|
|
95
|
+
response = Buffer.concat([response, chunk]);
|
|
96
|
+
if (response.byteLength > MAX_RESPONSE_BYTES) {
|
|
97
|
+
finish(new Error("Subagent peer bridge response exceeded its size limit"));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
socket.once("error", (error) => finish(error));
|
|
101
|
+
socket.once("close", () => {
|
|
102
|
+
if (settled) return;
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(response.toString("utf8")) as unknown;
|
|
105
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
|
|
106
|
+
finish(undefined, parsed as Record<string, unknown>);
|
|
107
|
+
} catch {
|
|
108
|
+
finish(new Error("Subagent peer bridge returned malformed JSON"));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isMailboxMessage(value: unknown): value is AgentMailboxMessage {
|
|
115
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
116
|
+
const message = value as Record<string, unknown>;
|
|
117
|
+
return (
|
|
118
|
+
typeof message.id === "string" &&
|
|
119
|
+
typeof message.senderId === "string" &&
|
|
120
|
+
typeof message.recipientId === "string" &&
|
|
121
|
+
typeof message.content === "string" &&
|
|
122
|
+
typeof message.createdAt === "number"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { defineTool, type ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { redactPrivateText } from "./context.js";
|
|
4
|
+
import { MAX_TOOL_MESSAGE_BYTES, truncateUtf8 } from "./limits.js";
|
|
5
|
+
import type { AgentMailboxMessage } from "./registry.js";
|
|
6
|
+
|
|
7
|
+
const MAX_PEER_MESSAGE_BYTES = 16 * 1024;
|
|
8
|
+
const MAX_DEDUPLICATION_KEY_LENGTH = 256;
|
|
9
|
+
const PEER_ENVELOPE_HEADER =
|
|
10
|
+
/^Message Type: SUBAGENT_(?:COMPLETION|PEER_MESSAGE)\nProtocol: pi-subagents:v1\nMessage ID: (msg_[^\s]+)\n(?:Completion ID: (completion:[^\s]+)\n)?Sender ID: [^\n]+\n(?:Sender Path: [^\n]+\n)?Payload:\n/u;
|
|
11
|
+
|
|
12
|
+
export interface ChildPeerClient {
|
|
13
|
+
send(target: string, message: string, deduplicationKey?: string): Promise<AgentMailboxMessage>;
|
|
14
|
+
list(): Promise<unknown>;
|
|
15
|
+
acknowledge(messageIds: readonly string[], completionIds: readonly string[]): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createChildPeerExtension(client: ChildPeerClient): ExtensionFactory {
|
|
19
|
+
return (pi) => {
|
|
20
|
+
pi.registerTool(
|
|
21
|
+
defineTool({
|
|
22
|
+
name: "subagent_peer_send",
|
|
23
|
+
label: "Send Peer Message",
|
|
24
|
+
description:
|
|
25
|
+
"Send a bounded queue-only message to /root or another retained agent by canonical task path. The runtime binds your sender identity; this tool never starts an idle turn.",
|
|
26
|
+
promptSnippet: "Send a queue-only message to another retained agent",
|
|
27
|
+
parameters: Type.Object(
|
|
28
|
+
{
|
|
29
|
+
target: Type.String({ minLength: 1, maxLength: 2_048 }),
|
|
30
|
+
message: Type.String({ minLength: 1, maxLength: MAX_PEER_MESSAGE_BYTES }),
|
|
31
|
+
deduplicationKey: Type.Optional(
|
|
32
|
+
Type.String({ minLength: 1, maxLength: MAX_DEDUPLICATION_KEY_LENGTH }),
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
{ additionalProperties: false },
|
|
36
|
+
),
|
|
37
|
+
async execute(_id, params) {
|
|
38
|
+
const message = await client.send(params.target, params.message, params.deduplicationKey);
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text" as const,
|
|
43
|
+
text: `Queued ${message.id} for ${message.recipientId}; delivery does not start an idle turn.`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
details: {
|
|
47
|
+
messageId: message.id,
|
|
48
|
+
recipientId: message.recipientId,
|
|
49
|
+
createdAt: message.createdAt,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
pi.registerTool(
|
|
56
|
+
defineTool({
|
|
57
|
+
name: "subagent_peer_list",
|
|
58
|
+
label: "List Peers",
|
|
59
|
+
description:
|
|
60
|
+
"List bounded identity and lifecycle metadata for /root and retained agents in this session.",
|
|
61
|
+
promptSnippet: "List retained peer task paths",
|
|
62
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
63
|
+
async execute() {
|
|
64
|
+
const peers = await client.list();
|
|
65
|
+
const text = truncateUtf8(JSON.stringify(peers, null, 2), MAX_TOOL_MESSAGE_BYTES).text;
|
|
66
|
+
return { content: [{ type: "text" as const, text }], details: { peers } };
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const acknowledgedMessageIds = new Set<string>();
|
|
72
|
+
const acknowledgedCompletionIds = new Set<string>();
|
|
73
|
+
pi.on("context", async (event) => {
|
|
74
|
+
const visible = visibleDeliveryIds(event.messages);
|
|
75
|
+
const messageIds = [...visible.messageIds].filter((id) => !acknowledgedMessageIds.has(id));
|
|
76
|
+
const completionIds = [...visible.completionIds].filter(
|
|
77
|
+
(id) => !acknowledgedCompletionIds.has(id),
|
|
78
|
+
);
|
|
79
|
+
if (messageIds.length === 0 && completionIds.length === 0) return;
|
|
80
|
+
await client.acknowledge(messageIds, completionIds);
|
|
81
|
+
for (const id of messageIds) acknowledgedMessageIds.add(id);
|
|
82
|
+
for (const id of completionIds) acknowledgedCompletionIds.add(id);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatPeerMessage(message: AgentMailboxMessage, senderPath?: string): string {
|
|
88
|
+
return truncateUtf8(
|
|
89
|
+
[
|
|
90
|
+
`Message Type: ${message.completionId ? "SUBAGENT_COMPLETION" : "SUBAGENT_PEER_MESSAGE"}`,
|
|
91
|
+
"Protocol: pi-subagents:v1",
|
|
92
|
+
`Message ID: ${message.id}`,
|
|
93
|
+
...(message.completionId ? [`Completion ID: ${message.completionId}`] : []),
|
|
94
|
+
`Sender ID: ${message.senderId}`,
|
|
95
|
+
...(senderPath ? [`Sender Path: ${senderPath}`] : []),
|
|
96
|
+
"Payload:",
|
|
97
|
+
redactPrivateText(message.content),
|
|
98
|
+
].join("\n"),
|
|
99
|
+
MAX_TOOL_MESSAGE_BYTES,
|
|
100
|
+
).text;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function visibleDeliveryIds(messages: readonly unknown[]): {
|
|
104
|
+
messageIds: Set<string>;
|
|
105
|
+
completionIds: Set<string>;
|
|
106
|
+
} {
|
|
107
|
+
const messageIds = new Set<string>();
|
|
108
|
+
const completionIds = new Set<string>();
|
|
109
|
+
for (const message of messages) {
|
|
110
|
+
for (const text of messageText(message)) {
|
|
111
|
+
const match = PEER_ENVELOPE_HEADER.exec(text);
|
|
112
|
+
if (!match) continue;
|
|
113
|
+
messageIds.add(match[1]);
|
|
114
|
+
if (match[2]) completionIds.add(match[2]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { messageIds, completionIds };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function messageText(message: unknown): string[] {
|
|
121
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) return [];
|
|
122
|
+
const candidate = message as Record<string, unknown>;
|
|
123
|
+
if (candidate.role !== "user") return [];
|
|
124
|
+
const content = candidate.content;
|
|
125
|
+
if (typeof content === "string") return [content];
|
|
126
|
+
if (!Array.isArray(content)) return [];
|
|
127
|
+
return content.flatMap((part) => {
|
|
128
|
+
if (!part || typeof part !== "object" || Array.isArray(part)) return [];
|
|
129
|
+
const text = (part as Record<string, unknown>).text;
|
|
130
|
+
return typeof text === "string" ? [text] : [];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { CompletionDelivery } from "./agents/types.js";
|
|
3
|
+
import { SUBAGENT_COMPLETION_MESSAGE_TYPE } from "./completion-render.js";
|
|
3
4
|
import { redactPrivateText } from "./context.js";
|
|
4
5
|
import { DEFAULT_MAX_CONTEXT_BYTES, MAX_TOOL_MESSAGE_BYTES, truncateUtf8 } from "./limits.js";
|
|
5
6
|
import type { AgentTurnCompletion, ManagedAgent } from "./registry.js";
|
|
@@ -16,7 +17,10 @@ interface CompletionMetadata {
|
|
|
16
17
|
runId: string;
|
|
17
18
|
generation: number;
|
|
18
19
|
agentId: string;
|
|
20
|
+
taskPath?: string;
|
|
21
|
+
recipientPath?: string;
|
|
19
22
|
agent: string;
|
|
23
|
+
task: string;
|
|
20
24
|
state: string;
|
|
21
25
|
transport?: string;
|
|
22
26
|
structuredResult?: ManagedAgent["structuredResult"];
|
|
@@ -25,7 +29,7 @@ interface CompletionMetadata {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
interface CompletionMessage {
|
|
28
|
-
customType:
|
|
32
|
+
customType: typeof SUBAGENT_COMPLETION_MESSAGE_TYPE;
|
|
29
33
|
content: string;
|
|
30
34
|
display: true;
|
|
31
35
|
details:
|
|
@@ -107,7 +111,11 @@ export class CompletionDeliveryBroker {
|
|
|
107
111
|
if (triggerTurn) this.wakeInFlight = true;
|
|
108
112
|
this.awaitingParentAck.push(...batch);
|
|
109
113
|
try {
|
|
110
|
-
|
|
114
|
+
// Pi treats explicit false as immediate insertion instead of queued steering while streaming.
|
|
115
|
+
this.pi.sendMessage(message, {
|
|
116
|
+
deliverAs: "steer",
|
|
117
|
+
...(triggerTurn ? { triggerTurn: true } : {}),
|
|
118
|
+
});
|
|
111
119
|
} catch (primaryError) {
|
|
112
120
|
this.removeAwaiting(batch);
|
|
113
121
|
if (triggerTurn) this.wakeInFlight = false;
|
|
@@ -202,7 +210,8 @@ function completionIdsFromContext(messages: readonly unknown[]): Set<string> {
|
|
|
202
210
|
for (const message of messages) {
|
|
203
211
|
if (!message || typeof message !== "object" || Array.isArray(message)) continue;
|
|
204
212
|
const record = message as Record<string, unknown>;
|
|
205
|
-
if (record.role !== "custom" || record.customType !==
|
|
213
|
+
if (record.role !== "custom" || record.customType !== SUBAGENT_COMPLETION_MESSAGE_TYPE)
|
|
214
|
+
continue;
|
|
206
215
|
const details = record.details;
|
|
207
216
|
if (!details || typeof details !== "object" || Array.isArray(details)) continue;
|
|
208
217
|
const metadata = details as Record<string, unknown>;
|
|
@@ -236,7 +245,7 @@ function buildCompletionMessage(completions: AgentTurnCompletion[]): CompletionM
|
|
|
236
245
|
if (completions.length === 1) {
|
|
237
246
|
const completion = completions[0];
|
|
238
247
|
return {
|
|
239
|
-
customType:
|
|
248
|
+
customType: SUBAGENT_COMPLETION_MESSAGE_TYPE,
|
|
240
249
|
content: buildDetachedCompletionMessage(completion),
|
|
241
250
|
display: true,
|
|
242
251
|
details: completionMetadata(completion),
|
|
@@ -256,7 +265,7 @@ function buildCompletionMessage(completions: AgentTurnCompletion[]): CompletionM
|
|
|
256
265
|
DEFAULT_MAX_CONTEXT_BYTES,
|
|
257
266
|
).text;
|
|
258
267
|
return {
|
|
259
|
-
customType:
|
|
268
|
+
customType: SUBAGENT_COMPLETION_MESSAGE_TYPE,
|
|
260
269
|
content,
|
|
261
270
|
display: true,
|
|
262
271
|
details: {
|
|
@@ -273,7 +282,10 @@ function completionMetadata(completion: AgentTurnCompletion): CompletionMetadata
|
|
|
273
282
|
runId: completion.runId,
|
|
274
283
|
generation: completion.generation,
|
|
275
284
|
agentId: completion.agent.id,
|
|
285
|
+
...(completion.agent.taskPath ? { taskPath: completion.agent.taskPath } : {}),
|
|
286
|
+
...(completion.recipientPath ? { recipientPath: completion.recipientPath } : {}),
|
|
276
287
|
agent: completion.agent.agent,
|
|
288
|
+
task: sanitizeCompletionLine(completion.task, 256) || "(unknown task)",
|
|
277
289
|
state: completion.agent.state,
|
|
278
290
|
...(completion.agent.telemetry?.transport
|
|
279
291
|
? { transport: completion.agent.telemetry.transport }
|
|
@@ -306,6 +318,8 @@ export function buildDetachedCompletionMessage(completion: AgentTurnCompletion):
|
|
|
306
318
|
`Run ID: ${completion.runId}`,
|
|
307
319
|
`Generation: ${completion.generation}`,
|
|
308
320
|
`Agent ID: ${completion.agent.id}`,
|
|
321
|
+
...(completion.agent.taskPath ? [`Agent Path: ${completion.agent.taskPath}`] : []),
|
|
322
|
+
...(completion.recipientPath ? [`Recipient Path: ${completion.recipientPath}`] : []),
|
|
309
323
|
`Agent: ${agentName}`,
|
|
310
324
|
`Task: ${task}`,
|
|
311
325
|
`State: ${completion.agent.state}`,
|