@maplezzk/pi-interactive-subagents 3.10.0 → 3.10.1

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
@@ -8,7 +8,7 @@ https://github.com/user-attachments/assets/30adb156-cfb4-4c47-84ca-dd4aa80cba9f
8
8
 
9
9
  ## How It Works
10
10
 
11
- Call `subagent()` and it **returns immediately**. The sub-agent runs in its own terminal pane. A live widget above the input shows all running agents with their current state — `starting`, `active`, `waiting`, `stalled`, or `running`. When a sub-agent finishes, its result is **steered back** into the main session as an async notification — triggering a new turn so the agent can process it.
11
+ Call `subagent()` and it **returns immediately**. The sub-agent runs in its own terminal pane. A live widget above the input shows all running agents with their current state — `starting`, `active`, `waiting`, `stalled`, or `running`. When a sub-agent finishes, its result is **steered back** into the main session as an async notification — triggering a new turn so the agent can process it. Completion reminders are injected only after the model stops normally; user aborts and provider errors stay quiet.
12
12
 
13
13
  ```
14
14
  ╭─ Subagents ──────────────────────────── 2 running ─╮
package/README.zh-CN.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 工作原理
8
8
 
9
- 调用 `subagent()` 后**立即返回**,子 agent 在自己的终端分屏中运行。输入框上方的实时 widget 展示所有运行中的 agent 及其状态(`starting`、`active`、`waiting`、`stalled`、`running`)。子 agent 完成后,结果以异步通知形式**回流**到主会话,触发新一轮处理。
9
+ 调用 `subagent()` 后**立即返回**,子 agent 在自己的终端分屏中运行。输入框上方的实时 widget 展示所有运行中的 agent 及其状态(`starting`、`active`、`waiting`、`stalled`、`running`)。子 agent 完成后,结果以异步通知形式**回流**到主会话,触发新一轮处理。完成提醒仅在模型正常停止后注入;用户手动终止或提供方异常不会触发。
10
10
 
11
11
  ```typescript
