@hheei/pi-subagents 0.7.0 → 0.8.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/LICENSES/Apache-2.0.txt +202 -0
- package/NOTICE +10 -0
- package/README.md +13 -13
- package/dist/child-control.d.ts +0 -8
- package/dist/child-control.js +40 -8
- package/dist/commands.js +1 -1
- package/dist/controller-ipc.d.ts +38 -0
- package/dist/controller-ipc.js +292 -0
- package/dist/controller-main.d.ts +1 -0
- package/dist/controller-main.js +26 -0
- package/dist/controller-role.d.ts +40 -0
- package/dist/controller-role.js +37 -0
- package/dist/controller-socket.d.ts +6 -0
- package/dist/controller-socket.js +43 -0
- package/dist/controller.d.ts +25 -0
- package/dist/controller.js +217 -0
- package/dist/delegation.d.ts +73 -0
- package/dist/delegation.js +71 -0
- package/dist/domain.d.ts +20 -18
- package/dist/domain.js +6 -1
- package/dist/extension.js +43 -26
- package/dist/host-adapter.js +1 -1
- package/dist/launch-spec.d.ts +1 -3
- package/dist/launch-spec.js +38 -15
- package/dist/lease.d.ts +0 -1
- package/dist/lease.js +8 -11
- package/dist/mailbox.js +110 -34
- package/dist/manager.d.ts +52 -6
- package/dist/manager.js +568 -32
- package/dist/mcp/app.html +948 -0
- package/dist/mcp/delegate.d.ts +30 -0
- package/dist/mcp/delegate.js +44 -0
- package/dist/mcp/views.d.ts +5 -0
- package/dist/mcp/views.js +12 -0
- package/dist/mcp-server.d.ts +6 -0
- package/dist/mcp-server.js +287 -0
- package/dist/mcp-stdio.d.ts +2 -0
- package/dist/mcp-stdio.js +111 -0
- package/dist/process-lock.d.ts +1 -1
- package/dist/process-lock.js +4 -2
- package/dist/protocol.d.ts +5 -5
- package/dist/protocol.js +3 -4
- package/dist/receipts.d.ts +80 -0
- package/dist/receipts.js +326 -0
- package/dist/runtime.d.ts +5 -26
- package/dist/runtime.js +15 -57
- package/dist/session-bootstrap.js +2 -6
- package/dist/state-db.d.ts +11 -4
- package/dist/state-db.js +102 -24
- package/dist/state.js +12 -3
- package/dist/tool-activation.d.ts +2 -2
- package/dist/tool-activation.js +5 -5
- package/dist/tool-presentation.d.ts +2 -6
- package/dist/tool-presentation.js +75 -122
- package/dist/tools.d.ts +0 -3
- package/dist/tools.js +104 -132
- package/package.json +12 -5
- package/dist/json-lines.d.ts +0 -14
- package/dist/json-lines.js +0 -95
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { DelegateSubagentInput } from "../domain.js";
|
|
3
|
+
export interface DelegateMcpInput extends DelegateSubagentInput {
|
|
4
|
+
/** Return immediately with a recoverable delegation ID instead of waiting for settlement. */
|
|
5
|
+
readonly wait?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* MCP-facing input contract. Both id and agent may be supplied: id is authoritative and agent is
|
|
9
|
+
* ignored. Do not express the target as an exclusive union in the advertised tool schema.
|
|
10
|
+
*/
|
|
11
|
+
export declare const delegateInputSchema: Type.TObject<{
|
|
12
|
+
task: Type.TString;
|
|
13
|
+
wait: Type.TOptional<Type.TBoolean>;
|
|
14
|
+
id: Type.TOptional<Type.TString>;
|
|
15
|
+
agent: Type.TOptional<Type.TString>;
|
|
16
|
+
cwd: Type.TOptional<Type.TString>;
|
|
17
|
+
}>;
|
|
18
|
+
export declare const delegateToolMetadata: {
|
|
19
|
+
readonly name: "delegate";
|
|
20
|
+
readonly description: "Delegate work to a new or existing Pi agent. Supply agent to start a new session, or id to continue an existing one; with both, id wins and agent is ignored. Returns the delegation's id, revision, status and hasOutput, and by default waits for the result.";
|
|
21
|
+
readonly inputSchema: Type.TObject<{
|
|
22
|
+
task: Type.TString;
|
|
23
|
+
wait: Type.TOptional<Type.TBoolean>;
|
|
24
|
+
id: Type.TOptional<Type.TString>;
|
|
25
|
+
agent: Type.TOptional<Type.TString>;
|
|
26
|
+
cwd: Type.TOptional<Type.TString>;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
/** Validate untrusted MCP arguments before submitting them to the manager. */
|
|
30
|
+
export declare function parseDelegateInput(input: unknown): DelegateMcpInput;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
|
+
import { stripHindsightContent } from "../launch-spec.js";
|
|
4
|
+
/**
|
|
5
|
+
* MCP-facing input contract. Both id and agent may be supplied: id is authoritative and agent is
|
|
6
|
+
* ignored. Do not express the target as an exclusive union in the advertised tool schema.
|
|
7
|
+
*/
|
|
8
|
+
export const delegateInputSchema = Type.Object({
|
|
9
|
+
task: Type.String({
|
|
10
|
+
minLength: 1,
|
|
11
|
+
description: "Work to perform in the selected child session.",
|
|
12
|
+
}),
|
|
13
|
+
wait: Type.Optional(Type.Boolean({
|
|
14
|
+
description: "Wait for the final result (default true). False returns a recoverable delegation ID immediately.",
|
|
15
|
+
})),
|
|
16
|
+
id: Type.Optional(Type.String({
|
|
17
|
+
description: "Existing child ID. Takes precedence over agent and reuses its session.",
|
|
18
|
+
})),
|
|
19
|
+
agent: Type.Optional(Type.String({
|
|
20
|
+
description: "Agent definition for a new child. Ignored whenever id is supplied.",
|
|
21
|
+
})),
|
|
22
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for a new child; ignored with id." })),
|
|
23
|
+
}, { additionalProperties: false });
|
|
24
|
+
export const delegateToolMetadata = {
|
|
25
|
+
name: "delegate",
|
|
26
|
+
description: "Delegate work to a new or existing Pi agent. Supply agent to start a new session, or id to continue an existing one; with both, id wins and agent is ignored. Returns the delegation's id, revision, status and hasOutput, and by default waits for the result.",
|
|
27
|
+
inputSchema: delegateInputSchema,
|
|
28
|
+
};
|
|
29
|
+
/** Validate untrusted MCP arguments before submitting them to the manager. */
|
|
30
|
+
export function parseDelegateInput(input) {
|
|
31
|
+
if (!Value.Check(delegateInputSchema, input)) {
|
|
32
|
+
throw new Error("Invalid delegate parameters");
|
|
33
|
+
}
|
|
34
|
+
if (stripHindsightContent(input.task) === "")
|
|
35
|
+
throw new Error("Task must not be empty");
|
|
36
|
+
if (input.id !== undefined) {
|
|
37
|
+
if (input.id.trim() === "")
|
|
38
|
+
throw new Error("Child id must not be empty");
|
|
39
|
+
}
|
|
40
|
+
else if (input.agent === undefined || input.agent.trim() === "") {
|
|
41
|
+
throw new Error("Agent is required without an id");
|
|
42
|
+
}
|
|
43
|
+
return input;
|
|
44
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const TASK_CARD_URI = "ui://hheei/subagents/task";
|
|
2
|
+
export declare const DASHBOARD_URI = "ui://hheei/subagents/dashboard";
|
|
3
|
+
export declare const APP_MIME = "text/html;profile=mcp-app";
|
|
4
|
+
/** Static MCP Apps page. `{{VIEW}}` is `task` or `dashboard`; the file ships beside this module in dist. */
|
|
5
|
+
export declare function readAppHtml(kind: "task" | "dashboard"): Promise<string>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export const TASK_CARD_URI = "ui://hheei/subagents/task";
|
|
3
|
+
export const DASHBOARD_URI = "ui://hheei/subagents/dashboard";
|
|
4
|
+
export const APP_MIME = "text/html;profile=mcp-app";
|
|
5
|
+
let template;
|
|
6
|
+
/** Static MCP Apps page. `{{VIEW}}` is `task` or `dashboard`; the file ships beside this module in dist. */
|
|
7
|
+
export async function readAppHtml(kind) {
|
|
8
|
+
template ??= readFile(new URL("./app.html", import.meta.url), "utf8");
|
|
9
|
+
const html = await template;
|
|
10
|
+
const title = kind === "task" ? "Subagent" : "Subagents";
|
|
11
|
+
return html.replaceAll("{{VIEW}}", kind).replaceAll("{{TITLE}}", title);
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
/**
|
|
3
|
+
* MCP Apps descriptor metadata. The ext-apps package is not a dependency: its peers pull React and
|
|
4
|
+
* Zod, and a host only needs both resource URI keys plus the mcp-app HTML MIME type.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createSubagentMcpServer(controllerSocket: () => Promise<string>, workspace: string): McpServer;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { fromJsonSchema, McpServer as Server } from "@modelcontextprotocol/server";
|
|
2
|
+
import { discoverAgents } from "./agent-resolver.js";
|
|
3
|
+
import { callController } from "./controller-ipc.js";
|
|
4
|
+
import { isOperationError } from "./domain.js";
|
|
5
|
+
import { delegateToolMetadata, parseDelegateInput } from "./mcp/delegate.js";
|
|
6
|
+
import { APP_MIME, DASHBOARD_URI, readAppHtml, TASK_CARD_URI } from "./mcp/views.js";
|
|
7
|
+
const APP_ONLY = { ui: { visibility: ["app"] } };
|
|
8
|
+
const BOTH = { ui: { visibility: ["model", "app"] } };
|
|
9
|
+
/**
|
|
10
|
+
* MCP Apps descriptor metadata. The ext-apps package is not a dependency: its peers pull React and
|
|
11
|
+
* Zod, and a host only needs both resource URI keys plus the mcp-app HTML MIME type.
|
|
12
|
+
*/
|
|
13
|
+
export function createSubagentMcpServer(controllerSocket, workspace) {
|
|
14
|
+
const server = new Server({ name: "pi_agents", version: "0.7.0" });
|
|
15
|
+
registerAppTool(server, delegateToolMetadata.name, TASK_CARD_URI, {
|
|
16
|
+
description: delegateToolMetadata.description,
|
|
17
|
+
inputSchema: fromJsonSchema(delegateToolMetadata.inputSchema),
|
|
18
|
+
handler: async (args, signal) => {
|
|
19
|
+
const input = parseDelegateInput(args);
|
|
20
|
+
return toolResult(await callController(await controllerSocket(), { op: "delegate", input }, signal));
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
registerPlain(server, "get", "Read one agent delegation: the newest by default, or one revision, with its output when asked.", {
|
|
24
|
+
type: "object",
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
required: ["id"],
|
|
27
|
+
properties: {
|
|
28
|
+
id: { type: "string", minLength: 1 },
|
|
29
|
+
revision: { type: "integer", minimum: 1 },
|
|
30
|
+
includeOutput: { type: "boolean" },
|
|
31
|
+
},
|
|
32
|
+
}, async (args, signal) => forward(controllerSocket, "get", {
|
|
33
|
+
id: requiredString(args, "id"),
|
|
34
|
+
...(optionalPositive(args, "revision") === undefined
|
|
35
|
+
? {}
|
|
36
|
+
: { revision: optionalPositive(args, "revision") }),
|
|
37
|
+
includeOutput: isRecord(args) && args.includeOutput === true,
|
|
38
|
+
}, signal), BOTH);
|
|
39
|
+
registerPlain(server, "stop", "Stop one agent runtime: open delegations fail, finished ones and the session are kept.", idSchema("id"), async (args, signal) => forward(controllerSocket, "stop", { id: requiredString(args, "id") }, signal), BOTH);
|
|
40
|
+
registerAppTool(server, "dashboard", DASHBOARD_URI, {
|
|
41
|
+
description: "List this workspace's agents with their runtime state and latest delegation.",
|
|
42
|
+
inputSchema: fromJsonSchema({ type: "object", additionalProperties: false, properties: {} }),
|
|
43
|
+
handler: async (_args, signal) => toolResult(await callController(await controllerSocket(), { op: "list", input: {} }, signal)),
|
|
44
|
+
});
|
|
45
|
+
registerPlain(server, "list", "List this workspace's agents with their runtime state and latest delegation.", { type: "object", additionalProperties: false, properties: {} }, async (_args, signal) => forward(controllerSocket, "list", {}, signal), APP_ONLY);
|
|
46
|
+
registerPlain(server, "transcript", "Read one page of an agent's session history.", {
|
|
47
|
+
type: "object",
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
required: ["id"],
|
|
50
|
+
properties: {
|
|
51
|
+
id: { type: "string", minLength: 1 },
|
|
52
|
+
cursor: { type: "integer", minimum: 0 },
|
|
53
|
+
limit: { type: "integer", minimum: 1, maximum: 50 },
|
|
54
|
+
},
|
|
55
|
+
}, async (args, signal) => forward(controllerSocket, "transcript", {
|
|
56
|
+
id: requiredString(args, "id"),
|
|
57
|
+
cursor: optionalNumber(args, "cursor"),
|
|
58
|
+
limit: optionalNumber(args, "limit"),
|
|
59
|
+
}, signal), APP_ONLY);
|
|
60
|
+
registerPlain(server, "receipts", "List recent delegations of this workspace as id and revision, without their output.", {
|
|
61
|
+
type: "object",
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
properties: {
|
|
64
|
+
cursor: { type: "integer", minimum: 0 },
|
|
65
|
+
limit: { type: "integer", minimum: 1, maximum: 50 },
|
|
66
|
+
},
|
|
67
|
+
}, async (args, signal) => forward(controllerSocket, "receipts", {
|
|
68
|
+
cursor: optionalNumber(args, "cursor"),
|
|
69
|
+
limit: optionalNumber(args, "limit"),
|
|
70
|
+
}, signal), APP_ONLY);
|
|
71
|
+
registerPlain(server, "catalog", "List agent definitions available to a new agent.", { type: "object", additionalProperties: false, properties: {} }, async () => {
|
|
72
|
+
const agents = await discoverAgents(workspace);
|
|
73
|
+
return {
|
|
74
|
+
text: agents.map((agent) => agent.name).join(", "),
|
|
75
|
+
structuredContent: {
|
|
76
|
+
agents: agents.map((agent) => ({ name: agent.name, enabled: agent.enabled === true })),
|
|
77
|
+
},
|
|
78
|
+
isError: false,
|
|
79
|
+
};
|
|
80
|
+
}, APP_ONLY);
|
|
81
|
+
registerHtml(server, "Agent task", TASK_CARD_URI, "task");
|
|
82
|
+
registerHtml(server, "Agent dashboard", DASHBOARD_URI, "dashboard");
|
|
83
|
+
return server;
|
|
84
|
+
}
|
|
85
|
+
async function forward(controllerSocket, op, input, signal) {
|
|
86
|
+
return toolResult(await callController(await controllerSocket(), { op, input }, signal));
|
|
87
|
+
}
|
|
88
|
+
function registerAppTool(server, name, resourceUri, tool) {
|
|
89
|
+
registerPlain(server, name, tool.description, undefined, tool.handler, { ui: { resourceUri, visibility: ["model", "app"] }, "ui/resourceUri": resourceUri }, tool.inputSchema);
|
|
90
|
+
}
|
|
91
|
+
function registerPlain(server, name, description, schema, handler, visibility, inputSchema = schema === undefined ? undefined : fromJsonSchema(schema)) {
|
|
92
|
+
server.registerTool(name, {
|
|
93
|
+
description,
|
|
94
|
+
...(inputSchema === undefined ? {} : { inputSchema }),
|
|
95
|
+
...(visibility === undefined ? {} : { _meta: visibility }),
|
|
96
|
+
}, async (args, ctx) => {
|
|
97
|
+
const payload = await handler(args, ctx.mcpReq.signal);
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text", text: payload.text }],
|
|
100
|
+
structuredContent: payload.structuredContent,
|
|
101
|
+
isError: payload.isError,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function registerHtml(server, name, uri, kind) {
|
|
106
|
+
server.registerResource(name, uri, { mimeType: APP_MIME, description: name }, async (resource) => ({
|
|
107
|
+
contents: [{ uri: resource.href, mimeType: APP_MIME, text: await readAppHtml(kind) }],
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
function toolResult(reply) {
|
|
111
|
+
if (!reply.ok || !isRecord(reply.result)) {
|
|
112
|
+
const error = reply.error ?? "Controller call failed";
|
|
113
|
+
return { text: error, structuredContent: { error }, isError: true };
|
|
114
|
+
}
|
|
115
|
+
const value = reply.result;
|
|
116
|
+
if (isOperationError(value)) {
|
|
117
|
+
return {
|
|
118
|
+
text: value.reason,
|
|
119
|
+
structuredContent: {
|
|
120
|
+
error: value.reason,
|
|
121
|
+
operation: value.operation,
|
|
122
|
+
...(typeof value.childId === "string" ? { id: value.childId } : {}),
|
|
123
|
+
...(Array.isArray(value.sideEffects)
|
|
124
|
+
? { sideEffects: value.sideEffects.filter(isString) }
|
|
125
|
+
: {}),
|
|
126
|
+
...(typeof value.safeToRetry === "boolean" ? { safeToRetry: value.safeToRetry } : {}),
|
|
127
|
+
},
|
|
128
|
+
isError: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
text: summarize(value),
|
|
133
|
+
structuredContent: project(value),
|
|
134
|
+
isError: failedDelegation(value),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function project(value) {
|
|
138
|
+
const progress = isRecord(value.progress) ? value.progress : undefined;
|
|
139
|
+
return {
|
|
140
|
+
...(typeof value.id === "string" ? { id: value.id } : {}),
|
|
141
|
+
...(typeof value.revision === "number" ? { revision: value.revision } : {}),
|
|
142
|
+
...(typeof value.status === "string" ? { status: value.status } : {}),
|
|
143
|
+
...(typeof value.task === "string" ? { task: value.task } : {}),
|
|
144
|
+
...(typeof value.hasOutput === "boolean" ? { hasOutput: value.hasOutput } : {}),
|
|
145
|
+
...(typeof value.resultText === "string" ? { resultText: value.resultText } : {}),
|
|
146
|
+
...(typeof value.agent === "string" ? { agent: value.agent } : {}),
|
|
147
|
+
...(typeof value.cwd === "string" ? { cwd: value.cwd } : {}),
|
|
148
|
+
...(typeof value.createdAt === "number" ? { createdAt: value.createdAt } : {}),
|
|
149
|
+
...(typeof value.delivery === "string" ? { delivery: value.delivery } : {}),
|
|
150
|
+
...(progress === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: {
|
|
153
|
+
progress: {
|
|
154
|
+
runtimeState: typeof progress.runtimeState === "string" ? progress.runtimeState : "unknown",
|
|
155
|
+
...(typeof progress.summary === "string" ? { summary: progress.summary } : {}),
|
|
156
|
+
...(typeof progress.activeTool === "string" ? { activeTool: progress.activeTool } : {}),
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
...(isRecord(value.error) ? { error: cleanError(value.error) } : {}),
|
|
160
|
+
...(typeof value.total === "number" ? { total: value.total } : {}),
|
|
161
|
+
...(typeof value.running === "number" ? { running: value.running } : {}),
|
|
162
|
+
...(typeof value.blocked === "number" ? { blocked: value.blocked } : {}),
|
|
163
|
+
...(typeof value.completed === "number" ? { completed: value.completed } : {}),
|
|
164
|
+
...(typeof value.failed === "number" ? { failed: value.failed } : {}),
|
|
165
|
+
...(Array.isArray(value.children)
|
|
166
|
+
? { children: value.children.filter(isRecord).map(cleanChild) }
|
|
167
|
+
: {}),
|
|
168
|
+
...(Array.isArray(value.entries) ? { entries: value.entries } : {}),
|
|
169
|
+
...(Array.isArray(value.receipts)
|
|
170
|
+
? { receipts: value.receipts.filter(isRecord).map(cleanHistory) }
|
|
171
|
+
: {}),
|
|
172
|
+
...(typeof value.nextCursor === "number" ? { nextCursor: value.nextCursor } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function cleanError(error) {
|
|
176
|
+
return {
|
|
177
|
+
reason: typeof error.reason === "string" ? error.reason : "Delegation failed",
|
|
178
|
+
safeToRetry: error.safeToRetry === true,
|
|
179
|
+
sideEffects: Array.isArray(error.sideEffects) ? error.sideEffects.filter(isString) : [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function summarize(value) {
|
|
183
|
+
if (Array.isArray(value.children)) {
|
|
184
|
+
const counts = ["total", "running", "blocked", "completed", "failed"]
|
|
185
|
+
.filter((key) => typeof value[key] === "number")
|
|
186
|
+
.map((key) => `${key} ${String(value[key])}`)
|
|
187
|
+
.join(" · ");
|
|
188
|
+
const lines = value.children.filter(isRecord).map((child) => {
|
|
189
|
+
const agent = typeof child.agent === "string" ? ` (${child.agent})` : "";
|
|
190
|
+
const state = typeof child.runtimeState === "string" ? child.runtimeState : "unknown";
|
|
191
|
+
const status = typeof child.status === "string" ? ` / ${child.status}` : "";
|
|
192
|
+
const revision = typeof child.revision === "number" ? ` · rev ${child.revision}` : "";
|
|
193
|
+
const summary = typeof child.summary === "string" ? ` — ${child.summary}` : "";
|
|
194
|
+
return `- ${String(child.id)}${agent}: ${state}${status}${revision}${summary}`;
|
|
195
|
+
});
|
|
196
|
+
const head = value.children.length === 0 ? "No agents" : counts;
|
|
197
|
+
return lines.length === 0 ? head : `${head}\n${lines.join("\n")}`;
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(value.entries))
|
|
200
|
+
return `${value.entries.length} entries`;
|
|
201
|
+
if (Array.isArray(value.receipts))
|
|
202
|
+
return `${value.receipts.length} receipts`;
|
|
203
|
+
if (typeof value.id === "string" && typeof value.status === "string") {
|
|
204
|
+
const agent = typeof value.agent === "string" ? ` (${value.agent})` : "";
|
|
205
|
+
const revision = typeof value.revision === "number" ? ` · rev ${value.revision}` : "";
|
|
206
|
+
const progress = isRecord(value.progress) ? value.progress : undefined;
|
|
207
|
+
const runtime = progress !== undefined && typeof progress.runtimeState === "string"
|
|
208
|
+
? ` (${progress.runtimeState})`
|
|
209
|
+
: "";
|
|
210
|
+
const active = progress !== undefined &&
|
|
211
|
+
typeof progress.activeTool === "string" &&
|
|
212
|
+
progress.activeTool !== ""
|
|
213
|
+
? `: running ${progress.activeTool}`
|
|
214
|
+
: "";
|
|
215
|
+
const error = isRecord(value.error) && typeof value.error.reason === "string"
|
|
216
|
+
? `\nError: ${value.error.reason}`
|
|
217
|
+
: "";
|
|
218
|
+
const body = typeof value.resultText === "string" && value.resultText !== ""
|
|
219
|
+
? `\n${value.resultText}`
|
|
220
|
+
: value.hasOutput === true
|
|
221
|
+
? "\nThe output was not requested; read it with includeOutput."
|
|
222
|
+
: "";
|
|
223
|
+
return `${value.id}${agent} ${value.status}${runtime}${revision}${active}${error}${body}`;
|
|
224
|
+
}
|
|
225
|
+
return "ok";
|
|
226
|
+
}
|
|
227
|
+
function cleanChild(child) {
|
|
228
|
+
return {
|
|
229
|
+
id: typeof child.id === "string" ? child.id : "",
|
|
230
|
+
...(typeof child.agent === "string" ? { agent: child.agent } : {}),
|
|
231
|
+
runtimeState: typeof child.runtimeState === "string" ? child.runtimeState : "unknown",
|
|
232
|
+
revision: typeof child.revision === "number" ? child.revision : 0,
|
|
233
|
+
status: typeof child.status === "string" ? child.status : "unknown",
|
|
234
|
+
hasOutput: child.hasOutput === true,
|
|
235
|
+
...(typeof child.summary === "string" ? { summary: child.summary } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function cleanHistory(receipt) {
|
|
239
|
+
return {
|
|
240
|
+
id: typeof receipt.id === "string" ? receipt.id : "",
|
|
241
|
+
revision: typeof receipt.revision === "number" ? receipt.revision : 0,
|
|
242
|
+
status: typeof receipt.status === "string" ? receipt.status : "unknown",
|
|
243
|
+
task: typeof receipt.task === "string" ? receipt.task : "",
|
|
244
|
+
hasOutput: receipt.hasOutput === true,
|
|
245
|
+
createdAt: typeof receipt.createdAt === "number" ? receipt.createdAt : 0,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function failedDelegation(value) {
|
|
249
|
+
return value.status === "failed" || value.status === "uncertain" || value.status === "blocked";
|
|
250
|
+
}
|
|
251
|
+
function isString(value) {
|
|
252
|
+
return typeof value === "string";
|
|
253
|
+
}
|
|
254
|
+
function idSchema(name) {
|
|
255
|
+
return {
|
|
256
|
+
type: "object",
|
|
257
|
+
additionalProperties: false,
|
|
258
|
+
required: [name],
|
|
259
|
+
properties: { [name]: { type: "string", minLength: 1 } },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function requiredString(args, key) {
|
|
263
|
+
const value = isRecord(args) ? args[key] : undefined;
|
|
264
|
+
if (typeof value !== "string" || value === "")
|
|
265
|
+
throw new Error(`${key} is required`);
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
function optionalNumber(args, key) {
|
|
269
|
+
if (!isRecord(args) || args[key] === undefined)
|
|
270
|
+
return undefined;
|
|
271
|
+
const value = args[key];
|
|
272
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
273
|
+
throw new Error(`${key} must be an integer`);
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
function optionalPositive(args, key) {
|
|
278
|
+
const value = optionalNumber(args, key);
|
|
279
|
+
if (value === undefined)
|
|
280
|
+
return undefined;
|
|
281
|
+
if (value < 1)
|
|
282
|
+
throw new Error(`${key} must be a positive integer`);
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
function isRecord(value) {
|
|
286
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
287
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
7
|
+
import { controllerSocketPath, recordedControllerPid, stopPreviousController, } from "./controller.js";
|
|
8
|
+
import { CONTROLLER_PROTOCOL, controllerProtocol } from "./controller-ipc.js";
|
|
9
|
+
import { createControllerSocket } from "./controller-socket.js";
|
|
10
|
+
import { createSubagentMcpServer } from "./mcp-server.js";
|
|
11
|
+
const workspace = readWorkspace(process.argv.slice(2));
|
|
12
|
+
const controllerSocket = createControllerSocket({
|
|
13
|
+
locate: () => controllerSocketPath(getAgentDir(), workspace),
|
|
14
|
+
ready: controllerReady,
|
|
15
|
+
start: () => spawnController(workspace, getAgentDir()),
|
|
16
|
+
});
|
|
17
|
+
serveStdio(() => createSubagentMcpServer(controllerSocket, workspace), { legacy: "serve" });
|
|
18
|
+
function readWorkspace(argv) {
|
|
19
|
+
const index = argv.indexOf("--cwd");
|
|
20
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
21
|
+
if (value === undefined || !value.startsWith("/")) {
|
|
22
|
+
console.error("pi-subagents-mcp requires --cwd /absolute/workspace");
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function spawnController(cwd, agentDir) {
|
|
28
|
+
const main = fileURLToPath(new URL("./controller-main.js", import.meta.url));
|
|
29
|
+
const child = spawn(process.execPath, [main, cwd, agentDir], {
|
|
30
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
31
|
+
});
|
|
32
|
+
let stderr = "";
|
|
33
|
+
child.stderr?.setEncoding("utf8");
|
|
34
|
+
child.stderr?.on("data", (chunk) => {
|
|
35
|
+
stderr = tail(stderr, chunk);
|
|
36
|
+
});
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let stdout = "";
|
|
39
|
+
let settled = false;
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
fail(new Error(stderr.trim() || "Controller did not become ready"));
|
|
42
|
+
}, 30_000);
|
|
43
|
+
const succeed = (path) => {
|
|
44
|
+
if (settled)
|
|
45
|
+
return;
|
|
46
|
+
settled = true;
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
child.stdout?.off("data", onStdout);
|
|
49
|
+
resolve(path);
|
|
50
|
+
};
|
|
51
|
+
const fail = (error) => {
|
|
52
|
+
if (settled)
|
|
53
|
+
return;
|
|
54
|
+
settled = true;
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
child.kill("SIGTERM");
|
|
57
|
+
reject(error);
|
|
58
|
+
};
|
|
59
|
+
const onStdout = (chunk) => {
|
|
60
|
+
if (settled)
|
|
61
|
+
return;
|
|
62
|
+
stdout = tail(stdout, chunk);
|
|
63
|
+
const line = stdout.split("\n").find((item) => item.startsWith("ready "));
|
|
64
|
+
if (line !== undefined)
|
|
65
|
+
succeed(line.slice("ready ".length).trim());
|
|
66
|
+
};
|
|
67
|
+
child.stdout?.setEncoding("utf8");
|
|
68
|
+
child.stdout?.on("data", onStdout);
|
|
69
|
+
child.once("error", (error) => {
|
|
70
|
+
fail(error);
|
|
71
|
+
});
|
|
72
|
+
child.once("exit", (code) => {
|
|
73
|
+
fail(new Error(stderr.trim() || `Controller exited ${code ?? "unknown"}`));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function tail(current, chunk) {
|
|
78
|
+
const next = current + chunk;
|
|
79
|
+
return next.length > 4096 ? next.slice(-4096) : next;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A controller from an earlier build answers on the same socket with a call surface this build does
|
|
83
|
+
* not speak. It is replaced instead of being used, so an upgrade never keeps running old code.
|
|
84
|
+
*/
|
|
85
|
+
async function controllerReady(socketPath) {
|
|
86
|
+
if (!(await socketReady(socketPath)))
|
|
87
|
+
return false;
|
|
88
|
+
const agentDir = getAgentDir();
|
|
89
|
+
const owner = recordedControllerPid(agentDir, workspace);
|
|
90
|
+
const probe = await controllerProtocol(socketPath);
|
|
91
|
+
if (probe.state === "ready" && probe.protocol === CONTROLLER_PROTOCOL)
|
|
92
|
+
return true;
|
|
93
|
+
// A timeout or a refused connection is not proof of an old build. Stopping here killed a
|
|
94
|
+
// controller that had already been replaced, or one that was merely slow.
|
|
95
|
+
if (probe.state !== "foreign")
|
|
96
|
+
return false;
|
|
97
|
+
await stopPreviousController(agentDir, workspace, owner);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
function socketReady(socketPath) {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const socket = createConnection(socketPath);
|
|
103
|
+
const finish = (ready) => {
|
|
104
|
+
socket.removeAllListeners();
|
|
105
|
+
socket.destroy();
|
|
106
|
+
resolve(ready);
|
|
107
|
+
};
|
|
108
|
+
socket.once("connect", () => finish(true));
|
|
109
|
+
socket.once("error", () => finish(false));
|
|
110
|
+
});
|
|
111
|
+
}
|
package/dist/process-lock.d.ts
CHANGED
|
@@ -7,5 +7,5 @@ export interface ProcessLock {
|
|
|
7
7
|
release(): void;
|
|
8
8
|
}
|
|
9
9
|
export declare function processLockPath(stateFile: string, parentSessionId: string, subagentId: string): string | undefined;
|
|
10
|
-
export declare function holdProcessLock(lockPath: string): ProcessLock;
|
|
10
|
+
export declare function holdProcessLock(lockPath: string, waitMs?: number): ProcessLock;
|
|
11
11
|
export declare function processLockHeld(lockPath: string): boolean;
|
package/dist/process-lock.js
CHANGED
|
@@ -7,11 +7,13 @@ export function processLockPath(stateFile, parentSessionId, subagentId) {
|
|
|
7
7
|
return undefined;
|
|
8
8
|
return join(dirname(stateFile), "locks", parentSessionId, `${subagentId}.lock`);
|
|
9
9
|
}
|
|
10
|
-
export function holdProcessLock(lockPath) {
|
|
10
|
+
export function holdProcessLock(lockPath, waitMs = 2_000) {
|
|
11
|
+
if (!Number.isInteger(waitMs) || waitMs < 0)
|
|
12
|
+
throw new Error("Invalid process lock wait");
|
|
11
13
|
mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
12
14
|
const db = new DatabaseSync(lockPath);
|
|
13
15
|
chmodSync(lockPath, 0o600);
|
|
14
|
-
db.exec(
|
|
16
|
+
db.exec(`PRAGMA busy_timeout = ${waitMs}`);
|
|
15
17
|
db.exec("BEGIN EXCLUSIVE");
|
|
16
18
|
let released = false;
|
|
17
19
|
return {
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { type Static, Type } from "typebox";
|
|
2
|
-
|
|
3
|
-
export declare const DEFAULT_MAX_PENDING_REQUESTS = 128;
|
|
2
|
+
/** How many queued child events a parent that stopped reading keeps before the oldest are dropped. */
|
|
4
3
|
export declare const DEFAULT_MAX_BUFFERED_EVENTS = 256;
|
|
5
4
|
/**
|
|
6
5
|
* Every operation one end of the bridge may ask of the other. The parent drives the child with the
|
|
7
6
|
* input and state operations; the child returns reports and its Task result as requests, so a
|
|
8
7
|
* report the parent refuses (a second Task result) is refused to the child that sent it.
|
|
9
8
|
*/
|
|
10
|
-
export declare const BRIDGE_OPERATIONS: readonly ["prompt", "steer", "
|
|
9
|
+
export declare const BRIDGE_OPERATIONS: readonly ["prompt", "steer", "abort", "get_state", "get_entries", "session_facts", "shutdown", "contact_parent", "task_result"];
|
|
11
10
|
export type BridgeOperation = (typeof BRIDGE_OPERATIONS)[number];
|
|
12
11
|
/** Child-branch event: the child left the bound session, quit, or was interrupted. */
|
|
13
12
|
export declare const CHILD_LIFECYCLE_EVENT: "child_lifecycle";
|
|
@@ -28,7 +27,6 @@ export declare const HelloFrameSchema: Type.TObject<{
|
|
|
28
27
|
subagentId: Type.TString;
|
|
29
28
|
runtimeIdentity: Type.TString;
|
|
30
29
|
endpoint: Type.TString;
|
|
31
|
-
token: Type.TString;
|
|
32
30
|
version: Type.TLiteral<1>;
|
|
33
31
|
type: Type.TLiteral<"hello">;
|
|
34
32
|
role: Type.TOptional<Type.TLiteral<"child">>;
|
|
@@ -41,7 +39,7 @@ export declare const RequestFrameSchema: Type.TObject<{
|
|
|
41
39
|
version: Type.TLiteral<1>;
|
|
42
40
|
type: Type.TLiteral<"request">;
|
|
43
41
|
id: Type.TString;
|
|
44
|
-
operation: Type.TUnion<Type.TLiteral<"contact_parent" | "
|
|
42
|
+
operation: Type.TUnion<Type.TLiteral<"contact_parent" | "prompt" | "steer" | "abort" | "get_state" | "get_entries" | "session_facts" | "shutdown" | "task_result">[]>;
|
|
45
43
|
payload: Type.TOptional<Type.TUnknown>;
|
|
46
44
|
}>;
|
|
47
45
|
export declare const BridgeErrorSchema: Type.TObject<{
|
|
@@ -80,6 +78,8 @@ export declare const ContactReportPayloadSchema: Type.TObject<{
|
|
|
80
78
|
reason: Type.TOptional<Type.TUnion<[Type.TLiteral<"success">, Type.TLiteral<"blocked">]>>;
|
|
81
79
|
message: Type.TString;
|
|
82
80
|
sessionId: Type.TOptional<Type.TString>;
|
|
81
|
+
/** The compaction this report belongs to, so it settles that operation and no other. */
|
|
82
|
+
controlId: Type.TOptional<Type.TString>;
|
|
83
83
|
}>;
|
|
84
84
|
export declare const ChildLifecyclePayloadSchema: Type.TObject<{
|
|
85
85
|
type: Type.TLiteral<"child_lifecycle">;
|
package/dist/protocol.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { PROTOCOL_VERSION } from "./domain.js";
|
|
4
|
-
|
|
5
|
-
export const DEFAULT_MAX_PENDING_REQUESTS = 128;
|
|
4
|
+
/** How many queued child events a parent that stopped reading keeps before the oldest are dropped. */
|
|
6
5
|
export const DEFAULT_MAX_BUFFERED_EVENTS = 256;
|
|
7
6
|
/**
|
|
8
7
|
* Every operation one end of the bridge may ask of the other. The parent drives the child with the
|
|
@@ -12,7 +11,6 @@ export const DEFAULT_MAX_BUFFERED_EVENTS = 256;
|
|
|
12
11
|
export const BRIDGE_OPERATIONS = [
|
|
13
12
|
"prompt",
|
|
14
13
|
"steer",
|
|
15
|
-
"follow_up",
|
|
16
14
|
"abort",
|
|
17
15
|
"get_state",
|
|
18
16
|
"get_entries",
|
|
@@ -45,7 +43,6 @@ const identityProperties = {
|
|
|
45
43
|
subagentId: nonEmptyString,
|
|
46
44
|
runtimeIdentity: nonEmptyString,
|
|
47
45
|
endpoint: nonEmptyString,
|
|
48
|
-
token: nonEmptyString,
|
|
49
46
|
};
|
|
50
47
|
export const HelloFrameSchema = Type.Object({
|
|
51
48
|
version: Type.Literal(PROTOCOL_VERSION),
|
|
@@ -88,6 +85,8 @@ export const ContactReportPayloadSchema = Type.Object({
|
|
|
88
85
|
reason: Type.Optional(Type.Union([Type.Literal("success"), Type.Literal("blocked")])),
|
|
89
86
|
message: nonEmptyString,
|
|
90
87
|
sessionId: Type.Optional(nonEmptyString),
|
|
88
|
+
/** The compaction this report belongs to, so it settles that operation and no other. */
|
|
89
|
+
controlId: Type.Optional(nonEmptyString),
|
|
91
90
|
}, { additionalProperties: false });
|
|
92
91
|
export const ChildLifecyclePayloadSchema = Type.Object({
|
|
93
92
|
type: Type.Literal("child_lifecycle"),
|