pi-subagents 0.80.0 → 0.81.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/CHANGELOG.md +28 -0
- package/bundle/agents.js +18 -0
- package/bundle/background-work.js +16 -0
- package/bundle/capability-ceiling.js +22 -0
- package/bundle/child-tool-plan.js +11 -0
- package/bundle/chunk-26C3WLYQ.js +8388 -0
- package/bundle/chunk-2XYWN7QJ.js +128 -0
- package/bundle/chunk-2ZSNH26H.js +4164 -0
- package/bundle/chunk-3DC5KBVP.js +235 -0
- package/bundle/chunk-3IF2PGW7.js +380 -0
- package/bundle/chunk-4QFZZV3O.js +57 -0
- package/bundle/chunk-563BHAZ5.js +1365 -0
- package/bundle/chunk-5LFLVSTM.js +260 -0
- package/bundle/chunk-62OCY2S4.js +14 -0
- package/bundle/chunk-74RT5YQE.js +326 -0
- package/bundle/chunk-7F6RKT6W.js +132 -0
- package/bundle/chunk-7J4ZXMY7.js +18 -0
- package/bundle/chunk-ARRKBB2B.js +179 -0
- package/bundle/chunk-BJN5LUQY.js +458 -0
- package/bundle/chunk-BPS2AGBZ.js +5421 -0
- package/bundle/chunk-CEKS3TFO.js +1337 -0
- package/bundle/chunk-CTPAE36Y.js +94 -0
- package/bundle/chunk-D4EHT5SJ.js +14 -0
- package/bundle/chunk-EYNJ6VL6.js +526 -0
- package/bundle/chunk-G5DKFLLI.js +1185 -0
- package/bundle/chunk-KSWQVGDW.js +1027 -0
- package/bundle/chunk-L5ZHE4L4.js +946 -0
- package/bundle/chunk-LSRUTHT7.js +57 -0
- package/bundle/chunk-N2BN7BF5.js +159 -0
- package/bundle/chunk-NF7IEKBH.js +490 -0
- package/bundle/chunk-NIXWDZZV.js +9 -0
- package/bundle/chunk-NNLASTQE.js +4082 -0
- package/bundle/chunk-TUWLXTAM.js +935 -0
- package/bundle/chunk-UKG4V45O.js +228 -0
- package/bundle/chunk-V2JWDB5U.js +753 -0
- package/bundle/chunk-V3BIUPLU.js +9163 -0
- package/bundle/chunk-V6MR5TOQ.js +270 -0
- package/bundle/chunk-VJHOELQP.js +8245 -0
- package/bundle/chunk-WSBNENFS.js +732 -0
- package/bundle/chunk-XMFLCKMQ.js +16664 -0
- package/bundle/chunk-YFCK7H2S.js +129 -0
- package/bundle/chunk-YYREO77V.js +137 -0
- package/bundle/chunk-Z7KVHQHL.js +173 -0
- package/bundle/chunk-ZBC44JJ3.js +8606 -0
- package/bundle/chunk-ZP3HODFH.js +175 -0
- package/bundle/control-channel.js +11 -0
- package/bundle/delegation.js +14 -0
- package/bundle/external-job-provider.js +20 -0
- package/bundle/external-runs.js +21 -0
- package/bundle/index.js +30 -0
- package/bundle/inspectors.js +8 -0
- package/bundle/intercom-bridge.js +11 -0
- package/bundle/preflight.js +425 -0
- package/bundle/project-panes.js +28 -0
- package/bundle/required-child-extensions.js +6 -0
- package/bundle/shared-types.js +8 -0
- package/bundle/workflow-resources.js +7 -0
- package/docs/extension-api.md +27 -0
- package/docs/observability.md +6 -2
- package/package.json +18 -18
- package/src/agents/frontmatter.d.ts +2 -0
- package/src/agents/frontmatter.js +9 -2
- package/src/api/delegation.d.ts +20 -0
- package/src/extension/index.js +27 -7
- package/src/runs/background/run-child-session.js +18 -0
- package/src/runs/background/stale-run-reconciler.js +2 -0
- package/src/runs/foreground/execution.js +39 -2
- package/src/runs/shared/structured-output.d.ts +1 -0
- package/src/runs/shared/structured-output.js +1 -0
- package/src/shared/accessible-dir.js +4 -1
- package/src/shared/provider-diagnostics.d.ts +9 -0
- package/src/shared/provider-diagnostics.js +75 -0
- package/src/shared/types.d.ts +3 -0
- package/src/slash/delegation-adapters.d.ts +3 -1
- package/src/slash/delegation-adapters.js +7 -0
- package/src/slash/prompt-template-bridge.js +3 -1
- package/src/slash/subagents-admin.d.ts +1 -0
- package/src/slash/subagents-admin.js +1 -1
- package/src/tui/fleet-layout.d.ts +20 -0
- package/src/tui/fleet-layout.js +17 -0
- package/src/tui/fleet-viewport.d.ts +12 -0
- package/src/tui/fleet-viewport.js +35 -0
- package/src/tui/fleet.d.ts +5 -6
- package/src/tui/fleet.js +69 -51
- package/src/workflows/structured-workflow-scripts.d.ts +1 -0
- package/src/workflows/structured-workflow-scripts.js +5 -4
- package/src/workflows/workflow-resources.js +2 -2
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// src/shared/types.ts
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
var SUBAGENT_LIFECYCLE_ARTIFACT_VERSION = 3;
|
|
5
|
+
var INTERCOM_DETACH_REQUEST_EVENT = "pi-intercom:detach-request";
|
|
6
|
+
var INTERCOM_DETACH_RESPONSE_EVENT = "pi-intercom:detach-response";
|
|
7
|
+
var INTERCOM_SESSION_IDENTITY_EVENT = "intercom:session-identity";
|
|
8
|
+
var SUBAGENT_ASYNC_STARTED_EVENT = "subagent:async-started";
|
|
9
|
+
var RUNNER_LAUNCHER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
10
|
+
var RUNNER_LAUNCHER_NAME_RULE = "must start with a letter or digit and use only letters, digits, '.', '_' or '-' (at most 128 characters)";
|
|
11
|
+
var SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
|
|
12
|
+
var SUBAGENT_PROCESS_TERMINAL_EVENT = "subagent:process-terminal";
|
|
13
|
+
var SUBAGENT_FOREGROUND_COMPLETE_EVENT = "subagent:foreground-complete";
|
|
14
|
+
var SUBAGENT_CONTROL_EVENT = "subagent:control-event";
|
|
15
|
+
var SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
|
|
16
|
+
var SUBAGENT_STEERING_NOTICE_EVENT = "subagent:steering-notice";
|
|
17
|
+
var SUBAGENT_CHILD_STATUS_EVENT = "subagent:child-status";
|
|
18
|
+
var SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
|
|
19
|
+
var SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
|
|
20
|
+
var FLEET_KEYBINDING_ACTIONS = [
|
|
21
|
+
"close",
|
|
22
|
+
"scrollUp",
|
|
23
|
+
"scrollDown",
|
|
24
|
+
"selectUp",
|
|
25
|
+
"selectDown",
|
|
26
|
+
"selectFirst",
|
|
27
|
+
"selectLast",
|
|
28
|
+
"pageUp",
|
|
29
|
+
"pageDown",
|
|
30
|
+
"refresh",
|
|
31
|
+
"steer",
|
|
32
|
+
"inspect",
|
|
33
|
+
"stop",
|
|
34
|
+
"toggleTools"
|
|
35
|
+
];
|
|
36
|
+
var DEFAULT_MAX_OUTPUT = {
|
|
37
|
+
bytes: 200 * 1024,
|
|
38
|
+
lines: 5e3
|
|
39
|
+
};
|
|
40
|
+
var DEFAULT_ARTIFACT_CONFIG = {
|
|
41
|
+
enabled: true,
|
|
42
|
+
dir: "session",
|
|
43
|
+
includeInput: true,
|
|
44
|
+
includeOutput: true,
|
|
45
|
+
includeJsonl: false,
|
|
46
|
+
includeTranscript: true,
|
|
47
|
+
includeMetadata: true,
|
|
48
|
+
cleanupDays: 7
|
|
49
|
+
};
|
|
50
|
+
function sanitizeTempScopeSegment(value) {
|
|
51
|
+
const sanitized = value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
52
|
+
return sanitized || "unknown";
|
|
53
|
+
}
|
|
54
|
+
function resolveTempScopeId(options) {
|
|
55
|
+
const env = options?.env ?? process.env;
|
|
56
|
+
const getuid = options && Object.hasOwn(options, "getuid") ? options.getuid : process.getuid?.bind(process);
|
|
57
|
+
if (typeof getuid === "function") {
|
|
58
|
+
return `uid-${getuid()}`;
|
|
59
|
+
}
|
|
60
|
+
for (const key of ["USERNAME", "USER", "LOGNAME"]) {
|
|
61
|
+
const value = env[key];
|
|
62
|
+
if (value) return `user-${sanitizeTempScopeSegment(value)}`;
|
|
63
|
+
}
|
|
64
|
+
const userInfo2 = options && Object.hasOwn(options, "userInfo") ? options.userInfo : os.userInfo;
|
|
65
|
+
try {
|
|
66
|
+
const username = userInfo2?.().username;
|
|
67
|
+
if (username) return `user-${sanitizeTempScopeSegment(username)}`;
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
const homedir2 = env.USERPROFILE ?? env.HOME;
|
|
71
|
+
if (homedir2) return `home-${sanitizeTempScopeSegment(homedir2)}`;
|
|
72
|
+
const resolveHomedir = options && Object.hasOwn(options, "homedir") ? options.homedir : os.homedir;
|
|
73
|
+
try {
|
|
74
|
+
const fallbackHomedir = resolveHomedir?.();
|
|
75
|
+
if (fallbackHomedir) return `home-${sanitizeTempScopeSegment(fallbackHomedir)}`;
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
return "shared";
|
|
79
|
+
}
|
|
80
|
+
var configuredTempRoot = process.env.PI_SUBAGENTS_TEMP_ROOT?.trim();
|
|
81
|
+
var TEMP_ROOT_DIR = configuredTempRoot ? path.resolve(configuredTempRoot) : path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
|
|
82
|
+
var RESULTS_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-results");
|
|
83
|
+
var ASYNC_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-runs");
|
|
84
|
+
var CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
|
|
85
|
+
var TEMP_ARTIFACTS_DIR = path.join(TEMP_ROOT_DIR, "artifacts");
|
|
86
|
+
var DIRS = {
|
|
87
|
+
results: RESULTS_DIR,
|
|
88
|
+
async: ASYNC_DIR,
|
|
89
|
+
chain: CHAIN_RUNS_DIR,
|
|
90
|
+
artifacts: TEMP_ARTIFACTS_DIR
|
|
91
|
+
};
|
|
92
|
+
var WIDGET_KEY = "subagent-async";
|
|
93
|
+
var SLASH_RESULT_TYPE = "subagent-slash-result";
|
|
94
|
+
var SLASH_TEXT_RESULT_TYPE = "subagent-slash-text-result";
|
|
95
|
+
var SLASH_TEXT_VIEW_ONLY_DETAILS = { viewOnly: true };
|
|
96
|
+
var SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
|
|
97
|
+
var SLASH_SUBAGENT_STARTED_EVENT = "subagent:slash:started";
|
|
98
|
+
var SLASH_SUBAGENT_RESPONSE_EVENT = "subagent:slash:response";
|
|
99
|
+
var SLASH_SUBAGENT_UPDATE_EVENT = "subagent:slash:update";
|
|
100
|
+
var SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
101
|
+
var POLL_INTERVAL_MS = 250;
|
|
102
|
+
var WIDGET_ANIMATION_INTERVAL_MS = 1e3;
|
|
103
|
+
var MAX_WIDGET_JOBS = 4;
|
|
104
|
+
var DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
105
|
+
var SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "validate", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "worktree.cleanup", "lane.status", "lane.recordMerge", "lane.recordSupersession", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.command", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "command.status", "command.yield", "command.cancel", "dismiss", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"];
|
|
106
|
+
var DEFAULT_FORK_PREAMBLE = "You are a delegated subagent running from a fork of the parent session. Treat the inherited conversation as reference-only context, not a live thread to continue. Do not continue or answer prior messages as if they are waiting for a reply. Your sole job is to execute the task below and return a focused result for that task using your tools.";
|
|
107
|
+
function getAsyncConfigPath(suffix) {
|
|
108
|
+
return path.join(TEMP_ROOT_DIR, `async-cfg-${suffix}.json`);
|
|
109
|
+
}
|
|
110
|
+
function wrapForkTask(task, preamble) {
|
|
111
|
+
if (preamble === false) return task;
|
|
112
|
+
const effectivePreamble = preamble ?? DEFAULT_FORK_PREAMBLE;
|
|
113
|
+
const wrappedPrefix = `${effectivePreamble}
|
|
114
|
+
|
|
115
|
+
Task:
|
|
116
|
+
`;
|
|
117
|
+
if (task.startsWith(wrappedPrefix)) return task;
|
|
118
|
+
return `${wrappedPrefix}${task}`;
|
|
119
|
+
}
|
|
120
|
+
function normalizeNonNegativeInteger(value) {
|
|
121
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
122
|
+
if (!Number.isInteger(parsed) || parsed < 0) return void 0;
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
function normalizeMaxSubagentDepth(value) {
|
|
126
|
+
return normalizeNonNegativeInteger(value);
|
|
127
|
+
}
|
|
128
|
+
var SUBAGENT_MAX_DEPTH_ENV = "PI_SUBAGENT_MAX_DEPTH";
|
|
129
|
+
function resolveCurrentMaxSubagentDepth(configMaxDepth, runtime) {
|
|
130
|
+
return normalizeMaxSubagentDepth(runtime ? runtime.maxDepth : process.env[SUBAGENT_MAX_DEPTH_ENV]) ?? normalizeMaxSubagentDepth(configMaxDepth) ?? DEFAULT_SUBAGENT_MAX_DEPTH;
|
|
131
|
+
}
|
|
132
|
+
function resolveChildMaxSubagentDepth(parentMaxDepth, agentMaxDepth) {
|
|
133
|
+
const normalizedParent = normalizeMaxSubagentDepth(parentMaxDepth) ?? DEFAULT_SUBAGENT_MAX_DEPTH;
|
|
134
|
+
const normalizedAgent = normalizeMaxSubagentDepth(agentMaxDepth);
|
|
135
|
+
return normalizedAgent === void 0 ? normalizedParent : Math.min(normalizedParent, normalizedAgent);
|
|
136
|
+
}
|
|
137
|
+
function resolveCurrentSubagentDepth(runtime) {
|
|
138
|
+
const depth = runtime ? runtime.depth : 0;
|
|
139
|
+
return Number.isFinite(depth) ? depth : 0;
|
|
140
|
+
}
|
|
141
|
+
function checkSubagentDepth(configMaxDepth, runtime) {
|
|
142
|
+
const depth = resolveCurrentSubagentDepth(runtime);
|
|
143
|
+
const maxDepth = resolveCurrentMaxSubagentDepth(configMaxDepth, runtime);
|
|
144
|
+
const blocked = depth >= maxDepth;
|
|
145
|
+
return { blocked, depth, maxDepth };
|
|
146
|
+
}
|
|
147
|
+
function resolveChildDepth(maxDepth, runtime) {
|
|
148
|
+
return {
|
|
149
|
+
depth: resolveCurrentSubagentDepth(runtime) + 1,
|
|
150
|
+
maxDepth: normalizeMaxSubagentDepth(maxDepth) ?? resolveCurrentMaxSubagentDepth(void 0, runtime)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function normalizeMaxSubagentSpawnsPerSession(value) {
|
|
154
|
+
return normalizeNonNegativeInteger(value);
|
|
155
|
+
}
|
|
156
|
+
function resolveMaxSubagentSpawnsPerSession(configMaxSpawns) {
|
|
157
|
+
const envMaxSpawns = normalizeMaxSubagentSpawnsPerSession(process.env.PI_SUBAGENT_MAX_SPAWNS_PER_SESSION);
|
|
158
|
+
if (envMaxSpawns !== void 0) return envMaxSpawns === 0 ? void 0 : envMaxSpawns;
|
|
159
|
+
const configuredMaxSpawns = normalizeMaxSubagentSpawnsPerSession(configMaxSpawns);
|
|
160
|
+
return configuredMaxSpawns === 0 ? void 0 : configuredMaxSpawns;
|
|
161
|
+
}
|
|
162
|
+
var DEFAULT_MAX_SUBAGENT_SPAWNS_PER_RUN = 64;
|
|
163
|
+
function normalizeMaxSubagentSpawnsPerRun(value) {
|
|
164
|
+
const normalized = normalizeNonNegativeInteger(value);
|
|
165
|
+
return normalized !== void 0 && normalized > 0 ? normalized : void 0;
|
|
166
|
+
}
|
|
167
|
+
function resolveMaxSubagentSpawnsPerRun(configMaxSpawns) {
|
|
168
|
+
return normalizeMaxSubagentSpawnsPerRun(process.env.PI_SUBAGENT_MAX_SPAWNS_PER_RUN) ?? normalizeMaxSubagentSpawnsPerRun(configMaxSpawns) ?? DEFAULT_MAX_SUBAGENT_SPAWNS_PER_RUN;
|
|
169
|
+
}
|
|
170
|
+
function formatBytes(bytes) {
|
|
171
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
172
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
173
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
174
|
+
}
|
|
175
|
+
function truncateOutput(output, config, artifactPath) {
|
|
176
|
+
const lines = output.split("\n");
|
|
177
|
+
const bytes = Buffer.byteLength(output, "utf-8");
|
|
178
|
+
if (bytes <= config.bytes && lines.length <= config.lines) {
|
|
179
|
+
return { text: output, truncated: false };
|
|
180
|
+
}
|
|
181
|
+
let truncatedLines = lines;
|
|
182
|
+
if (lines.length > config.lines) {
|
|
183
|
+
truncatedLines = lines.slice(0, config.lines);
|
|
184
|
+
}
|
|
185
|
+
let result = truncatedLines.join("\n");
|
|
186
|
+
if (Buffer.byteLength(result, "utf-8") > config.bytes) {
|
|
187
|
+
let low = 0;
|
|
188
|
+
let high = result.length;
|
|
189
|
+
while (low < high) {
|
|
190
|
+
const mid = Math.floor((low + high + 1) / 2);
|
|
191
|
+
if (Buffer.byteLength(result.slice(0, mid), "utf-8") <= config.bytes) {
|
|
192
|
+
low = mid;
|
|
193
|
+
} else {
|
|
194
|
+
high = mid - 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
result = result.slice(0, low);
|
|
198
|
+
}
|
|
199
|
+
const keptLines = result.split("\n").length;
|
|
200
|
+
const marker = `[TRUNCATED: showing first ${keptLines} of ${lines.length} lines, ${formatBytes(Buffer.byteLength(result))} of ${formatBytes(bytes)}${artifactPath ? ` - full output at ${artifactPath}` : ""}]
|
|
201
|
+
`;
|
|
202
|
+
return {
|
|
203
|
+
text: marker + result,
|
|
204
|
+
truncated: true,
|
|
205
|
+
originalBytes: bytes,
|
|
206
|
+
originalLines: lines.length,
|
|
207
|
+
artifactPath
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export {
|
|
212
|
+
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
213
|
+
INTERCOM_DETACH_REQUEST_EVENT,
|
|
214
|
+
INTERCOM_DETACH_RESPONSE_EVENT,
|
|
215
|
+
INTERCOM_SESSION_IDENTITY_EVENT,
|
|
216
|
+
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
217
|
+
RUNNER_LAUNCHER_NAME_PATTERN,
|
|
218
|
+
RUNNER_LAUNCHER_NAME_RULE,
|
|
219
|
+
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
220
|
+
SUBAGENT_PROCESS_TERMINAL_EVENT,
|
|
221
|
+
SUBAGENT_FOREGROUND_COMPLETE_EVENT,
|
|
222
|
+
SUBAGENT_CONTROL_EVENT,
|
|
223
|
+
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
224
|
+
SUBAGENT_STEERING_NOTICE_EVENT,
|
|
225
|
+
SUBAGENT_CHILD_STATUS_EVENT,
|
|
226
|
+
SUBAGENT_RESULT_INTERCOM_EVENT,
|
|
227
|
+
SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
|
|
228
|
+
FLEET_KEYBINDING_ACTIONS,
|
|
229
|
+
DEFAULT_MAX_OUTPUT,
|
|
230
|
+
DEFAULT_ARTIFACT_CONFIG,
|
|
231
|
+
TEMP_ROOT_DIR,
|
|
232
|
+
ASYNC_DIR,
|
|
233
|
+
CHAIN_RUNS_DIR,
|
|
234
|
+
TEMP_ARTIFACTS_DIR,
|
|
235
|
+
DIRS,
|
|
236
|
+
WIDGET_KEY,
|
|
237
|
+
SLASH_RESULT_TYPE,
|
|
238
|
+
SLASH_TEXT_RESULT_TYPE,
|
|
239
|
+
SLASH_TEXT_VIEW_ONLY_DETAILS,
|
|
240
|
+
SLASH_SUBAGENT_REQUEST_EVENT,
|
|
241
|
+
SLASH_SUBAGENT_STARTED_EVENT,
|
|
242
|
+
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
243
|
+
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
244
|
+
SLASH_SUBAGENT_CANCEL_EVENT,
|
|
245
|
+
POLL_INTERVAL_MS,
|
|
246
|
+
WIDGET_ANIMATION_INTERVAL_MS,
|
|
247
|
+
MAX_WIDGET_JOBS,
|
|
248
|
+
SUBAGENT_ACTIONS,
|
|
249
|
+
DEFAULT_FORK_PREAMBLE,
|
|
250
|
+
getAsyncConfigPath,
|
|
251
|
+
wrapForkTask,
|
|
252
|
+
resolveCurrentMaxSubagentDepth,
|
|
253
|
+
resolveChildMaxSubagentDepth,
|
|
254
|
+
checkSubagentDepth,
|
|
255
|
+
resolveChildDepth,
|
|
256
|
+
resolveMaxSubagentSpawnsPerSession,
|
|
257
|
+
normalizeMaxSubagentSpawnsPerRun,
|
|
258
|
+
resolveMaxSubagentSpawnsPerRun,
|
|
259
|
+
truncateOutput
|
|
260
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/api/inspectors.ts
|
|
2
|
+
var INSPECTOR_REGISTER_EVENT = "pi-subagents:inspector-register:v1";
|
|
3
|
+
function registerInspector(pi, plugin) {
|
|
4
|
+
const request = { version: 1, plugin };
|
|
5
|
+
pi.events.emit(INSPECTOR_REGISTER_EVENT, request);
|
|
6
|
+
if (!request.result) throw new Error("pi-subagents is not installed, not ready, or does not support inspector registration.");
|
|
7
|
+
if (!request.result.ok) throw request.result.error;
|
|
8
|
+
return request.result.registration;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
INSPECTOR_REGISTER_EVENT,
|
|
13
|
+
registerInspector
|
|
14
|
+
};
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// src/runs/shared/herdr-connection.ts
|
|
2
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as net from "node:net";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
var MAX_DISCOVERY_BYTES = 256 * 1024;
|
|
8
|
+
var MAX_RPC_BYTES = 4 * 1024 * 1024;
|
|
9
|
+
var SUBSCRIPTION_ACK_TIMEOUT_MS = 15e3;
|
|
10
|
+
var HERDR_REMOTE_PATH = "$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
11
|
+
var HERDR_SSH_BASE = ["-T", "-o", "BatchMode=yes", "-o", "NumberOfPasswordPrompts=0", "-o", "StrictHostKeyChecking=yes", "-o", "ConnectTimeout=10", "-o", "ConnectionAttempts=1", "-o", "ForwardAgent=no", "-o", "ExitOnForwardFailure=yes", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=4", "-o", "SendEnv=-*", "-o", "SetEnv=PI_SUBAGENTS_SSH_GUARD="];
|
|
12
|
+
function hardenedSshEnv(source = process.env) {
|
|
13
|
+
return { PATH: process.platform === "win32" ? source.PATH : "/usr/bin:/bin:/usr/sbin:/sbin", ...source.SystemRoot ? { SystemRoot: source.SystemRoot } : {}, ...source.WINDIR ? { WINDIR: source.WINDIR } : {} };
|
|
14
|
+
}
|
|
15
|
+
function herdrSshArgs(source = process.env) {
|
|
16
|
+
return [...HERDR_SSH_BASE, ...source.SSH_AUTH_SOCK ? ["-o", `IdentityAgent=${source.SSH_AUTH_SOCK}`] : []];
|
|
17
|
+
}
|
|
18
|
+
var HerdrRpcError = class extends Error {
|
|
19
|
+
code;
|
|
20
|
+
requestId;
|
|
21
|
+
constructor(message, requestId, code) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "HerdrRpcError";
|
|
24
|
+
this.requestId = requestId;
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var HerdrTransportError = class extends Error {
|
|
29
|
+
constructor(message, options) {
|
|
30
|
+
super(message, options);
|
|
31
|
+
this.name = "HerdrTransportError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
function decodeHerdrJsonLine(line) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(line));
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error("Herdr emitted malformed JSON or UTF-8.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function shellQuoteRemote(value) {
|
|
42
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
43
|
+
}
|
|
44
|
+
function remoteShellCommand(script, args = []) {
|
|
45
|
+
return `sh -c ${shellQuoteRemote(`PATH="${HERDR_REMOTE_PATH}"; export PATH; ${script}`)} sh${args.map((arg) => ` ${shellQuoteRemote(arg)}`).join("")}`;
|
|
46
|
+
}
|
|
47
|
+
function canonicalHerdrSession(session) {
|
|
48
|
+
return session && session !== "default" ? session : null;
|
|
49
|
+
}
|
|
50
|
+
function sshEnvCommand(session, command) {
|
|
51
|
+
const selected = canonicalHerdrSession(session), assignment = selected ? ` HERDR_SESSION=${shellQuoteRemote(selected)}` : "";
|
|
52
|
+
return `/usr/bin/env -u HERDR_SOCKET_PATH -u HERDR_SESSION${assignment} ${remoteShellCommand(command)}`;
|
|
53
|
+
}
|
|
54
|
+
function runHerdrRemoteCommand(machine, command, options = {}) {
|
|
55
|
+
return spawnSync(options.sshBin ?? "ssh", [...herdrSshArgs(options.env), machine.target, command], { encoding: "utf8", env: hardenedSshEnv(options.env), timeout: options.timeout ?? 15e3, maxBuffer: options.maxBuffer ?? MAX_DISCOVERY_BYTES, windowsHide: true });
|
|
56
|
+
}
|
|
57
|
+
function runHerdrRemoteCommandAsync(machine, command, options = {}) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const child = spawn(options.sshBin ?? "ssh", [...herdrSshArgs(options.env), machine.target, command], { env: hardenedSshEnv(options.env), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
60
|
+
const stdout = [], stderr = [], maxBuffer = options.maxBuffer ?? MAX_DISCOVERY_BYTES;
|
|
61
|
+
let stdoutBytes = 0, stderrBytes = 0, error, settled = false, escalation;
|
|
62
|
+
const finish = (status, spawnError = error) => {
|
|
63
|
+
if (settled) return;
|
|
64
|
+
settled = true;
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
if (escalation) clearTimeout(escalation);
|
|
67
|
+
resolve({ status, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8"), ...spawnError ? { error: spawnError } : {} });
|
|
68
|
+
};
|
|
69
|
+
const terminate = () => {
|
|
70
|
+
child.kill("SIGTERM");
|
|
71
|
+
escalation ??= setTimeout(() => {
|
|
72
|
+
if (!settled) {
|
|
73
|
+
child.kill("SIGKILL");
|
|
74
|
+
finish(child.exitCode ?? null);
|
|
75
|
+
}
|
|
76
|
+
}, 1e3);
|
|
77
|
+
escalation.unref?.();
|
|
78
|
+
};
|
|
79
|
+
const collect = (target, stream) => (chunk) => {
|
|
80
|
+
const bytes = stream === "stdout" ? stdoutBytes += chunk.byteLength : stderrBytes += chunk.byteLength;
|
|
81
|
+
if (bytes > maxBuffer) {
|
|
82
|
+
error ??= new Error(`${stream} maxBuffer length exceeded`);
|
|
83
|
+
terminate();
|
|
84
|
+
} else target.push(chunk);
|
|
85
|
+
};
|
|
86
|
+
child.stdout.on("data", collect(stdout, "stdout"));
|
|
87
|
+
child.stderr.on("data", collect(stderr, "stderr"));
|
|
88
|
+
child.once("error", (spawnError) => finish(null, spawnError));
|
|
89
|
+
child.once("close", (status) => finish(status));
|
|
90
|
+
const timer = setTimeout(terminate, options.timeout ?? 15e3);
|
|
91
|
+
timer.unref?.();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function parseHerdrEndpoint(value, expectedSession) {
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(value);
|
|
98
|
+
} catch {
|
|
99
|
+
throw new Error("Remote Herdr endpoint discovery returned malformed JSON.");
|
|
100
|
+
}
|
|
101
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Remote Herdr endpoint discovery returned no endpoint.");
|
|
102
|
+
const p = parsed;
|
|
103
|
+
if (typeof p.socket !== "string" || !path.posix.isAbsolute(p.socket) || typeof p.version !== "string" || typeof p.protocol !== "number") throw new Error("Remote Herdr endpoint discovery returned incomplete identity.");
|
|
104
|
+
const session = canonicalHerdrSession(typeof p.session === "string" ? p.session : null), expected = canonicalHerdrSession(expectedSession);
|
|
105
|
+
if (expected !== session) throw new Error(`Remote Herdr session identity mismatch: expected ${expected ?? "default"}, received ${session ?? "default"}.`);
|
|
106
|
+
if (p.running !== true || p.compatible !== true || p.endpoint_compatible === false) throw new Error("The selected remote Herdr session is stopped or incompatible.");
|
|
107
|
+
return { socket: p.socket, session, version: p.version, protocol: p.protocol, compatible: true, running: true };
|
|
108
|
+
}
|
|
109
|
+
async function discoverHerdrEndpoint(machine, options = {}) {
|
|
110
|
+
const discovery = 'herdr_path=$(command -v herdr) || exit 127; case "$herdr_path" in /*/herdr) ;; *) exit 126;; esac; exec "$herdr_path" status server --json';
|
|
111
|
+
const result = await runHerdrRemoteCommandAsync(machine, sshEnvCommand(machine.session, discovery), { ...options, timeout: 15e3, maxBuffer: MAX_DISCOVERY_BYTES });
|
|
112
|
+
if (result.error) throw new Error(`Remote Herdr discovery failed: ${result.error.message}`);
|
|
113
|
+
if (result.status !== 0) throw new Error(`Remote Herdr discovery failed with code ${result.status}: ${(result.stderr || result.stdout).trim()}`);
|
|
114
|
+
return parseHerdrEndpoint(result.stdout, machine.session);
|
|
115
|
+
}
|
|
116
|
+
var SocketRpcClient = class {
|
|
117
|
+
#socketPath;
|
|
118
|
+
#subscriptionAckTimeoutMs;
|
|
119
|
+
#next = 0;
|
|
120
|
+
constructor(socketPath, subscriptionAckTimeoutMs = SUBSCRIPTION_ACK_TIMEOUT_MS) {
|
|
121
|
+
this.#socketPath = socketPath;
|
|
122
|
+
this.#subscriptionAckTimeoutMs = subscriptionAckTimeoutMs;
|
|
123
|
+
}
|
|
124
|
+
call(method, params = {}, timeoutMs = 15e3) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const socket = net.createConnection(this.#socketPath);
|
|
127
|
+
let bytes = 0;
|
|
128
|
+
let data = Buffer.alloc(0);
|
|
129
|
+
let finished = false;
|
|
130
|
+
const id = String(++this.#next);
|
|
131
|
+
const timer = setTimeout(() => finish(new HerdrTransportError(`Herdr RPC '${method}' timed out.`)), timeoutMs);
|
|
132
|
+
timer.unref?.();
|
|
133
|
+
const finish = (error, value) => {
|
|
134
|
+
if (finished) return;
|
|
135
|
+
finished = true;
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
socket.destroy();
|
|
138
|
+
error ? reject(error) : resolve(value);
|
|
139
|
+
};
|
|
140
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ id, method, params })}
|
|
141
|
+
`));
|
|
142
|
+
socket.on("data", (chunk) => {
|
|
143
|
+
bytes += chunk.byteLength;
|
|
144
|
+
if (bytes > MAX_RPC_BYTES) {
|
|
145
|
+
finish(new Error("Herdr RPC response exceeded its byte bound."));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
data = Buffer.concat([data, chunk]);
|
|
149
|
+
const newline = data.indexOf(10);
|
|
150
|
+
if (newline < 0) return;
|
|
151
|
+
let frame;
|
|
152
|
+
try {
|
|
153
|
+
frame = decodeHerdrJsonLine(data.subarray(0, newline));
|
|
154
|
+
} catch {
|
|
155
|
+
finish(new Error("Herdr RPC returned malformed JSON or UTF-8."));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const record = frame;
|
|
159
|
+
if (record.id !== id) {
|
|
160
|
+
finish(new Error("Herdr RPC response identity mismatch."));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (record.error) finish(new HerdrRpcError(String(record.error.message ?? "Herdr RPC failed."), id, typeof record.error.code === "string" ? record.error.code : void 0));
|
|
164
|
+
else finish(void 0, record.result);
|
|
165
|
+
});
|
|
166
|
+
socket.on("error", (error) => finish(new HerdrTransportError(error.message, { cause: error })));
|
|
167
|
+
socket.on("close", () => finish(new HerdrTransportError(`Herdr RPC '${method}' ended before a response.`)));
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async subscribe(subscriptions, listener, onDisconnect) {
|
|
171
|
+
const socket = net.createConnection(this.#socketPath);
|
|
172
|
+
const id = String(++this.#next);
|
|
173
|
+
let buffer = Buffer.alloc(0), bytes = 0, acknowledged = false, intentional = false, notified = false, settled = false;
|
|
174
|
+
let resolveAck, rejectAck;
|
|
175
|
+
const acknowledgement = new Promise((resolve, reject) => {
|
|
176
|
+
resolveAck = resolve;
|
|
177
|
+
rejectAck = reject;
|
|
178
|
+
});
|
|
179
|
+
const failHandshake = (error) => {
|
|
180
|
+
if (settled) return;
|
|
181
|
+
settled = true;
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
socket.destroy();
|
|
184
|
+
rejectAck(error);
|
|
185
|
+
};
|
|
186
|
+
const disconnect = (error) => {
|
|
187
|
+
if (intentional || notified) return;
|
|
188
|
+
notified = true;
|
|
189
|
+
if (!acknowledged) failHandshake(error);
|
|
190
|
+
else onDisconnect?.(error);
|
|
191
|
+
};
|
|
192
|
+
const timer = setTimeout(() => failHandshake(new Error("Herdr subscription acknowledgement timed out.")), this.#subscriptionAckTimeoutMs);
|
|
193
|
+
timer.unref?.();
|
|
194
|
+
socket.on("data", (chunk) => {
|
|
195
|
+
bytes += chunk.byteLength;
|
|
196
|
+
if (bytes > (acknowledged ? MAX_RPC_BYTES * 32 : MAX_RPC_BYTES)) {
|
|
197
|
+
disconnect(new Error("Herdr subscription exceeded its byte bound."));
|
|
198
|
+
socket.destroy();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
202
|
+
for (; ; ) {
|
|
203
|
+
const i = buffer.indexOf(10);
|
|
204
|
+
if (i < 0) break;
|
|
205
|
+
const line = buffer.subarray(0, i);
|
|
206
|
+
buffer = buffer.subarray(i + 1);
|
|
207
|
+
let value;
|
|
208
|
+
try {
|
|
209
|
+
value = decodeHerdrJsonLine(line);
|
|
210
|
+
} catch {
|
|
211
|
+
disconnect(new Error("Herdr subscription emitted malformed JSON or UTF-8."));
|
|
212
|
+
socket.destroy();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const record = value;
|
|
216
|
+
if (!acknowledged) {
|
|
217
|
+
if (record.id === id) {
|
|
218
|
+
if (record.error) {
|
|
219
|
+
failHandshake(new HerdrRpcError(String(record.error.message ?? "Herdr subscription failed."), id, typeof record.error.code === "string" ? record.error.code : void 0));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (record.result?.type !== "subscription_started") {
|
|
223
|
+
failHandshake(new Error("Herdr subscription returned an invalid acknowledgement."));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
acknowledged = true;
|
|
227
|
+
settled = true;
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
resolveAck();
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (record.id === "" && record.error && record.error.code === "invalid_request") {
|
|
233
|
+
failHandshake(new HerdrRpcError(String(record.error.message ?? "Herdr subscription request was invalid."), "", "invalid_request"));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
failHandshake(new Error("Herdr subscription emitted an event before acknowledgement."));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (record.error) {
|
|
240
|
+
disconnect(new HerdrRpcError(String(record.error.message ?? "Herdr subscription failed."), typeof record.id === "string" ? record.id : "", typeof record.error.code === "string" ? record.error.code : void 0));
|
|
241
|
+
socket.destroy();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
listener(value);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
socket.on("error", (error) => disconnect(error));
|
|
248
|
+
socket.on("close", () => disconnect(new Error("Herdr subscription connection was lost.")));
|
|
249
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ id, method: "events.subscribe", params: { subscriptions } })}
|
|
250
|
+
`));
|
|
251
|
+
await acknowledgement;
|
|
252
|
+
return () => {
|
|
253
|
+
intentional = true;
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
socket.destroy();
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
async function waitForSocket(socketPath, child) {
|
|
260
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
261
|
+
if (child.exitCode !== null) throw new Error(`SSH StreamLocal forwarding exited with code ${child.exitCode}.`);
|
|
262
|
+
try {
|
|
263
|
+
const stat = fs.lstatSync(socketPath);
|
|
264
|
+
if (stat.isSocket()) {
|
|
265
|
+
fs.chmodSync(socketPath, 384);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
271
|
+
}
|
|
272
|
+
throw new Error("SSH StreamLocal forwarding did not create its local socket.");
|
|
273
|
+
}
|
|
274
|
+
async function terminateChild(child) {
|
|
275
|
+
const wait = (ms) => child.exitCode !== null || child.signalCode !== null ? Promise.resolve() : Promise.race([new Promise((resolve) => child.once("exit", () => resolve())), new Promise((resolve) => setTimeout(resolve, ms))]);
|
|
276
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
277
|
+
await wait(1e3);
|
|
278
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
279
|
+
child.kill("SIGKILL");
|
|
280
|
+
await wait(1e3);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function connectHerdrMachine(machine, options = {}) {
|
|
284
|
+
if (process.platform === "win32") throw new Error("Pane-native Herdr placement requires OpenSSH StreamLocal forwarding and is not supported on Windows.");
|
|
285
|
+
const endpoint = await discoverHerdrEndpoint(machine, options);
|
|
286
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-herdr-"));
|
|
287
|
+
fs.chmodSync(dir, 448);
|
|
288
|
+
const children = /* @__PURE__ */ new Set();
|
|
289
|
+
const forward = async (remotePath, name) => {
|
|
290
|
+
if (!path.posix.isAbsolute(remotePath)) throw new Error("Remote socket path must be absolute.");
|
|
291
|
+
const local = path.join(dir, `${name}.sock`);
|
|
292
|
+
const child = spawn(options.sshBin ?? "ssh", [...herdrSshArgs(options.env), "-N", "-o", "StreamLocalBindUnlink=yes", "-L", `${local}:${remotePath}`, machine.target], { env: hardenedSshEnv(options.env), stdio: "ignore", windowsHide: true });
|
|
293
|
+
children.add(child);
|
|
294
|
+
const stop = async () => {
|
|
295
|
+
await terminateChild(child);
|
|
296
|
+
children.delete(child);
|
|
297
|
+
try {
|
|
298
|
+
fs.rmSync(local, { force: true });
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
try {
|
|
303
|
+
await waitForSocket(local, child);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
await stop();
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
return { socketPath: local, close: stop };
|
|
309
|
+
};
|
|
310
|
+
const primary = await forward(endpoint.socket, "herdr").catch((error) => {
|
|
311
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
312
|
+
throw error;
|
|
313
|
+
});
|
|
314
|
+
return { endpoint, socketPath: primary.socketPath, client: new SocketRpcClient(primary.socketPath), forwardRemoteSocket: forward, close: async () => {
|
|
315
|
+
await Promise.all([...children].map(terminateChild));
|
|
316
|
+
children.clear();
|
|
317
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
318
|
+
} };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export {
|
|
322
|
+
remoteShellCommand,
|
|
323
|
+
runHerdrRemoteCommand,
|
|
324
|
+
runHerdrRemoteCommandAsync,
|
|
325
|
+
connectHerdrMachine
|
|
326
|
+
};
|