pi-squad 0.16.2 → 0.16.3

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 CHANGED
@@ -139,7 +139,7 @@ Bundled agent definitions are copied to `~/.pi/squad/agents/` on first run. Edit
139
139
 
140
140
  **@mention routing**: Agents write `@frontend what token format?` in their output. The router delivers it in real-time via RPC `steer()`.
141
141
 
142
- **Main-session steering**: `squad_message` resolves an agent name to its live task, durably records the full message, and writes the documented JSONL RPC command `{"type":"steer","message":"..."}` to the child. pi-squad waits for Pi's final `agent_settled` event before closing the child; low-level `agent_end` no longer kills queued steering/follow-up continuations.
142
+ **Main-session request/reply**: `squad_message` resolves an agent name to its live task, durably records the full request, and writes the documented JSONL RPC command `{"type":"steer","message":"..."}` to the child. By default (`expectReply: true`), the next substantive agent response is durably marked as the reply, pushed into the main Pi session, and wakes it via follow-up delivery; ordinary later activity remains panel-only. The pending reply marker survives scheduler reconstruction. Use `expectReply: false` for fire-and-forget steering. pi-squad waits for Pi's final `agent_settled` event before closing the child; low-level `agent_end` never kills queued continuations.
143
143
 
144
144
  ### Smart Planner
145
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squad",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
4
4
  "description": "Multi-agent collaboration extension for pi — task decomposition, dependency management, parallel execution, TUI panel",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/index.ts CHANGED
