@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,217 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { isRecord } from "@hheei/pi-ext-core";
|
|
6
|
+
import { CONTROLLER_PROTOCOL, closeControllerIpc, serveControllerIpc } from "./controller-ipc.js";
|
|
7
|
+
import { beginControllerRole, endControllerRole } from "./controller-role.js";
|
|
8
|
+
import piSubagentsExtension, { skillCatalogFromLoaded } from "./extension.js";
|
|
9
|
+
import { holdProcessLock } from "./process-lock.js";
|
|
10
|
+
export class StandaloneControllerError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
parentSessionId;
|
|
13
|
+
constructor(code, message, parentSessionId) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "StandaloneControllerError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.parentSessionId = parentSessionId;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function controllerSocketPath(agentDir, workspace) {
|
|
21
|
+
return join(controllerDirectory(resolve(agentDir)), `${workspaceKey(resolve(workspace))}.sock`);
|
|
22
|
+
}
|
|
23
|
+
export async function startStandaloneController(options) {
|
|
24
|
+
const workspace = resolve(options.workspace);
|
|
25
|
+
const agentDir = resolve(options.agentDir);
|
|
26
|
+
const directory = controllerDirectory(agentDir);
|
|
27
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
28
|
+
const id = workspaceKey(workspace);
|
|
29
|
+
const lock = lockController(join(directory, `${id}.lock`));
|
|
30
|
+
let role;
|
|
31
|
+
let runtime;
|
|
32
|
+
try {
|
|
33
|
+
const bindingPath = join(directory, `${id}.json`);
|
|
34
|
+
const existing = readBinding(bindingPath);
|
|
35
|
+
if (existing !== undefined && existing.workspace !== workspace) {
|
|
36
|
+
throw new Error("Standalone controller binding does not match this workspace");
|
|
37
|
+
}
|
|
38
|
+
const sessionDir = join(agentDir, "subagents", "sessions");
|
|
39
|
+
const sessionManager = existing === undefined
|
|
40
|
+
? SessionManager.create(workspace, sessionDir)
|
|
41
|
+
: existsSync(existing.sessionFile)
|
|
42
|
+
? SessionManager.open(existing.sessionFile)
|
|
43
|
+
: // Pi writes the session file only after a user or assistant message.
|
|
44
|
+
SessionManager.create(workspace, sessionDir, { id: existing.parentSessionId });
|
|
45
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
46
|
+
const parentSessionId = sessionManager.getSessionId();
|
|
47
|
+
if (sessionFile === undefined)
|
|
48
|
+
throw new Error("Standalone controller session has no file");
|
|
49
|
+
if (existing !== undefined && existing.parentSessionId !== parentSessionId) {
|
|
50
|
+
throw new Error("Standalone controller restored a different parent session");
|
|
51
|
+
}
|
|
52
|
+
writeBinding(bindingPath, {
|
|
53
|
+
workspace,
|
|
54
|
+
parentSessionId,
|
|
55
|
+
sessionFile,
|
|
56
|
+
pid: process.pid,
|
|
57
|
+
protocol: CONTROLLER_PROTOCOL,
|
|
58
|
+
});
|
|
59
|
+
runtime = await createAgentSessionRuntime(async ({ cwd, sessionManager: bound }) => {
|
|
60
|
+
const services = await createAgentSessionServices({
|
|
61
|
+
cwd,
|
|
62
|
+
agentDir,
|
|
63
|
+
resourceLoaderOptions: {
|
|
64
|
+
noExtensions: true,
|
|
65
|
+
extensionFactories: [piSubagentsExtension],
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
...(await createAgentSessionFromServices({ services, sessionManager: bound })),
|
|
70
|
+
services,
|
|
71
|
+
diagnostics: services.diagnostics,
|
|
72
|
+
};
|
|
73
|
+
}, { cwd: workspace, agentDir, sessionManager });
|
|
74
|
+
role = beginControllerRole(skillCatalogFromLoaded(runtime.services.resourceLoader.getSkills().skills));
|
|
75
|
+
await runtime.session.bindExtensions({ mode: "print" });
|
|
76
|
+
if (!role.applied)
|
|
77
|
+
throw new Error("Standalone controller extension did not take ownership");
|
|
78
|
+
if (role.modelId === undefined || role.credential !== "present") {
|
|
79
|
+
throw new StandaloneControllerError("model_unavailable", "Standalone controller has no usable Pi model or credential", parentSessionId);
|
|
80
|
+
}
|
|
81
|
+
const socketPath = join(directory, `${id}.sock`);
|
|
82
|
+
const manager = await role.manager;
|
|
83
|
+
const ipc = await serveControllerIpc({ socketPath, parentSessionId, manager });
|
|
84
|
+
const started = { runtime, role, lock, ipc };
|
|
85
|
+
role = undefined;
|
|
86
|
+
runtime = undefined;
|
|
87
|
+
// Both signals and a second shutdown call reach this: dispose once, and release the role
|
|
88
|
+
// and lock even when closing the socket or the runtime throws.
|
|
89
|
+
let disposal;
|
|
90
|
+
return {
|
|
91
|
+
workspace,
|
|
92
|
+
parentSessionId,
|
|
93
|
+
sessionFile,
|
|
94
|
+
socketPath,
|
|
95
|
+
dispose() {
|
|
96
|
+
disposal ??= (async () => {
|
|
97
|
+
try {
|
|
98
|
+
await closeControllerIpc(started.ipc);
|
|
99
|
+
await started.runtime.dispose();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
endControllerRole(started.role);
|
|
103
|
+
started.lock.release();
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
return disposal;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (runtime !== undefined)
|
|
112
|
+
await runtime.dispose();
|
|
113
|
+
if (role !== undefined)
|
|
114
|
+
endControllerRole(role);
|
|
115
|
+
lock.release();
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function controllerDirectory(agentDir) {
|
|
120
|
+
return join(agentDir, "subagents", "controllers");
|
|
121
|
+
}
|
|
122
|
+
function workspaceKey(workspace) {
|
|
123
|
+
return createHash("sha256").update(workspace).digest("hex").slice(0, 32);
|
|
124
|
+
}
|
|
125
|
+
function lockController(lockPath) {
|
|
126
|
+
try {
|
|
127
|
+
return holdProcessLock(lockPath, 0);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (isLockBusy(error)) {
|
|
131
|
+
throw new StandaloneControllerError("busy", "Standalone controller is already running for this workspace");
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function isLockBusy(error) {
|
|
137
|
+
return typeof error === "object" && error !== null && "errcode" in error && error.errcode === 5;
|
|
138
|
+
}
|
|
139
|
+
function readBinding(path) {
|
|
140
|
+
let text;
|
|
141
|
+
try {
|
|
142
|
+
text = readFileSync(path, "utf8");
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (isRecord(error) && error.code === "ENOENT")
|
|
146
|
+
return undefined;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
let value;
|
|
150
|
+
try {
|
|
151
|
+
value = JSON.parse(text);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
throw new Error(`Invalid standalone controller binding: ${path}`, { cause: error });
|
|
155
|
+
}
|
|
156
|
+
if (!isRecord(value) ||
|
|
157
|
+
typeof value.workspace !== "string" ||
|
|
158
|
+
typeof value.parentSessionId !== "string" ||
|
|
159
|
+
typeof value.sessionFile !== "string") {
|
|
160
|
+
throw new Error(`Invalid standalone controller binding: ${path}`);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
workspace: value.workspace,
|
|
164
|
+
parentSessionId: value.parentSessionId,
|
|
165
|
+
sessionFile: value.sessionFile,
|
|
166
|
+
...(typeof value.pid === "number" ? { pid: value.pid } : {}),
|
|
167
|
+
...(typeof value.protocol === "number" ? { protocol: value.protocol } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* A controller left behind by an earlier build keeps this workspace's socket and lock. Its calls are
|
|
172
|
+
* not this build's to make, so it is stopped here and the caller starts its own.
|
|
173
|
+
*/
|
|
174
|
+
export async function stopPreviousController(agentDir, workspace, expectedPid) {
|
|
175
|
+
const resolved = resolve(workspace);
|
|
176
|
+
const id = workspaceKey(resolved);
|
|
177
|
+
const path = join(controllerDirectory(resolve(agentDir)), `${id}.json`);
|
|
178
|
+
const binding = readBinding(path);
|
|
179
|
+
const pid = binding?.pid;
|
|
180
|
+
// The process that failed the probe may already have been replaced. Killing whatever pid is here
|
|
181
|
+
// now would stop the replacement.
|
|
182
|
+
if (expectedPid !== undefined && pid !== expectedPid)
|
|
183
|
+
return;
|
|
184
|
+
if (pid === undefined) {
|
|
185
|
+
throw new Error(`A previous pi-subagents controller owns ${resolved} and does not record its process; quit and reopen the client that started it`);
|
|
186
|
+
}
|
|
187
|
+
if (readBinding(path)?.pid !== pid)
|
|
188
|
+
return;
|
|
189
|
+
process.kill(pid, "SIGTERM");
|
|
190
|
+
const deadline = Date.now() + 10_000;
|
|
191
|
+
while (Date.now() < deadline) {
|
|
192
|
+
if (!isRunning(pid))
|
|
193
|
+
return;
|
|
194
|
+
await new Promise((done) => setTimeout(done, 50));
|
|
195
|
+
}
|
|
196
|
+
throw new Error(`A previous pi-subagents controller for ${resolved} did not stop`);
|
|
197
|
+
}
|
|
198
|
+
/** Pid recorded for this workspace, when the binding names one. */
|
|
199
|
+
export function recordedControllerPid(agentDir, workspace) {
|
|
200
|
+
const resolved = resolve(workspace);
|
|
201
|
+
const id = workspaceKey(resolved);
|
|
202
|
+
return readBinding(join(controllerDirectory(resolve(agentDir)), `${id}.json`))?.pid;
|
|
203
|
+
}
|
|
204
|
+
function isRunning(pid) {
|
|
205
|
+
try {
|
|
206
|
+
process.kill(pid, 0);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
return !(isRecord(error) && error.code === "ESRCH");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function writeBinding(path, binding) {
|
|
214
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
215
|
+
writeFileSync(temporary, `${JSON.stringify(binding)}\n`, { mode: 0o600 });
|
|
216
|
+
renameSync(temporary, path);
|
|
217
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { PublicSubagent, SubagentState } from "./domain.js";
|
|
2
|
+
import { type Receipt, type ReceiptStatus } from "./receipts.js";
|
|
3
|
+
/** What the agent's runtime is doing now, kept apart from the selected delegation's status. */
|
|
4
|
+
export interface DelegationProgress {
|
|
5
|
+
readonly runtimeState: SubagentState;
|
|
6
|
+
readonly summary?: string;
|
|
7
|
+
readonly activeTool?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Why a delegation did not produce a result, with what it may already have done. */
|
|
10
|
+
export interface DelegationError {
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
readonly safeToRetry: boolean;
|
|
13
|
+
readonly sideEffects: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Public identity of one delegation: the agent session id plus its monotonic revision. Internal
|
|
17
|
+
* database keys never appear here, so both consumers speak the same vocabulary.
|
|
18
|
+
*/
|
|
19
|
+
export interface DelegationView {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly revision: number;
|
|
22
|
+
readonly status: ReceiptStatus;
|
|
23
|
+
readonly task: string;
|
|
24
|
+
readonly hasOutput: boolean;
|
|
25
|
+
readonly agent?: string;
|
|
26
|
+
readonly cwd?: string;
|
|
27
|
+
readonly resultText?: string;
|
|
28
|
+
readonly progress?: DelegationProgress;
|
|
29
|
+
readonly error?: DelegationError;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Projects one stored delegation. A result only exists for a completed or blocked delegation: the
|
|
33
|
+
* text of a failed or uncertain one is the reason it failed, not child output.
|
|
34
|
+
*/
|
|
35
|
+
export declare function delegationView(receipt: Receipt, child?: PublicSubagent, options?: {
|
|
36
|
+
readonly includeOutput?: boolean;
|
|
37
|
+
}): DelegationView;
|
|
38
|
+
/**
|
|
39
|
+
* The same projection with the result body when the caller asks for it. An explicit read still
|
|
40
|
+
* obeys the shared output budget: the model gets the head, the tail, and the path to everything it
|
|
41
|
+
* did not see, never an unbounded body.
|
|
42
|
+
*/
|
|
43
|
+
export declare function agentView(receipt: Receipt, child?: PublicSubagent, options?: {
|
|
44
|
+
readonly includeOutput?: boolean;
|
|
45
|
+
}): Promise<DelegationView>;
|
|
46
|
+
/** One agent's row in a dashboard: its runtime now, and the status of its latest delegation. */
|
|
47
|
+
export interface AgentSummary {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly agent?: string;
|
|
50
|
+
readonly runtimeState: SubagentState;
|
|
51
|
+
readonly revision: number;
|
|
52
|
+
readonly status: ReceiptStatus;
|
|
53
|
+
readonly hasOutput: boolean;
|
|
54
|
+
readonly summary?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface DashboardView {
|
|
57
|
+
readonly total: number;
|
|
58
|
+
readonly running: number;
|
|
59
|
+
readonly blocked: number;
|
|
60
|
+
readonly completed: number;
|
|
61
|
+
readonly failed: number;
|
|
62
|
+
readonly children: readonly AgentSummary[];
|
|
63
|
+
}
|
|
64
|
+
/** One stored delegation as history shows it: identity, status and task, never the body. */
|
|
65
|
+
export interface DelegationHistoryEntry {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly revision: number;
|
|
68
|
+
readonly status: ReceiptStatus;
|
|
69
|
+
readonly task: string;
|
|
70
|
+
readonly hasOutput: boolean;
|
|
71
|
+
readonly createdAt: number;
|
|
72
|
+
}
|
|
73
|
+
export declare function historyEntry(receipt: Receipt): DelegationHistoryEntry | undefined;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { truncateWithSpill } from "@hheei/pi-ext-core";
|
|
2
|
+
import { receiptCarriesOutput } from "./receipts.js";
|
|
3
|
+
/**
|
|
4
|
+
* Projects one stored delegation. A result only exists for a completed or blocked delegation: the
|
|
5
|
+
* text of a failed or uncertain one is the reason it failed, not child output.
|
|
6
|
+
*/
|
|
7
|
+
export function delegationView(receipt, child, options) {
|
|
8
|
+
if (receipt.childId === undefined || receipt.revision === undefined) {
|
|
9
|
+
throw new Error(`Delegation ${receipt.delegationId} has no public identity`);
|
|
10
|
+
}
|
|
11
|
+
const text = receipt.resultText;
|
|
12
|
+
const hasOutput = receiptCarriesOutput(receipt.status, text);
|
|
13
|
+
const failed = text !== undefined && !hasOutput;
|
|
14
|
+
return {
|
|
15
|
+
id: receipt.childId,
|
|
16
|
+
revision: receipt.revision,
|
|
17
|
+
status: receipt.status,
|
|
18
|
+
task: receipt.task,
|
|
19
|
+
hasOutput,
|
|
20
|
+
...(options?.includeOutput === true && text !== undefined && hasOutput
|
|
21
|
+
? { resultText: text }
|
|
22
|
+
: {}),
|
|
23
|
+
...(failed && text !== undefined
|
|
24
|
+
? {
|
|
25
|
+
error: {
|
|
26
|
+
reason: text,
|
|
27
|
+
safeToRetry: receipt.safeToRetry ?? false,
|
|
28
|
+
sideEffects: receipt.sideEffects ?? [],
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
: {}),
|
|
32
|
+
...(child === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: {
|
|
35
|
+
agent: child.agent,
|
|
36
|
+
cwd: child.cwd,
|
|
37
|
+
progress: {
|
|
38
|
+
runtimeState: child.state,
|
|
39
|
+
...(child.summary === undefined ? {} : { summary: child.summary }),
|
|
40
|
+
...(child.activeTool === undefined ? {} : { activeTool: child.activeTool }),
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The same projection with the result body when the caller asks for it. An explicit read still
|
|
47
|
+
* obeys the shared output budget: the model gets the head, the tail, and the path to everything it
|
|
48
|
+
* did not see, never an unbounded body.
|
|
49
|
+
*/
|
|
50
|
+
export async function agentView(receipt, child, options) {
|
|
51
|
+
const view = delegationView(receipt, child);
|
|
52
|
+
const text = receipt.resultText;
|
|
53
|
+
if (options?.includeOutput !== true || !view.hasOutput || text === undefined)
|
|
54
|
+
return view;
|
|
55
|
+
const bounded = await truncateWithSpill(text, "subagent-output");
|
|
56
|
+
return { ...view, resultText: bounded.text };
|
|
57
|
+
}
|
|
58
|
+
export function historyEntry(receipt) {
|
|
59
|
+
const id = receipt.childId;
|
|
60
|
+
const revision = receipt.revision;
|
|
61
|
+
if (id === undefined || revision === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
revision,
|
|
66
|
+
status: receipt.status,
|
|
67
|
+
task: receipt.task,
|
|
68
|
+
hasOutput: receiptCarriesOutput(receipt.status, receipt.resultText),
|
|
69
|
+
createdAt: receipt.createdAt,
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/domain.d.ts
CHANGED
|
@@ -8,16 +8,6 @@ export declare function normalizeSubagentState(value: unknown): SubagentState |
|
|
|
8
8
|
* is created, because it decides who holds the process for the rest of the child's life.
|
|
9
9
|
*/
|
|
10
10
|
export type Presentation = "panel" | "background";
|
|
11
|
-
/**
|
|
12
|
-
* How `agent_send` hands its message to a child.
|
|
13
|
-
*
|
|
14
|
-
* `steer` enters the current assistant turn once its tool calls are in, before the next model
|
|
15
|
-
* request, so it redirects work already in flight. `follow_up` waits until the child's run has no
|
|
16
|
-
* tool calls or steering left, then continues as a new turn in the same run. There is no "auto":
|
|
17
|
-
* the two queues already are the two situations, so the caller picks instead of guessing from the
|
|
18
|
-
* child's state.
|
|
19
|
-
*/
|
|
20
|
-
export type SendMode = "steer" | "follow_up";
|
|
21
11
|
export type VisualSubagentState = SubagentState;
|
|
22
12
|
export declare function toVisualSubagentState(state: SubagentState, interrupted?: string): VisualSubagentState;
|
|
23
13
|
export declare const VISUAL_SUBAGENT_GLYPH: Record<VisualSubagentState, string>;
|
|
@@ -33,12 +23,10 @@ export interface ChildIdentity {
|
|
|
33
23
|
readonly subagentId: string;
|
|
34
24
|
readonly runtimeIdentity: string;
|
|
35
25
|
readonly endpoint: string;
|
|
36
|
-
readonly token: string;
|
|
37
26
|
}
|
|
38
27
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* time and never written to a registry snapshot.
|
|
28
|
+
* What a launch spec may contain and what may be persisted: the child's identity minus nothing,
|
|
29
|
+
* because the mailbox is authenticated by the process lock its writer holds, not by a token.
|
|
42
30
|
*/
|
|
43
31
|
export interface ChildBridgeEnvironment {
|
|
44
32
|
readonly parentSessionId: string;
|
|
@@ -211,11 +199,20 @@ export interface SpawnSubagentInput {
|
|
|
211
199
|
* Where to run this child. `auto` (the default) means a panel whenever this parent has a
|
|
212
200
|
* presentation host, and the background otherwise; the Task tool pins `background` because a
|
|
213
201
|
* Task is settled by its parent and never handed to a human. This is not a model-facing
|
|
214
|
-
* parameter: `
|
|
202
|
+
* parameter: `agents_delegate` always spawns with `auto`.
|
|
215
203
|
*/
|
|
216
204
|
readonly presentation?: Presentation | "auto";
|
|
217
|
-
|
|
218
|
-
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Input for a single delegated turn. An existing child ID always takes precedence over the agent
|
|
208
|
+
* definition: the child already owns its launch configuration, model, and session history.
|
|
209
|
+
*/
|
|
210
|
+
export interface DelegateSubagentInput {
|
|
211
|
+
readonly task: string;
|
|
212
|
+
readonly id?: string;
|
|
213
|
+
readonly agent?: string;
|
|
214
|
+
/** Only used when creating a child; ignored when id is supplied. */
|
|
215
|
+
readonly cwd?: string;
|
|
219
216
|
}
|
|
220
217
|
/** True when a manager call returned a structured failure instead of a value. */
|
|
221
218
|
export declare function isOperationError(value: unknown): value is OperationError;
|
|
@@ -228,7 +225,6 @@ export declare const BRIDGE_ENVIRONMENT_KEYS: {
|
|
|
228
225
|
readonly childId: "PI_SUBAGENTS_CHILD_ID";
|
|
229
226
|
readonly runtimeId: "PI_SUBAGENTS_RUNTIME_ID";
|
|
230
227
|
readonly endpoint: "PI_SUBAGENTS_ENDPOINT";
|
|
231
|
-
readonly token: "PI_SUBAGENTS_TOKEN";
|
|
232
228
|
};
|
|
233
229
|
/** Non-secret agent label for the child TUI identity line. Not part of handshake. */
|
|
234
230
|
export declare const CHILD_AGENT_ENV_KEY: "PI_SUBAGENTS_AGENT";
|
|
@@ -236,6 +232,12 @@ export declare const CHILD_AGENT_ENV_KEY: "PI_SUBAGENTS_AGENT";
|
|
|
236
232
|
export declare const CHILD_TITLE_ENV_KEY: "PI_SUBAGENTS_TITLE";
|
|
237
233
|
/** Bound Pi session id for the child branch. Not part of the runner handshake. */
|
|
238
234
|
export declare const CHILD_SESSION_ENV_KEY: "PI_SUBAGENTS_SESSION_ID";
|
|
235
|
+
/**
|
|
236
|
+
* Set on the standalone controller process. Its children must not discover Pi settings extensions:
|
|
237
|
+
* another installed copy of this package registers the same bridge tool as the explicit -e entry,
|
|
238
|
+
* and Pi aborts the child before it connects.
|
|
239
|
+
*/
|
|
240
|
+
export declare const STANDALONE_CONTROLLER_ENV_KEY: "PI_SUBAGENTS_STANDALONE_CONTROLLER";
|
|
239
241
|
/** Pi session ids reach argv and file names, so they must stay in Pi's accepted alphabet. */
|
|
240
242
|
export declare function isSessionId(value: unknown): value is string;
|
|
241
243
|
export declare function isThinkingLevel(value: unknown): value is ThinkingLevel;
|
package/dist/domain.js
CHANGED
|
@@ -60,7 +60,6 @@ export const BRIDGE_ENVIRONMENT_KEYS = {
|
|
|
60
60
|
childId: "PI_SUBAGENTS_CHILD_ID",
|
|
61
61
|
runtimeId: "PI_SUBAGENTS_RUNTIME_ID",
|
|
62
62
|
endpoint: "PI_SUBAGENTS_ENDPOINT",
|
|
63
|
-
token: "PI_SUBAGENTS_TOKEN",
|
|
64
63
|
};
|
|
65
64
|
/** Non-secret agent label for the child TUI identity line. Not part of handshake. */
|
|
66
65
|
export const CHILD_AGENT_ENV_KEY = "PI_SUBAGENTS_AGENT";
|
|
@@ -68,6 +67,12 @@ export const CHILD_AGENT_ENV_KEY = "PI_SUBAGENTS_AGENT";
|
|
|
68
67
|
export const CHILD_TITLE_ENV_KEY = "PI_SUBAGENTS_TITLE";
|
|
69
68
|
/** Bound Pi session id for the child branch. Not part of the runner handshake. */
|
|
70
69
|
export const CHILD_SESSION_ENV_KEY = "PI_SUBAGENTS_SESSION_ID";
|
|
70
|
+
/**
|
|
71
|
+
* Set on the standalone controller process. Its children must not discover Pi settings extensions:
|
|
72
|
+
* another installed copy of this package registers the same bridge tool as the explicit -e entry,
|
|
73
|
+
* and Pi aborts the child before it connects.
|
|
74
|
+
*/
|
|
75
|
+
export const STANDALONE_CONTROLLER_ENV_KEY = "PI_SUBAGENTS_STANDALONE_CONTROLLER";
|
|
71
76
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
72
77
|
/** Pi session ids reach argv and file names, so they must stay in Pi's accepted alphabet. */
|
|
73
78
|
export function isSessionId(value) {
|
package/dist/extension.js
CHANGED
|
@@ -2,14 +2,16 @@ import { createRenderThrottle, DEFAULT_MAX_BYTES, errorMessage, fitsOutputBudget
|
|
|
2
2
|
import { discoverAgents } from "./agent-resolver.js";
|
|
3
3
|
import { registerChildBridge } from "./child-bridge.js";
|
|
4
4
|
import { registerParentCommands } from "./commands.js";
|
|
5
|
+
import { activeControllerRole } from "./controller-role.js";
|
|
5
6
|
import { BRIDGE_ENVIRONMENT_KEYS, isThinkingLevel } from "./domain.js";
|
|
6
7
|
import { createCmuxHostAdapter, createHerdrHostAdapter, listCmuxAttachments, listHerdrAttachments, rebindCmuxSurface, rebindHerdrTab, selectHostAdapter, systemHostCommandRunner, } from "./host-adapter.js";
|
|
7
8
|
import { appendSubagentInfo, displaySubagentText, registerSubagentInfo } from "./info.js";
|
|
8
9
|
import { createMailboxTransport } from "./mailbox.js";
|
|
9
10
|
import { SubagentManager } from "./manager.js";
|
|
10
11
|
import { transformSubagentsMarkdown } from "./markdown.js";
|
|
12
|
+
import { sweepUncertainDispatch } from "./receipts.js";
|
|
11
13
|
import { createSubagentRegistry } from "./registry.js";
|
|
12
|
-
import {
|
|
14
|
+
import { launchChild, openChildPanel, prepareRuntimeDirectory } from "./runtime.js";
|
|
13
15
|
import { persistSubagentIntent, resolveSubagentLaunch } from "./session-bootstrap.js";
|
|
14
16
|
import { createSubagentStatus } from "./status.js";
|
|
15
17
|
import { registerInteractiveToolActivation } from "./tool-activation.js";
|
|
@@ -39,7 +41,6 @@ function childIdentityFromEnv(env) {
|
|
|
39
41
|
subagentId: requiredBridgeValue(env, BRIDGE_ENVIRONMENT_KEYS.childId),
|
|
40
42
|
runtimeIdentity: requiredBridgeValue(env, BRIDGE_ENVIRONMENT_KEYS.runtimeId),
|
|
41
43
|
endpoint: requiredBridgeValue(env, BRIDGE_ENVIRONMENT_KEYS.endpoint),
|
|
42
|
-
token: requiredBridgeValue(env, BRIDGE_ENVIRONMENT_KEYS.token),
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
/**
|
|
@@ -139,26 +140,43 @@ export default function piSubagentsExtension(pi) {
|
|
|
139
140
|
const context = runtime.extension;
|
|
140
141
|
const parentSessionId = context.sessionManager.getSessionId();
|
|
141
142
|
const registry = createSubagentRegistry({ parentSessionId });
|
|
142
|
-
|
|
143
|
+
sweepUncertainDispatch(registry.path, parentSessionId);
|
|
143
144
|
// Presentation is chosen once per parent session: herdr first, then cmux, and a parent with
|
|
144
145
|
// neither runs every child in the background. Successful selection stays silent; only actual
|
|
145
146
|
// failures and fallback diagnostics are reported by the launch path.
|
|
146
147
|
const ownerId = parentSessionId;
|
|
147
148
|
const ownsAttachment = (identity) => identity.createdBy === ownerId;
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
149
|
+
const controller = activeControllerRole();
|
|
150
|
+
const hostSelection = controller
|
|
151
|
+
? {
|
|
152
|
+
available: false,
|
|
153
|
+
selectedHost: null,
|
|
154
|
+
reason: "standalone controller keeps children in the background",
|
|
155
|
+
}
|
|
156
|
+
: await selectHostAdapter({
|
|
157
|
+
adapters: {
|
|
158
|
+
herdr: createHerdrHostAdapter({ ownerId, ownsAttachment }),
|
|
159
|
+
cmux: createCmuxHostAdapter({ ownerId, ownsAttachment }),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const channel = controller ??
|
|
163
|
+
createParentChannel(pi, {
|
|
164
|
+
isIdle: () => context.isIdle(),
|
|
165
|
+
ui: context.ui,
|
|
166
|
+
delivery: getBackgroundDelivery(pi),
|
|
167
|
+
signal: runtime.signal,
|
|
168
|
+
});
|
|
161
169
|
runtime.resources.add("parent-channel", () => channel.dispose());
|
|
170
|
+
if (controller !== undefined) {
|
|
171
|
+
controller.applied = true;
|
|
172
|
+
const model = context.model;
|
|
173
|
+
if (model !== undefined && model.id !== "unknown") {
|
|
174
|
+
controller.provider = model.provider;
|
|
175
|
+
controller.modelId = model.id;
|
|
176
|
+
const key = await context.modelRegistry.getApiKeyForProvider(model.provider);
|
|
177
|
+
controller.credential = typeof key === "string" && key !== "" ? "present" : "missing";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
162
180
|
let manager;
|
|
163
181
|
// One sqlite file is the ledger and the mailbox. Presence is not liveness.
|
|
164
182
|
const mailbox = createMailboxTransport({
|
|
@@ -171,17 +189,18 @@ export default function piSubagentsExtension(pi) {
|
|
|
171
189
|
runtime.resources.add("subagent-bridge", () => mailbox.close());
|
|
172
190
|
// A definition names skills the way Pi lists them, so the parent's loaded skill set is
|
|
173
191
|
// the only name → path catalog; Pi hands it over before every prompt of this session.
|
|
174
|
-
let skillCatalog = [];
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
192
|
+
let skillCatalog = controller?.skills ?? [];
|
|
193
|
+
if (controller === undefined) {
|
|
194
|
+
const unsubscribeSkillCatalog = pi.on("before_agent_start", (event) => {
|
|
195
|
+
const skills = event.systemPromptOptions?.skills ?? [];
|
|
196
|
+
skillCatalog = skillCatalogFromLoaded(skills);
|
|
197
|
+
});
|
|
198
|
+
runtime.resources.add("subagent-skill-catalog", unsubscribeSkillCatalog);
|
|
199
|
+
}
|
|
180
200
|
manager = new SubagentManager({
|
|
181
201
|
parentSessionId,
|
|
182
202
|
registry,
|
|
183
203
|
channel,
|
|
184
|
-
tokens,
|
|
185
204
|
async resolve(input) {
|
|
186
205
|
const model = context.model;
|
|
187
206
|
if (model === undefined)
|
|
@@ -213,7 +232,6 @@ export default function piSubagentsExtension(pi) {
|
|
|
213
232
|
return launchChild({
|
|
214
233
|
registry,
|
|
215
234
|
record,
|
|
216
|
-
tokens,
|
|
217
235
|
waitForBridge: (childId, signal) => mailbox.waitUntilSeen(childId, {
|
|
218
236
|
timeoutMs: START_WINDOW_MS,
|
|
219
237
|
...(signal === undefined ? {} : { signal }),
|
|
@@ -229,7 +247,6 @@ export default function piSubagentsExtension(pi) {
|
|
|
229
247
|
openPanel: (record) => openChildPanel({
|
|
230
248
|
registry,
|
|
231
249
|
record,
|
|
232
|
-
tokens,
|
|
233
250
|
host: hostSelection.adapter,
|
|
234
251
|
waitForBridge: (childId, signal) => mailbox.waitUntilSeen(childId, {
|
|
235
252
|
timeoutMs: START_WINDOW_MS,
|
|
@@ -260,7 +277,6 @@ export default function piSubagentsExtension(pi) {
|
|
|
260
277
|
const launch = await openChildPanel({
|
|
261
278
|
registry,
|
|
262
279
|
record,
|
|
263
|
-
tokens,
|
|
264
280
|
host: hostSelection.adapter,
|
|
265
281
|
waitForBridge: (childId, signal) => mailbox.waitUntilSeen(childId, {
|
|
266
282
|
timeoutMs: START_WINDOW_MS,
|
|
@@ -288,6 +304,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
288
304
|
}
|
|
289
305
|
: {}),
|
|
290
306
|
});
|
|
307
|
+
controller?.bindManager(manager);
|
|
291
308
|
// Recovery must never take the extension down with it: a registry this version cannot
|
|
292
309
|
// read (for example records written by an older release) is reported in the session and
|
|
293
310
|
// the user is told how to reset it.
|
package/dist/host-adapter.js
CHANGED
|
@@ -184,7 +184,7 @@ export function createCmuxHostAdapter(options) {
|
|
|
184
184
|
noun: "surface",
|
|
185
185
|
// TODO(cmux-focus): cmux does not report which surface a human is looking at, so
|
|
186
186
|
// observation cannot answer `focused` and idle reclaim fails open here: a cmux child
|
|
187
|
-
// is only ever closed by
|
|
187
|
+
// is only ever closed by agents_stop.
|
|
188
188
|
reportsFocus: false,
|
|
189
189
|
observe: async () => {
|
|
190
190
|
const process = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
|
package/dist/launch-spec.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface LaunchSpec {
|
|
|
12
12
|
readonly cwd: string;
|
|
13
13
|
readonly presentation: Presentation;
|
|
14
14
|
readonly stdio: "pipe" | "inherit";
|
|
15
|
-
/** Bridge variables to add to the child environment
|
|
15
|
+
/** Bridge variables to add to the child environment. */
|
|
16
16
|
readonly env: Readonly<Record<string, string>>;
|
|
17
17
|
readonly config: EffectiveLaunchConfig;
|
|
18
18
|
}
|
|
@@ -25,8 +25,6 @@ export interface BuildLaunchSpecOptions {
|
|
|
25
25
|
readonly bridge: ChildBridgeEnvironment;
|
|
26
26
|
}
|
|
27
27
|
export declare function assembleChildPrompt(instructions: string): string;
|
|
28
|
-
/** Adds the per-runtime controller token. Tokens stay out of launch spec snapshots and the registry. */
|
|
29
|
-
export declare function withBridgeToken(env: Readonly<Record<string, string>>, token: string): Record<string, string>;
|
|
30
28
|
/**
|
|
31
29
|
* Absolute path to this package's extension entry, which every child loads with
|
|
32
30
|
* `-e` so the child branch exists even when extension discovery is disabled.
|