pi-squad 0.16.3 → 0.16.5
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 +24 -15
- package/package.json +1 -1
- package/src/agent-pool.ts +123 -44
- package/src/index.ts +258 -223
- 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/review.ts +15 -0
- package/src/router.ts +37 -35
- package/src/scheduler.ts +300 -73
- package/src/store.ts +195 -1
- package/src/types.ts +27 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* message-view.ts — Scrollable message log for a task.
|
|
3
|
-
*
|
|
4
|
-
* TUI
|
|
3
|
+
* The durable history is never sliced; only the fixed-height viewport is
|
|
4
|
+
* collected for the TUI so large histories cannot change component height.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
@@ -9,11 +9,6 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
9
9
|
import type { TaskMessage } from "../types.js";
|
|
10
10
|
import * as store from "../store.js";
|
|
11
11
|
|
|
12
|
-
/** Max messages to render (most recent). Older messages are skipped. */
|
|
13
|
-
const MAX_MESSAGES = 30;
|
|
14
|
-
/** Max lines per text message before truncation */
|
|
15
|
-
const MAX_TEXT_LINES = 5;
|
|
16
|
-
|
|
17
12
|
export class MessageView {
|
|
18
13
|
private theme: Theme;
|
|
19
14
|
private squadId: string;
|
|
@@ -79,21 +74,13 @@ export class MessageView {
|
|
|
79
74
|
return pad(header, maxLines);
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const msgLines: string[] = [];
|
|
87
|
-
if (skipped > 0) {
|
|
88
|
-
msgLines.push(fit(th.fg("dim", ` ··· ${skipped} older messages ···`), w));
|
|
89
|
-
msgLines.push("");
|
|
90
|
-
}
|
|
91
|
-
msgLines.push(...this.renderMessages(messages, w));
|
|
92
|
-
|
|
93
|
-
// Fixed layout: header + scrollable content + status line = maxLines exactly
|
|
77
|
+
// Fixed layout: header + scrollable content + status line = maxLines exactly.
|
|
78
|
+
// Count lazily, then collect only the visible window. This keeps the renderer
|
|
79
|
+
// bounded without dropping any durable message or body line.
|
|
94
80
|
const statusLines = 1; // always show status/scroll bar
|
|
95
81
|
const contentHeight = Math.max(1, maxLines - header.length - statusLines);
|
|
96
|
-
const
|
|
82
|
+
const messageLineCount = this.countMessageLines(allMessages, w);
|
|
83
|
+
const maxScroll = Math.max(0, messageLineCount - contentHeight);
|
|
97
84
|
|
|
98
85
|
// Auto-scroll to bottom unless user scrolled up
|
|
99
86
|
if (!this.userScrolled) {
|
|
@@ -108,10 +95,10 @@ export class MessageView {
|
|
|
108
95
|
// Build output — exact height every time
|
|
109
96
|
const lines = [...header];
|
|
110
97
|
|
|
111
|
-
// Content area: pad to exact contentHeight
|
|
112
|
-
const visible =
|
|
98
|
+
// Content area: collect and pad to exact contentHeight.
|
|
99
|
+
const visible = this.collectMessageLines(allMessages, w, this.scrollOffset, contentHeight);
|
|
113
100
|
while (visible.length < contentHeight) visible.push("");
|
|
114
|
-
lines.push(...visible
|
|
101
|
+
lines.push(...visible);
|
|
115
102
|
|
|
116
103
|
// Status bar (always present, keeps layout stable)
|
|
117
104
|
const pct = maxScroll > 0 ? Math.round((this.scrollOffset / maxScroll) * 100) : 100;
|
|
@@ -124,10 +111,32 @@ export class MessageView {
|
|
|
124
111
|
return lines.slice(0, maxLines);
|
|
125
112
|
}
|
|
126
113
|
|
|
127
|
-
private
|
|
114
|
+
private countMessageLines(messages: TaskMessage[], width: number): number {
|
|
115
|
+
let count = 0;
|
|
116
|
+
for (const _line of this.iterMessageLines(messages, width)) count++;
|
|
117
|
+
return count;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private collectMessageLines(
|
|
121
|
+
messages: TaskMessage[],
|
|
122
|
+
width: number,
|
|
123
|
+
start: number,
|
|
124
|
+
limit: number,
|
|
125
|
+
): string[] {
|
|
126
|
+
const visible: string[] = [];
|
|
127
|
+
let index = 0;
|
|
128
|
+
for (const line of this.iterMessageLines(messages, width)) {
|
|
129
|
+
if (index++ < start) continue;
|
|
130
|
+
visible.push(line);
|
|
131
|
+
if (visible.length >= limit) break;
|
|
132
|
+
}
|
|
133
|
+
return visible;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private *iterMessageLines(messages: TaskMessage[], width: number): Generator<string> {
|
|
128
137
|
const th = this.theme;
|
|
129
|
-
const lines: string[] = [];
|
|
130
138
|
let lastFrom: string | null = null;
|
|
139
|
+
let hasOutput = false;
|
|
131
140
|
|
|
132
141
|
for (const msg of messages) {
|
|
133
142
|
if (msg.type === "status" && msg.from === "system" && msg.text === "Agent starting work") continue;
|
|
@@ -136,54 +145,47 @@ export class MessageView {
|
|
|
136
145
|
lastFrom = msg.from;
|
|
137
146
|
|
|
138
147
|
if (showHeader) {
|
|
139
|
-
if (
|
|
148
|
+
if (hasOutput) yield "";
|
|
140
149
|
const time = fmtTime(msg.ts);
|
|
141
|
-
const color = msg.from === "human"
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
const color = msg.from === "human" || msg.from === "orchestrator"
|
|
151
|
+
? "accent"
|
|
152
|
+
: msg.from === "system" ? "dim" : "success";
|
|
153
|
+
const name = senderLabel(msg.from);
|
|
154
|
+
yield fit(` ${th.fg("dim", time)} ${th.fg(color as any, name)}`, width);
|
|
155
|
+
hasOutput = true;
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
switch (msg.type) {
|
|
147
159
|
case "tool": {
|
|
148
160
|
const name = msg.name || msg.text;
|
|
149
|
-
// Tool args can contain multi-line bash commands — take first line only
|
|
161
|
+
// Tool args can contain multi-line bash commands — take first line only.
|
|
150
162
|
const rawArg = (msg.args?.path || msg.args?.command || "").toString();
|
|
151
163
|
const arg = rawArg.split("\n")[0];
|
|
152
|
-
|
|
164
|
+
yield fit(` ${th.fg("muted", `→ ${name}${arg ? " " + arg : ""}`)}`, width);
|
|
153
165
|
break;
|
|
154
166
|
}
|
|
155
|
-
case "mention":
|
|
156
|
-
|
|
167
|
+
case "mention":
|
|
168
|
+
yield fit(` ${th.fg("accent", `@${msg.to || "?"}`)} ${th.fg("dim", msg.text)}`, width);
|
|
157
169
|
break;
|
|
158
|
-
}
|
|
159
170
|
case "text":
|
|
160
171
|
case "message":
|
|
161
|
-
case "reply":
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
for (const tl of show) {
|
|
165
|
-
// Wrap long lines instead of truncating
|
|
166
|
-
const wrapped = wrap(` ${tl}`, width, " ");
|
|
167
|
-
lines.push(...wrapped);
|
|
168
|
-
}
|
|
169
|
-
if (textLines.length > MAX_TEXT_LINES) {
|
|
170
|
-
lines.push(fit(` ${th.fg("dim", `... +${textLines.length - MAX_TEXT_LINES} lines`)}`, width));
|
|
172
|
+
case "reply":
|
|
173
|
+
for (const textLine of msg.text.replace(/\r/g, "").split("\n")) {
|
|
174
|
+
for (const line of wrap(` ${textLine}`, width, " ")) yield line;
|
|
171
175
|
}
|
|
172
176
|
break;
|
|
173
|
-
}
|
|
174
177
|
case "done":
|
|
175
|
-
|
|
178
|
+
for (const line of wrap(` ✓ ${msg.text}`, width, " ")) yield line;
|
|
176
179
|
break;
|
|
177
180
|
case "error":
|
|
178
|
-
|
|
181
|
+
for (const line of wrap(` ✗ ${msg.text}`, width, " ")) yield line;
|
|
179
182
|
break;
|
|
180
183
|
case "status":
|
|
181
|
-
|
|
184
|
+
yield fit(` ${th.fg("dim", msg.text)}`, width);
|
|
182
185
|
break;
|
|
183
186
|
}
|
|
187
|
+
hasOutput = true;
|
|
184
188
|
}
|
|
185
|
-
|
|
186
|
-
return lines;
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
191
|
|
|
@@ -235,6 +237,12 @@ function stripAnsi(str: string): string {
|
|
|
235
237
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
236
238
|
}
|
|
237
239
|
|
|
240
|
+
function senderLabel(from: string): string {
|
|
241
|
+
if (from === "human") return "YOU";
|
|
242
|
+
if (from === "orchestrator") return "ORCHESTRATOR";
|
|
243
|
+
return from;
|
|
244
|
+
}
|
|
245
|
+
|
|
238
246
|
function pad(lines: string[], max: number): string[] {
|
|
239
247
|
while (lines.length < max) lines.push("");
|
|
240
248
|
return lines.slice(0, max);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
14
14
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
15
15
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import type { TaskStatus } from "../types.js";
|
|
16
|
+
import type { TaskMessage, TaskStatus } from "../types.js";
|
|
17
17
|
import * as store from "../store.js";
|
|
18
18
|
|
|
19
19
|
function statusIcon(status: TaskStatus, th: Theme): string {
|
|
@@ -76,14 +76,33 @@ export function setupSquadWidget(
|
|
|
76
76
|
const totalCost = tasks.reduce((sum, t) => sum + t.usage.cost, 0);
|
|
77
77
|
const doneCount = tasks.filter((t) => t.status === "done").length;
|
|
78
78
|
const elapsed = Date.now() - new Date(squad.created).getTime();
|
|
79
|
+
const taskMessages = new Map<string, TaskMessage[]>();
|
|
80
|
+
const recentOrchestratorByTask = new Map<string, TaskMessage>();
|
|
81
|
+
const recentMessageKeys: string[] = [];
|
|
82
|
+
for (const task of tasks) {
|
|
83
|
+
if (task.status !== "in_progress") continue;
|
|
84
|
+
const messages = store.loadMessages(state.squadId, task.id);
|
|
85
|
+
taskMessages.set(task.id, messages);
|
|
86
|
+
const latest = messages.at(-1);
|
|
87
|
+
recentMessageKeys.push(`${task.id}:${messages.length}:${latest?.id || latest?.ts || "none"}`);
|
|
88
|
+
const recentOrchestrator = [...messages.slice(-5)].reverse().find((message) =>
|
|
89
|
+
message.from === "orchestrator" &&
|
|
90
|
+
(message.type === "text" || message.type === "message" ||
|
|
91
|
+
message.type === "reply" || message.type === "mention"),
|
|
92
|
+
);
|
|
93
|
+
if (recentOrchestrator) recentOrchestratorByTask.set(task.id, recentOrchestrator);
|
|
94
|
+
}
|
|
79
95
|
|
|
80
96
|
const sIcon = squad.status === "done" ? th.fg("success", "✓")
|
|
81
97
|
: squad.status === "failed" ? th.fg("error", "✗")
|
|
82
98
|
: squad.status === "review" ? th.fg("warning", "◆")
|
|
83
99
|
: th.fg("warning", "⏳");
|
|
84
100
|
|
|
101
|
+
const orchestratorSignal = recentOrchestratorByTask.size > 0
|
|
102
|
+
? ` ${th.fg("accent", `✉ ${recentOrchestratorByTask.size > 1 ? recentOrchestratorByTask.size + " " : ""}ORCH`)}`
|
|
103
|
+
: "";
|
|
85
104
|
lines.push(
|
|
86
|
-
`${sIcon} ${th.fg("accent", "squad")} ${th.fg("dim", squad.goal.slice(0, 35))} ` +
|
|
105
|
+
`${sIcon} ${th.fg("accent", "squad")}${orchestratorSignal} ${th.fg("dim", squad.goal.slice(0, 35))} ` +
|
|
87
106
|
`${th.fg("muted", `${doneCount}/${tasks.length}`)} ` +
|
|
88
107
|
`${th.fg("dim", `$${totalCost.toFixed(2)}`)} ` +
|
|
89
108
|
`${th.fg("dim", formatElapsed(elapsed))} ` +
|
|
@@ -111,13 +130,19 @@ export function setupSquadWidget(
|
|
|
111
130
|
const runningFor = task.started ? Date.now() - new Date(task.started).getTime() : 0;
|
|
112
131
|
const timeColor = runningFor > 180_000 ? "warning" : "dim";
|
|
113
132
|
line += ` ${th.fg(timeColor as any, formatElapsed(runningFor))}`;
|
|
114
|
-
const messages =
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
const messages = taskMessages.get(task.id) || [];
|
|
134
|
+
const recentOrchestrator = recentOrchestratorByTask.get(task.id);
|
|
135
|
+
if (recentOrchestrator) {
|
|
136
|
+
const preview = recentOrchestrator.text.split("\n")[0].slice(0, 24);
|
|
137
|
+
line += ` ${th.fg("accent", "← ORCH")} ${th.fg("dim", preview)}`;
|
|
138
|
+
} else {
|
|
139
|
+
const lastTool = [...messages].reverse().find(m => m.type === "tool");
|
|
140
|
+
if (lastTool) {
|
|
141
|
+
const rawDetail = (lastTool.args?.path || lastTool.args?.command || "").toString();
|
|
142
|
+
const detail = rawDetail.split("\n")[0]; // first line only
|
|
143
|
+
const toolStr = `→ ${lastTool.name || lastTool.text}`;
|
|
144
|
+
line += ` ${th.fg("dim", (detail ? `${toolStr} ${detail}` : toolStr).slice(0, 30))}`;
|
|
145
|
+
}
|
|
121
146
|
}
|
|
122
147
|
} else if (task.status === "blocked") {
|
|
123
148
|
const blockers = task.depends.filter((d) => {
|
|
@@ -136,7 +161,7 @@ export function setupSquadWidget(
|
|
|
136
161
|
lines.push(` ${th.fg("dim", ` +${tasks.length - maxVisible} more · ^q detail`)}`);
|
|
137
162
|
}
|
|
138
163
|
|
|
139
|
-
const cacheKey = `${squad.status}:${tasks.map(t => `${t.id}=${t.status}:${t.usage.turns}`).join(",")}`;
|
|
164
|
+
const cacheKey = `${squad.status}:${tasks.map(t => `${t.id}=${t.status}:${t.usage.turns}`).join(",")}:${recentMessageKeys.join(",")}`;
|
|
140
165
|
|
|
141
166
|
const statusText = squad.status === "done"
|
|
142
167
|
? th.fg("success", `✓ squad ${doneCount}/${tasks.length}`)
|
package/src/panel/task-list.ts
CHANGED
|
@@ -200,15 +200,25 @@ export class TaskListView {
|
|
|
200
200
|
const recent = messages.slice(-3);
|
|
201
201
|
|
|
202
202
|
for (const msg of recent) {
|
|
203
|
-
|
|
203
|
+
const isConversation = msg.type === "text" || msg.type === "message" ||
|
|
204
|
+
msg.type === "reply" || msg.type === "mention";
|
|
205
|
+
if (msg.from === "orchestrator" && isConversation) {
|
|
206
|
+
const preview = msg.text.split("\n")[0];
|
|
207
|
+
lines.push(truncateToWidth(
|
|
208
|
+
` ${th.fg("accent", "ORCHESTRATOR")} ${th.fg("dim", `"${preview}"`)}`,
|
|
209
|
+
width,
|
|
210
|
+
"…",
|
|
211
|
+
));
|
|
212
|
+
} else if (msg.type === "tool") {
|
|
204
213
|
const toolStr = `→ ${msg.name || msg.text}`;
|
|
205
214
|
const rawArgs = (msg.args?.path || msg.args?.command || "").toString();
|
|
206
215
|
const argsStr = rawArgs.split("\n")[0]; // first line only
|
|
207
216
|
const preview = argsStr ? `${toolStr} ${argsStr}` : toolStr;
|
|
208
217
|
lines.push(truncateToWidth(` ${th.fg("muted", preview)}`, width, "…"));
|
|
209
|
-
} else if (
|
|
218
|
+
} else if (isConversation && msg.from !== "system") {
|
|
210
219
|
const preview = msg.text.split("\n")[0];
|
|
211
|
-
|
|
220
|
+
const sender = msg.from === "human" ? "YOU" : msg.from;
|
|
221
|
+
lines.push(truncateToWidth(` ${th.fg("dim", `${sender}: "${preview}"`)}`, width, "…"));
|
|
212
222
|
}
|
|
213
223
|
}
|
|
214
224
|
|
package/src/review.ts
CHANGED
|
@@ -11,6 +11,18 @@ export interface OrchestratorReviewInput {
|
|
|
11
11
|
issues: string[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Start same-squad rework without discarding completed review evidence.
|
|
16
|
+
* The next all-tasks-done transition creates a new active pending review.
|
|
17
|
+
*/
|
|
18
|
+
export function beginOrchestratorRework(squad: Squad): void {
|
|
19
|
+
if (squad.review && squad.review.status !== "pending") {
|
|
20
|
+
squad.reviewHistory = [...(squad.reviewHistory ?? []), { ...squad.review }];
|
|
21
|
+
}
|
|
22
|
+
delete squad.review;
|
|
23
|
+
squad.status = "running";
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
/** Move a squad from agent execution into mandatory independent main-session review. */
|
|
15
27
|
export function beginOrchestratorReview(squad: Squad): void {
|
|
16
28
|
squad.status = "review";
|
|
@@ -35,6 +47,9 @@ export function recordOrchestratorReview(squad: Squad, input: OrchestratorReview
|
|
|
35
47
|
if (squad.status !== "review" || !squad.review) {
|
|
36
48
|
throw new Error(`Squad '${squad.id}' is not awaiting orchestrator review`);
|
|
37
49
|
}
|
|
50
|
+
if (squad.review.status !== "pending") {
|
|
51
|
+
throw new Error(`Squad '${squad.id}' review attempt is already ${squad.review.status}; begin same-squad rework before submitting a fresh review`);
|
|
52
|
+
}
|
|
38
53
|
|
|
39
54
|
const contractChecks = cleanList(input.contractChecks);
|
|
40
55
|
const verificationEvidence = cleanList(input.verificationEvidence);
|
package/src/router.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* router.ts — @mention parsing and cross-agent message delivery.
|
|
3
3
|
*
|
|
4
4
|
* Parses assistant text for @agentname patterns.
|
|
5
|
-
* Routes messages to target
|
|
6
|
-
* or
|
|
5
|
+
* Routes messages to exact target tasks via steer() if running,
|
|
6
|
+
* or a durable task-owned mailbox for delivery on next spawn.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { AgentPool } from "./agent-pool.js";
|
|
@@ -83,30 +83,41 @@ export class Router {
|
|
|
83
83
|
text: message,
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.pool.steer(targetTaskId, steerMessage);
|
|
86
|
+
// Agent-name mentions are safe only when exactly one live task owns that
|
|
87
|
+
// role. Multiple concurrent same-role tasks must never receive guessed mail.
|
|
88
|
+
const liveTargets = store.loadAllTasks(this.squadId).filter(
|
|
89
|
+
(task) => task.agent === targetAgent && this.pool.isRunning(task.id),
|
|
90
|
+
);
|
|
91
|
+
const targetTaskId = liveTargets.length === 1 ? liveTargets[0].id : undefined;
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
store.
|
|
93
|
+
if (targetTaskId) {
|
|
94
|
+
const queued = store.queueTaskMessage(this.squadId, targetTaskId, {
|
|
96
95
|
ts: store.now(),
|
|
97
96
|
from: fromAgent,
|
|
98
97
|
type: "mention",
|
|
99
98
|
to: targetAgent,
|
|
100
99
|
text: message,
|
|
101
100
|
});
|
|
101
|
+
const steerMessage = `[squad] Message from @${fromAgent} (working on ${sourceTaskId}):\n${message}`;
|
|
102
|
+
void this.pool.steer(targetTaskId, steerMessage).then((delivered) => {
|
|
103
|
+
if (delivered) store.acknowledgeTaskMessages(this.squadId, targetTaskId, [queued.id]);
|
|
104
|
+
});
|
|
102
105
|
return false;
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
const targetTasks = store.loadAllTasks(this.squadId).filter((task) => task.agent === targetAgent);
|
|
106
|
-
const
|
|
107
|
-
task.status === "
|
|
109
|
+
const futureTasks = targetTasks.filter((task) =>
|
|
110
|
+
task.status === "in_progress" || task.status === "pending" || task.status === "suspended" || task.status === "blocked",
|
|
108
111
|
);
|
|
109
|
-
|
|
112
|
+
const interrupted = futureTasks.filter((task) => task.status === "in_progress");
|
|
113
|
+
// Never guess between two tasks assigned to the same role. A uniquely
|
|
114
|
+
// interrupted task is authoritative; otherwise only one future task is safe.
|
|
115
|
+
const futureTask = interrupted.length === 1
|
|
116
|
+
? interrupted[0]
|
|
117
|
+
: futureTasks.length === 1
|
|
118
|
+
? futureTasks[0]
|
|
119
|
+
: undefined;
|
|
120
|
+
if (futureTasks.length === 0) {
|
|
110
121
|
const completed = targetTasks.filter((task) => task.status === "done" && task.output);
|
|
111
122
|
if (completed.length > 0) {
|
|
112
123
|
const durableReply = [
|
|
@@ -120,21 +131,20 @@ export class Router {
|
|
|
120
131
|
to: fromAgent,
|
|
121
132
|
text: durableReply,
|
|
122
133
|
};
|
|
123
|
-
store.
|
|
134
|
+
const queued = store.queueTaskMessage(this.squadId, sourceTaskId, reply);
|
|
124
135
|
if (this.pool.isRunning(sourceTaskId)) {
|
|
125
|
-
this.pool.steer(sourceTaskId, durableReply)
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
void this.pool.steer(sourceTaskId, durableReply).then((delivered) => {
|
|
137
|
+
if (delivered) store.acknowledgeTaskMessages(this.squadId, sourceTaskId, [queued.id]);
|
|
138
|
+
});
|
|
128
139
|
}
|
|
129
140
|
return true;
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
|
|
133
|
-
// Target has future work and may spawn again
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
this.pool.queueMessage(targetAgent, {
|
|
144
|
+
// Target has future work and may spawn again. Bind the message to exactly
|
|
145
|
+
// one durable task, never to the shared role name.
|
|
146
|
+
if (futureTask) {
|
|
147
|
+
store.queueTaskMessage(this.squadId, futureTask.id, {
|
|
138
148
|
ts: store.now(),
|
|
139
149
|
from: fromAgent,
|
|
140
150
|
type: "mention",
|
|
@@ -149,7 +159,7 @@ export class Router {
|
|
|
149
159
|
* Route a human message to an agent.
|
|
150
160
|
*/
|
|
151
161
|
routeHumanMessage(taskId: string, message: string): void {
|
|
152
|
-
store.
|
|
162
|
+
const queued = store.queueTaskMessage(this.squadId, taskId, {
|
|
153
163
|
ts: store.now(),
|
|
154
164
|
from: "human",
|
|
155
165
|
type: "message",
|
|
@@ -157,17 +167,9 @@ export class Router {
|
|
|
157
167
|
});
|
|
158
168
|
|
|
159
169
|
if (this.pool.isRunning(taskId)) {
|
|
160
|
-
this.pool.steer(taskId, `[squad] Human: ${message}`)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (task) {
|
|
164
|
-
this.pool.queueMessage(task.agent, {
|
|
165
|
-
ts: store.now(),
|
|
166
|
-
from: "human",
|
|
167
|
-
type: "message",
|
|
168
|
-
text: message,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
170
|
+
void this.pool.steer(taskId, `[squad] Human: ${message}`).then((delivered) => {
|
|
171
|
+
if (delivered) store.acknowledgeTaskMessages(this.squadId, taskId, [queued.id]);
|
|
172
|
+
});
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
|