@@ -504,11 +504,12 @@ export default function (pi: ExtensionAPI) {
504
504
  pi.registerTool({
505
505
  name: "squad_message",
506
506
  label: "Squad Message",
507
- description: "Send a message to a specific agent or task in the running squad.",
507
+ description: "Send a request to a specific agent/task. The agent's next substantive assistant response is durably forwarded back to the main Pi session and wakes it.",
508
508
  parameters: Type.Object({
509
509
  message: Type.String({ description: "Message to send" }),
510
510
  taskId: Type.Optional(Type.String({ description: "Target task ID" })),
511
511
  agent: Type.Optional(Type.String({ description: "Target agent name" })),
512
+ expectReply: Type.Optional(Type.Boolean({ description: "Forward the agent's next substantive response back to main Pi and wake it (default true)" })),
512
513
  }),
513
514
 
514
515
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
@@ -528,7 +529,7 @@ export default function (pi: ExtensionAPI) {
528
529
  return { content: [{ type: "text" as const, text: "Could not determine target task. Provide taskId or an agent name that is currently running." }], details: undefined };
529
530
  }
530
531
 
531
- const sent = await activeScheduler!.sendHumanMessage(taskId, params.message);
532
+ const sent = await activeScheduler!.sendHumanMessage(taskId, params.message, params.expectReply ?? true);
532
533
  const status = sent ? "delivered" : "queued for when the agent starts";
533
534
 
534
535
  return { content: [{ type: "text" as const, text: `Message ${status}: "${params.message}"` }], details: undefined };
@@ -627,6 +628,14 @@ export default function (pi: ExtensionAPI) {
627
628
  forceWidgetUpdate();
628
629
  break;
629
630
  }
631
+ case "orchestrator_reply": {
632
+ pi.sendMessage({
633
+ customType: "squad-agent-reply",
634
+ content: `[squad] Direct reply from '${event.agentName}' on task '${event.taskId}':\n${event.message}`,
635
+ display: true,
636
+ }, { triggerTurn: true, deliverAs: "followUp" });
637
+ break;
638
+ }
630
639
  case "escalation": {
631
640
  pi.sendMessage({
632
641
  customType: "squad-escalation",
@@ -1708,6 +1717,17 @@ async function startSquad(
1708
1717
  break;
1709
1718
  }
1710
1719
 
1720
+ case "orchestrator_reply": {
1721
+ // Push the complete requested response back into the main session and
1722
+ // wake it; ordinary task activity remains panel-only.
1723
+ pi.sendMessage({
1724
+ customType: "squad-agent-reply",
1725
+ content: `[squad] Direct reply from '${event.agentName}' on task '${event.taskId}':\n${event.message}`,
1726
+ display: true,
1727
+ }, { triggerTurn: true, deliverAs: "followUp" });
1728
+ break;
1729
+ }
1730
+
1711
1731
  case "escalation": {
1712
1732
  // Escalation — agent needs help. triggerTurn so the main agent
1713
1733
  // can respond and relay help.
package/src/scheduler.ts CHANGED
@@ -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
 
@@ -568,6 +569,26 @@ export class Scheduler {
568
569
  // presentation layers may viewport them, but source data must never truncate.
569
570
  text,
570
571
  });
572
+
573
+ // A squad_message is a request/response channel, not fire-and-forget.
574
+ // Persist an acknowledgement marker before emitting so restart/focus
575
+ // changes cannot forward the same response twice.
576
+ if (this.hasPendingOrchestratorRequest(event.taskId)) {
577
+ store.appendMessage(this.squadId, event.taskId, {
578
+ ts: store.now(),
579
+ from: event.agentName,
580
+ type: "reply",
581
+ to: "orchestrator",
582
+ text: "Response forwarded to main orchestrator",
583
+ });
584
+ this.emit({
585
+ type: "orchestrator_reply",
586
+ squadId: this.squadId,
587
+ taskId: event.taskId,
588
+ agentName: event.agentName,
589
+ message: text,
590
+ });
591
+ }
571
592
  }
572
593
 
573
594
  // Track usage
@@ -1118,31 +1139,47 @@ export class Scheduler {
1118
1139
  // External Actions
1119
1140
  // =========================================================================
1120
1141
 
1121
- /** Send a human message to a task's agent */
1122
- async sendHumanMessage(taskId: string, message: string): Promise<boolean> {
1142
+ /** Send a main-orchestrator request to a task's agent and await its next reply. */
1143
+ async sendHumanMessage(taskId: string, message: string, expectsReply = true): Promise<boolean> {
1144
+ const task = store.loadTask(this.squadId, taskId);
1145
+ if (!task) return false;
1146
+
1123
1147
  store.appendMessage(this.squadId, taskId, {
1124
1148
  ts: store.now(),
1125
- from: "human",
1149
+ from: "orchestrator",
1126
1150
  type: "message",
1127
1151
  text: message,
1152
+ expectsReply,
1128
1153
  });
1129
1154
 
1130
- if (this.pool.isRunning(taskId)) {
1131
- return this.pool.steer(taskId, `[squad] Human: ${message}`);
1132
- }
1133
- // Queue for when agent spawns
1134
- const task = store.loadTask(this.squadId, taskId);
1135
- if (task) {
1136
- this.pool.queueMessage(task.agent, {
1137
- ts: store.now(),
1138
- from: "human",
1139
- type: "message",
1140
- text: message,
1141
- });
1155
+ const request = expectsReply
1156
+ ? `[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.`
1157
+ : `[squad] Main orchestrator message:\n${message}`;
1158
+ if (this.pool.isRunning(taskId) && await this.pool.steer(taskId, request)) {
1159
+ return true;
1142
1160
  }
1161
+
1162
+ // Queue for when the assigned agent starts/restarts. The durable request
1163
+ // marker lets a reconstructed scheduler still forward the eventual reply.
1164
+ this.pool.queueMessage(task.agent, {
1165
+ ts: store.now(),
1166
+ from: "orchestrator",
1167
+ type: "message",
1168
+ text: expectsReply ? `${message}\n\n[Direct response required by main orchestrator]` : message,
1169
+ expectsReply,
1170
+ });
1143
1171
  return false;
1144
1172
  }
1145
1173
 
1174
+ private hasPendingOrchestratorRequest(taskId: string): boolean {
1175
+ let pending = false;
1176
+ for (const message of store.loadMessages(this.squadId, taskId)) {
1177
+ if (message.from === "orchestrator" && message.expectsReply) pending = true;
1178
+ if (message.type === "reply" && message.to === "orchestrator") pending = false;
1179
+ }
1180
+ return pending;
1181
+ }
1182
+
1146
1183
  /** Pause a running task */
1147
1184
  async pauseTask(taskId: string): Promise<void> {
1148
1185
  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 answer
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."
package/src/types.ts CHANGED
@@ -165,6 +165,8 @@ export interface TaskMessage {
165
165
  type: MessageType;
166
166
  text: string;
167
167
  to?: string;
168
+ /** Main-orchestrator message expects the next substantive agent response. */
169
+ expectsReply?: boolean;
168
170
  name?: string;
169
171
  args?: Record<string, unknown>;
170
172
  }