pi-squad 0.16.2 → 0.16.4
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/README.md +18 -12
- package/package.json +1 -1
- package/src/agent-pool.ts +123 -44
- package/src/index.ts +161 -158
- package/src/panel/message-view.ts +58 -50
- package/src/panel/squad-widget.ts +35 -10
- package/src/panel/task-list.ts +13 -3
- package/src/router.ts +37 -35
- package/src/scheduler.ts +292 -65
- package/src/skills/squad-supervisor/SKILL.md +2 -1
- package/src/store.ts +195 -1
- package/src/types.ts +26 -0
package/src/scheduler.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
-
import type { AgentDef, Squad, SquadConfig, Task, TaskMessage, TaskStatus } from "./types.js";
|
|
14
|
+
import type { AgentDef, Squad, SquadConfig, Task, TaskMailboxEntry, TaskMessage, TaskStatus } from "./types.js";
|
|
15
15
|
import { AgentPool, type AgentEvent } from "./agent-pool.js";
|
|
16
16
|
import { Monitor } from "./monitor.js";
|
|
17
17
|
import { Router } from "./router.js";
|
|
@@ -34,6 +34,7 @@ export type SchedulerEventType =
|
|
|
34
34
|
| "task_rework"
|
|
35
35
|
| "squad_review_required"
|
|
36
36
|
| "squad_failed"
|
|
37
|
+
| "orchestrator_reply"
|
|
37
38
|
| "escalation"
|
|
38
39
|
| "activity";
|
|
39
40
|
|
|
@@ -240,6 +241,32 @@ export class Scheduler {
|
|
|
240
241
|
|
|
241
242
|
const tasks = store.loadAllTasks(this.squadId);
|
|
242
243
|
|
|
244
|
+
// 0. Mailbox recovery is level-triggered from disk. If the previous
|
|
245
|
+
// process stopped after queueTaskMessage's atomic write but before status
|
|
246
|
+
// mutation/RPC delivery, reopen exactly that task on reconstruction.
|
|
247
|
+
let recoveredMailbox = false;
|
|
248
|
+
for (const task of tasks) {
|
|
249
|
+
if (this.pool.isRunning(task.id)) continue;
|
|
250
|
+
if (store.loadPendingTaskMessages(this.squadId, task.id).length === 0) continue;
|
|
251
|
+
if (task.status === "done") await this.invalidateDescendants(task.id);
|
|
252
|
+
const dependenciesDone = task.depends.every(
|
|
253
|
+
(depId) => tasks.find((candidate) => candidate.id === depId)?.status === "done",
|
|
254
|
+
);
|
|
255
|
+
const nextStatus: TaskStatus = dependenciesDone ? "pending" : "blocked";
|
|
256
|
+
if (task.status !== nextStatus || task.completed !== null || task.error !== null) {
|
|
257
|
+
task.status = nextStatus;
|
|
258
|
+
task.completed = null;
|
|
259
|
+
task.error = null;
|
|
260
|
+
store.updateTaskStatus(this.squadId, task.id, nextStatus, { completed: null, error: null });
|
|
261
|
+
}
|
|
262
|
+
recoveredMailbox = true;
|
|
263
|
+
}
|
|
264
|
+
if (recoveredMailbox && squad.status !== "running") {
|
|
265
|
+
squad.status = "running";
|
|
266
|
+
delete squad.review;
|
|
267
|
+
store.saveSquad(squad);
|
|
268
|
+
}
|
|
269
|
+
|
|
243
270
|
// 1. Blocked → pending when all deps are done (respects autoUnblock config)
|
|
244
271
|
if (squad.config.autoUnblock) {
|
|
245
272
|
for (const task of tasks) {
|
|
@@ -322,7 +349,12 @@ export class Scheduler {
|
|
|
322
349
|
/** Get tasks that are ready to execute (pending + all deps done) */
|
|
323
350
|
private getReadyTasks(tasks: Task[]): Task[] {
|
|
324
351
|
return tasks.filter((task) => {
|
|
325
|
-
|
|
352
|
+
// A reconstructed scheduler has an empty process pool. Persisted
|
|
353
|
+
// in_progress means the previous process stopped mid-task, so resume its
|
|
354
|
+
// bound session instead of stranding it or creating a fresh context.
|
|
355
|
+
const needsProcess = task.status === "pending" ||
|
|
356
|
+
(task.status === "in_progress" && !this.pool.isRunning(task.id));
|
|
357
|
+
if (!needsProcess) return false;
|
|
326
358
|
return task.depends.every((depId) => {
|
|
327
359
|
const dep = tasks.find((t) => t.id === depId);
|
|
328
360
|
return dep?.status === "done";
|
|
@@ -375,9 +407,24 @@ export class Scheduler {
|
|
|
375
407
|
}
|
|
376
408
|
}
|
|
377
409
|
|
|
378
|
-
|
|
410
|
+
const resumeSession = store.loadTaskSession(this.squadId, task.id) ?? undefined;
|
|
411
|
+
const pendingMailbox = store.loadPendingTaskMessages(this.squadId, task.id);
|
|
412
|
+
// Legacy tasks predate task-owned Pi sessions. Their first durable spawn
|
|
413
|
+
// must receive the complete persisted history/output as a migration seed;
|
|
414
|
+
// otherwise reopening silently discards all prior context.
|
|
415
|
+
const legacyHistory = !resumeSession && (task.started !== null || task.completed !== null || task.output !== null)
|
|
416
|
+
? store.loadMessages(this.squadId, task.id)
|
|
417
|
+
: [];
|
|
418
|
+
const legacySeed = legacyHistory.length > 0 || (!resumeSession && task.output !== null)
|
|
419
|
+
? { messages: legacyHistory, output: task.output }
|
|
420
|
+
: undefined;
|
|
421
|
+
|
|
422
|
+
// Keep the original start time across resumes. A resumed/live process owns
|
|
423
|
+
// in_progress until Pi emits final agent_settled.
|
|
379
424
|
store.updateTaskStatus(this.squadId, task.id, "in_progress", {
|
|
380
|
-
started: store.now(),
|
|
425
|
+
started: task.started ?? store.now(),
|
|
426
|
+
completed: null,
|
|
427
|
+
error: null,
|
|
381
428
|
});
|
|
382
429
|
|
|
383
430
|
store.appendMessage(this.squadId, task.id, {
|
|
@@ -394,11 +441,13 @@ export class Scheduler {
|
|
|
394
441
|
agentName: task.agent,
|
|
395
442
|
});
|
|
396
443
|
|
|
397
|
-
//
|
|
398
|
-
|
|
444
|
+
// Context inheritance creates a session only on the task's first run.
|
|
445
|
+
// Every later run must reopen the immutable task binding.
|
|
446
|
+
const forkSessionFile = resumeSession ? undefined : this.resolveForkSession(task, squad, agentDef);
|
|
447
|
+
const taskSessionDir = store.getTaskSessionDir(this.squadId, task.id);
|
|
399
448
|
|
|
400
449
|
try {
|
|
401
|
-
await this.pool.spawn({
|
|
450
|
+
const agent = await this.pool.spawn({
|
|
402
451
|
taskId: task.id,
|
|
403
452
|
agentDef,
|
|
404
453
|
protocolOptions: {
|
|
@@ -407,19 +456,28 @@ export class Scheduler {
|
|
|
407
456
|
task,
|
|
408
457
|
agentDef,
|
|
409
458
|
modifiedFiles,
|
|
410
|
-
queuedMessages:
|
|
459
|
+
queuedMessages: pendingMailbox.map((entry) => entry.message),
|
|
411
460
|
},
|
|
412
461
|
cwd: squad.cwd,
|
|
413
462
|
skillPaths: this.skillPaths,
|
|
414
|
-
...(
|
|
415
|
-
? {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
},
|
|
420
|
-
}
|
|
421
|
-
: {}),
|
|
463
|
+
...(resumeSession
|
|
464
|
+
? { resumeSession }
|
|
465
|
+
: forkSessionFile
|
|
466
|
+
? { forkSession: { file: forkSessionFile, sessionDir: taskSessionDir } }
|
|
467
|
+
: { sessionDir: taskSessionDir }),
|
|
422
468
|
});
|
|
469
|
+
store.bindTaskSession(this.squadId, task.id, agent.session);
|
|
470
|
+
|
|
471
|
+
const accepted = await this.pool.prompt(
|
|
472
|
+
task.id,
|
|
473
|
+
this.buildTaskPrompt(task, Boolean(resumeSession), pendingMailbox, legacySeed),
|
|
474
|
+
);
|
|
475
|
+
if (!accepted) throw new Error(`Agent ${task.agent} did not accept the task prompt`);
|
|
476
|
+
store.acknowledgeTaskMessages(
|
|
477
|
+
this.squadId,
|
|
478
|
+
task.id,
|
|
479
|
+
pendingMailbox.map((entry) => entry.id),
|
|
480
|
+
);
|
|
423
481
|
} catch (error) {
|
|
424
482
|
this.handleTaskFailed(task.id, (error as Error).message);
|
|
425
483
|
}
|
|
@@ -544,6 +602,77 @@ export class Scheduler {
|
|
|
544
602
|
return sessionFile;
|
|
545
603
|
}
|
|
546
604
|
|
|
605
|
+
private buildTaskPrompt(
|
|
606
|
+
task: Task,
|
|
607
|
+
resumed: boolean,
|
|
608
|
+
entries: TaskMailboxEntry[],
|
|
609
|
+
legacySeed?: { messages: TaskMessage[]; output: string | null },
|
|
610
|
+
): string {
|
|
611
|
+
const lines = resumed
|
|
612
|
+
? [
|
|
613
|
+
`Resume your existing task: ${task.title}`,
|
|
614
|
+
"Continue from the durable Pi session context. Do not restart the task from scratch.",
|
|
615
|
+
]
|
|
616
|
+
: [
|
|
617
|
+
`Your task: ${task.title}`,
|
|
618
|
+
"",
|
|
619
|
+
task.description || "",
|
|
620
|
+
];
|
|
621
|
+
|
|
622
|
+
if (legacySeed) {
|
|
623
|
+
const pendingIds = new Set(entries.map((entry) => entry.id));
|
|
624
|
+
lines.push("", "Legacy task migration: the following persisted history is the complete prior context. Preserve it and continue from it.");
|
|
625
|
+
for (const message of legacySeed.messages) {
|
|
626
|
+
if (message.id && pendingIds.has(message.id)) continue;
|
|
627
|
+
lines.push("", `[${message.ts}] ${message.from} (${message.type}):`, message.text);
|
|
628
|
+
}
|
|
629
|
+
if (legacySeed.output !== null) {
|
|
630
|
+
lines.push("", "Prior durable task output:", legacySeed.output);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (entries.length > 0) {
|
|
635
|
+
lines.push("", "New durable messages for this task:");
|
|
636
|
+
for (const entry of entries) {
|
|
637
|
+
lines.push("", `[${entry.message.ts}] ${entry.message.from}:`, entry.message.text);
|
|
638
|
+
if (entry.message.expectsReply) {
|
|
639
|
+
lines.push("[Direct response required by main orchestrator]");
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
lines.push("", "Read and act on every complete message above.");
|
|
643
|
+
} else if (resumed) {
|
|
644
|
+
lines.push("", "The previous process ended before final settlement. Continue the unfinished work and report the final result.");
|
|
645
|
+
}
|
|
646
|
+
return lines.join("\n");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private handleUnexpectedAgentExit(event: AgentEvent): void {
|
|
650
|
+
const exitCode = event.data?.exitCode ?? 1;
|
|
651
|
+
const turnCount = event.data?.turnCount ?? 0;
|
|
652
|
+
const stderr = event.data?.stderr || "";
|
|
653
|
+
const retryKey = `spawn-retry:${event.taskId}`;
|
|
654
|
+
if (!this.spawnRetries.has(retryKey)) {
|
|
655
|
+
this.spawnRetries.add(retryKey);
|
|
656
|
+
const reason = turnCount === 0
|
|
657
|
+
? "exited with 0 turns (likely rate limit or API error)"
|
|
658
|
+
: `exited before final agent_settled after ${turnCount} turns`;
|
|
659
|
+
logError("squad-scheduler", `Agent ${event.agentName} ${reason}, code=${exitCode}. Retrying in 2s... stderr: ${stderr}`);
|
|
660
|
+
store.updateTaskStatus(this.squadId, event.taskId, "pending");
|
|
661
|
+
store.appendMessage(this.squadId, event.taskId, {
|
|
662
|
+
ts: store.now(),
|
|
663
|
+
from: "system",
|
|
664
|
+
type: "status",
|
|
665
|
+
text: `Agent ${reason}. Resuming the same task session...`,
|
|
666
|
+
});
|
|
667
|
+
setTimeout(() => {
|
|
668
|
+
if (this.running) this.scheduleReadyTasks();
|
|
669
|
+
}, 2000);
|
|
670
|
+
} else {
|
|
671
|
+
this.handleTaskFailed(event.taskId, `Agent exited with code ${exitCode} before final agent_settled (retry exhausted). ${stderr}`);
|
|
672
|
+
}
|
|
673
|
+
this.updateContext();
|
|
674
|
+
}
|
|
675
|
+
|
|
547
676
|
// =========================================================================
|
|
548
677
|
// Event Handlers
|
|
549
678
|
// =========================================================================
|
|
@@ -568,6 +697,26 @@ export class Scheduler {
|
|
|
568
697
|
// presentation layers may viewport them, but source data must never truncate.
|
|
569
698
|
text,
|
|
570
699
|
});
|
|
700
|
+
|
|
701
|
+
// A squad_message is a request/response channel, not fire-and-forget.
|
|
702
|
+
// Persist an acknowledgement marker before emitting so restart/focus
|
|
703
|
+
// changes cannot forward the same response twice.
|
|
704
|
+
if (this.hasPendingOrchestratorRequest(event.taskId)) {
|
|
705
|
+
store.appendMessage(this.squadId, event.taskId, {
|
|
706
|
+
ts: store.now(),
|
|
707
|
+
from: event.agentName,
|
|
708
|
+
type: "reply",
|
|
709
|
+
to: "orchestrator",
|
|
710
|
+
text: "Response forwarded to main orchestrator",
|
|
711
|
+
});
|
|
712
|
+
this.emit({
|
|
713
|
+
type: "orchestrator_reply",
|
|
714
|
+
squadId: this.squadId,
|
|
715
|
+
taskId: event.taskId,
|
|
716
|
+
agentName: event.agentName,
|
|
717
|
+
message: text,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
571
720
|
}
|
|
572
721
|
|
|
573
722
|
// Track usage
|
|
@@ -617,50 +766,45 @@ export class Scheduler {
|
|
|
617
766
|
break;
|
|
618
767
|
}
|
|
619
768
|
|
|
620
|
-
|
|
621
|
-
|
|
769
|
+
case "agent_end": {
|
|
770
|
+
// Pi's low-level agent_end is not completion. AgentPool normally keeps
|
|
771
|
+
// it internal; if observed here, preserve in_progress. Only an actual
|
|
772
|
+
// child-process exit carries unexpectedExit and enters retry handling.
|
|
773
|
+
if (!event.data?.unexpectedExit) break;
|
|
774
|
+
this.handleUnexpectedAgentExit(event);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
case "agent_settled": {
|
|
779
|
+
// A mailbox entry not acknowledged by Pi outranks this run's candidate
|
|
780
|
+
// completion. Reopen the same session so accepted-at-least-once delivery
|
|
781
|
+
// occurs before the task can become done.
|
|
782
|
+
if (store.loadPendingTaskMessages(this.squadId, event.taskId).length > 0) {
|
|
783
|
+
store.updateTaskStatus(this.squadId, event.taskId, "pending", { completed: null });
|
|
784
|
+
store.appendMessage(this.squadId, event.taskId, {
|
|
785
|
+
ts: store.now(),
|
|
786
|
+
from: "system",
|
|
787
|
+
type: "status",
|
|
788
|
+
text: "Agent settled with pending durable messages; resuming the same task session",
|
|
789
|
+
});
|
|
790
|
+
if (this.running) void this.reconcile();
|
|
791
|
+
this.updateContext();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
622
795
|
const turnCount = event.data?.turnCount ?? 0;
|
|
623
796
|
const toolCallCount = event.data?.toolCallCount ?? 0;
|
|
624
|
-
|
|
625
|
-
// Tool use is strong evidence of work, but planning/review tasks can
|
|
626
|
-
// legitimately deliver a substantive report without calling a tool.
|
|
627
|
-
// Accept durable assistant output; the mandatory main-orchestrator
|
|
628
|
-
// review gate still decides whether the work satisfies the contract.
|
|
629
797
|
const hasSubstantiveOutput = store.loadMessages(this.squadId, event.taskId)
|
|
630
798
|
.some((message) => message.from === event.agentName && message.type === "text" && message.text.trim().length > 0);
|
|
631
799
|
const hadMeaningfulWork = turnCount > 0 && (toolCallCount > 0 || hasSubstantiveOutput);
|
|
632
800
|
if (hadMeaningfulWork) {
|
|
633
801
|
this.handleTaskCompleted(event.taskId).then(() => this.updateContext());
|
|
634
802
|
} else {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (!this.spawnRetries.has(retryKey)) {
|
|
640
|
-
this.spawnRetries.add(retryKey);
|
|
641
|
-
const stderr = event.data?.stderr || "";
|
|
642
|
-
const reason = turnCount === 0
|
|
643
|
-
? `exited with 0 turns (likely rate limit or API error)`
|
|
644
|
-
: `exited with ${turnCount} turns but no tool calls or substantive output`;
|
|
645
|
-
logError("squad-scheduler", `Agent ${event.agentName} ${reason}, code=${exitCode}. Retrying in 2s... stderr: ${stderr}`);
|
|
646
|
-
store.updateTaskStatus(this.squadId, event.taskId, "pending");
|
|
647
|
-
store.appendMessage(this.squadId, event.taskId, {
|
|
648
|
-
ts: store.now(),
|
|
649
|
-
from: "system",
|
|
650
|
-
type: "status",
|
|
651
|
-
text: `Agent ${reason}. Retrying...`,
|
|
652
|
-
});
|
|
653
|
-
// Delay retry to let resources settle
|
|
654
|
-
setTimeout(() => {
|
|
655
|
-
if (this.running) this.scheduleReadyTasks();
|
|
656
|
-
}, 2000);
|
|
657
|
-
} else {
|
|
658
|
-
const stderr = event.data?.stderr || "";
|
|
659
|
-
this.handleTaskFailed(event.taskId, `Agent exited with code ${exitCode} (retry exhausted). ${stderr}`);
|
|
660
|
-
}
|
|
661
|
-
this.updateContext();
|
|
803
|
+
this.handleUnexpectedAgentExit({
|
|
804
|
+
...event,
|
|
805
|
+
data: { ...event.data, unexpectedExit: true },
|
|
806
|
+
});
|
|
662
807
|
}
|
|
663
|
-
// Skip the updateContext() below — handled in the branches above
|
|
664
808
|
return;
|
|
665
809
|
}
|
|
666
810
|
|
|
@@ -1118,31 +1262,114 @@ export class Scheduler {
|
|
|
1118
1262
|
// External Actions
|
|
1119
1263
|
// =========================================================================
|
|
1120
1264
|
|
|
1121
|
-
/**
|
|
1122
|
-
|
|
1123
|
-
|
|
1265
|
+
/**
|
|
1266
|
+
* A reopened dependency invalidates every transitive descendant, including
|
|
1267
|
+
* descendants that had already completed. They retain history/output for
|
|
1268
|
+
* audit and durable-session continuation, but must rerun in dependency order.
|
|
1269
|
+
*/
|
|
1270
|
+
private async invalidateDescendants(reopenedTaskId: string): Promise<void> {
|
|
1271
|
+
const tasks = store.loadAllTasks(this.squadId);
|
|
1272
|
+
const byDependency = new Map<string, Task[]>();
|
|
1273
|
+
for (const task of tasks) {
|
|
1274
|
+
for (const dependency of task.depends) {
|
|
1275
|
+
const dependents = byDependency.get(dependency) ?? [];
|
|
1276
|
+
dependents.push(task);
|
|
1277
|
+
byDependency.set(dependency, dependents);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const queue = [...(byDependency.get(reopenedTaskId) ?? [])];
|
|
1282
|
+
const seen = new Set<string>();
|
|
1283
|
+
const kills: Promise<void>[] = [];
|
|
1284
|
+
while (queue.length > 0) {
|
|
1285
|
+
const descendant = queue.shift()!;
|
|
1286
|
+
if (seen.has(descendant.id)) continue;
|
|
1287
|
+
seen.add(descendant.id);
|
|
1288
|
+
queue.push(...(byDependency.get(descendant.id) ?? []));
|
|
1289
|
+
store.updateTaskStatus(this.squadId, descendant.id, "blocked", {
|
|
1290
|
+
completed: null,
|
|
1291
|
+
error: null,
|
|
1292
|
+
});
|
|
1293
|
+
store.appendMessage(this.squadId, descendant.id, {
|
|
1294
|
+
ts: store.now(),
|
|
1295
|
+
from: "system",
|
|
1296
|
+
type: "status",
|
|
1297
|
+
text: `Blocked — dependency ancestry reopened at ${reopenedTaskId}; prior result requires revalidation`,
|
|
1298
|
+
});
|
|
1299
|
+
if (this.pool.isRunning(descendant.id)) kills.push(this.pool.kill(descendant.id));
|
|
1300
|
+
}
|
|
1301
|
+
await Promise.all(kills);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
private async reopenTaskForMessage(task: Task): Promise<void> {
|
|
1305
|
+
if (task.status === "done") await this.invalidateDescendants(task.id);
|
|
1306
|
+
const tasks = store.loadAllTasks(this.squadId);
|
|
1307
|
+
const dependenciesDone = task.depends.every(
|
|
1308
|
+
(depId) => tasks.find((candidate) => candidate.id === depId)?.status === "done",
|
|
1309
|
+
);
|
|
1310
|
+
const nextStatus: TaskStatus = dependenciesDone ? "pending" : "blocked";
|
|
1311
|
+
store.updateTaskStatus(this.squadId, task.id, nextStatus, {
|
|
1312
|
+
error: null,
|
|
1313
|
+
completed: null,
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
const squad = store.loadSquad(this.squadId);
|
|
1317
|
+
if (squad && squad.status !== "running") {
|
|
1318
|
+
squad.status = "running";
|
|
1319
|
+
// Any prior acceptance/review applies to the pre-resume result. The
|
|
1320
|
+
// normal all-done path will require a fresh independent review.
|
|
1321
|
+
delete squad.review;
|
|
1322
|
+
store.saveSquad(squad);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/** Send a main-orchestrator request to one exact task and await its next reply. */
|
|
1327
|
+
async sendHumanMessage(taskId: string, message: string, expectsReply = true): Promise<boolean> {
|
|
1328
|
+
const task = store.loadTask(this.squadId, taskId);
|
|
1329
|
+
if (!task) return false;
|
|
1330
|
+
|
|
1331
|
+
// Mailbox-first: a process/scheduler crash can occur at any later point
|
|
1332
|
+
// without losing or redirecting this message to another task of the role.
|
|
1333
|
+
const queued = store.queueTaskMessage(this.squadId, taskId, {
|
|
1124
1334
|
ts: store.now(),
|
|
1125
|
-
from: "
|
|
1335
|
+
from: "orchestrator",
|
|
1126
1336
|
type: "message",
|
|
1127
1337
|
text: message,
|
|
1338
|
+
expectsReply,
|
|
1128
1339
|
});
|
|
1129
1340
|
|
|
1341
|
+
const request = expectsReply
|
|
1342
|
+
? `[squad] Main orchestrator requests a direct response:\n${message}\n\nReply directly in your next assistant message. That complete message will be forwarded automatically to the main session.`
|
|
1343
|
+
: `[squad] Main orchestrator message:\n${message}`;
|
|
1130
1344
|
if (this.pool.isRunning(taskId)) {
|
|
1131
|
-
|
|
1345
|
+
if (await this.pool.steer(taskId, request)) {
|
|
1346
|
+
store.acknowledgeTaskMessages(this.squadId, taskId, [queued.id]);
|
|
1347
|
+
return true;
|
|
1348
|
+
}
|
|
1349
|
+
// The process may still be completing its current run. Preserve
|
|
1350
|
+
// in_progress while it is live; agent_settled will observe the pending
|
|
1351
|
+
// mailbox and reopen the same session instead of marking done.
|
|
1352
|
+
if (this.pool.isRunning(taskId)) return false;
|
|
1132
1353
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
if (
|
|
1136
|
-
this.
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
type: "message",
|
|
1140
|
-
text: message,
|
|
1141
|
-
});
|
|
1354
|
+
|
|
1355
|
+
await this.reopenTaskForMessage(task);
|
|
1356
|
+
if (this.running) {
|
|
1357
|
+
await this.reconcile();
|
|
1358
|
+
return store.loadPendingTaskMessages(this.squadId, taskId)
|
|
1359
|
+
.every((entry) => entry.id !== queued.id);
|
|
1142
1360
|
}
|
|
1143
1361
|
return false;
|
|
1144
1362
|
}
|
|
1145
1363
|
|
|
1364
|
+
private hasPendingOrchestratorRequest(taskId: string): boolean {
|
|
1365
|
+
let pending = false;
|
|
1366
|
+
for (const message of store.loadMessages(this.squadId, taskId)) {
|
|
1367
|
+
if (message.from === "orchestrator" && message.expectsReply) pending = true;
|
|
1368
|
+
if (message.type === "reply" && message.to === "orchestrator") pending = false;
|
|
1369
|
+
}
|
|
1370
|
+
return pending;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1146
1373
|
/** Pause a running task */
|
|
1147
1374
|
async pauseTask(taskId: string): Promise<void> {
|
|
1148
1375
|
if (this.pool.isRunning(taskId)) {
|
|
@@ -130,7 +130,8 @@ Common escalation patterns:
|
|
|
130
130
|
Use `squad_message` with:
|
|
131
131
|
- `taskId` — target a specific task
|
|
132
132
|
- `agent` — target whichever task an agent is working on
|
|
133
|
-
- `message` — your instruction or
|
|
133
|
+
- `message` — your instruction or question
|
|
134
|
+
- `expectReply` — defaults to `true`; the agent's next substantive response is durably pushed back into the main Pi session and wakes you. Set `false` only for fire-and-forget steering.
|
|
134
135
|
|
|
135
136
|
Keep messages **specific and actionable**:
|
|
136
137
|
- Good: "Use RS256 for JWT signing. The secret is in env var JWT_SECRET."
|