omnirush 0.8.4 → 0.8.6
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/assets/collect-once.ts +6 -5
- package/assets/extensions/omnirush/agents-lib.ts +574 -52
- package/assets/extensions/omnirush/agents.ts +384 -84
- package/assets/extensions/omnirush/auth.js +399 -115
- package/assets/extensions/omnirush/collector.ts +90 -23
- package/assets/extensions/omnirush/commands.ts +12 -15
- package/assets/extensions/omnirush/sota-lib.ts +129 -0
- package/assets/extensions/omnirush/sota.ts +69 -51
- package/package.json +1 -1
- package/src/bin.js +48 -21
|
@@ -4,20 +4,33 @@
|
|
|
4
4
|
// the SAME workspace:
|
|
5
5
|
// - role presets: code-searcher (read-only exploration),
|
|
6
6
|
// researcher-web (web_fetch research), general-worker (full tools)
|
|
7
|
-
// - parallel execution,
|
|
8
|
-
// -
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// - parallel execution, every task at once unless max_parallel throttles
|
|
8
|
+
// - no wall-clock limit: a child runs until it is done. Only an explicit
|
|
9
|
+
// timeout_minutes, the inactivity watchdog (no output at all for
|
|
10
|
+
// OMNIRUSH_AGENT_INACTIVITY_MINUTES, default 30, longer while one of
|
|
11
|
+
// its tools runs), a cancel, or the session ending stop it
|
|
12
|
+
// - blocking (default) or background dispatch (`wait: false`): the call
|
|
13
|
+
// returns at once with agent ids and the parent keeps working; the
|
|
14
|
+
// results come back as a message in the conversation when the batch
|
|
15
|
+
// (or each child, notify "each") finishes — steered into the running
|
|
16
|
+
// turn, or starting a new turn when the parent is idle. In one-shot
|
|
17
|
+
// runs (print/json mode) the run does not end while background
|
|
18
|
+
// children are out: it waits for them and continues with their results
|
|
19
|
+
// - agents_status / agents_wait / agents_result / agents_cancel manage
|
|
20
|
+
// running children
|
|
11
21
|
//
|
|
12
22
|
// Tracing is free: every child writes its own session JSONL under
|
|
13
23
|
// <agentDir>/sessions/<cwd-slug>/ (children run without --no-session)
|
|
14
24
|
// under a session id the parent picks, and the parent session's
|
|
15
25
|
// collector records each child's conversation as a sub-agent of the
|
|
16
26
|
// turn (session.child events with parent and depth, as the desktop
|
|
17
|
-
// records task sub-agents)
|
|
18
|
-
//
|
|
27
|
+
// records task sub-agents) — a background child that finishes in a later
|
|
28
|
+
// turn is recorded as it progresses, and one the session ends under is
|
|
29
|
+
// stopped and recorded as interrupted. Children get
|
|
30
|
+
// OMNIRUSH_PARENT_SESSION, so a child process uploads nothing of its own.
|
|
19
31
|
//
|
|
20
|
-
// `/agents` lists this session's subagents (running + finished)
|
|
32
|
+
// `/agents` lists this session's subagents (running + finished);
|
|
33
|
+
// `/agents cancel <id|all>` stops running ones.
|
|
21
34
|
|
|
22
35
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
23
36
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -25,14 +38,28 @@ import { Type } from "typebox";
|
|
|
25
38
|
|
|
26
39
|
import {
|
|
27
40
|
AGENT_ROLES,
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
AGENTS_HOOK,
|
|
42
|
+
AgentManager,
|
|
43
|
+
agentStatusSummary,
|
|
44
|
+
type AgentRunner,
|
|
45
|
+
type AgentsHook,
|
|
46
|
+
type AgentRecord,
|
|
47
|
+
childInactivityMs,
|
|
48
|
+
renderAgentStatus,
|
|
30
49
|
renderChildResults,
|
|
50
|
+
renderDelivery,
|
|
51
|
+
modelSelectionFromContext,
|
|
31
52
|
runChildAgent,
|
|
32
53
|
type ChildResult,
|
|
33
54
|
type ChildTask,
|
|
34
55
|
} from "./agents-lib";
|
|
35
56
|
|
|
57
|
+
/** A timeout_minutes below this is taken for a placeholder, not a limit. */
|
|
58
|
+
const MIN_TIMEOUT_MINUTES = 1;
|
|
59
|
+
|
|
60
|
+
/** customType of the message that brings background results back. */
|
|
61
|
+
export const DELIVERY_TYPE = "omnirush-agents-result";
|
|
62
|
+
|
|
36
63
|
const SpawnAgentsParams = Type.Object({
|
|
37
64
|
tasks: Type.Array(
|
|
38
65
|
Type.Object({
|
|
@@ -41,33 +68,127 @@ const SpawnAgentsParams = Type.Object({
|
|
|
41
68
|
}),
|
|
42
69
|
task: Type.String({ description: "Self-contained task description for the child agent (it sees ONLY this, not your conversation)" }),
|
|
43
70
|
model: Type.Optional(Type.String({
|
|
44
|
-
description: '
|
|
71
|
+
description: 'Optional model[:effort] for THIS task (e.g. "muse-spark-1.3", "gpt-6-sol:high"). Omit (or leave empty) to inherit the parent session\'s current model and effort',
|
|
45
72
|
})),
|
|
46
73
|
}),
|
|
47
74
|
{ description: "Tasks to delegate; they all run in parallel", minItems: 1 },
|
|
48
75
|
),
|
|
76
|
+
wait: Type.Optional(Type.Boolean({
|
|
77
|
+
description: "true (default): block until every child finished and return their results. false: background dispatch — return at once with agent ids and keep working; the results arrive later as a message in this conversation (no polling needed)",
|
|
78
|
+
})),
|
|
79
|
+
notify: Type.Optional(StringEnum(["batch", "each"] as const, {
|
|
80
|
+
description: 'Background dispatch only: deliver one message when the whole batch finished ("batch", default) or one per child as each finishes ("each")',
|
|
81
|
+
})),
|
|
49
82
|
timeout_minutes: Type.Optional(Type.Number({
|
|
50
|
-
description:
|
|
51
|
-
minimum: 0.1,
|
|
52
|
-
maximum: 60,
|
|
83
|
+
description: "Leave out (or 0) unless the user asked for a hard limit: children then run until they are done, however long that takes. A value of 1 or more kills a child after that many minutes (its partial result is kept); smaller values are ignored",
|
|
53
84
|
})),
|
|
54
85
|
max_parallel: Type.Optional(Type.Number({
|
|
55
|
-
description: "
|
|
56
|
-
minimum: 1,
|
|
86
|
+
description: "Leave out (or 0) to run every task in parallel (no cap, the default); a positive number throttles to that many children at once",
|
|
57
87
|
})),
|
|
58
88
|
});
|
|
59
89
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
90
|
+
const IdsParam = Type.Optional(Type.Array(Type.String(), {
|
|
91
|
+
description: 'Agent ids from spawn_agents (e.g. "a3"); omit (or "all") for every agent of this session',
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
function resultOf(record: AgentRecord): ChildResult & { agent_id: string } {
|
|
95
|
+
return { ...(record.result as ChildResult), agent_id: record.id };
|
|
66
96
|
}
|
|
67
97
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
98
|
+
/** Results of finished records plus a status line for the others. */
|
|
99
|
+
function renderSome(records: AgentRecord[], unknown: string[] = []): string {
|
|
100
|
+
const finished = records.filter((record) => record.result);
|
|
101
|
+
const open = records.filter((record) => !record.result);
|
|
102
|
+
const parts: string[] = [];
|
|
103
|
+
if (finished.length > 0) parts.push(renderChildResults(finished.map((record) => record.result as ChildResult), finished.map((record) => record.id)));
|
|
104
|
+
if (open.length > 0) parts.push(`Open sub-agents:\n${renderAgentStatus(open)}`);
|
|
105
|
+
if (unknown.length > 0) parts.push(`Unknown agent ids: ${unknown.join(", ")}`);
|
|
106
|
+
return parts.join("\n\n") || "No matching sub-agents.";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default function (pi: ExtensionAPI, options: { run?: AgentRunner } = {}) {
|
|
110
|
+
const watchdogMinutes = Math.round(childInactivityMs() / 60_000);
|
|
111
|
+
/** The session this runtime is on (deliveries go to it). */
|
|
112
|
+
let currentSession = "";
|
|
113
|
+
let cwd = process.cwd();
|
|
114
|
+
let inheritedModel = "";
|
|
115
|
+
let lastContext: any = null;
|
|
116
|
+
let statusTimer: ReturnType<typeof setTimeout> | null = null;
|
|
117
|
+
/** An agent run is under way (one-shot runs deliver through the settle loop otherwise). */
|
|
118
|
+
let busy = false;
|
|
119
|
+
let oneShot = false;
|
|
120
|
+
|
|
121
|
+
function publishStatus(): void {
|
|
122
|
+
statusTimer = null;
|
|
123
|
+
const session = currentSession;
|
|
124
|
+
if (!session || !lastContext?.ui?.setStatus) return;
|
|
125
|
+
const records = manager.list(session);
|
|
126
|
+
if (records.length > 0) lastContext.ui.setStatus("omnirush-agents", agentStatusSummary(records));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function scheduleStatus(): void {
|
|
130
|
+
if (statusTimer) return;
|
|
131
|
+
statusTimer = setTimeout(publishStatus, 250);
|
|
132
|
+
statusTimer.unref?.();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const send = (records: AgentRecord[]) => {
|
|
136
|
+
pi.sendMessage(
|
|
137
|
+
{
|
|
138
|
+
customType: DELIVERY_TYPE,
|
|
139
|
+
content: renderDelivery(records),
|
|
140
|
+
display: true,
|
|
141
|
+
details: { results: records.map(resultOf) },
|
|
142
|
+
},
|
|
143
|
+
// Busy: into the running turn at its next step. Idle: a new turn.
|
|
144
|
+
{ triggerTurn: true, deliverAs: "steer" },
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const deliver = () => {
|
|
149
|
+
if (!currentSession) return;
|
|
150
|
+
// One-shot and between runs: the agent_settled loop delivers.
|
|
151
|
+
if (oneShot && !busy) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const group of manager.takeDeliverable(currentSession)) send(group.records);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const manager = new AgentManager({
|
|
158
|
+
run: options.run ?? ((task, options) => runChildAgent(task, {
|
|
159
|
+
cwd: options.cwd ?? cwd,
|
|
160
|
+
parentSessionId: options.parentSessionId,
|
|
161
|
+
sessionId: options.sessionId,
|
|
162
|
+
signal: options.signal,
|
|
163
|
+
onActivity: options.onActivity,
|
|
164
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
165
|
+
})),
|
|
166
|
+
// After the settle's own bookkeeping (waiters took theirs).
|
|
167
|
+
onDeliverable: () => queueMicrotask(deliver),
|
|
168
|
+
onChanged: scheduleStatus,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const hook: AgentsHook = {
|
|
172
|
+
async interruptAll(parentSessionId?: string) {
|
|
173
|
+
const stopped = await manager.interruptAll(parentSessionId);
|
|
174
|
+
return stopped.map((record) => ({ sessionId: record.sessionId, id: record.id, role: record.role }));
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
(globalThis as any)[AGENTS_HOOK] = hook;
|
|
178
|
+
|
|
179
|
+
const sessionOf = (ctx: any): string => {
|
|
180
|
+
const id = String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
181
|
+
if (id && id !== currentSession) {
|
|
182
|
+
currentSession = id;
|
|
183
|
+
inheritedModel = "";
|
|
184
|
+
}
|
|
185
|
+
lastContext = ctx ?? lastContext;
|
|
186
|
+
const selected = modelSelectionFromContext(ctx);
|
|
187
|
+
if (selected) inheritedModel = selected;
|
|
188
|
+
if (ctx?.cwd) cwd = String(ctx.cwd);
|
|
189
|
+
if (ctx?.mode === "print" || ctx?.mode === "json") oneShot = true;
|
|
190
|
+
return id;
|
|
191
|
+
};
|
|
71
192
|
|
|
72
193
|
pi.registerTool({
|
|
73
194
|
name: "spawn_agents",
|
|
@@ -76,107 +197,286 @@ export default function (pi: ExtensionAPI) {
|
|
|
76
197
|
"Delegate tasks to parallel subagents with isolated context windows.",
|
|
77
198
|
"Each child is a headless agent run in this workspace with a role preset:",
|
|
78
199
|
"code-searcher (read-only exploration), researcher-web (web research via web_search + web_fetch), general-worker (full tools).",
|
|
79
|
-
|
|
200
|
+
"Children run in parallel — every task at once unless max_parallel throttles it — and have no time limit: they run until done",
|
|
201
|
+
`(timeout_minutes sets one explicitly${watchdogMinutes > 0 ? `; a child that prints nothing at all for ${watchdogMinutes}+ min is stopped as hung` : ""}).`,
|
|
202
|
+
"Default (wait true) blocks until all children finish and returns their results.",
|
|
203
|
+
"wait:false dispatches them in the BACKGROUND: the call returns at once with agent ids, you keep working, and their results arrive automatically as a message in this conversation when they finish.",
|
|
204
|
+
"Manage background agents with agents_status, agents_wait, agents_result and agents_cancel.",
|
|
80
205
|
'Each task can run on a DIFFERENT model via its "model" field — e.g. cheap muse workers for wide swarm sweeps under an astra parent.',
|
|
81
206
|
"Every task description must be SELF-CONTAINED: the child cannot see this conversation.",
|
|
82
207
|
"Use for parallelizable work: broad code surveys, independent research questions, independent implementation chunks.",
|
|
83
208
|
].join(" "),
|
|
84
|
-
promptSnippet: "spawn_agents: delegate parallel tasks to role-preset subagents",
|
|
209
|
+
promptSnippet: "spawn_agents: delegate parallel tasks to role-preset subagents (blocking, or wait:false for background work)",
|
|
85
210
|
promptGuidelines: [
|
|
86
211
|
"Use spawn_agents when 2+ independent sub-tasks would each take several tool calls (surveys, research, independent fixes); write each task brief as a self-contained mini-prompt with the goal, relevant paths and the expected report format.",
|
|
212
|
+
"Short fan-outs whose results you need right away (quick searches, lookups): the default blocking call.",
|
|
213
|
+
"Long work (implementation chunks, builds, test suites, long research — anything that may take many minutes) while you have other things to do: spawn_agents with wait:false, then keep working; the results arrive on their own as a message, so do not poll agents_status in a loop. Call agents_wait only when you have nothing else to do and need the results before going on.",
|
|
87
214
|
"Do not use spawn_agents for a single quick action — doing it yourself is cheaper.",
|
|
88
215
|
],
|
|
89
216
|
parameters: SpawnAgentsParams,
|
|
90
217
|
|
|
91
218
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
219
|
+
const parentSessionId = sessionOf(ctx);
|
|
92
220
|
const tasks: ChildTask[] = [];
|
|
93
221
|
for (const raw of params.tasks ?? []) {
|
|
94
222
|
if (!raw || typeof raw.task !== "string" || !raw.task.trim()) {
|
|
95
223
|
throw new Error("invalid tasks: every entry needs a non-empty task string");
|
|
96
224
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
225
|
+
// An omitted model inherits the live parent selection. Explicit task
|
|
226
|
+
// models remain available for deliberate mixed-model swarms.
|
|
227
|
+
const requestedModel = typeof raw.model === "string" ? raw.model.trim() : "";
|
|
101
228
|
tasks.push({
|
|
102
229
|
role: raw.role,
|
|
103
230
|
task: raw.task.trim(),
|
|
104
|
-
...(
|
|
231
|
+
...((requestedModel || inheritedModel) ? { model: requestedModel || inheritedModel } : {}),
|
|
105
232
|
});
|
|
106
233
|
}
|
|
107
234
|
if (tasks.length === 0) throw new Error("no tasks given");
|
|
108
|
-
const parentSessionId = String(ctx?.sessionManager?.getSessionId?.() ?? "");
|
|
109
235
|
if (!parentSessionId) throw new Error("no parent session id — subagents cannot be traced");
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
236
|
+
const workdir = ctx?.cwd ? String(ctx.cwd) : process.cwd();
|
|
237
|
+
// Only a deliberate limit counts: models fill optional numbers with
|
|
238
|
+
// their minimum (a 0.1-minute "limit" killed every child in 6 s).
|
|
239
|
+
const timeoutMs = typeof params.timeout_minutes === "number" && params.timeout_minutes >= MIN_TIMEOUT_MINUTES
|
|
240
|
+
? params.timeout_minutes * 60_000
|
|
113
241
|
: undefined;
|
|
114
|
-
// No cap: every task runs at once unless the caller
|
|
115
|
-
|
|
242
|
+
// No cap: every task runs at once unless the caller deliberately
|
|
243
|
+
// supplies max_parallel.
|
|
244
|
+
const hasParallelLimit = typeof params.max_parallel === "number" && params.max_parallel >= 1;
|
|
245
|
+
const concurrency = hasParallelLimit
|
|
116
246
|
? Math.floor(params.max_parallel)
|
|
117
|
-
:
|
|
118
|
-
|
|
119
|
-
const entries = registry.get(parentSessionId) ?? [];
|
|
120
|
-
registry.set(parentSessionId, entries);
|
|
121
|
-
const marks = tasks.map((task) => {
|
|
122
|
-
const entry: AgentRegistryEntry = {
|
|
123
|
-
role: task.role,
|
|
124
|
-
task: task.task,
|
|
125
|
-
status: "running",
|
|
126
|
-
startedAt: Date.now(),
|
|
127
|
-
durationMs: null,
|
|
128
|
-
};
|
|
129
|
-
entries.push(entry);
|
|
130
|
-
return entry;
|
|
131
|
-
});
|
|
247
|
+
: undefined;
|
|
248
|
+
const background = params.wait === false;
|
|
132
249
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const settled: ChildResult[] = [];
|
|
136
|
-
const results = await mapWithConcurrency<ChildTask, ChildResult>(
|
|
137
|
-
tasks,
|
|
250
|
+
const dispatched = manager.dispatch(parentSessionId, tasks, {
|
|
251
|
+
background,
|
|
138
252
|
concurrency,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
253
|
+
notify: params.notify === "each" ? "each" : "batch",
|
|
254
|
+
...(background ? {} : { signal }),
|
|
255
|
+
onSettled: (record) => {
|
|
256
|
+
if (background) return;
|
|
257
|
+
onUpdate?.({
|
|
258
|
+
content: [{ type: "text", text: `${agentStatusSummary(dispatched.records)} (latest: ${record.id} ${record.role})` }],
|
|
259
|
+
details: { results: dispatched.records.filter((candidate) => candidate.result).map(resultOf) },
|
|
145
260
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
settled.push(result);
|
|
261
|
+
},
|
|
262
|
+
onChanged: () => {
|
|
263
|
+
if (background) return;
|
|
150
264
|
onUpdate?.({
|
|
151
|
-
content: [{ type: "text", text:
|
|
152
|
-
details: { results:
|
|
265
|
+
content: [{ type: "text", text: agentStatusSummary(manager.list(parentSessionId)) }],
|
|
266
|
+
details: { results: manager.list(parentSessionId).filter((candidate) => candidate.result).map(resultOf) },
|
|
153
267
|
});
|
|
154
|
-
return result;
|
|
155
268
|
},
|
|
156
|
-
|
|
269
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
270
|
+
cwd: workdir,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (background) {
|
|
274
|
+
const lines = dispatched.records.map((record) => `- ${record.id}: ${record.role}${record.model ? ` [${record.model}]` : ""} — ${record.task.length > 100 ? `${record.task.slice(0, 99)}…` : record.task}`);
|
|
275
|
+
return {
|
|
276
|
+
content: [{
|
|
277
|
+
type: "text",
|
|
278
|
+
text: [
|
|
279
|
+
`spawn_agents: dispatched ${dispatched.records.length} background sub-agent${dispatched.records.length === 1 ? "" : "s"} (batch ${dispatched.batch}):`,
|
|
280
|
+
...lines,
|
|
281
|
+
"",
|
|
282
|
+
params.notify === "each"
|
|
283
|
+
? "Keep working: each result arrives as a message in this conversation when that agent finishes."
|
|
284
|
+
: "Keep working: the results arrive as one message in this conversation when all of them finished.",
|
|
285
|
+
"agents_status shows progress; agents_wait blocks for them; agents_cancel stops them.",
|
|
286
|
+
].join("\n"),
|
|
287
|
+
}],
|
|
288
|
+
details: {
|
|
289
|
+
background: true,
|
|
290
|
+
batch: dispatched.batch,
|
|
291
|
+
results: dispatched.records.map((record) => ({
|
|
292
|
+
agent_id: record.id,
|
|
293
|
+
role: record.role,
|
|
294
|
+
task: record.task,
|
|
295
|
+
session_id: record.sessionId,
|
|
296
|
+
...(record.model ? { model: record.model } : {}),
|
|
297
|
+
parallel_limit: record.parallelLimit,
|
|
298
|
+
status: "running",
|
|
299
|
+
})),
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await dispatched.done;
|
|
305
|
+
const records = dispatched.records;
|
|
306
|
+
return {
|
|
307
|
+
content: [{ type: "text", text: renderChildResults(records.map((record) => record.result as ChildResult), records.map((record) => record.id)) }],
|
|
308
|
+
details: { results: records.map(resultOf) },
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
pi.registerTool({
|
|
314
|
+
name: "agents_status",
|
|
315
|
+
label: "Agents Status",
|
|
316
|
+
description: "Status of this session's sub-agents (from spawn_agents): id, state, elapsed time, turns, last activity. Background results are delivered automatically — use this to check progress, not to poll in a loop.",
|
|
317
|
+
promptSnippet: "agents_status: list sub-agents and their progress",
|
|
318
|
+
parameters: Type.Object({ ids: IdsParam }),
|
|
319
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
320
|
+
const { records, unknown } = manager.resolve(sessionOf(ctx), params.ids);
|
|
321
|
+
const text = renderAgentStatus(records) + (unknown.length ? `\nUnknown agent ids: ${unknown.join(", ")}` : "");
|
|
322
|
+
return {
|
|
323
|
+
content: [{ type: "text", text }],
|
|
324
|
+
details: {
|
|
325
|
+
agents: records.map((record) => ({
|
|
326
|
+
agent_id: record.id,
|
|
327
|
+
status: record.status,
|
|
328
|
+
role: record.role,
|
|
329
|
+
task: record.task,
|
|
330
|
+
session_id: record.sessionId,
|
|
331
|
+
...(record.model ? { model: record.model } : {}),
|
|
332
|
+
parallel_limit: record.parallelLimit,
|
|
333
|
+
turns: record.turns,
|
|
334
|
+
tools_running: record.toolsRunning,
|
|
335
|
+
last_activity_at: record.lastActivityAt,
|
|
336
|
+
})),
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
pi.registerTool({
|
|
343
|
+
name: "agents_wait",
|
|
344
|
+
label: "Wait For Agents",
|
|
345
|
+
description: "Block until the given background sub-agents finish (default: every unfinished one) and return their results. Optional timeout_minutes bounds the WAIT only — the agents keep running past it.",
|
|
346
|
+
promptSnippet: "agents_wait: block until background sub-agents finish",
|
|
347
|
+
parameters: Type.Object({
|
|
348
|
+
ids: IdsParam,
|
|
349
|
+
timeout_minutes: Type.Optional(Type.Number({ description: "Leave out (or 0) to wait until they finish; otherwise stop WAITING after this many minutes (the agents keep running)" })),
|
|
350
|
+
}),
|
|
351
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
352
|
+
const session = sessionOf(ctx);
|
|
353
|
+
const explicit = Array.isArray(params.ids) && params.ids.length > 0 && !params.ids.includes("all");
|
|
354
|
+
const { records: resolved, unknown } = manager.resolve(session, params.ids);
|
|
355
|
+
// Default: what is still out or not yet read.
|
|
356
|
+
const records = explicit ? resolved : resolved.filter((record) => !record.result || !record.delivered);
|
|
357
|
+
if (records.length === 0 && unknown.length === 0) {
|
|
358
|
+
return { content: [{ type: "text", text: "No background sub-agents to wait for." }], details: { results: [] } };
|
|
359
|
+
}
|
|
360
|
+
onUpdate?.({ content: [{ type: "text", text: `agents_wait: waiting for ${records.filter((record) => !record.result).length} sub-agent(s)` }], details: {} });
|
|
361
|
+
await manager.wait(records, {
|
|
362
|
+
signal,
|
|
363
|
+
...(typeof params.timeout_minutes === "number" && params.timeout_minutes > 0 ? { timeoutMs: params.timeout_minutes * 60_000 } : {}),
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
content: [{ type: "text", text: renderSome(records, unknown) }],
|
|
367
|
+
details: { results: records.filter((record) => record.result).map(resultOf) },
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
});
|
|
157
371
|
|
|
372
|
+
pi.registerTool({
|
|
373
|
+
name: "agents_result",
|
|
374
|
+
label: "Agent Result",
|
|
375
|
+
description: "The full result of one sub-agent by id (or its status while it still runs).",
|
|
376
|
+
promptSnippet: "agents_result: fetch a finished sub-agent's result",
|
|
377
|
+
parameters: Type.Object({ id: Type.String({ description: 'Agent id from spawn_agents (e.g. "a3")' }) }),
|
|
378
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
379
|
+
const { records, unknown } = manager.resolve(sessionOf(ctx), [params.id]);
|
|
380
|
+
records.filter((record) => record.result).forEach((record) => { record.delivered = true; });
|
|
158
381
|
return {
|
|
159
|
-
content: [{ type: "text", text:
|
|
160
|
-
details: { results },
|
|
382
|
+
content: [{ type: "text", text: renderSome(records, unknown) }],
|
|
383
|
+
details: { results: records.filter((record) => record.result).map(resultOf) },
|
|
161
384
|
};
|
|
162
385
|
},
|
|
163
386
|
});
|
|
164
387
|
|
|
388
|
+
pi.registerTool({
|
|
389
|
+
name: "agents_cancel",
|
|
390
|
+
label: "Cancel Agents",
|
|
391
|
+
description: 'Stop running sub-agents by id ("all" for every one of this session); returns what each had produced so far.',
|
|
392
|
+
promptSnippet: "agents_cancel: stop running sub-agents",
|
|
393
|
+
parameters: Type.Object({
|
|
394
|
+
ids: Type.Array(Type.String(), { description: 'Agent ids to stop, or ["all"]', minItems: 1 }),
|
|
395
|
+
}),
|
|
396
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
397
|
+
const { records, unknown } = manager.resolve(sessionOf(ctx), params.ids);
|
|
398
|
+
const live = records.filter((record) => !record.result);
|
|
399
|
+
await manager.cancel(live, "cancelled", { delivered: true });
|
|
400
|
+
return {
|
|
401
|
+
content: [{ type: "text", text: live.length ? renderSome(live, unknown) : `Nothing to cancel${unknown.length ? ` (unknown ids: ${unknown.join(", ")})` : ""}.` }],
|
|
402
|
+
details: { results: live.map(resultOf) },
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
pi.on("session_start", (_event: any, ctx: any) => {
|
|
408
|
+
sessionOf(ctx);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
pi.on("agent_start", (_event: any, ctx: any) => {
|
|
412
|
+
sessionOf(ctx);
|
|
413
|
+
busy = true;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* One-shot runs (print/json mode) end when the agent settles: background
|
|
418
|
+
* children still out would be lost. The run waits for them here and
|
|
419
|
+
* continues with their results (a new run nested in this settle, whose
|
|
420
|
+
* own settle resumes this one when everything is done).
|
|
421
|
+
*/
|
|
422
|
+
let resumeOuter: (() => void) | null = null;
|
|
423
|
+
pi.on("agent_settled", async (_event: any, ctx: any) => {
|
|
424
|
+
busy = false;
|
|
425
|
+
const session = sessionOf(ctx);
|
|
426
|
+
if (!oneShot) {
|
|
427
|
+
// Anything that finished right as the run ended.
|
|
428
|
+
deliver();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const outer = resumeOuter;
|
|
432
|
+
resumeOuter = null;
|
|
433
|
+
try {
|
|
434
|
+
for (;;) {
|
|
435
|
+
if (typeof ctx?.isIdle === "function" && !ctx.isIdle()) {
|
|
436
|
+
// A run started (a delivery): its settle resumes this one.
|
|
437
|
+
await new Promise<void>((resolve) => { resumeOuter = resolve; });
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
if (!session || !manager.hasPending(session)) break;
|
|
441
|
+
const groups = manager.takeDeliverable(session);
|
|
442
|
+
if (groups.length === 0) {
|
|
443
|
+
await manager.changed();
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
busy = true;
|
|
447
|
+
const records = groups.flatMap((group) => group.records);
|
|
448
|
+
send(records);
|
|
449
|
+
if (typeof ctx?.isIdle === "function" && ctx.isIdle()) busy = false;
|
|
450
|
+
}
|
|
451
|
+
} finally {
|
|
452
|
+
outer?.();
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
pi.on("session_shutdown", async () => {
|
|
457
|
+
// Children the session ends under are stopped (the collector, when it
|
|
458
|
+
// runs, already did this before its last capture).
|
|
459
|
+
await manager.interruptAll().catch(() => undefined);
|
|
460
|
+
});
|
|
461
|
+
|
|
165
462
|
pi.registerCommand("agents", {
|
|
166
|
-
description: "List this session's subagents (running and finished)",
|
|
167
|
-
handler: async (
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
463
|
+
description: "List this session's subagents (running and finished); /agents cancel <id|all> stops running ones",
|
|
464
|
+
handler: async (args: string, ctx: any) => {
|
|
465
|
+
const session = sessionOf(ctx);
|
|
466
|
+
const words = String(args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
467
|
+
if (words[0] === "cancel") {
|
|
468
|
+
const { records } = manager.resolve(session, words.slice(1).length ? words.slice(1) : ["all"]);
|
|
469
|
+
const live = records.filter((record) => !record.result);
|
|
470
|
+
await manager.cancel(live);
|
|
471
|
+
ctx?.ui?.notify?.(live.length ? `Cancelled ${live.map((record) => record.id).join(", ")}.` : "No running subagents to cancel.");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const records = manager.list(session);
|
|
475
|
+
if (records.length === 0) {
|
|
171
476
|
ctx?.ui?.notify?.("No subagents have been spawned in this session yet.");
|
|
172
477
|
return;
|
|
173
478
|
}
|
|
174
|
-
|
|
175
|
-
const minutes = Math.round(((entry.durationMs ?? Date.now() - entry.startedAt) / 60_000) * 10) / 10;
|
|
176
|
-
const task = entry.task.length > 60 ? `${entry.task.slice(0, 59)}…` : entry.task;
|
|
177
|
-
return ` ${index + 1}. [${entry.status}] ${entry.role} (${minutes} min) — ${task}`;
|
|
178
|
-
});
|
|
179
|
-
ctx?.ui?.notify?.(`Subagents of this session:\n${lines.join("\n")}`);
|
|
479
|
+
ctx?.ui?.notify?.(renderAgentStatus(records));
|
|
180
480
|
},
|
|
181
481
|
});
|
|
182
482
|
}
|