@oh-my-pi/pi-agent-core 15.5.4 → 15.5.6
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/dist/types/index.d.ts +1 -0
- package/dist/types/utils/yield.d.ts +38 -13
- package/package.json +4 -4
- package/src/agent.ts +12 -1
- package/src/index.ts +2 -0
- package/src/utils/yield.ts +58 -12
package/dist/types/index.d.ts
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cooperative yield utility for preventing Bun event-loop busy-wait.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
4
|
+
* ## Root Cause
|
|
5
|
+
*
|
|
6
|
+
* Bun 1.3.x (JavaScriptCore) event loop busy-waits (spins in userspace)
|
|
7
|
+
* when the only pending work is an unresolved Promise — even if there are
|
|
8
|
+
* active I/O watchers (stdin, child process pipes, etc.). The event loop
|
|
9
|
+
* continuously polls for microtask resolution instead of blocking in
|
|
10
|
+
* `epoll_wait`, consuming ~100% of a CPU core.
|
|
11
|
+
*
|
|
12
|
+
* This affects any `await` on a never-resolved Promise, including:
|
|
13
|
+
* - `Promise.withResolvers()` used for user input callbacks
|
|
14
|
+
* - `await proc.exited` for long-running child processes
|
|
15
|
+
* - Agent loop iterations waiting for the next tool call
|
|
16
|
+
*
|
|
17
|
+
* ## Fix
|
|
18
|
+
*
|
|
19
|
+
* A recurring `setInterval` keeps the event loop sleeping in `epoll_wait`.
|
|
20
|
+
* The `EventLoopKeepalive` class and `keepaliveWhile()` wrapper provide a
|
|
21
|
+
* clean way to install and clean up this keepalive timer.
|
|
22
|
+
*
|
|
23
|
+
* The older `yieldIfDue()` and `ExponentialYield` approaches (compensated
|
|
24
|
+
* sleep loops) are retained for the agent-loop hot-path where Promises
|
|
25
|
+
* resolve frequently and the keepalive alone is insufficient.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Await a Promise with an event-loop keepalive active.
|
|
29
|
+
*
|
|
30
|
+
* This is the primary fix for Bun's busy-wait on unresolved Promises.
|
|
31
|
+
* Use it wherever you `await` a Promise that may remain unresolved for
|
|
32
|
+
* an extended period (user input, long-running subprocess, etc.).
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* // Before (100% CPU while waiting):
|
|
36
|
+
* const input = await mode.getUserInput();
|
|
37
|
+
*
|
|
38
|
+
* // After (proper epoll_wait sleep):
|
|
39
|
+
* const input = await keepaliveWhile(mode.getUserInput());
|
|
40
|
+
* ```
|
|
17
41
|
*/
|
|
42
|
+
export declare function keepaliveWhile<T>(promise: Promise<T>): Promise<T>;
|
|
18
43
|
/**
|
|
19
44
|
* Yield to the Bun event loop, sleeping for at least 20 ms — but at most
|
|
20
45
|
* once every {@link YIELD_INTERVAL_MS}. Callers in hot paths can invoke
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "15.5.
|
|
4
|
+
"version": "15.5.6",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "15.5.
|
|
39
|
-
"@oh-my-pi/pi-natives": "15.5.
|
|
40
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
38
|
+
"@oh-my-pi/pi-ai": "15.5.6",
|
|
39
|
+
"@oh-my-pi/pi-natives": "15.5.6",
|
|
40
|
+
"@oh-my-pi/pi-utils": "15.5.6",
|
|
41
41
|
"@opentelemetry/api": "^1.9.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
package/src/agent.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Agent class that uses the agent-loop directly.
|
|
2
2
|
* No transport abstraction - calls streamSimple via the loop.
|
|
3
3
|
*/
|
|
4
|
+
|
|
5
|
+
import { isPromise } from "node:util/types";
|
|
4
6
|
import {
|
|
5
7
|
type AssistantMessage,
|
|
6
8
|
type AssistantMessageEvent,
|
|
@@ -1074,7 +1076,16 @@ export class Agent {
|
|
|
1074
1076
|
|
|
1075
1077
|
#emit(e: AgentEvent) {
|
|
1076
1078
|
for (const listener of this.#listeners) {
|
|
1077
|
-
|
|
1079
|
+
try {
|
|
1080
|
+
const result = listener(e) as unknown;
|
|
1081
|
+
if (isPromise(result)) {
|
|
1082
|
+
result.catch(err => {
|
|
1083
|
+
console.error("Agent listener rejected:", err instanceof Error ? err.message : err);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
console.error("Agent listener threw:", err instanceof Error ? err.message : err);
|
|
1088
|
+
}
|
|
1078
1089
|
}
|
|
1079
1090
|
}
|
|
1080
1091
|
|
package/src/index.ts
CHANGED
package/src/utils/yield.ts
CHANGED
|
@@ -1,23 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cooperative yield utility for preventing Bun event-loop busy-wait.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the microtask queue is continuously non-empty. In long-running agent loops
|
|
6
|
-
* (LLM streaming, tool execution) this causes ~100% CPU usage even when the
|
|
7
|
-
* process is simply waiting for I/O.
|
|
4
|
+
* ## Root Cause
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* Bun 1.3.x (JavaScriptCore) event loop busy-waits (spins in userspace)
|
|
7
|
+
* when the only pending work is an unresolved Promise — even if there are
|
|
8
|
+
* active I/O watchers (stdin, child process pipes, etc.). The event loop
|
|
9
|
+
* continuously polls for microtask resolution instead of blocking in
|
|
10
|
+
* `epoll_wait`, consuming ~100% of a CPU core.
|
|
14
11
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* This affects any `await` on a never-resolved Promise, including:
|
|
13
|
+
* - `Promise.withResolvers()` used for user input callbacks
|
|
14
|
+
* - `await proc.exited` for long-running child processes
|
|
15
|
+
* - Agent loop iterations waiting for the next tool call
|
|
16
|
+
*
|
|
17
|
+
* ## Fix
|
|
18
|
+
*
|
|
19
|
+
* A recurring `setInterval` keeps the event loop sleeping in `epoll_wait`.
|
|
20
|
+
* The `EventLoopKeepalive` class and `keepaliveWhile()` wrapper provide a
|
|
21
|
+
* clean way to install and clean up this keepalive timer.
|
|
22
|
+
*
|
|
23
|
+
* The older `yieldIfDue()` and `ExponentialYield` approaches (compensated
|
|
24
|
+
* sleep loops) are retained for the agent-loop hot-path where Promises
|
|
25
|
+
* resolve frequently and the keepalive alone is insufficient.
|
|
17
26
|
*/
|
|
18
27
|
|
|
19
28
|
import { scheduler } from "node:timers/promises";
|
|
20
29
|
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// EventLoopKeepalive — the primary fix for idle-state busy-wait
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const KEEPALIVE_INTERVAL_MS = 86_400_000; // 24 hours — re-armed each interval
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Await a Promise with an event-loop keepalive active.
|
|
38
|
+
*
|
|
39
|
+
* This is the primary fix for Bun's busy-wait on unresolved Promises.
|
|
40
|
+
* Use it wherever you `await` a Promise that may remain unresolved for
|
|
41
|
+
* an extended period (user input, long-running subprocess, etc.).
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* // Before (100% CPU while waiting):
|
|
45
|
+
* const input = await mode.getUserInput();
|
|
46
|
+
*
|
|
47
|
+
* // After (proper epoll_wait sleep):
|
|
48
|
+
* const input = await keepaliveWhile(mode.getUserInput());
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export async function keepaliveWhile<T>(promise: Promise<T>): Promise<T> {
|
|
52
|
+
const ka = setInterval(() => {}, KEEPALIVE_INTERVAL_MS);
|
|
53
|
+
try {
|
|
54
|
+
ka.unref();
|
|
55
|
+
return await promise;
|
|
56
|
+
} finally {
|
|
57
|
+
clearInterval(ka);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// yieldIfDue — retained for agent-loop hot-path
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
21
65
|
const YIELD_SLEEP_MS = 20;
|
|
22
66
|
const YIELD_INTERVAL_MS = 50;
|
|
23
67
|
|
|
@@ -63,7 +107,9 @@ export async function yieldIfDue(): Promise<void> {
|
|
|
63
107
|
lastYieldAt = Date.now();
|
|
64
108
|
}
|
|
65
109
|
|
|
66
|
-
//
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// ExponentialYield — retained for bash-executor long waits
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
67
113
|
|
|
68
114
|
const EXP_DEFAULT_MIN_MS = 20;
|
|
69
115
|
const EXP_DEFAULT_MAX_MS = 10_000;
|