larkin 0.4.19 → 0.4.21
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/dist/agent/context-prompt.cjs +2 -2
- package/dist/agent/context-prompt.mjs +2 -2
- package/dist/agent/missed-outbound-scan.cjs +123 -0
- package/dist/agent/missed-outbound-scan.mjs +112 -0
- package/dist/feishu/host-shell.cjs +9 -0
- package/dist/feishu/host-shell.mjs +9 -0
- package/dist/feishu/message-policy.cjs +2 -0
- package/dist/feishu/message-policy.mjs +2 -0
- package/dist/runtime/pi-inline-extensions.mjs +40 -9
- package/dist/runtime/pi-subagent-injection.mjs +103 -10
- package/dist/runtime/pi-subagents.bundle.js +131 -58
- package/dist/runtime/pi-supervised-command-extension.mjs +72 -0
- package/dist/runtime/pi-supervised-command.bundle.js +4735 -0
- package/dist/runtime/pi-supervised-command.mjs +420 -0
- package/dist/setup/bot-register.mjs +53 -27
- package/dist/setup/grant-scopes.mjs +13 -4
- package/dist/setup/tenant-scope-grant.mjs +16 -0
- package/package.json +1 -1
- package/patches/@tintinweb%2Fpi-subagents@0.14.3.patch +712 -16
|
@@ -1,17 +1,317 @@
|
|
|
1
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-1cce67bbcb821bd9 b/.bun-tag-1cce67bbcb821bd9
|
|
2
|
+
new file mode 100644
|
|
3
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
4
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-2a826c9b186cb67c b/.bun-tag-2a826c9b186cb67c
|
|
5
|
+
new file mode 100644
|
|
6
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
7
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-3fadcd1707a055f8 b/.bun-tag-3fadcd1707a055f8
|
|
8
|
+
new file mode 100644
|
|
9
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
10
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-521d9b2ad9a104fa b/.bun-tag-521d9b2ad9a104fa
|
|
11
|
+
new file mode 100644
|
|
12
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
1
13
|
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-58e08ce8246a76a0 b/.bun-tag-58e08ce8246a76a0
|
|
2
14
|
new file mode 100644
|
|
3
15
|
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
16
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-60f990c0070fb268 b/.bun-tag-60f990c0070fb268
|
|
17
|
+
new file mode 100644
|
|
18
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
4
19
|
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-6ef69510e99ece5d b/.bun-tag-6ef69510e99ece5d
|
|
5
20
|
new file mode 100644
|
|
6
21
|
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
7
22
|
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-80c5d241789d23ea b/.bun-tag-80c5d241789d23ea
|
|
8
23
|
new file mode 100644
|
|
9
24
|
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
25
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-8c139481f6770606 b/.bun-tag-8c139481f6770606
|
|
26
|
+
new file mode 100644
|
|
27
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
28
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-93fb9cd92e52c135 b/.bun-tag-93fb9cd92e52c135
|
|
29
|
+
new file mode 100644
|
|
30
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
31
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-a192c07cdd151919 b/.bun-tag-a192c07cdd151919
|
|
32
|
+
new file mode 100644
|
|
33
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
34
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-a6be946c74d62c48 b/.bun-tag-a6be946c74d62c48
|
|
35
|
+
new file mode 100644
|
|
36
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
37
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-bd9424a2f5d10a5e b/.bun-tag-bd9424a2f5d10a5e
|
|
38
|
+
new file mode 100644
|
|
39
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
40
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-f064cf221732e6fa b/.bun-tag-f064cf221732e6fa
|
|
41
|
+
new file mode 100644
|
|
42
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
43
|
+
diff --git a/node_modules/@tintinweb/pi-subagents/.bun-tag-fcccdb30b0e20493 b/.bun-tag-fcccdb30b0e20493
|
|
44
|
+
new file mode 100644
|
|
45
|
+
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
|
46
|
+
diff --git a/dist/agent-manager.js b/dist/agent-manager.js
|
|
47
|
+
index 5619e266cb828b197965c4953ee9fff8a9d3a9a7..23de2b5bc0a27acbcbb1f133625d3c95bd9a5f7f 100644
|
|
48
|
+
--- a/dist/agent-manager.js
|
|
49
|
+
+++ b/dist/agent-manager.js
|
|
50
|
+
@@ -50,11 +50,14 @@ export class AgentManager {
|
|
51
|
+
queue = [];
|
|
52
|
+
/** Number of currently running background agents. */
|
|
53
|
+
runningBackground = 0;
|
|
54
|
+
- constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart, onCompact) {
|
|
55
|
+
+ constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart, onCompact, supervisedKey) {
|
|
56
|
+
+ this.supervisedById = new Map();
|
|
57
|
+
+ this.reaps = new Map();
|
|
58
|
+
this.onComplete = onComplete;
|
|
59
|
+
this.onStart = onStart;
|
|
60
|
+
this.onCompact = onCompact;
|
|
61
|
+
this.maxConcurrent = maxConcurrent;
|
|
62
|
+
+ this.supervisedKey = supervisedKey;
|
|
63
|
+
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
64
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
65
|
+
this.cleanupInterval.unref();
|
|
66
|
+
@@ -77,7 +80,18 @@ export class AgentManager {
|
|
67
|
+
// call, not minutes later at drain. Throw (not warn): programmatic callers
|
|
68
|
+
// can fix and retry; the RPC layer converts throws into error envelopes.
|
|
69
|
+
assertValidSpawnCwd(options.cwd);
|
|
70
|
+
+ if (Object.prototype.hasOwnProperty.call(options, "allowSupervisedCommands")) {
|
|
71
|
+
+ throw new Error("allowSupervisedCommands is not a public spawn option");
|
|
72
|
+
+ }
|
|
73
|
+
+ const supervisedPath = this.supervisedKey ? options[this.supervisedKey] : undefined;
|
|
74
|
+
+ if (typeof supervisedPath === "string" && supervisedPath.length === 0) {
|
|
75
|
+
+ throw new Error("supervised command bundle is missing");
|
|
76
|
+
+ }
|
|
77
|
+
const id = randomUUID().slice(0, 17);
|
|
78
|
+
+ if (typeof supervisedPath === "string")
|
|
79
|
+
+ this.supervisedById.set(id, supervisedPath);
|
|
80
|
+
+ else
|
|
81
|
+
+ this.supervisedById.delete(id);
|
|
82
|
+
const abortController = new AbortController();
|
|
83
|
+
const record = {
|
|
84
|
+
id,
|
|
85
|
+
@@ -166,6 +180,7 @@ export class AgentManager {
|
|
86
|
+
isolated: options.isolated,
|
|
87
|
+
inheritContext: options.inheritContext,
|
|
88
|
+
thinkingLevel: options.thinkingLevel,
|
|
89
|
+
+ allowSupervisedCommands: this.supervisedById.get(id),
|
|
90
|
+
// Worktree wins for the working dir (the agent must run in the copy —
|
|
91
|
+
// which, with a custom cwd, was created from that target). Config stays
|
|
92
|
+
// with the parent project when a caller-supplied cwd is in play; it must
|
|
93
|
+
@@ -202,25 +217,29 @@ export class AgentManager {
|
|
94
|
+
options.onSessionCreated?.(session);
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
- .then(({ responseText, session, aborted, steered, failure }) => {
|
|
98
|
+
+ .then(async ({ responseText, session, aborted, steered, failure }) => {
|
|
99
|
+
+ let nextStatus = record.status;
|
|
100
|
+
// Don't overwrite status if externally stopped via abort()
|
|
101
|
+
if (record.status !== "stopped") {
|
|
102
|
+
// Precedence: a hard abort keeps "aborted"; then a failed final turn
|
|
103
|
+
// (provider error that pi resolved instead of rejecting, #144) is an
|
|
104
|
+
// honest "error" — not a completion with an empty or stale result.
|
|
105
|
+
if (aborted) {
|
|
106
|
+
- record.status = "aborted";
|
|
107
|
+
+ nextStatus = "aborted";
|
|
108
|
+
}
|
|
109
|
+
else if (failure) {
|
|
110
|
+
- record.status = "error";
|
|
111
|
+
+ nextStatus = "error";
|
|
112
|
+
record.error = failure;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
- record.status = steered ? "steered" : "completed";
|
|
116
|
+
+ nextStatus = steered ? "steered" : "completed";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
record.result = responseText;
|
|
120
|
+
record.session = session;
|
|
121
|
+
+ await this.finalize(id, record, nextStatus === "error" ? "error" : "complete");
|
|
122
|
+
+ if (record.status !== "stopped")
|
|
123
|
+
+ record.status = nextStatus;
|
|
124
|
+
record.completedAt ??= Date.now();
|
|
125
|
+
detach();
|
|
126
|
+
// Final flush of streaming output file
|
|
127
|
+
@@ -262,12 +281,11 @@ export class AgentManager {
|
|
128
|
+
}
|
|
129
|
+
return responseText;
|
|
130
|
+
})
|
|
131
|
+
- .catch((err) => {
|
|
132
|
+
- // Don't overwrite status if externally stopped via abort()
|
|
133
|
+
- if (record.status !== "stopped") {
|
|
134
|
+
- record.status = "error";
|
|
135
|
+
- }
|
|
136
|
+
+ .catch(async (err) => {
|
|
137
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
138
|
+
+ await this.finalize(id, record, "error");
|
|
139
|
+
+ if (record.status !== "stopped")
|
|
140
|
+
+ record.status = "error";
|
|
141
|
+
record.completedAt ??= Date.now();
|
|
142
|
+
detach();
|
|
143
|
+
// Final flush of streaming output file on error
|
|
144
|
+
@@ -439,10 +457,27 @@ export class AgentManager {
|
|
145
|
+
if (record.status !== "running")
|
|
146
|
+
return false;
|
|
147
|
+
record.abortController?.abort();
|
|
148
|
+
- record.status = "stopped";
|
|
149
|
+
- record.completedAt = Date.now();
|
|
150
|
+
+ const done = this.finalize(id, record, "abort").then(() => {
|
|
151
|
+
+ record.status = "stopped";
|
|
152
|
+
+ record.completedAt ??= Date.now();
|
|
153
|
+
+ });
|
|
154
|
+
+ this.reaps.set(id, done);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
+ finalize(id, record, reason) {
|
|
158
|
+
+ const existing = this.reaps.get(id);
|
|
159
|
+
+ if (existing)
|
|
160
|
+
+ return existing;
|
|
161
|
+
+ const work = (async () => {
|
|
162
|
+
+ const session = record.session;
|
|
163
|
+
+ const runner = session?._extensionRunner;
|
|
164
|
+
+ if (runner?.emit)
|
|
165
|
+
+ await runner.emit({ type: "session_shutdown", reason });
|
|
166
|
+
+ this.supervisedById.delete(id);
|
|
167
|
+
+ })();
|
|
168
|
+
+ this.reaps.set(id, work);
|
|
169
|
+
+ return work;
|
|
170
|
+
+ }
|
|
171
|
+
/** Dispose a record's session and remove it from the map. */
|
|
172
|
+
removeRecord(id, record) {
|
|
173
|
+
record.session?.dispose?.();
|
|
174
|
+
@@ -478,28 +513,14 @@ export class AgentManager {
|
|
175
|
+
hasRunning() {
|
|
176
|
+
return [...this.agents.values()].some(r => r.status === "running" || r.status === "queued");
|
|
177
|
+
}
|
|
178
|
+
- /** Abort all running and queued agents immediately. */
|
|
179
|
+
- abortAll() {
|
|
180
|
+
+ async abortAll() {
|
|
181
|
+
let count = 0;
|
|
182
|
+
- // Clear queued agents first
|
|
183
|
+
- for (const queued of this.queue) {
|
|
184
|
+
- const record = this.agents.get(queued.id);
|
|
185
|
+
- if (record) {
|
|
186
|
+
- record.status = "stopped";
|
|
187
|
+
- record.completedAt = Date.now();
|
|
188
|
+
+ const ids = [...this.agents.keys()];
|
|
189
|
+
+ for (const id of ids) {
|
|
190
|
+
+ if (this.abort(id))
|
|
191
|
+
count++;
|
|
192
|
+
- }
|
|
193
|
+
- }
|
|
194
|
+
- this.queue = [];
|
|
195
|
+
- // Abort running agents
|
|
196
|
+
- for (const record of this.agents.values()) {
|
|
197
|
+
- if (record.status === "running") {
|
|
198
|
+
- record.abortController?.abort();
|
|
199
|
+
- record.status = "stopped";
|
|
200
|
+
- record.completedAt = Date.now();
|
|
201
|
+
- count++;
|
|
202
|
+
- }
|
|
203
|
+
}
|
|
204
|
+
+ await Promise.all([...this.reaps.values()]);
|
|
205
|
+
return count;
|
|
206
|
+
}
|
|
207
|
+
/** Wait for all running and queued agents to complete (including queued ones). */
|
|
208
|
+
@@ -517,14 +538,16 @@ export class AgentManager {
|
|
209
|
+
await Promise.allSettled(pending);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
- dispose() {
|
|
213
|
+
+ async dispose() {
|
|
214
|
+
clearInterval(this.cleanupInterval);
|
|
215
|
+
- // Clear queue
|
|
216
|
+
+ await this.abortAll();
|
|
217
|
+
this.queue = [];
|
|
218
|
+
- for (const record of this.agents.values()) {
|
|
219
|
+
- record.session?.dispose();
|
|
220
|
+
+ for (const [id, record] of this.agents) {
|
|
221
|
+
+ this.supervisedById.delete(id);
|
|
222
|
+
+ record.session?.dispose?.();
|
|
223
|
+
}
|
|
224
|
+
this.agents.clear();
|
|
225
|
+
+ this.supervisedById.clear();
|
|
226
|
+
// Prune any orphaned git worktrees (crash recovery)
|
|
227
|
+
try {
|
|
228
|
+
pruneWorktrees(process.cwd());
|
|
229
|
+
diff --git a/dist/agent-runner.js b/dist/agent-runner.js
|
|
230
|
+
index 40df116e6afdd84b6b9c74c65ddec25a14ce66bb..a38552e22b5cd0683ef438885c0c97e336e017fc 100644
|
|
231
|
+
--- a/dist/agent-runner.js
|
|
232
|
+
+++ b/dist/agent-runner.js
|
|
233
|
+
@@ -1,9 +1,10 @@
|
|
234
|
+
/**
|
|
235
|
+
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
236
|
+
*/
|
|
237
|
+
-import { readFileSync } from "node:fs";
|
|
238
|
+
+import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
239
|
+
import { homedir } from "node:os";
|
|
240
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
241
|
+
+import { fileURLToPath } from "node:url";
|
|
242
|
+
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
243
|
+
import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
|
|
244
|
+
import { buildParentContext, extractText } from "./context.js";
|
|
245
|
+
@@ -23,6 +24,26 @@ export const SUBAGENT_TOOL_NAMES = {
|
|
246
|
+
GET_RESULT: "get_subagent_result",
|
|
247
|
+
STEER: "steer_subagent",
|
|
248
|
+
};
|
|
249
|
+
+export function supervisedSiblingPath() {
|
|
250
|
+
+ const fromEnv = process.env.LARKIN_PI_SUPERVISED_BUNDLE;
|
|
251
|
+
+ if (typeof fromEnv === "string" && fromEnv.length > 0)
|
|
252
|
+
+ return fromEnv;
|
|
253
|
+
+ return join(dirname(fileURLToPath(import.meta.url)), "pi-supervised-command.bundle.js");
|
|
254
|
+
+}
|
|
255
|
+
+function resolveTrustedSupervisedBundle(candidate) {
|
|
256
|
+
+ if (candidate === undefined)
|
|
257
|
+
+ return undefined;
|
|
258
|
+
+ if (typeof candidate !== "string" || candidate.length === 0)
|
|
259
|
+
+ throw new Error("supervised command bundle is missing");
|
|
260
|
+
+ const sibling = supervisedSiblingPath();
|
|
261
|
+
+ for (const file of [sibling, candidate]) {
|
|
262
|
+
+ if (!existsSync(file) || lstatSync(file).isSymbolicLink() || !lstatSync(file).isFile())
|
|
263
|
+
+ throw new Error("supervised command bundle is missing");
|
|
264
|
+
+ }
|
|
265
|
+
+ if (realpathSync(candidate) !== realpathSync(sibling))
|
|
266
|
+
+ throw new Error("supervised command bundle is missing");
|
|
267
|
+
+ return realpathSync(sibling);
|
|
268
|
+
+}
|
|
269
|
+
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
270
|
+
const EXCLUDED_TOOL_NAMES = Object.values(SUBAGENT_TOOL_NAMES);
|
|
271
|
+
/**
|
|
272
|
+
@@ -483,7 +504,11 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
273
|
+
// It's only needed when we're neither loading everything without excludes
|
|
274
|
+
// (`extensions: true` or a `"*"` wildcard) nor nothing (`noExtensions`).
|
|
275
|
+
const loadAll = extensions === true || extensionsSpec?.wildcard === true;
|
|
276
|
+
- const additionalExtensionPaths = extensionsSpec?.paths.length ? extensionsSpec.paths : undefined;
|
|
277
|
+
+ const supervisedPath = resolveTrustedSupervisedBundle(options.allowSupervisedCommands);
|
|
278
|
+
+ const additionalExtensionPaths = [
|
|
279
|
+
+ ...(extensionsSpec?.paths ?? []),
|
|
280
|
+
+ ...(supervisedPath ? [supervisedPath] : []),
|
|
281
|
+
+ ];
|
|
282
|
+
// Pre-filter discovered set, captured by the override — the exclude-typo warning
|
|
283
|
+
// must compare against this, not the surviving set (absence from survivors is
|
|
284
|
+
// an exclude *succeeding*).
|
|
285
|
+
@@ -506,7 +531,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
286
|
+
cwd: configCwd,
|
|
287
|
+
agentDir,
|
|
288
|
+
noExtensions,
|
|
289
|
+
- additionalExtensionPaths,
|
|
290
|
+
+ additionalExtensionPaths: additionalExtensionPaths.length ? additionalExtensionPaths : undefined,
|
|
291
|
+
extensionsOverride,
|
|
292
|
+
noSkills,
|
|
293
|
+
noPromptTemplates: true,
|
|
10
294
|
diff --git a/dist/index.js b/dist/index.js
|
|
11
|
-
index ab65ef15b5c1da803c0378b1d0ba383645198028..
|
|
295
|
+
index ab65ef15b5c1da803c0378b1d0ba383645198028..c6e7ccd02f016c36c6f098323e88c07063862805 100644
|
|
12
296
|
--- a/dist/index.js
|
|
13
297
|
+++ b/dist/index.js
|
|
14
|
-
@@ -
|
|
298
|
+
@@ -10,12 +10,13 @@
|
|
299
|
+
* /agents — Interactive agent management menu
|
|
300
|
+
*/
|
|
301
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
302
|
+
-import { join } from "node:path";
|
|
303
|
+
+import { dirname, join } from "node:path";
|
|
304
|
+
+import { fileURLToPath } from "node:url";
|
|
305
|
+
import { defineTool, getAgentDir, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
306
|
+
import { Container, Key, matchesKey, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
307
|
+
import { Type } from "@sinclair/typebox";
|
|
308
|
+
import { AgentManager } from "./agent-manager.js";
|
|
309
|
+
-import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
310
|
+
+import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent, supervisedSiblingPath } from "./agent-runner.js";
|
|
311
|
+
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
|
|
312
|
+
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
313
|
+
import { loadCustomAgents } from "./custom-agents.js";
|
|
314
|
+
@@ -69,6 +70,45 @@ function abortable(promise, signal) {
|
|
15
315
|
});
|
|
16
316
|
});
|
|
17
317
|
}
|
|
@@ -53,10 +353,11 @@ index ab65ef15b5c1da803c0378b1d0ba383645198028..dc18e00d2ce4bddb6ee77608364d3c8b
|
|
|
53
353
|
+ return (await waitWithDeadline(record.promise, remainingMs, signal)).timedOut;
|
|
54
354
|
+}
|
|
55
355
|
+const LARKIN_BOUNDED_WAIT_CAPABILITY = "larkin-pi-subagents-bounded-wait-v1";
|
|
356
|
+
+const LARKIN_SUPERVISED_COMMAND_CAPABILITY = "larkin-pi-supervised-command-v1";
|
|
56
357
|
export function renderRunningAgentStatus(frame, statsText, activity, theme) {
|
|
57
358
|
const container = new Container();
|
|
58
359
|
container.addChild(new Text(theme.fg("accent", frame) + (statsText ? " " + statsText : ""), 0, 0));
|
|
59
|
-
@@ -280,9 +
|
|
360
|
+
@@ -280,9 +320,14 @@ export default function (pi) {
|
|
60
361
|
// before they reach pi.sendMessage (fire-and-forget).
|
|
61
362
|
const pendingNudges = new Map();
|
|
62
363
|
const NUDGE_HOLD_MS = 200;
|
|
@@ -72,7 +373,53 @@ index ab65ef15b5c1da803c0378b1d0ba383645198028..dc18e00d2ce4bddb6ee77608364d3c8b
|
|
|
72
373
|
function scheduleNudge(key, send, delay = NUDGE_HOLD_MS) {
|
|
73
374
|
cancelNudge(key);
|
|
74
375
|
pendingNudges.set(key, setTimeout(() => {
|
|
75
|
-
@@ -
|
|
376
|
+
@@ -378,6 +423,13 @@ export default function (pi) {
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// Background completion: route through group join or send individual nudge
|
|
380
|
+
+ function supervisedCommandBundlePath() {
|
|
381
|
+
+ const sibling = supervisedSiblingPath();
|
|
382
|
+
+ if (!existsSync(sibling))
|
|
383
|
+
+ throw new Error("supervised command bundle is missing");
|
|
384
|
+
+ return sibling;
|
|
385
|
+
+ }
|
|
386
|
+
+ const supervisedKey = Symbol("larkin-supervised-commands");
|
|
387
|
+
const manager = new AgentManager((record) => {
|
|
388
|
+
// Emit lifecycle event based on terminal status
|
|
389
|
+
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
390
|
+
@@ -432,7 +484,7 @@ export default function (pi) {
|
|
391
|
+
tokensBefore: info.tokensBefore,
|
|
392
|
+
compactionCount: record.compactionCount,
|
|
393
|
+
});
|
|
394
|
+
- });
|
|
395
|
+
+ }, supervisedKey);
|
|
396
|
+
// Expose manager via Symbol.for() global registry for cross-package access.
|
|
397
|
+
// Standard Node.js pattern for cross-package singletons (used by OpenTelemetry, etc.).
|
|
398
|
+
//
|
|
399
|
+
@@ -525,12 +577,12 @@ export default function (pi) {
|
|
400
|
+
delete globalThis[MANAGER_KEY];
|
|
401
|
+
}
|
|
402
|
+
scheduler.stop();
|
|
403
|
+
- manager.abortAll();
|
|
404
|
+
+ await manager.abortAll();
|
|
405
|
+
for (const timer of pendingNudges.values())
|
|
406
|
+
clearTimeout(timer);
|
|
407
|
+
pendingNudges.clear();
|
|
408
|
+
fleet.dispose();
|
|
409
|
+
- manager.dispose();
|
|
410
|
+
+ await manager.dispose();
|
|
411
|
+
});
|
|
412
|
+
// Live widget: show running agents above editor.
|
|
413
|
+
// widgetMode (default "background") selects what the widget shows: "all" =
|
|
414
|
+
@@ -1124,6 +1176,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
415
|
+
inheritContext,
|
|
416
|
+
thinkingLevel: thinking,
|
|
417
|
+
isBackground: true,
|
|
418
|
+
+ ...(params.run_in_background === true ? { [supervisedKey]: supervisedCommandBundlePath() } : {}),
|
|
419
|
+
isolation,
|
|
420
|
+
invocation: agentInvocation,
|
|
421
|
+
...bgCallbacks,
|
|
422
|
+
@@ -1293,6 +1346,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
76
423
|
wait: Type.Optional(Type.Boolean({
|
|
77
424
|
description: "If true, wait for the agent to complete before returning. Default: false.",
|
|
78
425
|
})),
|
|
@@ -82,7 +429,7 @@ index ab65ef15b5c1da803c0378b1d0ba383645198028..dc18e00d2ce4bddb6ee77608364d3c8b
|
|
|
82
429
|
verbose: Type.Optional(Type.Boolean({
|
|
83
430
|
description: "If true, include the agent's full conversation (messages + tool calls). Default: false.",
|
|
84
431
|
})),
|
|
85
|
-
@@ -1307,12 +
|
|
432
|
+
@@ -1307,12 +1363,15 @@ Terse command-style prompts produce shallow, generic work.
|
|
86
433
|
// completion notification can still be delivered.
|
|
87
434
|
// Queued agents have no promise yet (it's created when the queue starts
|
|
88
435
|
// them), so poll until they leave the queue, then await like a running one.
|
|
@@ -103,7 +450,7 @@ index ab65ef15b5c1da803c0378b1d0ba383645198028..dc18e00d2ce4bddb6ee77608364d3c8b
|
|
|
103
450
|
}
|
|
104
451
|
const displayName = getDisplayName(record.type);
|
|
105
452
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
106
|
-
@@ -1327,10 +
|
|
453
|
+
@@ -1327,10 +1386,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
107
454
|
statsParts.push(`Compactions: ${record.compactionCount}`);
|
|
108
455
|
statsParts.push(`Duration: ${duration}`);
|
|
109
456
|
let output = `Agent: ${record.id}\n` +
|
|
@@ -119,14 +466,318 @@ index ab65ef15b5c1da803c0378b1d0ba383645198028..dc18e00d2ce4bddb6ee77608364d3c8b
|
|
|
119
466
|
}
|
|
120
467
|
else if (record.status === "error") {
|
|
121
468
|
output += `Error: ${record.error}${partialOutputSuffix(record)}`;
|
|
469
|
+
diff --git a/src/agent-manager.ts b/src/agent-manager.ts
|
|
470
|
+
index 318f88588a05aa1459e7c86288aa886ea85a28b1..8e552a73b9ef1e269a2a8f712e50ae1382c54ad6 100644
|
|
471
|
+
--- a/src/agent-manager.ts
|
|
472
|
+
+++ b/src/agent-manager.ts
|
|
473
|
+
@@ -113,16 +113,21 @@ export class AgentManager {
|
|
474
|
+
/** Number of currently running background agents. */
|
|
475
|
+
private runningBackground = 0;
|
|
476
|
+
|
|
477
|
+
+ private supervisedKey?: symbol;
|
|
478
|
+
+ private supervisedById = new Map<string, string>();
|
|
479
|
+
+
|
|
480
|
+
constructor(
|
|
481
|
+
onComplete?: OnAgentComplete,
|
|
482
|
+
maxConcurrent = DEFAULT_MAX_CONCURRENT,
|
|
483
|
+
onStart?: OnAgentStart,
|
|
484
|
+
onCompact?: OnAgentCompact,
|
|
485
|
+
+ supervisedKey?: symbol,
|
|
486
|
+
) {
|
|
487
|
+
this.onComplete = onComplete;
|
|
488
|
+
this.onStart = onStart;
|
|
489
|
+
this.onCompact = onCompact;
|
|
490
|
+
this.maxConcurrent = maxConcurrent;
|
|
491
|
+
+ this.supervisedKey = supervisedKey;
|
|
492
|
+
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
493
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
494
|
+
this.cleanupInterval.unref();
|
|
495
|
+
@@ -154,8 +159,19 @@ export class AgentManager {
|
|
496
|
+
// call, not minutes later at drain. Throw (not warn): programmatic callers
|
|
497
|
+
// can fix and retry; the RPC layer converts throws into error envelopes.
|
|
498
|
+
assertValidSpawnCwd(options.cwd);
|
|
499
|
+
+ if (Object.prototype.hasOwnProperty.call(options, "allowSupervisedCommands")) {
|
|
500
|
+
+ throw new Error("allowSupervisedCommands is not a public spawn option");
|
|
501
|
+
+ }
|
|
502
|
+
+ const supervisedPath = this.supervisedKey
|
|
503
|
+
+ ? (options as Record<symbol, unknown>)[this.supervisedKey]
|
|
504
|
+
+ : undefined;
|
|
505
|
+
+ if (typeof supervisedPath === "string" && supervisedPath.length === 0) {
|
|
506
|
+
+ throw new Error("supervised command bundle is missing");
|
|
507
|
+
+ }
|
|
508
|
+
|
|
509
|
+
const id = randomUUID().slice(0, 17);
|
|
510
|
+
+ if (typeof supervisedPath === "string") this.supervisedById.set(id, supervisedPath);
|
|
511
|
+
+ else this.supervisedById.delete(id);
|
|
512
|
+
const abortController = new AbortController();
|
|
513
|
+
const record: AgentRecord = {
|
|
514
|
+
id,
|
|
515
|
+
@@ -190,6 +206,7 @@ export class AgentManager {
|
|
516
|
+
try {
|
|
517
|
+
this.startAgent(id, record, args);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
+ this.supervisedById.delete(id);
|
|
520
|
+
this.agents.delete(id);
|
|
521
|
+
throw err;
|
|
522
|
+
}
|
|
523
|
+
@@ -252,6 +269,7 @@ export class AgentManager {
|
|
524
|
+
isolated: options.isolated,
|
|
525
|
+
inheritContext: options.inheritContext,
|
|
526
|
+
thinkingLevel: options.thinkingLevel,
|
|
527
|
+
+ allowSupervisedCommands: this.supervisedById.get(id),
|
|
528
|
+
// Worktree wins for the working dir (the agent must run in the copy —
|
|
529
|
+
// which, with a custom cwd, was created from that target). Config stays
|
|
530
|
+
// with the parent project when a caller-supplied cwd is in play; it must
|
|
531
|
+
@@ -287,23 +305,26 @@ export class AgentManager {
|
|
532
|
+
options.onSessionCreated?.(session);
|
|
533
|
+
},
|
|
534
|
+
})
|
|
535
|
+
- .then(({ responseText, session, aborted, steered, failure }) => {
|
|
536
|
+
+ .then(async ({ responseText, session, aborted, steered, failure }) => {
|
|
537
|
+
// Don't overwrite status if externally stopped via abort()
|
|
538
|
+
+ let nextStatus = record.status;
|
|
539
|
+
if (record.status !== "stopped") {
|
|
540
|
+
// Precedence: a hard abort keeps "aborted"; then a failed final turn
|
|
541
|
+
// (provider error that pi resolved instead of rejecting, #144) is an
|
|
542
|
+
// honest "error" — not a completion with an empty or stale result.
|
|
543
|
+
if (aborted) {
|
|
544
|
+
- record.status = "aborted";
|
|
545
|
+
+ nextStatus = "aborted";
|
|
546
|
+
} else if (failure) {
|
|
547
|
+
- record.status = "error";
|
|
548
|
+
+ nextStatus = "error";
|
|
549
|
+
record.error = failure;
|
|
550
|
+
} else {
|
|
551
|
+
- record.status = steered ? "steered" : "completed";
|
|
552
|
+
+ nextStatus = steered ? "steered" : "completed";
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
record.result = responseText;
|
|
556
|
+
record.session = session;
|
|
557
|
+
+ await this.finalize(id, record, nextStatus === "error" ? "error" : "complete");
|
|
558
|
+
+ if (record.status !== "stopped") record.status = nextStatus;
|
|
559
|
+
record.completedAt ??= Date.now();
|
|
560
|
+
|
|
561
|
+
detach();
|
|
562
|
+
@@ -339,12 +360,10 @@ export class AgentManager {
|
|
563
|
+
}
|
|
564
|
+
return responseText;
|
|
565
|
+
})
|
|
566
|
+
- .catch((err) => {
|
|
567
|
+
- // Don't overwrite status if externally stopped via abort()
|
|
568
|
+
- if (record.status !== "stopped") {
|
|
569
|
+
- record.status = "error";
|
|
570
|
+
- }
|
|
571
|
+
+ .catch(async (err) => {
|
|
572
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
573
|
+
+ await this.finalize(id, record, "error");
|
|
574
|
+
+ if (record.status !== "stopped") record.status = "error";
|
|
575
|
+
record.completedAt ??= Date.now();
|
|
576
|
+
|
|
577
|
+
detach();
|
|
578
|
+
@@ -525,18 +544,41 @@ export class AgentManager {
|
|
579
|
+
this.queue = this.queue.filter(q => q.id !== id);
|
|
580
|
+
record.status = "stopped";
|
|
581
|
+
record.completedAt = Date.now();
|
|
582
|
+
+ this.supervisedById.delete(id);
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
- if (record.status !== "running") return false;
|
|
587
|
+
+ if (record.status !== "running") {
|
|
588
|
+
+ this.supervisedById.delete(id);
|
|
589
|
+
+ return false;
|
|
590
|
+
+ }
|
|
591
|
+
record.abortController?.abort();
|
|
592
|
+
- record.status = "stopped";
|
|
593
|
+
- record.completedAt = Date.now();
|
|
594
|
+
+ const done = this.finalize(id, record, "abort").then(() => {
|
|
595
|
+
+ record.status = "stopped";
|
|
596
|
+
+ record.completedAt ??= Date.now();
|
|
597
|
+
+ });
|
|
598
|
+
+ this.reaps.set(id, done);
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
+ private reaps = new Map<string, Promise<void>>();
|
|
603
|
+
+
|
|
604
|
+
+ private finalize(id: string, record: AgentRecord, reason: string): Promise<void> {
|
|
605
|
+
+ const existing = this.reaps.get(id);
|
|
606
|
+
+ if (existing) return existing;
|
|
607
|
+
+ const work = (async () => {
|
|
608
|
+
+ const session = record.session as { _extensionRunner?: { emit?: (event: unknown) => Promise<unknown> } } | undefined;
|
|
609
|
+
+ const runner = session?._extensionRunner;
|
|
610
|
+
+ if (runner?.emit) await runner.emit({ type: "session_shutdown", reason });
|
|
611
|
+
+ this.supervisedById.delete(id);
|
|
612
|
+
+ })();
|
|
613
|
+
+ this.reaps.set(id, work);
|
|
614
|
+
+ return work;
|
|
615
|
+
+ }
|
|
616
|
+
+
|
|
617
|
+
/** Dispose a record's session and remove it from the map. */
|
|
618
|
+
private removeRecord(id: string, record: AgentRecord): void {
|
|
619
|
+
+ this.supervisedById.delete(id);
|
|
620
|
+
record.session?.dispose?.();
|
|
621
|
+
record.session = undefined;
|
|
622
|
+
this.agents.delete(id);
|
|
623
|
+
@@ -572,28 +614,14 @@ export class AgentManager {
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
- /** Abort all running and queued agents immediately. */
|
|
628
|
+
- abortAll(): number {
|
|
629
|
+
+ /** Abort all running and queued agents and wait until child session_shutdown/reap finishes. */
|
|
630
|
+
+ async abortAll(): Promise<number> {
|
|
631
|
+
let count = 0;
|
|
632
|
+
- // Clear queued agents first
|
|
633
|
+
- for (const queued of this.queue) {
|
|
634
|
+
- const record = this.agents.get(queued.id);
|
|
635
|
+
- if (record) {
|
|
636
|
+
- record.status = "stopped";
|
|
637
|
+
- record.completedAt = Date.now();
|
|
638
|
+
- count++;
|
|
639
|
+
- }
|
|
640
|
+
- }
|
|
641
|
+
- this.queue = [];
|
|
642
|
+
- // Abort running agents
|
|
643
|
+
- for (const record of this.agents.values()) {
|
|
644
|
+
- if (record.status === "running") {
|
|
645
|
+
- record.abortController?.abort();
|
|
646
|
+
- record.status = "stopped";
|
|
647
|
+
- record.completedAt = Date.now();
|
|
648
|
+
- count++;
|
|
649
|
+
- }
|
|
650
|
+
+ const ids = [...this.agents.keys()];
|
|
651
|
+
+ for (const id of ids) {
|
|
652
|
+
+ if (this.abort(id)) count++;
|
|
653
|
+
}
|
|
654
|
+
+ await Promise.all([...this.reaps.values()]);
|
|
655
|
+
return count;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
@@ -612,14 +640,17 @@ export class AgentManager {
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
- dispose() {
|
|
663
|
+
+ async dispose() {
|
|
664
|
+
clearInterval(this.cleanupInterval);
|
|
665
|
+
- // Clear queue
|
|
666
|
+
+ await this.abortAll();
|
|
667
|
+
this.queue = [];
|
|
668
|
+
- for (const record of this.agents.values()) {
|
|
669
|
+
- record.session?.dispose();
|
|
670
|
+
+ for (const [id, record] of this.agents) {
|
|
671
|
+
+ this.supervisedById.delete(id);
|
|
672
|
+
+ const session = record.session as { dispose?: () => void } | undefined;
|
|
673
|
+
+ session?.dispose?.();
|
|
674
|
+
}
|
|
675
|
+
this.agents.clear();
|
|
676
|
+
+ this.supervisedById.clear();
|
|
677
|
+
// Prune any orphaned git worktrees (crash recovery)
|
|
678
|
+
try { pruneWorktrees(process.cwd()); } catch { /* ignore */ }
|
|
679
|
+
// Also prune repos that caller-supplied cwds created worktrees in — a clean
|
|
680
|
+
diff --git a/src/agent-runner.ts b/src/agent-runner.ts
|
|
681
|
+
index 444eb84ab3769c6dba8d1e764b88ac99d968d392..f297775376233e0c66c586d9fe186df378b068f5 100644
|
|
682
|
+
--- a/src/agent-runner.ts
|
|
683
|
+
+++ b/src/agent-runner.ts
|
|
684
|
+
@@ -2,9 +2,10 @@
|
|
685
|
+
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
686
|
+
*/
|
|
687
|
+
|
|
688
|
+
-import { readFileSync } from "node:fs";
|
|
689
|
+
+import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
690
|
+
import { homedir } from "node:os";
|
|
691
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
692
|
+
+import { fileURLToPath } from "node:url";
|
|
693
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
694
|
+
import type { ExtensionContext, LoadExtensionsResult } from "@earendil-works/pi-coding-agent";
|
|
695
|
+
import {
|
|
696
|
+
@@ -38,6 +39,29 @@ export const SUBAGENT_TOOL_NAMES = {
|
|
697
|
+
STEER: "steer_subagent",
|
|
698
|
+
} as const;
|
|
699
|
+
|
|
700
|
+
+export function supervisedSiblingPath(): string {
|
|
701
|
+
+ const fromEnv = process.env.LARKIN_PI_SUPERVISED_BUNDLE;
|
|
702
|
+
+ if (typeof fromEnv === "string" && fromEnv.length > 0) return fromEnv;
|
|
703
|
+
+ return join(dirname(fileURLToPath(import.meta.url)), "pi-supervised-command.bundle.js");
|
|
704
|
+
+}
|
|
705
|
+
+
|
|
706
|
+
+function resolveTrustedSupervisedBundle(candidate: string | undefined): string | undefined {
|
|
707
|
+
+ if (candidate === undefined) return undefined;
|
|
708
|
+
+ if (typeof candidate !== "string" || candidate.length === 0) {
|
|
709
|
+
+ throw new Error("supervised command bundle is missing");
|
|
710
|
+
+ }
|
|
711
|
+
+ const sibling = supervisedSiblingPath();
|
|
712
|
+
+ for (const file of [sibling, candidate]) {
|
|
713
|
+
+ if (!existsSync(file) || lstatSync(file).isSymbolicLink() || !lstatSync(file).isFile()) {
|
|
714
|
+
+ throw new Error("supervised command bundle is missing");
|
|
715
|
+
+ }
|
|
716
|
+
+ }
|
|
717
|
+
+ if (realpathSync(candidate) !== realpathSync(sibling)) {
|
|
718
|
+
+ throw new Error("supervised command bundle is missing");
|
|
719
|
+
+ }
|
|
720
|
+
+ return realpathSync(sibling);
|
|
721
|
+
+}
|
|
722
|
+
+
|
|
723
|
+
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
724
|
+
const EXCLUDED_TOOL_NAMES: string[] = Object.values(SUBAGENT_TOOL_NAMES);
|
|
725
|
+
|
|
726
|
+
@@ -358,6 +382,8 @@ export interface RunOptions {
|
|
727
|
+
isolated?: boolean;
|
|
728
|
+
inheritContext?: boolean;
|
|
729
|
+
thinkingLevel?: ThinkingLevel;
|
|
730
|
+
+ /** Absolute path to the supervised-command extension. Set only by Agent(run_in_background: true). */
|
|
731
|
+
+ allowSupervisedCommands?: string;
|
|
732
|
+
/** Override working directory (e.g. for worktree isolation). */
|
|
733
|
+
cwd?: string;
|
|
734
|
+
/**
|
|
735
|
+
@@ -612,7 +638,11 @@ export async function runAgent(
|
|
736
|
+
// It's only needed when we're neither loading everything without excludes
|
|
737
|
+
// (`extensions: true` or a `"*"` wildcard) nor nothing (`noExtensions`).
|
|
738
|
+
const loadAll = extensions === true || extensionsSpec?.wildcard === true;
|
|
739
|
+
- const additionalExtensionPaths = extensionsSpec?.paths.length ? extensionsSpec.paths : undefined;
|
|
740
|
+
+ const supervisedPath = resolveTrustedSupervisedBundle(options.allowSupervisedCommands);
|
|
741
|
+
+ const additionalExtensionPaths = [
|
|
742
|
+
+ ...(extensionsSpec?.paths ?? []),
|
|
743
|
+
+ ...(supervisedPath ? [supervisedPath] : []),
|
|
744
|
+
+ ];
|
|
745
|
+
// Pre-filter discovered set, captured by the override — the exclude-typo warning
|
|
746
|
+
// must compare against this, not the surviving set (absence from survivors is
|
|
747
|
+
// an exclude *succeeding*).
|
|
748
|
+
@@ -636,7 +666,7 @@ export async function runAgent(
|
|
749
|
+
cwd: configCwd,
|
|
750
|
+
agentDir,
|
|
751
|
+
noExtensions,
|
|
752
|
+
- additionalExtensionPaths,
|
|
753
|
+
+ additionalExtensionPaths: additionalExtensionPaths.length ? additionalExtensionPaths : undefined,
|
|
754
|
+
extensionsOverride,
|
|
755
|
+
noSkills,
|
|
756
|
+
noPromptTemplates: true,
|
|
122
757
|
diff --git a/src/index.ts b/src/index.ts
|
|
123
|
-
index 828938e6d710dc5a8f99a6185e37f9592b12352d..
|
|
758
|
+
index 828938e6d710dc5a8f99a6185e37f9592b12352d..379fb9567b671d0349699212eabc8e20151f5e95 100644
|
|
124
759
|
--- a/src/index.ts
|
|
125
760
|
+++ b/src/index.ts
|
|
126
|
-
@@ -
|
|
761
|
+
@@ -11,12 +11,13 @@
|
|
762
|
+
*/
|
|
763
|
+
|
|
764
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
765
|
+
-import { join } from "node:path";
|
|
766
|
+
+import { dirname, join } from "node:path";
|
|
767
|
+
+import { fileURLToPath } from "node:url";
|
|
768
|
+
import { defineTool, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, getAgentDir, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
769
|
+
import { Container, Key, matchesKey, type SettingItem, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
770
|
+
import { Type } from "@sinclair/typebox";
|
|
771
|
+
import { AgentManager } from "./agent-manager.js";
|
|
772
|
+
-import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
773
|
+
+import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent, supervisedSiblingPath } from "./agent-runner.js";
|
|
774
|
+
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
|
|
775
|
+
import { type RpcHandle, registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
776
|
+
import { loadCustomAgents } from "./custom-agents.js";
|
|
777
|
+
@@ -91,6 +92,55 @@ function abortable<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
127
778
|
});
|
|
128
779
|
}
|
|
129
|
-
|
|
780
|
+
|
|
130
781
|
+const RESULT_WAIT_QUEUE_POLL_MS = 50;
|
|
131
782
|
+const DEFAULT_RESULT_WAIT_TIMEOUT_MS = 30_000;
|
|
132
783
|
+const MAX_RESULT_WAIT_TIMEOUT_MS = 60_000;
|
|
@@ -174,11 +825,12 @@ index 828938e6d710dc5a8f99a6185e37f9592b12352d..fb2d3119a50c6aa9c8864037af65ff95
|
|
|
174
825
|
+}
|
|
175
826
|
+
|
|
176
827
|
+export const LARKIN_BOUNDED_WAIT_CAPABILITY = "larkin-pi-subagents-bounded-wait-v1";
|
|
828
|
+
+export const LARKIN_SUPERVISED_COMMAND_CAPABILITY = "larkin-pi-supervised-command-v1";
|
|
177
829
|
+
|
|
178
830
|
export function renderRunningAgentStatus(
|
|
179
831
|
frame: string,
|
|
180
832
|
statsText: string,
|
|
181
|
-
@@ -330,10 +
|
|
833
|
+
@@ -330,10 +380,14 @@ export default function (pi: ExtensionAPI) {
|
|
182
834
|
// before they reach pi.sendMessage (fire-and-forget).
|
|
183
835
|
const pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
|
|
184
836
|
const NUDGE_HOLD_MS = 200;
|
|
@@ -195,7 +847,51 @@ index 828938e6d710dc5a8f99a6185e37f9592b12352d..fb2d3119a50c6aa9c8864037af65ff95
|
|
|
195
847
|
function scheduleNudge(key: string, send: () => void, delay = NUDGE_HOLD_MS) {
|
|
196
848
|
cancelNudge(key);
|
|
197
849
|
pendingNudges.set(key, setTimeout(() => {
|
|
198
|
-
@@ -
|
|
850
|
+
@@ -433,6 +487,12 @@ export default function (pi: ExtensionAPI) {
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Background completion: route through group join or send individual nudge
|
|
854
|
+
+ function supervisedCommandBundlePath(): string {
|
|
855
|
+
+ const sibling = supervisedSiblingPath();
|
|
856
|
+
+ if (!existsSync(sibling)) throw new Error("supervised command bundle is missing");
|
|
857
|
+
+ return sibling;
|
|
858
|
+
+ }
|
|
859
|
+
+ const supervisedKey = Symbol("larkin-supervised-commands");
|
|
860
|
+
const manager = new AgentManager((record) => {
|
|
861
|
+
// Emit lifecycle event based on terminal status
|
|
862
|
+
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
863
|
+
@@ -490,7 +550,7 @@ export default function (pi: ExtensionAPI) {
|
|
864
|
+
tokensBefore: info.tokensBefore,
|
|
865
|
+
compactionCount: record.compactionCount,
|
|
866
|
+
});
|
|
867
|
+
- });
|
|
868
|
+
+ }, supervisedKey);
|
|
869
|
+
|
|
870
|
+
// Expose manager via Symbol.for() global registry for cross-package access.
|
|
871
|
+
// Standard Node.js pattern for cross-package singletons (used by OpenTelemetry, etc.).
|
|
872
|
+
@@ -588,11 +648,11 @@ export default function (pi: ExtensionAPI) {
|
|
873
|
+
delete (globalThis as any)[MANAGER_KEY];
|
|
874
|
+
}
|
|
875
|
+
scheduler.stop();
|
|
876
|
+
- manager.abortAll();
|
|
877
|
+
+ await manager.abortAll();
|
|
878
|
+
for (const timer of pendingNudges.values()) clearTimeout(timer);
|
|
879
|
+
pendingNudges.clear();
|
|
880
|
+
fleet.dispose();
|
|
881
|
+
- manager.dispose();
|
|
882
|
+
+ await manager.dispose();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Live widget: show running agents above editor.
|
|
886
|
+
@@ -1259,6 +1319,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
887
|
+
inheritContext,
|
|
888
|
+
thinkingLevel: thinking,
|
|
889
|
+
isBackground: true,
|
|
890
|
+
+ ...(params.run_in_background === true ? { [supervisedKey]: supervisedCommandBundlePath() } : {}),
|
|
891
|
+
isolation,
|
|
892
|
+
invocation: agentInvocation,
|
|
893
|
+
...bgCallbacks,
|
|
894
|
+
@@ -1453,6 +1514,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
199
895
|
description: "If true, wait for the agent to complete before returning. Default: false.",
|
|
200
896
|
}),
|
|
201
897
|
),
|
|
@@ -207,7 +903,7 @@ index 828938e6d710dc5a8f99a6185e37f9592b12352d..fb2d3119a50c6aa9c8864037af65ff95
|
|
|
207
903
|
verbose: Type.Optional(
|
|
208
904
|
Type.Boolean({
|
|
209
905
|
description: "If true, include the agent's full conversation (messages + tool calls). Default: false.",
|
|
210
|
-
@@ -1470,14 +
|
|
906
|
+
@@ -1470,14 +1536,15 @@ Terse command-style prompts produce shallow, generic work.
|
|
211
907
|
// completion notification can still be delivered.
|
|
212
908
|
// Queued agents have no promise yet (it's created when the queue starts
|
|
213
909
|
// them), so poll until they leave the queue, then await like a running one.
|
|
@@ -228,16 +924,16 @@ index 828938e6d710dc5a8f99a6185e37f9592b12352d..fb2d3119a50c6aa9c8864037af65ff95
|
|
|
228
924
|
+ resultWaitConsumedInParentTurn = true;
|
|
229
925
|
+ timedOut = await waitForSubagentResult(record, waitTimeoutMs, signal);
|
|
230
926
|
}
|
|
231
|
-
|
|
927
|
+
|
|
232
928
|
const displayName = getDisplayName(record.type);
|
|
233
|
-
@@ -1492,11 +
|
|
234
|
-
|
|
929
|
+
@@ -1492,11 +1559,13 @@ Terse command-style prompts produce shallow, generic work.
|
|
930
|
+
|
|
235
931
|
let output =
|
|
236
932
|
`Agent: ${record.id}\n` +
|
|
237
933
|
- `Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
238
934
|
+ `Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)}${timedOut ? " | timedOut: true" : ""} | ${statsParts.join(" | ")}\n` +
|
|
239
935
|
`Description: ${record.description}\n\n`;
|
|
240
|
-
|
|
936
|
+
|
|
241
937
|
- if (record.status === "running") {
|
|
242
938
|
- output += "Agent is still running. Use wait: true or check back later.";
|
|
243
939
|
+ if (record.status === "running" || record.status === "queued") {
|