@clanker-code/pi-subagents 0.10.7 → 0.10.8
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/CHANGELOG.md +5 -0
- package/dist/index.js +14 -3
- package/dist/wait.d.ts +18 -4
- package/dist/wait.js +48 -3
- package/package.json +1 -1
- package/src/index.ts +13 -3
- package/src/wait.ts +55 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.8] - 2026-06-23
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **`get_subagent_result` wait:true now detects queued user messages** — when the parent session has user messages waiting (e.g. the user typed while waiting), the tool returns early with a `pending_message` outcome instead of blocking for the full wait timeout. The queued message is delivered to the parent LLM immediately. Uses `ctx.hasPendingMessages()` polling during the wait. The subagent continues running undisturbed.
|
|
14
|
+
|
|
10
15
|
## [0.10.7] - 2026-06-23
|
|
11
16
|
|
|
12
17
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import { AgentWidget, buildInvocationTags, describeActivity, formatContextWindow
|
|
|
42
42
|
import { menuSelect } from "./ui/menu-select.js";
|
|
43
43
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
44
44
|
import { addUsage, getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
45
|
-
import { formatWaitTimeout, raceWait, waitTimeoutMessage } from "./wait.js";
|
|
45
|
+
import { formatWaitTimeout, pollPendingMessages, raceWait, waitTimeoutMessage } from "./wait.js";
|
|
46
46
|
// ---- Shared helpers ----
|
|
47
47
|
/** Tool execute return value for a text response. */
|
|
48
48
|
function textResult(msg, details) {
|
|
@@ -928,7 +928,7 @@ export default function (pi) {
|
|
|
928
928
|
description: "Return a lightweight tail/filter view of the agent's result or live output file, with line numbers. Ignored when verbose is true.",
|
|
929
929
|
})),
|
|
930
930
|
}),
|
|
931
|
-
execute: async (_toolCallId, params, signal, _onUpdate,
|
|
931
|
+
execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
|
|
932
932
|
const record = manager.getRecord(params.agent_id);
|
|
933
933
|
if (!record) {
|
|
934
934
|
return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
|
|
@@ -946,7 +946,18 @@ export default function (pi) {
|
|
|
946
946
|
let waitOutcome = "completed";
|
|
947
947
|
if (params.wait && record.status === "running" && record.promise) {
|
|
948
948
|
cancelNudge(params.agent_id);
|
|
949
|
-
|
|
949
|
+
// Poll for queued user messages so we can return early and let the
|
|
950
|
+
// parent LLM process them immediately instead of blocking for the
|
|
951
|
+
// full wait timeout.
|
|
952
|
+
const pending = typeof ctx?.hasPendingMessages === "function"
|
|
953
|
+
? pollPendingMessages(() => ctx.hasPendingMessages())
|
|
954
|
+
: undefined;
|
|
955
|
+
try {
|
|
956
|
+
waitOutcome = await raceWait(record.promise, signal, getWaitTimeoutSeconds(), pending?.promise);
|
|
957
|
+
}
|
|
958
|
+
finally {
|
|
959
|
+
pending?.cancel();
|
|
960
|
+
}
|
|
950
961
|
if (waitOutcome === "completed") {
|
|
951
962
|
record.resultConsumed = true;
|
|
952
963
|
}
|
package/dist/wait.d.ts
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
export type WaitOutcome = "completed" | "timeout" | "aborted";
|
|
1
|
+
export type WaitOutcome = "completed" | "timeout" | "aborted" | "pending_message";
|
|
2
2
|
/** Human-readable "Xm Ys" for a duration in seconds. */
|
|
3
3
|
export declare function formatWaitTimeout(seconds: number): string;
|
|
4
4
|
/**
|
|
5
|
-
* Race an agent completion promise against the configured wait timeout
|
|
6
|
-
* parent abort signal. The subagent is
|
|
5
|
+
* Race an agent completion promise against the configured wait timeout, the
|
|
6
|
+
* parent abort signal, and an optional pending-message check. The subagent is
|
|
7
|
+
* never aborted here.
|
|
8
|
+
*
|
|
9
|
+
* @param pendingCheck - Optional promise that resolves when the parent session
|
|
10
|
+
* has queued user messages waiting to be delivered. When it resolves, the
|
|
11
|
+
* wait ends early so the parent turn can process the incoming message.
|
|
7
12
|
*/
|
|
8
|
-
export declare function raceWait(promise: Promise<string>, signal: AbortSignal | undefined, timeoutSeconds: number): Promise<WaitOutcome>;
|
|
13
|
+
export declare function raceWait(promise: Promise<string>, signal: AbortSignal | undefined, timeoutSeconds: number, pendingCheck?: Promise<void>): Promise<WaitOutcome>;
|
|
9
14
|
/** Message returned when a wait ends with the agent still running. */
|
|
10
15
|
export declare function waitTimeoutMessage(outcome: WaitOutcome, timeoutSeconds: number): string;
|
|
16
|
+
/**
|
|
17
|
+
* Create a promise that resolves when the parent session has queued user
|
|
18
|
+
* messages. Polls at the given interval until `hasPendingMessages()` returns
|
|
19
|
+
* true. The caller should race this against the agent completion / timeout.
|
|
20
|
+
*/
|
|
21
|
+
export declare function pollPendingMessages(hasPendingMessages: () => boolean, intervalMs?: number): {
|
|
22
|
+
promise: Promise<void>;
|
|
23
|
+
cancel: () => void;
|
|
24
|
+
};
|
package/dist/wait.js
CHANGED
|
@@ -5,10 +5,15 @@ export function formatWaitTimeout(seconds) {
|
|
|
5
5
|
return m > 0 ? `${m}m${s > 0 ? ` ${s}s` : ""}` : `${s}s`;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Race an agent completion promise against the configured wait timeout
|
|
9
|
-
* parent abort signal. The subagent is
|
|
8
|
+
* Race an agent completion promise against the configured wait timeout, the
|
|
9
|
+
* parent abort signal, and an optional pending-message check. The subagent is
|
|
10
|
+
* never aborted here.
|
|
11
|
+
*
|
|
12
|
+
* @param pendingCheck - Optional promise that resolves when the parent session
|
|
13
|
+
* has queued user messages waiting to be delivered. When it resolves, the
|
|
14
|
+
* wait ends early so the parent turn can process the incoming message.
|
|
10
15
|
*/
|
|
11
|
-
export function raceWait(promise, signal, timeoutSeconds) {
|
|
16
|
+
export function raceWait(promise, signal, timeoutSeconds, pendingCheck) {
|
|
12
17
|
return new Promise((resolve) => {
|
|
13
18
|
let settled = false;
|
|
14
19
|
const finish = (outcome) => {
|
|
@@ -23,6 +28,7 @@ export function raceWait(promise, signal, timeoutSeconds) {
|
|
|
23
28
|
const onAbort = () => finish("aborted");
|
|
24
29
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
25
30
|
promise.then(() => finish("completed"));
|
|
31
|
+
pendingCheck?.then(() => finish("pending_message"));
|
|
26
32
|
});
|
|
27
33
|
}
|
|
28
34
|
/** Message returned when a wait ends with the agent still running. */
|
|
@@ -33,5 +39,44 @@ export function waitTimeoutMessage(outcome, timeoutSeconds) {
|
|
|
33
39
|
if (outcome === "aborted") {
|
|
34
40
|
return `Agent is still running. The wait was cancelled by the user (parent turn aborted). The subagent was NOT stopped — it continues in the background.\nCall get_subagent_result with wait: true again to keep waiting, use peek to check progress, or omit wait to check status.`;
|
|
35
41
|
}
|
|
42
|
+
if (outcome === "pending_message") {
|
|
43
|
+
return `Agent is still running. The wait was interrupted by an incoming user message. The subagent was NOT stopped — it continues in the background.\nThe queued message will be delivered after this tool returns.\nCall get_subagent_result with wait: true again to keep waiting, use peek to check progress, or omit wait to check status.`;
|
|
44
|
+
}
|
|
36
45
|
return "Agent is still running. Use peek to check recent progress, wait: true to block until it finishes, or check back later.";
|
|
37
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a promise that resolves when the parent session has queued user
|
|
49
|
+
* messages. Polls at the given interval until `hasPendingMessages()` returns
|
|
50
|
+
* true. The caller should race this against the agent completion / timeout.
|
|
51
|
+
*/
|
|
52
|
+
export function pollPendingMessages(hasPendingMessages, intervalMs = 1000) {
|
|
53
|
+
let settled = false;
|
|
54
|
+
let resolve;
|
|
55
|
+
const promise = new Promise((r) => { resolve = r; });
|
|
56
|
+
// Check immediately in case a message arrived between the tool call
|
|
57
|
+
// start and this poll setup.
|
|
58
|
+
if (hasPendingMessages()) {
|
|
59
|
+
settled = true;
|
|
60
|
+
resolve();
|
|
61
|
+
return { promise, cancel: () => { } };
|
|
62
|
+
}
|
|
63
|
+
const timer = setInterval(() => {
|
|
64
|
+
if (settled)
|
|
65
|
+
return;
|
|
66
|
+
if (hasPendingMessages()) {
|
|
67
|
+
settled = true;
|
|
68
|
+
clearInterval(timer);
|
|
69
|
+
resolve();
|
|
70
|
+
}
|
|
71
|
+
}, intervalMs);
|
|
72
|
+
return {
|
|
73
|
+
promise,
|
|
74
|
+
cancel: () => {
|
|
75
|
+
if (!settled) {
|
|
76
|
+
settled = true;
|
|
77
|
+
clearInterval(timer);
|
|
78
|
+
resolve();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -55,7 +55,7 @@ import type { WidgetAgentSnapshot, WidgetDisplayMode } from "./ui/agent-widget-t
|
|
|
55
55
|
import { menuSelect } from "./ui/menu-select.js";
|
|
56
56
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
57
57
|
import { addUsage, getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
58
|
-
import { formatWaitTimeout, raceWait, type WaitOutcome, waitTimeoutMessage } from "./wait.js";
|
|
58
|
+
import { formatWaitTimeout, pollPendingMessages, raceWait, type WaitOutcome, waitTimeoutMessage } from "./wait.js";
|
|
59
59
|
|
|
60
60
|
// ---- Shared helpers ----
|
|
61
61
|
|
|
@@ -1077,7 +1077,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1077
1077
|
}),
|
|
1078
1078
|
),
|
|
1079
1079
|
}),
|
|
1080
|
-
execute: async (_toolCallId, params, signal, _onUpdate,
|
|
1080
|
+
execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
|
|
1081
1081
|
const record = manager.getRecord(params.agent_id);
|
|
1082
1082
|
if (!record) {
|
|
1083
1083
|
return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
|
|
@@ -1099,7 +1099,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1099
1099
|
let waitOutcome: WaitOutcome = "completed";
|
|
1100
1100
|
if (params.wait && record.status === "running" && record.promise) {
|
|
1101
1101
|
cancelNudge(params.agent_id);
|
|
1102
|
-
|
|
1102
|
+
// Poll for queued user messages so we can return early and let the
|
|
1103
|
+
// parent LLM process them immediately instead of blocking for the
|
|
1104
|
+
// full wait timeout.
|
|
1105
|
+
const pending = typeof ctx?.hasPendingMessages === "function"
|
|
1106
|
+
? pollPendingMessages(() => ctx.hasPendingMessages())
|
|
1107
|
+
: undefined;
|
|
1108
|
+
try {
|
|
1109
|
+
waitOutcome = await raceWait(record.promise, signal, getWaitTimeoutSeconds(), pending?.promise);
|
|
1110
|
+
} finally {
|
|
1111
|
+
pending?.cancel();
|
|
1112
|
+
}
|
|
1103
1113
|
if (waitOutcome === "completed") {
|
|
1104
1114
|
record.resultConsumed = true;
|
|
1105
1115
|
}
|
package/src/wait.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WaitOutcome = "completed" | "timeout" | "aborted";
|
|
1
|
+
export type WaitOutcome = "completed" | "timeout" | "aborted" | "pending_message";
|
|
2
2
|
|
|
3
3
|
/** Human-readable "Xm Ys" for a duration in seconds. */
|
|
4
4
|
export function formatWaitTimeout(seconds: number): string {
|
|
@@ -8,13 +8,19 @@ export function formatWaitTimeout(seconds: number): string {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Race an agent completion promise against the configured wait timeout
|
|
12
|
-
* parent abort signal. The subagent is
|
|
11
|
+
* Race an agent completion promise against the configured wait timeout, the
|
|
12
|
+
* parent abort signal, and an optional pending-message check. The subagent is
|
|
13
|
+
* never aborted here.
|
|
14
|
+
*
|
|
15
|
+
* @param pendingCheck - Optional promise that resolves when the parent session
|
|
16
|
+
* has queued user messages waiting to be delivered. When it resolves, the
|
|
17
|
+
* wait ends early so the parent turn can process the incoming message.
|
|
13
18
|
*/
|
|
14
19
|
export function raceWait(
|
|
15
20
|
promise: Promise<string>,
|
|
16
21
|
signal: AbortSignal | undefined,
|
|
17
22
|
timeoutSeconds: number,
|
|
23
|
+
pendingCheck?: Promise<void>,
|
|
18
24
|
): Promise<WaitOutcome> {
|
|
19
25
|
return new Promise((resolve) => {
|
|
20
26
|
let settled = false;
|
|
@@ -29,6 +35,7 @@ export function raceWait(
|
|
|
29
35
|
const onAbort = () => finish("aborted");
|
|
30
36
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
31
37
|
promise.then(() => finish("completed"));
|
|
38
|
+
pendingCheck?.then(() => finish("pending_message"));
|
|
32
39
|
});
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -40,5 +47,50 @@ export function waitTimeoutMessage(outcome: WaitOutcome, timeoutSeconds: number)
|
|
|
40
47
|
if (outcome === "aborted") {
|
|
41
48
|
return `Agent is still running. The wait was cancelled by the user (parent turn aborted). The subagent was NOT stopped — it continues in the background.\nCall get_subagent_result with wait: true again to keep waiting, use peek to check progress, or omit wait to check status.`;
|
|
42
49
|
}
|
|
50
|
+
if (outcome === "pending_message") {
|
|
51
|
+
return `Agent is still running. The wait was interrupted by an incoming user message. The subagent was NOT stopped — it continues in the background.\nThe queued message will be delivered after this tool returns.\nCall get_subagent_result with wait: true again to keep waiting, use peek to check progress, or omit wait to check status.`;
|
|
52
|
+
}
|
|
43
53
|
return "Agent is still running. Use peek to check recent progress, wait: true to block until it finishes, or check back later.";
|
|
44
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a promise that resolves when the parent session has queued user
|
|
58
|
+
* messages. Polls at the given interval until `hasPendingMessages()` returns
|
|
59
|
+
* true. The caller should race this against the agent completion / timeout.
|
|
60
|
+
*/
|
|
61
|
+
export function pollPendingMessages(
|
|
62
|
+
hasPendingMessages: () => boolean,
|
|
63
|
+
intervalMs = 1000,
|
|
64
|
+
): { promise: Promise<void>; cancel: () => void } {
|
|
65
|
+
let settled = false;
|
|
66
|
+
let resolve!: () => void;
|
|
67
|
+
const promise = new Promise<void>((r) => { resolve = r; });
|
|
68
|
+
|
|
69
|
+
// Check immediately in case a message arrived between the tool call
|
|
70
|
+
// start and this poll setup.
|
|
71
|
+
if (hasPendingMessages()) {
|
|
72
|
+
settled = true;
|
|
73
|
+
resolve();
|
|
74
|
+
return { promise, cancel: () => {} };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const timer = setInterval(() => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
if (hasPendingMessages()) {
|
|
80
|
+
settled = true;
|
|
81
|
+
clearInterval(timer);
|
|
82
|
+
resolve();
|
|
83
|
+
}
|
|
84
|
+
}, intervalMs);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
promise,
|
|
88
|
+
cancel: () => {
|
|
89
|
+
if (!settled) {
|
|
90
|
+
settled = true;
|
|
91
|
+
clearInterval(timer);
|
|
92
|
+
resolve();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|