12
12
  subagent({ name: "Scout: Auth", agent: "scout", task: "分析 auth 模块" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maplezzk/pi-interactive-subagents",
3
- "version": "3.10.0",
3
+ "version": "3.10.1",
4
4
  "description": "Interactive async subagents for pi — spawn, orchestrate, and manage sub-agent sessions in multiplexer panes. Fork of HazAT/pi-interactive-subagents.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -17,7 +17,7 @@
17
17
  "README.zh-CN.md"
18
18
  ],
19
19
  "scripts": {
20
- "test": "tsx --test test/test.ts",
20
+ "test": "tsx --test test/subagent-done-nudge.test.ts test/test.ts",
21
21
  "test:integration": "tsx --test --test-concurrency=1 test/integration/*.test.ts",
22
22
  "typecheck": "tsc --noEmit --pretty false",
23
23
  "build": "npm run typecheck",
@@ -13,7 +13,7 @@
13
13
  * 现在不论 autoExit env 如何,agent 都必须主动调用 subagent_done 或 caller_ping
14
14
  * 才能结束。如果 agent 不调,会被 nudge 提醒。
15
15
  */
16
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
17
17
  import { Box, Text } from "@earendil-works/pi-tui";
18
18
  import { Type } from "@sinclair/typebox";
19
19
  import { writeFileSync } from "node:fs";
@@ -22,23 +22,41 @@ import { createTranslator, loadCatalog } from "pi-extensions-i18n";
22
22
  import { createSubagentActivityRecorder } from "./activity.ts";
23
23
 
24
24
  const i18n = createTranslator(loadCatalog(new URL("../../locales/index.json", import.meta.url)));
25
+ const ASSISTANT_ROLE = "assistant";
26
+ const NORMAL_STOP_REASON = "stop";
27
+ const ABORTED_STOP_REASON = "aborted";
25
28
 
29
+ /** Treat input as manual takeover only after the first agent run has started. */
26
30
  export function shouldMarkUserTookOver(agentStarted: boolean): boolean {
27
31
  return agentStarted;
28
32
  }
29
33
 
34
+ /** Return true only when the latest assistant message ended by the model stopping normally. */
35
+ export function shouldScheduleAgentEndNudge(
36
+ messages: readonly { role?: string; stopReason?: string }[] | undefined,
37
+ ): boolean {
38
+ if (!messages) return false;
39
+
40
+ for (let i = messages.length - 1; i >= 0; i--) {
41
+ const message = messages[i];
42
+ if (message?.role === ASSISTANT_ROLE) {
43
+ return message.stopReason === NORMAL_STOP_REASON;
44
+ }
45
+ }
46
+
47
+ return false;
48
+ }
49
+
50
+ /** Preserve the former auto-exit decision for callers that still use this helper. */
30
51
  export function shouldAutoExitOnAgentEnd(
31
52
  _userTookOver: boolean,
32
- messages: any[] | undefined,
53
+ messages: readonly { role?: string; stopReason?: string }[] | undefined,
33
54
  ): boolean {
34
- // Manual input should not strand an auto-exit subagent. If the latest agent
35
- // turn completed normally, close the session. Escape/abort still leaves it
36
- // open for inspection or another prompt.
37
55
  if (messages) {
38
56
  for (let i = messages.length - 1; i >= 0; i--) {
39
- const msg = messages[i];
40
- if (msg?.role === "assistant") {
41
- return msg.stopReason !== "aborted";
57
+ const message = messages[i];
58
+ if (message?.role === ASSISTANT_ROLE) {
59
+ return message.stopReason !== ABORTED_STOP_REASON;
42
60
  }
43
61
  }
44
62
  }
@@ -46,6 +64,7 @@ export function shouldAutoExitOnAgentEnd(
46
64
  return true;
47
65
  }
48
66
 
67
+ /** Parse the comma-separated denied-tool setting and discard blank entries. */
49
68
  export function parseDeniedTools(rawValue: string | undefined): string[] {
50
69
  return (rawValue ?? "")
51
70
  .split(",")
@@ -81,6 +100,7 @@ export default function (pi: ExtensionAPI) {
81
100
  let userInputAfterAgentEnd = false;
82
101
  let nudgeTimer: ReturnType<typeof setTimeout> | null = null;
83
102
 
103
+ /** Cancel and forget the pending completion reminder, if any. */
84
104
  function clearNudgeTimer(): void {
85
105
  if (nudgeTimer !== null) {
86
106
  clearTimeout(nudgeTimer);
@@ -89,18 +109,17 @@ export default function (pi: ExtensionAPI) {
89
109
  }
90
110
 
91
111
  /**
92
- * After a non-auto-exit subagent finishes generating, schedule a nudge
93
- * reminding it to call subagent_done if it hasn't already.
112
+ * After a subagent stops normally, schedule a nudge reminding it to call
113
+ * subagent_done if it hasn't already. Error and aborted runs are excluded.
94
114
  *
95
115
  * Each call replaces any pending nudge, so repeated agent_end events
96
- * (e.g. during multi-turn tool use) automatically reset the timer.
97
- * The nudge only fires if no new agent activity or user input arrives
98
- * within NUDGE_DELAY_MS.
116
+ * automatically reset the timer. The nudge only fires if no new agent
117
+ * activity or user input arrives within NUDGE_DELAY_MS.
99
118
  */
100
119
  function scheduleAgentEndNudge(): void {
101
120
  clearNudgeTimer();
102
121
  // 不论 autoExit 是否启用,都必须 nudge — autoExit 已被移除,
103
- // agent 结束 turn 后只能靠主动调用 subagent_done 才能真正退出。
122
+ // agent 正常结束 turn 后只能靠主动调用 subagent_done 才能真正退出。
104
123
  if (NUDGE_DISABLED || doneCalled) return;
105
124
 
106
125
  nudgeTimer = setTimeout(() => {
@@ -114,10 +133,11 @@ export default function (pi: ExtensionAPI) {
114
133
  }, NUDGE_DELAY_MS);
115
134
  }
116
135
 
117
- function renderWidget(ctx: { ui: { setWidget: Function } }, _theme: any) {
136
+ /** Render the subagent identity and tool availability widget. */
137
+ function renderWidget(ctx: Pick<ExtensionContext, "ui">): void {
118
138
  ctx.ui.setWidget(
119
139
  "subagent-tools",
120
- (_tui: any, theme: any) => {
140
+ (_tui, theme) => {
121
141
  const box = new Box(1, 0, (text: string) => theme.bg("toolSuccessBg", text));
122
142
 
123
143
  const label = subagentAgent || subagentName;
@@ -178,7 +198,7 @@ export default function (pi: ExtensionAPI) {
178
198
  toolNames = tools.map((t) => t.name).sort();
179
199
  denied = parseDeniedTools(deniedToolsValue);
180
200
 
181
- renderWidget(ctx, null);
201
+ renderWidget(ctx);
182
202
  });
183
203
 
184
204
  pi.on("input", () => {
@@ -216,10 +236,13 @@ export default function (pi: ExtensionAPI) {
216
236
  // subagent_done(或 caller_ping)。如果 agent 不调,下面会有 nudge 提醒。
217
237
  recorder.agentEndWaiting();
218
238
 
219
- // For non-auto-exit agents: schedule a nudge in case the AI forgot to call
220
- // subagent_done. This is automatically cleared/reset on any subsequent
221
- // agent activity or user input.
222
- scheduleAgentEndNudge();
239
+ // Only a normal model stop means the agent itself chose to finish.
240
+ // Provider errors and user aborts must stay quiet.
241
+ if (shouldScheduleAgentEndNudge(event.messages)) {
242
+ scheduleAgentEndNudge();
243
+ } else {
244
+ clearNudgeTimer();
245
+ }
223
246
  });
224
247
 
225
248
  pi.on("turn_start", (event) => {
@@ -272,7 +295,7 @@ export default function (pi: ExtensionAPI) {
272
295
  description: "Toggle subagent tools widget",
273
296
  handler: (ctx) => {
274
297
  expanded = !expanded;
275
- renderWidget(ctx, null);
298
+ renderWidget(ctx);
276
299
  },
277
300
  });
278
301