pi-maestro-teammate 0.1.0 → 0.2.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/package.json +1 -1
- package/src/extension/index.ts +350 -31
- package/src/extension/schemas.ts +18 -0
- package/src/runs/execution.ts +22 -1
- package/src/shared/types.ts +29 -6
package/package.json
CHANGED
package/src/extension/index.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Teammate Extension Entry Point
|
|
3
3
|
*
|
|
4
|
-
* Registers
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Registers tools:
|
|
5
|
+
* - teammate: dispatch tasks to sub-agents (single/parallel/chain, await/detach)
|
|
6
|
+
* - teammate-send: send messages to named running agents
|
|
7
|
+
* - teammate-list: list active/named agents
|
|
8
|
+
*
|
|
9
|
+
* P1: inbox message queue + named agent registry + detach mode
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { randomUUID } from "node:crypto";
|
|
@@ -13,11 +16,12 @@ import type {
|
|
|
13
16
|
ToolDefinition,
|
|
14
17
|
} from "@earendil-works/pi-coding-agent";
|
|
15
18
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
16
|
-
import { TeammateParams } from "./schemas.ts";
|
|
19
|
+
import { TeammateParams, TeammateSendParams, TeammateListParams } from "./schemas.ts";
|
|
17
20
|
import {
|
|
18
21
|
runTeammate,
|
|
19
22
|
runParallel,
|
|
20
23
|
runChain,
|
|
24
|
+
sendToChildStdin,
|
|
21
25
|
type RunTeammateParams,
|
|
22
26
|
} from "../runs/execution.ts";
|
|
23
27
|
import {
|
|
@@ -28,10 +32,13 @@ import type {
|
|
|
28
32
|
Details,
|
|
29
33
|
TeammateState,
|
|
30
34
|
AgentProgress,
|
|
35
|
+
ActiveAgent,
|
|
36
|
+
MessageEnvelope,
|
|
31
37
|
} from "../shared/types.ts";
|
|
32
38
|
import {
|
|
33
39
|
TEAMMATE_COMPLETE_EVENT,
|
|
34
40
|
TEAMMATE_STARTED_EVENT,
|
|
41
|
+
TEAMMATE_MESSAGE_EVENT,
|
|
35
42
|
} from "../shared/types.ts";
|
|
36
43
|
|
|
37
44
|
export default function registerTeammateExtension(pi: ExtensionAPI): void {
|
|
@@ -43,8 +50,13 @@ export default function registerTeammateExtension(pi: ExtensionAPI): void {
|
|
|
43
50
|
baseCwd: "",
|
|
44
51
|
currentSessionId: null,
|
|
45
52
|
activeRuns: new Map(),
|
|
53
|
+
namedAgents: new Map(),
|
|
46
54
|
};
|
|
47
55
|
|
|
56
|
+
// =========================================================================
|
|
57
|
+
// Tool 1: teammate — dispatch
|
|
58
|
+
// =========================================================================
|
|
59
|
+
|
|
48
60
|
const tool: ToolDefinition<typeof TeammateParams, Details> = {
|
|
49
61
|
name: "teammate",
|
|
50
62
|
label: "Teammate",
|
|
@@ -56,9 +68,12 @@ Modes:
|
|
|
56
68
|
- Chain: { chain: [{ agent: "scout", task: "Find auth code" }, { agent: "delegate", task: "Fix: {previous}" }] }
|
|
57
69
|
|
|
58
70
|
Three-axis control:
|
|
59
|
-
- name: Optional addressable name for cross-agent routing
|
|
71
|
+
- name: Optional addressable name for cross-agent routing (enables teammate-send)
|
|
60
72
|
- reply_to: Result routing — "caller" (direct return) or "main" (broadcast to parent session)
|
|
61
|
-
- lifecycle: "ephemeral" (default, one-shot) or "resident" (persistent)
|
|
73
|
+
- lifecycle: "ephemeral" (default, one-shot) or "resident" (persistent, stays alive for messages)
|
|
74
|
+
|
|
75
|
+
Execution mode:
|
|
76
|
+
- mode: "await" (default) blocks until result; "detach" returns immediately with a handle
|
|
62
77
|
|
|
63
78
|
Structured output:
|
|
64
79
|
- outputSchema: JSON Schema to validate and parse child output as structured data`,
|
|
@@ -77,16 +92,42 @@ Structured output:
|
|
|
77
92
|
const correlationId = randomUUID();
|
|
78
93
|
|
|
79
94
|
const abortController = new AbortController();
|
|
80
|
-
|
|
95
|
+
// P2-1: Deadlock detection — check if reply_to creates a cycle
|
|
96
|
+
if (params.name && params.reply_to !== "main" && params.reply_to !== "caller") {
|
|
97
|
+
const wouldCycle = detectReplyCycle(state, params.name, params.reply_to);
|
|
98
|
+
if (wouldCycle) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: `[deadlock] reply_to="${params.reply_to}" would create a cycle with agent "${params.name}". Falling back to reply_to="caller".`,
|
|
103
|
+
}],
|
|
104
|
+
isError: false,
|
|
105
|
+
details: { mode: "single", results: [] },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const activeAgent: ActiveAgent = {
|
|
81
111
|
agent: params.agent ?? "parallel",
|
|
112
|
+
name: params.name,
|
|
82
113
|
correlationId,
|
|
83
114
|
startedAt: Date.now(),
|
|
84
115
|
abortController,
|
|
85
|
-
|
|
116
|
+
lifecycle: params.lifecycle ?? "ephemeral",
|
|
117
|
+
inbox: [],
|
|
118
|
+
lastActivityAt: Date.now(),
|
|
119
|
+
replyTo: params.reply_to,
|
|
120
|
+
};
|
|
121
|
+
state.activeRuns.set(correlationId, activeAgent);
|
|
122
|
+
|
|
123
|
+
if (params.name) {
|
|
124
|
+
state.namedAgents.set(params.name, correlationId);
|
|
125
|
+
}
|
|
86
126
|
|
|
87
127
|
pi.events.emit(TEAMMATE_STARTED_EVENT, {
|
|
88
128
|
id,
|
|
89
129
|
agent: params.agent ?? "parallel",
|
|
130
|
+
name: params.name,
|
|
90
131
|
correlationId,
|
|
91
132
|
});
|
|
92
133
|
|
|
@@ -99,7 +140,12 @@ Structured output:
|
|
|
99
140
|
baseCwd: state.baseCwd || ctx.cwd,
|
|
100
141
|
signal: abortController.signal,
|
|
101
142
|
parentSessionFile,
|
|
143
|
+
onChildSpawned: (stdin: import("node:stream").Writable) => {
|
|
144
|
+
activeAgent.stdin = stdin;
|
|
145
|
+
drainInbox(activeAgent);
|
|
146
|
+
},
|
|
102
147
|
onProgress: (data: AgentProgress) => {
|
|
148
|
+
activeAgent.lastActivityAt = Date.now();
|
|
103
149
|
if (!onUpdate) return;
|
|
104
150
|
onUpdate({
|
|
105
151
|
content: [{
|
|
@@ -137,13 +183,8 @@ Structured output:
|
|
|
137
183
|
.map((r) => `[${r.agent}] ${r.exitCode === 0 ? "OK" : "FAIL"}: ${r.messages[r.messages.length - 1]?.content ?? "(no output)"}`)
|
|
138
184
|
.join("\n\n");
|
|
139
185
|
|
|
140
|
-
pi
|
|
141
|
-
|
|
142
|
-
agent: "parallel",
|
|
143
|
-
correlationId,
|
|
144
|
-
exitCode: hasError ? 1 : 0,
|
|
145
|
-
durationMs: Math.max(...results.map((r) => r.durationMs)),
|
|
146
|
-
});
|
|
186
|
+
emitComplete(pi, id, "parallel", correlationId, hasError ? 1 : 0,
|
|
187
|
+
Math.max(...results.map((r) => r.durationMs)));
|
|
147
188
|
|
|
148
189
|
return {
|
|
149
190
|
content: [{ type: "text", text: summaries }],
|
|
@@ -164,13 +205,8 @@ Structured output:
|
|
|
164
205
|
const hasError = results.some((r) => r.exitCode !== 0);
|
|
165
206
|
const lastMessage = lastResult?.messages[lastResult.messages.length - 1]?.content ?? "(no output)";
|
|
166
207
|
|
|
167
|
-
pi
|
|
168
|
-
|
|
169
|
-
agent: "chain",
|
|
170
|
-
correlationId,
|
|
171
|
-
exitCode: hasError ? 1 : 0,
|
|
172
|
-
durationMs: results.reduce((sum, r) => sum + r.durationMs, 0),
|
|
173
|
-
});
|
|
208
|
+
emitComplete(pi, id, "chain", correlationId, hasError ? 1 : 0,
|
|
209
|
+
results.reduce((sum, r) => sum + r.durationMs, 0));
|
|
174
210
|
|
|
175
211
|
return {
|
|
176
212
|
content: [{ type: "text", text: lastMessage }],
|
|
@@ -179,7 +215,26 @@ Structured output:
|
|
|
179
215
|
};
|
|
180
216
|
}
|
|
181
217
|
|
|
182
|
-
// ---
|
|
218
|
+
// --- DETACH MODE ---
|
|
219
|
+
if (params.mode === "detach") {
|
|
220
|
+
const detachPromise = runTeammate(params, makeOptions());
|
|
221
|
+
|
|
222
|
+
detachPromise.then((result) => {
|
|
223
|
+
emitComplete(pi, id, params.agent, correlationId, result.exitCode, result.durationMs);
|
|
224
|
+
cleanupAgent(state, correlationId, params.name);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
content: [{
|
|
229
|
+
type: "text",
|
|
230
|
+
text: `[detached] Agent "${params.agent}" dispatched in background. correlationId=${correlationId}${params.name ? `, name="${params.name}"` : ""}. Use teammate-send to inject messages, teammate-list to check status.`,
|
|
231
|
+
}],
|
|
232
|
+
isError: false,
|
|
233
|
+
details: { mode: "single", results: [] },
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- SINGLE AWAIT MODE ---
|
|
183
238
|
const result = await runTeammate(params, makeOptions());
|
|
184
239
|
|
|
185
240
|
const lastMessage =
|
|
@@ -195,17 +250,13 @@ Structured output:
|
|
|
195
250
|
toolResult.details!.structuredOutput = result.structuredOutput;
|
|
196
251
|
}
|
|
197
252
|
|
|
198
|
-
pi.
|
|
199
|
-
id,
|
|
200
|
-
agent: params.agent,
|
|
201
|
-
correlationId,
|
|
202
|
-
exitCode: result.exitCode,
|
|
203
|
-
durationMs: result.durationMs,
|
|
204
|
-
});
|
|
253
|
+
emitComplete(pi, id, params.agent, correlationId, result.exitCode, result.durationMs);
|
|
205
254
|
|
|
206
255
|
return toolResult;
|
|
207
256
|
} finally {
|
|
208
|
-
|
|
257
|
+
if (params.mode !== "detach") {
|
|
258
|
+
cleanupAgent(state, correlationId, params.name);
|
|
259
|
+
}
|
|
209
260
|
signal.removeEventListener("abort", abortForward);
|
|
210
261
|
}
|
|
211
262
|
},
|
|
@@ -219,18 +270,286 @@ Structured output:
|
|
|
219
270
|
},
|
|
220
271
|
};
|
|
221
272
|
|
|
273
|
+
// =========================================================================
|
|
274
|
+
// Tool 2: teammate-send — send message to named agent
|
|
275
|
+
// =========================================================================
|
|
276
|
+
|
|
277
|
+
const sendTool: ToolDefinition<typeof TeammateSendParams, { delivered: boolean }> = {
|
|
278
|
+
name: "teammate-send",
|
|
279
|
+
label: "Teammate Send",
|
|
280
|
+
description: `Send a message to a named, running teammate agent.
|
|
281
|
+
|
|
282
|
+
The target agent must have been dispatched with a "name" field and still be running.
|
|
283
|
+
Messages are injected into the agent's stdin as user messages.
|
|
284
|
+
|
|
285
|
+
Use "kind: notification" for context injection (fire-and-forget).
|
|
286
|
+
Use "kind: task" to assign additional work.`,
|
|
287
|
+
|
|
288
|
+
parameters: TeammateSendParams,
|
|
289
|
+
|
|
290
|
+
async execute(
|
|
291
|
+
_id: string,
|
|
292
|
+
params: { to: string; message: string; kind?: "notification" | "task" },
|
|
293
|
+
): Promise<AgentToolResult<{ delivered: boolean }>> {
|
|
294
|
+
const correlationId = state.namedAgents.get(params.to);
|
|
295
|
+
if (!correlationId) {
|
|
296
|
+
const available = Array.from(state.namedAgents.keys());
|
|
297
|
+
return {
|
|
298
|
+
content: [{
|
|
299
|
+
type: "text",
|
|
300
|
+
text: `Agent "${params.to}" not found. ${available.length > 0 ? `Available named agents: ${available.join(", ")}` : "No named agents are currently running."}`,
|
|
301
|
+
}],
|
|
302
|
+
isError: true,
|
|
303
|
+
details: { delivered: false },
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const agent = state.activeRuns.get(correlationId);
|
|
308
|
+
if (!agent) {
|
|
309
|
+
state.namedAgents.delete(params.to);
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: "text", text: `Agent "${params.to}" is no longer running.` }],
|
|
312
|
+
isError: true,
|
|
313
|
+
details: { delivered: false },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const envelope: MessageEnvelope = {
|
|
318
|
+
id: randomUUID(),
|
|
319
|
+
from: "caller",
|
|
320
|
+
to: params.to,
|
|
321
|
+
kind: params.kind ?? "notification",
|
|
322
|
+
payload: params.message,
|
|
323
|
+
timestamp: Date.now(),
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (agent.stdin) {
|
|
327
|
+
const sent = sendToChildStdin(agent.stdin, params.message);
|
|
328
|
+
if (sent) {
|
|
329
|
+
agent.lastActivityAt = Date.now();
|
|
330
|
+
pi.events.emit(TEAMMATE_MESSAGE_EVENT, envelope);
|
|
331
|
+
return {
|
|
332
|
+
content: [{
|
|
333
|
+
type: "text",
|
|
334
|
+
text: `Message delivered to "${params.to}" (kind: ${envelope.kind}).`,
|
|
335
|
+
}],
|
|
336
|
+
isError: false,
|
|
337
|
+
details: { delivered: true },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
agent.inbox.push(envelope);
|
|
343
|
+
return {
|
|
344
|
+
content: [{
|
|
345
|
+
type: "text",
|
|
346
|
+
text: `Message queued for "${params.to}" (stdin not ready yet, will deliver when available).`,
|
|
347
|
+
}],
|
|
348
|
+
isError: false,
|
|
349
|
+
details: { delivered: false },
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// =========================================================================
|
|
355
|
+
// Tool 3: teammate-list — list active agents
|
|
356
|
+
// =========================================================================
|
|
357
|
+
|
|
358
|
+
const listTool: ToolDefinition<typeof TeammateListParams, { agents: unknown[] }> = {
|
|
359
|
+
name: "teammate-list",
|
|
360
|
+
label: "Teammate List",
|
|
361
|
+
description: `List active teammate agents.
|
|
362
|
+
|
|
363
|
+
Views:
|
|
364
|
+
- "active": All running agents (default)
|
|
365
|
+
- "named": Only named/addressable agents
|
|
366
|
+
- "all": All agents including completed metadata`,
|
|
367
|
+
|
|
368
|
+
parameters: TeammateListParams,
|
|
369
|
+
|
|
370
|
+
async execute(
|
|
371
|
+
_id: string,
|
|
372
|
+
params: { view?: "active" | "named" | "all" },
|
|
373
|
+
): Promise<AgentToolResult<{ agents: unknown[] }>> {
|
|
374
|
+
const view = params.view ?? "active";
|
|
375
|
+
const agents: Array<{
|
|
376
|
+
agent: string;
|
|
377
|
+
name?: string;
|
|
378
|
+
correlationId: string;
|
|
379
|
+
lifecycle: string;
|
|
380
|
+
startedAt: string;
|
|
381
|
+
durationMs: number;
|
|
382
|
+
inboxSize: number;
|
|
383
|
+
hasStdin: boolean;
|
|
384
|
+
}> = [];
|
|
385
|
+
|
|
386
|
+
for (const [cid, entry] of state.activeRuns) {
|
|
387
|
+
if (view === "named" && !entry.name) continue;
|
|
388
|
+
|
|
389
|
+
agents.push({
|
|
390
|
+
agent: entry.agent,
|
|
391
|
+
name: entry.name,
|
|
392
|
+
correlationId: cid,
|
|
393
|
+
lifecycle: entry.lifecycle,
|
|
394
|
+
startedAt: new Date(entry.startedAt).toISOString(),
|
|
395
|
+
durationMs: Date.now() - entry.startedAt,
|
|
396
|
+
idleMs: Date.now() - entry.lastActivityAt,
|
|
397
|
+
inboxSize: entry.inbox.length,
|
|
398
|
+
hasStdin: Boolean(entry.stdin?.writable),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const lines = agents.length > 0
|
|
403
|
+
? agents.map((a) =>
|
|
404
|
+
`[${a.agent}]${a.name ? ` name="${a.name}"` : ""} ${a.lifecycle} | up ${Math.round(a.durationMs / 1000)}s | idle ${Math.round(a.idleMs / 1000)}s | inbox: ${a.inboxSize} | stdin: ${a.hasStdin ? "ready" : "pending"}`
|
|
405
|
+
).join("\n")
|
|
406
|
+
: "No active teammate agents.";
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
content: [{ type: "text", text: lines }],
|
|
410
|
+
isError: false,
|
|
411
|
+
details: { agents },
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// =========================================================================
|
|
417
|
+
// Register all tools
|
|
418
|
+
// =========================================================================
|
|
419
|
+
|
|
222
420
|
pi.registerTool(tool);
|
|
421
|
+
pi.registerTool(sendTool);
|
|
422
|
+
pi.registerTool(listTool);
|
|
423
|
+
|
|
424
|
+
// =========================================================================
|
|
425
|
+
// Session lifecycle + P2-2 reaper
|
|
426
|
+
// =========================================================================
|
|
427
|
+
|
|
428
|
+
let reaperTimer: ReturnType<typeof setInterval> | null = null;
|
|
223
429
|
|
|
224
430
|
pi.on("session_start", (_event, ctx) => {
|
|
225
431
|
state.baseCwd = ctx.cwd;
|
|
226
432
|
state.currentSessionId = ctx.sessionManager?.getSessionId() ?? null;
|
|
433
|
+
reaperTimer = startReaper(state, pi);
|
|
227
434
|
});
|
|
228
435
|
|
|
229
436
|
pi.on("session_shutdown", () => {
|
|
437
|
+
if (reaperTimer) {
|
|
438
|
+
clearInterval(reaperTimer);
|
|
439
|
+
reaperTimer = null;
|
|
440
|
+
}
|
|
230
441
|
for (const run of state.activeRuns.values()) {
|
|
231
442
|
run.abortController.abort();
|
|
232
443
|
}
|
|
233
444
|
state.activeRuns.clear();
|
|
445
|
+
state.namedAgents.clear();
|
|
234
446
|
state.currentSessionId = null;
|
|
235
447
|
});
|
|
236
448
|
}
|
|
449
|
+
|
|
450
|
+
// ===========================================================================
|
|
451
|
+
// Helpers
|
|
452
|
+
// ===========================================================================
|
|
453
|
+
|
|
454
|
+
function emitComplete(
|
|
455
|
+
pi: ExtensionAPI,
|
|
456
|
+
id: string,
|
|
457
|
+
agent: string,
|
|
458
|
+
correlationId: string,
|
|
459
|
+
exitCode: number,
|
|
460
|
+
durationMs: number,
|
|
461
|
+
): void {
|
|
462
|
+
pi.events.emit(TEAMMATE_COMPLETE_EVENT, {
|
|
463
|
+
id, agent, correlationId, exitCode, durationMs,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function cleanupAgent(
|
|
468
|
+
state: TeammateState,
|
|
469
|
+
correlationId: string,
|
|
470
|
+
name?: string,
|
|
471
|
+
): void {
|
|
472
|
+
state.activeRuns.delete(correlationId);
|
|
473
|
+
if (name) {
|
|
474
|
+
state.namedAgents.delete(name);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function drainInbox(agent: ActiveAgent): void {
|
|
479
|
+
if (!agent.stdin || !agent.inbox.length) return;
|
|
480
|
+
for (const msg of agent.inbox) {
|
|
481
|
+
sendToChildStdin(agent.stdin, msg.payload);
|
|
482
|
+
}
|
|
483
|
+
agent.inbox.length = 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ===========================================================================
|
|
487
|
+
// P2-1: Reply-cycle deadlock detection
|
|
488
|
+
// ===========================================================================
|
|
489
|
+
|
|
490
|
+
function detectReplyCycle(
|
|
491
|
+
state: TeammateState,
|
|
492
|
+
fromName: string | undefined,
|
|
493
|
+
replyToName: string | undefined,
|
|
494
|
+
): boolean {
|
|
495
|
+
if (!fromName || !replyToName) return false;
|
|
496
|
+
if (fromName === replyToName) return true;
|
|
497
|
+
|
|
498
|
+
const visited = new Set<string>([fromName]);
|
|
499
|
+
let current = replyToName;
|
|
500
|
+
|
|
501
|
+
while (current) {
|
|
502
|
+
if (visited.has(current)) return true;
|
|
503
|
+
visited.add(current);
|
|
504
|
+
|
|
505
|
+
const cid = state.namedAgents.get(current);
|
|
506
|
+
if (!cid) break;
|
|
507
|
+
const agent = state.activeRuns.get(cid);
|
|
508
|
+
if (!agent?.replyTo) break;
|
|
509
|
+
current = agent.replyTo;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ===========================================================================
|
|
516
|
+
// P2-2: Resident agent idle timeout + reaper
|
|
517
|
+
// ===========================================================================
|
|
518
|
+
|
|
519
|
+
const IDLE_WARN_MS = 5 * 60 * 1000; // 5 min: send nudge
|
|
520
|
+
const IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 min: force terminate
|
|
521
|
+
const REAPER_INTERVAL_MS = 60 * 1000;
|
|
522
|
+
|
|
523
|
+
function startReaper(state: TeammateState, pi: ExtensionAPI): ReturnType<typeof setInterval> {
|
|
524
|
+
const nudged = new Set<string>();
|
|
525
|
+
|
|
526
|
+
return setInterval(() => {
|
|
527
|
+
const now = Date.now();
|
|
528
|
+
|
|
529
|
+
for (const [cid, agent] of state.activeRuns) {
|
|
530
|
+
if (agent.lifecycle !== "resident") continue;
|
|
531
|
+
const idle = now - agent.lastActivityAt;
|
|
532
|
+
|
|
533
|
+
if (idle > IDLE_TIMEOUT_MS) {
|
|
534
|
+
agent.abortController.abort();
|
|
535
|
+
pi.events.emit(TEAMMATE_COMPLETE_EVENT, {
|
|
536
|
+
id: cid,
|
|
537
|
+
agent: agent.agent,
|
|
538
|
+
correlationId: agent.correlationId,
|
|
539
|
+
exitCode: 0,
|
|
540
|
+
durationMs: now - agent.startedAt,
|
|
541
|
+
reason: "idle_timeout",
|
|
542
|
+
});
|
|
543
|
+
nudged.delete(cid);
|
|
544
|
+
cleanupAgent(state, cid, agent.name);
|
|
545
|
+
} else if (idle > IDLE_WARN_MS && !nudged.has(cid)) {
|
|
546
|
+
if (agent.stdin) {
|
|
547
|
+
sendToChildStdin(agent.stdin,
|
|
548
|
+
`[system] Idle for ${Math.round(idle / 60000)} minutes. Continue your current task or wrap up. You will be terminated after ${Math.round(IDLE_TIMEOUT_MS / 60000)} minutes of inactivity.`);
|
|
549
|
+
agent.lastActivityAt = now;
|
|
550
|
+
}
|
|
551
|
+
nudged.add(cid);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}, REAPER_INTERVAL_MS);
|
|
555
|
+
}
|
package/src/extension/schemas.ts
CHANGED
|
@@ -146,3 +146,21 @@ export const TeammateParams = Type.Object({
|
|
|
146
146
|
}),
|
|
147
147
|
),
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
export const TeammateSendParams = Type.Object({
|
|
151
|
+
to: Type.String({
|
|
152
|
+
description: "Target agent name (must be a named, running agent)",
|
|
153
|
+
}),
|
|
154
|
+
message: Type.String({
|
|
155
|
+
description: "Message content to send to the agent",
|
|
156
|
+
}),
|
|
157
|
+
kind: Type.Optional(
|
|
158
|
+
StringEnum(["notification", "task"]),
|
|
159
|
+
),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
export const TeammateListParams = Type.Object({
|
|
163
|
+
view: Type.Optional(
|
|
164
|
+
StringEnum(["active", "named", "all"]),
|
|
165
|
+
),
|
|
166
|
+
});
|
package/src/runs/execution.ts
CHANGED
|
@@ -15,7 +15,8 @@ import * as os from "node:os";
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import { resolveAgent, type AgentConfig } from "../agents/agents.ts";
|
|
17
17
|
import { resolveReplyTo, type ReplyTarget } from "../shared/routing.ts";
|
|
18
|
-
import type { SingleResult, Usage, AgentProgress } from "../shared/types.ts";
|
|
18
|
+
import type { SingleResult, Usage, AgentProgress, ActiveAgent } from "../shared/types.ts";
|
|
19
|
+
import type { Writable } from "node:stream";
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Public param / option interfaces
|
|
@@ -45,6 +46,7 @@ export interface RunTeammateOptions {
|
|
|
45
46
|
signal?: AbortSignal;
|
|
46
47
|
onProgress?: (data: AgentProgress) => void;
|
|
47
48
|
parentSessionFile?: string;
|
|
49
|
+
onChildSpawned?: (stdin: Writable) => void;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// ---------------------------------------------------------------------------
|
|
@@ -406,6 +408,11 @@ async function runSingleAttempt(
|
|
|
406
408
|
return;
|
|
407
409
|
}
|
|
408
410
|
|
|
411
|
+
// P1: Expose stdin for message injection
|
|
412
|
+
if (child.stdin) {
|
|
413
|
+
options.onChildSpawned?.(child.stdin);
|
|
414
|
+
}
|
|
415
|
+
|
|
409
416
|
// Report initial progress
|
|
410
417
|
options.onProgress?.(progress);
|
|
411
418
|
|
|
@@ -673,3 +680,17 @@ export async function runChain(
|
|
|
673
680
|
|
|
674
681
|
return results;
|
|
675
682
|
}
|
|
683
|
+
|
|
684
|
+
// ---------------------------------------------------------------------------
|
|
685
|
+
// P1: Send message to running agent via stdin
|
|
686
|
+
// ---------------------------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
export function sendToChildStdin(stdin: Writable, message: string): boolean {
|
|
689
|
+
if (!stdin.writable) return false;
|
|
690
|
+
const payload = JSON.stringify({
|
|
691
|
+
type: "user",
|
|
692
|
+
content: message,
|
|
693
|
+
});
|
|
694
|
+
stdin.write(payload + "\n");
|
|
695
|
+
return true;
|
|
696
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -47,16 +47,39 @@ export interface Details {
|
|
|
47
47
|
}>;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export type MessageKind = "task" | "notification" | "result";
|
|
51
|
+
|
|
52
|
+
export interface MessageEnvelope {
|
|
53
|
+
id: string;
|
|
54
|
+
from: string;
|
|
55
|
+
to: string;
|
|
56
|
+
kind: MessageKind;
|
|
57
|
+
correlation_id?: string;
|
|
58
|
+
payload: string;
|
|
59
|
+
timestamp: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ActiveAgent {
|
|
63
|
+
agent: string;
|
|
64
|
+
name?: string;
|
|
65
|
+
correlationId: string;
|
|
66
|
+
startedAt: number;
|
|
67
|
+
abortController: AbortController;
|
|
68
|
+
stdin?: import("node:stream").Writable;
|
|
69
|
+
lifecycle: "ephemeral" | "resident";
|
|
70
|
+
inbox: MessageEnvelope[];
|
|
71
|
+
pendingResolve?: (result: SingleResult) => void;
|
|
72
|
+
lastActivityAt: number;
|
|
73
|
+
replyTo?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
50
76
|
export interface TeammateState {
|
|
51
77
|
baseCwd: string;
|
|
52
78
|
currentSessionId: string | null;
|
|
53
|
-
activeRuns: Map<string,
|
|
54
|
-
|
|
55
|
-
correlationId: string;
|
|
56
|
-
startedAt: number;
|
|
57
|
-
abortController: AbortController;
|
|
58
|
-
}>;
|
|
79
|
+
activeRuns: Map<string, ActiveAgent>;
|
|
80
|
+
namedAgents: Map<string, string>;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
83
|
export const TEAMMATE_COMPLETE_EVENT = "teammate:complete";
|
|
62
84
|
export const TEAMMATE_STARTED_EVENT = "teammate:started";
|
|
85
|
+
export const TEAMMATE_MESSAGE_EVENT = "teammate:message";
|