@oh-my-pi/pi-coding-agent 16.3.9 → 16.3.10
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 +19 -0
- package/dist/cli.js +2902 -2902
- package/dist/types/modes/interactive-mode.d.ts +5 -3
- package/dist/types/modes/session-observer-registry.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +11 -7
- package/dist/types/task/renderer.d.ts +0 -1
- package/dist/types/tools/browser/run-cancellation.d.ts +11 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +1 -0
- package/dist/types/tools/tool-errors.d.ts +1 -1
- package/package.json +12 -12
- package/src/modes/components/__tests__/skill-message.test.ts +2 -0
- package/src/modes/components/agent-hub.ts +17 -2
- package/src/modes/components/skill-message.ts +1 -1
- package/src/modes/index.ts +0 -7
- package/src/modes/interactive-mode.ts +54 -18
- package/src/modes/session-observer-registry.ts +11 -8
- package/src/sdk.ts +18 -0
- package/src/session/agent-session.ts +25 -13
- package/src/task/render.test.ts +134 -1
- package/src/task/render.ts +24 -6
- package/src/task/renderer.ts +0 -1
- package/src/tools/browser/cmux/cmux-tab.ts +7 -3
- package/src/tools/browser/run-cancellation.ts +28 -8
- package/src/tools/browser/tab-protocol.ts +1 -1
- package/src/tools/browser/tab-supervisor.ts +4 -4
- package/src/tools/browser/tab-worker.ts +19 -7
- package/src/tools/irc.ts +18 -2
- package/src/tools/tool-errors.ts +3 -3
package/src/task/render.test.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { afterEach, beforeAll, describe, expect, it } from "bun:test";
|
|
|
2
2
|
import { Settings } from "../config/settings";
|
|
3
3
|
import { getThemeByName, setThemeInstance, type Theme } from "../modes/theme/theme";
|
|
4
4
|
import { renderResult } from "./render";
|
|
5
|
-
import
|
|
5
|
+
import { taskToolRenderer } from "./renderer";
|
|
6
|
+
import type { AgentProgress, SingleResult, TaskToolDetails } from "./types";
|
|
6
7
|
|
|
7
8
|
const strip = (lines: readonly string[]): string =>
|
|
8
9
|
lines
|
|
@@ -42,6 +43,71 @@ function makeProgress(recentOutput: string[]): AgentProgress {
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function makeParentWithNestedProgress(childCount: number): AgentProgress {
|
|
47
|
+
const children = Array.from(
|
|
48
|
+
{ length: childCount },
|
|
49
|
+
(_, index): AgentProgress => ({
|
|
50
|
+
...makeProgress([]),
|
|
51
|
+
index,
|
|
52
|
+
id: `Nested${index + 1}`,
|
|
53
|
+
task: `nested child ${index + 1}`,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
...makeProgress([]),
|
|
58
|
+
id: "Parent",
|
|
59
|
+
task: "parent task",
|
|
60
|
+
inflightTaskDetails: {
|
|
61
|
+
projectAgentsDir: null,
|
|
62
|
+
results: [],
|
|
63
|
+
totalDurationMs: 1,
|
|
64
|
+
progress: children,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeSingleResult(index: number, overrides?: Partial<SingleResult>): SingleResult {
|
|
70
|
+
return {
|
|
71
|
+
index,
|
|
72
|
+
id: `Done${index + 1}`,
|
|
73
|
+
agent: "task",
|
|
74
|
+
agentSource: "bundled",
|
|
75
|
+
task: `finished child ${index + 1}`,
|
|
76
|
+
exitCode: 0,
|
|
77
|
+
output: "ok",
|
|
78
|
+
stderr: "",
|
|
79
|
+
truncated: false,
|
|
80
|
+
durationMs: 5,
|
|
81
|
+
tokens: 0,
|
|
82
|
+
requests: 1,
|
|
83
|
+
...overrides,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeParentWithNestedResults(childCount: number): TaskToolDetails {
|
|
88
|
+
const nested: TaskToolDetails = {
|
|
89
|
+
projectAgentsDir: null,
|
|
90
|
+
// Failure on the LAST child in display order (equal durations, index tiebreak):
|
|
91
|
+
// a plain head-of-list pick would elide it, so its visibility proves the
|
|
92
|
+
// failure-first selection of `selectCollapsedResults`.
|
|
93
|
+
results: Array.from({ length: childCount }, (_, index) =>
|
|
94
|
+
makeSingleResult(index, index === childCount - 1 ? { exitCode: 1, error: "boom" } : undefined),
|
|
95
|
+
),
|
|
96
|
+
totalDurationMs: 1,
|
|
97
|
+
};
|
|
98
|
+
const parent = makeSingleResult(0, { id: "Parent", extractedToolData: { task: [nested] } });
|
|
99
|
+
return { projectAgentsDir: null, results: [parent], totalDurationMs: 1 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderResultText(details: TaskToolDetails, expanded: boolean, uiTheme: Theme): string {
|
|
103
|
+
const component = renderResult(
|
|
104
|
+
{ content: [{ type: "text", text: "Ran 1 agent" }], details },
|
|
105
|
+
{ expanded, isPartial: false },
|
|
106
|
+
uiTheme,
|
|
107
|
+
);
|
|
108
|
+
return strip(component.render(120));
|
|
109
|
+
}
|
|
110
|
+
|
|
45
111
|
function renderProgressText(progress: AgentProgress, expanded: boolean, uiTheme: Theme): string {
|
|
46
112
|
const details: TaskToolDetails = {
|
|
47
113
|
projectAgentsDir: null,
|
|
@@ -118,4 +184,71 @@ describe("task live progress rendering", () => {
|
|
|
118
184
|
expect(collapsedText).not.toContain("line 1");
|
|
119
185
|
expect(collapsedText).not.toContain("raw output:");
|
|
120
186
|
});
|
|
187
|
+
|
|
188
|
+
it("caps collapsed nested task progress at four rows plus an elision line", () => {
|
|
189
|
+
setViewportRows(40);
|
|
190
|
+
const text = renderProgressText(makeParentWithNestedProgress(6), false, uiTheme);
|
|
191
|
+
|
|
192
|
+
expect(text).not.toContain("Nested1");
|
|
193
|
+
expect(text).not.toContain("Nested2");
|
|
194
|
+
expect(text).toContain("Nested3");
|
|
195
|
+
expect(text).toContain("Nested4");
|
|
196
|
+
expect(text).toContain("Nested5");
|
|
197
|
+
expect(text).toContain("Nested6");
|
|
198
|
+
expect(text).toContain("2 more agents");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("shows every nested task progress row when expanded", () => {
|
|
202
|
+
setViewportRows(40);
|
|
203
|
+
const text = renderProgressText(makeParentWithNestedProgress(6), true, uiTheme);
|
|
204
|
+
|
|
205
|
+
for (let index = 1; index <= 6; index++) {
|
|
206
|
+
expect(text).toContain(`Nested${index}`);
|
|
207
|
+
}
|
|
208
|
+
expect(text).not.toContain("more agents");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("caps collapsed finalized nested task results and keeps the failed child visible", () => {
|
|
212
|
+
setViewportRows(40);
|
|
213
|
+
const text = renderResultText(makeParentWithNestedResults(6), false, uiTheme);
|
|
214
|
+
|
|
215
|
+
expect(text).toContain("Done6"); // failed child wins a slot despite sorting last
|
|
216
|
+
expect(text).toContain("2 more agents");
|
|
217
|
+
const visibleChildren = [1, 2, 3, 4, 5, 6].filter(index => text.includes(`Done${index}`));
|
|
218
|
+
expect(visibleChildren).toHaveLength(4);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("shows every finalized nested task result when expanded", () => {
|
|
222
|
+
setViewportRows(40);
|
|
223
|
+
const text = renderResultText(makeParentWithNestedResults(6), true, uiTheme);
|
|
224
|
+
|
|
225
|
+
for (let index = 1; index <= 6; index++) {
|
|
226
|
+
expect(text).toContain(`Done${index}`);
|
|
227
|
+
}
|
|
228
|
+
expect(text).not.toContain("more agents");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("does not request spinner ticks for static partial progress", () => {
|
|
232
|
+
expect("animatedPartialResult" in taskToolRenderer).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("renders running progress identically across spinner frames", () => {
|
|
236
|
+
const progress = makeProgress([]);
|
|
237
|
+
const details: TaskToolDetails = {
|
|
238
|
+
projectAgentsDir: null,
|
|
239
|
+
results: [],
|
|
240
|
+
totalDurationMs: 1,
|
|
241
|
+
progress: [progress],
|
|
242
|
+
};
|
|
243
|
+
const render = (spinnerFrame: number) =>
|
|
244
|
+
strip(
|
|
245
|
+
renderResult(
|
|
246
|
+
{ content: [{ type: "text", text: "Running 1 agent..." }], details },
|
|
247
|
+
{ expanded: false, isPartial: true, spinnerFrame },
|
|
248
|
+
uiTheme,
|
|
249
|
+
).render(120),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(render(0)).toBe(render(1));
|
|
253
|
+
});
|
|
121
254
|
});
|
package/src/task/render.ts
CHANGED
|
@@ -1679,10 +1679,16 @@ function renderNestedTaskResults(
|
|
|
1679
1679
|
continue;
|
|
1680
1680
|
}
|
|
1681
1681
|
const ordered = orderResultsForDisplay(details.results);
|
|
1682
|
-
ordered
|
|
1683
|
-
|
|
1682
|
+
const visible = expanded ? ordered : selectCollapsedResults(ordered);
|
|
1683
|
+
const hiddenCount = ordered.length - visible.length;
|
|
1684
|
+
visible.forEach((result, index) => {
|
|
1685
|
+
const { prefix, continuePrefix } = nestedMarkers(hiddenCount === 0 && index === visible.length - 1, theme);
|
|
1684
1686
|
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1));
|
|
1685
1687
|
});
|
|
1688
|
+
if (hiddenCount > 0) {
|
|
1689
|
+
const { prefix } = nestedMarkers(true, theme);
|
|
1690
|
+
lines.push(`${prefix} ${theme.fg("dim", formatMoreItems(hiddenCount, "agent"))}`);
|
|
1691
|
+
}
|
|
1686
1692
|
seen.delete(details);
|
|
1687
1693
|
}
|
|
1688
1694
|
return lines;
|
|
@@ -1716,18 +1722,26 @@ function renderNestedTaskTree(
|
|
|
1716
1722
|
const hasResults = Boolean(details.results && details.results.length > 0);
|
|
1717
1723
|
if (hasResults) {
|
|
1718
1724
|
const ordered = orderResultsForDisplay(details.results);
|
|
1719
|
-
ordered
|
|
1720
|
-
|
|
1725
|
+
const visible = expanded ? ordered : selectCollapsedResults(ordered);
|
|
1726
|
+
const hiddenCount = ordered.length - visible.length;
|
|
1727
|
+
visible.forEach((result, index) => {
|
|
1728
|
+
const { prefix, continuePrefix } = nestedMarkers(hiddenCount === 0 && index === visible.length - 1, theme);
|
|
1721
1729
|
lines.push(...renderAgentResult(result, prefix, continuePrefix, expanded, theme, seen, depth + 1));
|
|
1722
1730
|
});
|
|
1731
|
+
if (hiddenCount > 0) {
|
|
1732
|
+
const { prefix } = nestedMarkers(true, theme);
|
|
1733
|
+
lines.push(`${prefix} ${theme.fg("dim", formatMoreItems(hiddenCount, "agent"))}`);
|
|
1734
|
+
}
|
|
1723
1735
|
seen.delete(details);
|
|
1724
1736
|
continue;
|
|
1725
1737
|
}
|
|
1726
1738
|
const inflight = details.progress;
|
|
1727
1739
|
if (inflight && inflight.length > 0) {
|
|
1728
1740
|
const ordered = orderProgressForDisplay(inflight);
|
|
1729
|
-
ordered.
|
|
1730
|
-
|
|
1741
|
+
const visible = expanded ? ordered : ordered.slice(Math.max(0, ordered.length - COLLAPSED_AGENT_LIMIT));
|
|
1742
|
+
const hiddenCount = ordered.length - visible.length;
|
|
1743
|
+
visible.forEach((prog, index) => {
|
|
1744
|
+
const { prefix, continuePrefix } = nestedMarkers(hiddenCount === 0 && index === visible.length - 1, theme);
|
|
1731
1745
|
lines.push(
|
|
1732
1746
|
...renderAgentProgress(
|
|
1733
1747
|
prog,
|
|
@@ -1742,6 +1756,10 @@ function renderNestedTaskTree(
|
|
|
1742
1756
|
),
|
|
1743
1757
|
);
|
|
1744
1758
|
});
|
|
1759
|
+
if (hiddenCount > 0) {
|
|
1760
|
+
const { prefix } = nestedMarkers(true, theme);
|
|
1761
|
+
lines.push(`${prefix} ${theme.fg("dim", formatMoreItems(hiddenCount, "agent"))}`);
|
|
1762
|
+
}
|
|
1745
1763
|
}
|
|
1746
1764
|
seen.delete(details);
|
|
1747
1765
|
}
|
package/src/task/renderer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { logger, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { logger, postmortem, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { JsRuntime, type RuntimeHooks } from "../../../eval/js/shared/runtime";
|
|
6
6
|
import type { JsDisplayOutput } from "../../../eval/js/shared/types";
|
|
7
7
|
import { callSessionTool } from "../../../eval/js/tool-bridge";
|
|
@@ -1306,7 +1306,11 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1306
1306
|
if (timeoutSignal.aborted) {
|
|
1307
1307
|
reject(new ToolError(`Browser code execution timed out after ${opts.timeoutMs}ms`));
|
|
1308
1308
|
} else {
|
|
1309
|
-
reject(
|
|
1309
|
+
reject(
|
|
1310
|
+
signal.reason instanceof ToolAbortError
|
|
1311
|
+
? signal.reason
|
|
1312
|
+
: new ToolAbortError(undefined, { cause: signal.reason }),
|
|
1313
|
+
);
|
|
1310
1314
|
}
|
|
1311
1315
|
};
|
|
1312
1316
|
if (signal.aborted) onAbort();
|
|
@@ -1336,7 +1340,7 @@ export async function runCmuxCode(tab: CmuxTab, opts: RunCmuxCodeOptions): Promi
|
|
|
1336
1340
|
return { displays, returnValue: cloneSafe(returnValue), screenshots };
|
|
1337
1341
|
} finally {
|
|
1338
1342
|
signal.removeEventListener("abort", onAbort);
|
|
1339
|
-
runAc.abort(new ToolAbortError("Browser run ended"));
|
|
1343
|
+
runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
|
|
1340
1344
|
tab.clearRunContext();
|
|
1341
1345
|
}
|
|
1342
1346
|
}
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { throwIfAborted } from "../tool-errors";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Marks a run-scoped promise as observed without changing its behavior for awaited callers.
|
|
6
|
+
*
|
|
7
|
+
* Browser run teardown aborts can reject promises created for evaluated code after user code
|
|
8
|
+
* has stopped observing them (for example fire-and-forget `wait()`/facade calls). In 16.3.0
|
|
9
|
+
* those zero-consumer rejections reached the process-level `unhandledRejection` handler and
|
|
10
|
+
* killed every subagent sharing the process (issues #4499/#4672). Attaching a no-op rejection
|
|
11
|
+
* handler at creation makes the promise observed while returning the original promise so callers
|
|
12
|
+
* that do await it still receive the rejection.
|
|
13
|
+
*/
|
|
14
|
+
export function markHandled<T>(promise: Promise<T>): Promise<T> {
|
|
15
|
+
void promise.catch(() => undefined);
|
|
16
|
+
return promise;
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
/** Sleeps inside evaluated browser code while honoring the owning run's cancellation signal. */
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
export function waitForBrowserRun(ms: number, signal: AbortSignal): Promise<void> {
|
|
21
|
+
const promise = (async (): Promise<void> => {
|
|
22
|
+
throwIfAborted(signal);
|
|
23
|
+
await untilAborted(signal, () => Bun.sleep(ms));
|
|
24
|
+
throwIfAborted(signal);
|
|
25
|
+
})();
|
|
26
|
+
return markHandled(promise);
|
|
9
27
|
}
|
|
10
28
|
|
|
11
29
|
/** Binds a long-lived browser facade to one evaluated run's abort signal. */
|
|
@@ -24,10 +42,12 @@ export function bindBrowserRunFacade<T extends object>(target: T, signal: AbortS
|
|
|
24
42
|
if (result && typeof result === "object") {
|
|
25
43
|
const then = Reflect.get(result, "then");
|
|
26
44
|
if (typeof then === "function") {
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
return markHandled(
|
|
46
|
+
Promise.resolve(result).then(resolved => {
|
|
47
|
+
throwIfAborted(signal);
|
|
48
|
+
return resolved;
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
31
51
|
}
|
|
32
52
|
}
|
|
33
53
|
throwIfAborted(signal);
|
|
@@ -66,7 +66,7 @@ export type ToolReply = { ok: true; value: unknown } | { ok: false; error: RunEr
|
|
|
66
66
|
export type WorkerInbound =
|
|
67
67
|
| { type: "init"; payload: WorkerInitPayload }
|
|
68
68
|
| { type: "run"; id: string; name: string; code: string; timeoutMs: number; session: SessionSnapshot }
|
|
69
|
-
| { type: "abort"; id: string }
|
|
69
|
+
| { type: "abort"; id: string; expectedCleanup?: boolean }
|
|
70
70
|
| { type: "tool-reply"; id: string; reply: ToolReply }
|
|
71
71
|
| { type: "close" };
|
|
72
72
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPuppeteerDir, logger, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { getPuppeteerDir, logger, postmortem, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Page, Target } from "puppeteer-core";
|
|
3
3
|
import { callSessionTool } from "../../eval/js/tool-bridge";
|
|
4
4
|
import { webpExclusionForModel } from "../../utils/image-loading";
|
|
@@ -486,11 +486,11 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
486
486
|
}
|
|
487
487
|
const wasAlive = tab.state === "alive";
|
|
488
488
|
tab.state = "dead";
|
|
489
|
-
const closeError = new ToolError(`Tab ${JSON.stringify(name)} was closed`);
|
|
489
|
+
const closeError = postmortem.markExpectedCleanupError(new ToolError(`Tab ${JSON.stringify(name)} was closed`));
|
|
490
490
|
for (const [id, pending] of tab.pending) {
|
|
491
491
|
if (tab.backend === "worker") {
|
|
492
492
|
try {
|
|
493
|
-
tab.worker.send({ type: "abort", id });
|
|
493
|
+
tab.worker.send({ type: "abort", id, expectedCleanup: true });
|
|
494
494
|
} catch {}
|
|
495
495
|
}
|
|
496
496
|
for (const ctrl of pending.toolCalls.values()) ctrl.abort(closeError);
|
|
@@ -744,7 +744,7 @@ async function forceKillTab(name: string, reason: string): Promise<void> {
|
|
|
744
744
|
const tab = tabs.get(name);
|
|
745
745
|
if (!tab) return;
|
|
746
746
|
tab.state = "dead";
|
|
747
|
-
const error = new ToolError(reason);
|
|
747
|
+
const error = postmortem.markExpectedCleanupError(new ToolError(reason));
|
|
748
748
|
for (const pending of tab.pending.values()) pending.reject(error);
|
|
749
749
|
tab.pending.clear();
|
|
750
750
|
if (tab.backend === "cmux") {
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { postmortem, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import type { HTMLElement } from "linkedom";
|
|
7
7
|
import type {
|
|
8
8
|
Browser,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
loadPuppeteerInWorker,
|
|
37
37
|
} from "./launch";
|
|
38
38
|
import { extractReadableFromHtml, type ReadableFormat } from "./readable";
|
|
39
|
-
import { waitForBrowserRun } from "./run-cancellation";
|
|
39
|
+
import { markHandled, waitForBrowserRun } from "./run-cancellation";
|
|
40
40
|
import type {
|
|
41
41
|
Observation,
|
|
42
42
|
ObservationEntry,
|
|
@@ -593,7 +593,12 @@ export class WorkerCore {
|
|
|
593
593
|
await this.#run(msg);
|
|
594
594
|
return;
|
|
595
595
|
case "abort":
|
|
596
|
-
if (this.#active?.id === msg.id)
|
|
596
|
+
if (this.#active?.id === msg.id) {
|
|
597
|
+
const reason = msg.expectedCleanup
|
|
598
|
+
? postmortem.markExpectedCleanupError(new ToolAbortError())
|
|
599
|
+
: new ToolAbortError();
|
|
600
|
+
this.#active.ac.abort(reason);
|
|
601
|
+
}
|
|
597
602
|
return;
|
|
598
603
|
case "tool-reply":
|
|
599
604
|
this.#deliverToolReply(msg.id, msg.reply);
|
|
@@ -732,6 +737,10 @@ export class WorkerCore {
|
|
|
732
737
|
});
|
|
733
738
|
const { promise: cancelRejection, reject: rejectCancel } = Promise.withResolvers<never>();
|
|
734
739
|
const onCancel = (): void => {
|
|
740
|
+
const abortError =
|
|
741
|
+
signal.reason instanceof ToolAbortError
|
|
742
|
+
? signal.reason
|
|
743
|
+
: new ToolAbortError(undefined, { cause: signal.reason });
|
|
735
744
|
if (timeoutSignal.aborted) {
|
|
736
745
|
const stalled = describeInflight(active.inflight);
|
|
737
746
|
rejectCancel(
|
|
@@ -740,11 +749,14 @@ export class WorkerCore {
|
|
|
740
749
|
),
|
|
741
750
|
);
|
|
742
751
|
} else {
|
|
743
|
-
rejectCancel(
|
|
752
|
+
rejectCancel(abortError);
|
|
744
753
|
}
|
|
745
754
|
// Cancel in-flight tool calls so user code's awaited proxies reject promptly.
|
|
755
|
+
const toolAbort = timeoutSignal.aborted
|
|
756
|
+
? postmortem.markExpectedCleanupError(new ToolAbortError(undefined, { cause: timeoutSignal.reason }))
|
|
757
|
+
: abortError;
|
|
746
758
|
for (const pending of active.pendingTools.values()) {
|
|
747
|
-
pending.reject(
|
|
759
|
+
pending.reject(toolAbort);
|
|
748
760
|
}
|
|
749
761
|
active.pendingTools.clear();
|
|
750
762
|
};
|
|
@@ -771,7 +783,7 @@ export class WorkerCore {
|
|
|
771
783
|
this.#transport.send({ type: "result", id: msg.id, ok: false, error: errorPayload(error) });
|
|
772
784
|
} finally {
|
|
773
785
|
if (this.#active?.id === msg.id) this.#active = null;
|
|
774
|
-
runAc.abort(new ToolAbortError("Browser run ended"));
|
|
786
|
+
runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
|
|
775
787
|
}
|
|
776
788
|
}
|
|
777
789
|
|
|
@@ -889,7 +901,7 @@ export class WorkerCore {
|
|
|
889
901
|
const waitMs = (explicit?: number): number => resolveWaitTimeout(timeoutMs, explicit);
|
|
890
902
|
const INF = Number.POSITIVE_INFINITY;
|
|
891
903
|
const op = <T>(label: string, perOpMs: number, fn: (sig: AbortSignal) => Promise<T>): Promise<T> =>
|
|
892
|
-
this.#runOp(active, label, signal, perOpMs, fn);
|
|
904
|
+
markHandled(this.#runOp(active, label, signal, perOpMs, fn));
|
|
893
905
|
return {
|
|
894
906
|
name,
|
|
895
907
|
page,
|
package/src/tools/irc.ts
CHANGED
|
@@ -171,7 +171,7 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
171
171
|
case "send":
|
|
172
172
|
return this.#executeSend(registry, senderId, params, signal);
|
|
173
173
|
case "wait":
|
|
174
|
-
return this.#executeWait(senderId, params, signal);
|
|
174
|
+
return this.#executeWait(registry, senderId, params, signal);
|
|
175
175
|
case "inbox":
|
|
176
176
|
return this.#executeInbox(registry, senderId, params);
|
|
177
177
|
default:
|
|
@@ -367,8 +367,24 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
async #executeWait(
|
|
370
|
+
async #executeWait(
|
|
371
|
+
registry: AgentRegistry,
|
|
372
|
+
senderId: string,
|
|
373
|
+
params: IrcParams,
|
|
374
|
+
signal?: AbortSignal,
|
|
375
|
+
): Promise<AgentToolResult<IrcDetails>> {
|
|
371
376
|
const from = params.from?.trim() || undefined;
|
|
377
|
+
const session = registry.get(senderId)?.session;
|
|
378
|
+
const pending =
|
|
379
|
+
typeof session?.drainPendingIrcInboxMessages === "function"
|
|
380
|
+
? session.drainPendingIrcInboxMessages(senderId, { from, limit: 1 })[0]
|
|
381
|
+
: undefined;
|
|
382
|
+
if (pending) {
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: "text", text: formatIncoming(pending) }],
|
|
385
|
+
details: { op: "wait", from: senderId, waited: pending },
|
|
386
|
+
};
|
|
387
|
+
}
|
|
372
388
|
const timeoutMs = this.#resolveTimeoutMs(params);
|
|
373
389
|
const waited = await IrcBus.global().wait(senderId, { from }, timeoutMs, signal);
|
|
374
390
|
if (!waited) {
|
package/src/tools/tool-errors.ts
CHANGED
|
@@ -30,8 +30,8 @@ export class ToolError extends Error {
|
|
|
30
30
|
export class ToolAbortError extends Error {
|
|
31
31
|
static readonly MESSAGE = "Operation aborted";
|
|
32
32
|
|
|
33
|
-
constructor(message: string = ToolAbortError.MESSAGE) {
|
|
34
|
-
super(message);
|
|
33
|
+
constructor(message: string = ToolAbortError.MESSAGE, options?: ErrorOptions) {
|
|
34
|
+
super(message, options);
|
|
35
35
|
this.name = "ToolAbortError";
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -43,7 +43,7 @@ export class ToolAbortError extends Error {
|
|
|
43
43
|
export function throwIfAborted(signal?: AbortSignal): void {
|
|
44
44
|
if (signal?.aborted) {
|
|
45
45
|
const reason = signal.reason instanceof Error ? signal.reason : undefined;
|
|
46
|
-
throw reason instanceof ToolAbortError ? reason : new ToolAbortError();
|
|
46
|
+
throw reason instanceof ToolAbortError ? reason : new ToolAbortError(undefined, { cause: signal.reason });
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|