@oh-my-pi/pi-agent-core 16.4.3 → 16.4.5
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 +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/pause.d.ts +46 -0
- package/package.json +7 -7
- package/src/agent-loop.ts +9 -0
- package/src/index.ts +2 -0
- package/src/pause.ts +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.4.5] - 2026-07-11
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added a process-global pause gate (`agentPauseGate`) to safely pause agent loops before model calls or tool executions, allowing them to be resumed later or aborted cleanly.
|
|
10
|
+
|
|
5
11
|
## [16.4.3] - 2026-07-11
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from "./agent.js";
|
|
|
2
2
|
export * from "./agent-loop.js";
|
|
3
3
|
export * from "./append-only-context.js";
|
|
4
4
|
export * from "./compaction.js";
|
|
5
|
+
export * from "./pause.js";
|
|
5
6
|
export * from "./proxy.js";
|
|
6
7
|
export * from "./replay-policy.js";
|
|
7
8
|
export * from "./run-collector.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-global pause gate for agent loops.
|
|
3
|
+
*
|
|
4
|
+
* Every agent in a process — main session, in-process subagents, advisor —
|
|
5
|
+
* funnels through {@link ../agent-loop!agentLoop}, which polls this gate at its
|
|
6
|
+
* two action boundaries: before each model call and before each tool call
|
|
7
|
+
* starts. Engaging the gate therefore freezes all of them at the next safe
|
|
8
|
+
* point without aborting anything: in-flight provider streams and
|
|
9
|
+
* already-started tool executions run to completion, then every loop parks
|
|
10
|
+
* until {@link AgentPauseGate.resume}. Queued steering/follow-up messages stay
|
|
11
|
+
* queued and deliver normally after resume.
|
|
12
|
+
*
|
|
13
|
+
* A run's own `AbortSignal` still unwinds a parked loop immediately: the park
|
|
14
|
+
* releases on abort (without releasing the gate), so cancelling one run never
|
|
15
|
+
* requires resuming the whole process.
|
|
16
|
+
*
|
|
17
|
+
* Hosts drive the singleton {@link agentPauseGate} (e.g. the TUI `/pause`
|
|
18
|
+
* command); library code only ever reads it.
|
|
19
|
+
*/
|
|
20
|
+
/** Listener invoked with the new state on every pause/resume transition. */
|
|
21
|
+
export type AgentPauseListener = (paused: boolean) => void;
|
|
22
|
+
/** Freeze switch shared by every agent loop in the process. See module docs. */
|
|
23
|
+
export declare class AgentPauseGate {
|
|
24
|
+
#private;
|
|
25
|
+
/** True while the gate is engaged. */
|
|
26
|
+
get paused(): boolean;
|
|
27
|
+
/** Epoch ms when the current pause began; undefined when running. */
|
|
28
|
+
get pausedAt(): number | undefined;
|
|
29
|
+
/** Engage the gate. Returns false (and does nothing) when already paused. */
|
|
30
|
+
pause(): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Release the gate, waking every parked loop. Returns the pause duration in
|
|
33
|
+
* ms, or undefined when the gate was not engaged.
|
|
34
|
+
*/
|
|
35
|
+
resume(): number | undefined;
|
|
36
|
+
/** Subscribe to pause/resume transitions. Returns an unsubscribe function. */
|
|
37
|
+
onChange(listener: AgentPauseListener): () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Park until the gate is released. Resolves immediately when not paused.
|
|
40
|
+
* An abort on `signal` releases only this wait — the gate stays engaged —
|
|
41
|
+
* so a cancelled run unwinds while the rest of the process stays frozen.
|
|
42
|
+
*/
|
|
43
|
+
waitUntilResumed(signal?: AbortSignal): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/** The process-wide gate polled by the agent loop. */
|
|
46
|
+
export declare const agentPauseGate: AgentPauseGate;
|
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": "16.4.
|
|
4
|
+
"version": "16.4.5",
|
|
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,12 +35,12 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "16.4.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "16.4.
|
|
40
|
-
"@oh-my-pi/pi-natives": "16.4.
|
|
41
|
-
"@oh-my-pi/pi-utils": "16.4.
|
|
42
|
-
"@oh-my-pi/pi-wire": "16.4.
|
|
43
|
-
"@oh-my-pi/snapcompact": "16.4.
|
|
38
|
+
"@oh-my-pi/pi-ai": "16.4.5",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "16.4.5",
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.4.5",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.4.5",
|
|
42
|
+
"@oh-my-pi/pi-wire": "16.4.5",
|
|
43
|
+
"@oh-my-pi/snapcompact": "16.4.5",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
41
41
|
import { sanitizeText, structuredCloneJSON } from "@oh-my-pi/pi-utils";
|
|
42
42
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
43
|
+
import { agentPauseGate } from "./pause";
|
|
43
44
|
import { type AgentRunCoverage, type AgentRunSummary, ToolCallBlockedError } from "./run-collector";
|
|
44
45
|
import {
|
|
45
46
|
type AgentTelemetry,
|
|
@@ -816,6 +817,10 @@ async function runLoopBody(
|
|
|
816
817
|
// Yield at the top of each iteration to prevent busy-wait when
|
|
817
818
|
// the agent loop is executing tool calls back-to-back.
|
|
818
819
|
await yieldIfDue();
|
|
820
|
+
// Park at the turn boundary while the process-wide pause gate is
|
|
821
|
+
// engaged (host /pause). An external abort releases the park so a
|
|
822
|
+
// cancelled run still unwinds while everything else stays frozen.
|
|
823
|
+
if (agentPauseGate.paused) await agentPauseGate.waitUntilResumed(signal);
|
|
819
824
|
if (!firstTurn) {
|
|
820
825
|
stream.push({ type: "turn_start" });
|
|
821
826
|
} else {
|
|
@@ -1922,6 +1927,10 @@ async function executeToolCalls(
|
|
|
1922
1927
|
record.skipped = true;
|
|
1923
1928
|
return;
|
|
1924
1929
|
}
|
|
1930
|
+
// Park before starting this tool while the process-wide pause gate is
|
|
1931
|
+
// engaged. Tools already executing are unaffected (pausing never aborts);
|
|
1932
|
+
// a batch interrupted mid-pause unwinds via the signal checks below.
|
|
1933
|
+
if (agentPauseGate.paused) await agentPauseGate.waitUntilResumed(record.signal);
|
|
1925
1934
|
|
|
1926
1935
|
const { toolCall, tool } = record;
|
|
1927
1936
|
let argsForExecution = toolCall.arguments as Record<string, unknown>;
|
package/src/index.ts
CHANGED
package/src/pause.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-global pause gate for agent loops.
|
|
3
|
+
*
|
|
4
|
+
* Every agent in a process — main session, in-process subagents, advisor —
|
|
5
|
+
* funnels through {@link ../agent-loop!agentLoop}, which polls this gate at its
|
|
6
|
+
* two action boundaries: before each model call and before each tool call
|
|
7
|
+
* starts. Engaging the gate therefore freezes all of them at the next safe
|
|
8
|
+
* point without aborting anything: in-flight provider streams and
|
|
9
|
+
* already-started tool executions run to completion, then every loop parks
|
|
10
|
+
* until {@link AgentPauseGate.resume}. Queued steering/follow-up messages stay
|
|
11
|
+
* queued and deliver normally after resume.
|
|
12
|
+
*
|
|
13
|
+
* A run's own `AbortSignal` still unwinds a parked loop immediately: the park
|
|
14
|
+
* releases on abort (without releasing the gate), so cancelling one run never
|
|
15
|
+
* requires resuming the whole process.
|
|
16
|
+
*
|
|
17
|
+
* Hosts drive the singleton {@link agentPauseGate} (e.g. the TUI `/pause`
|
|
18
|
+
* command); library code only ever reads it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Listener invoked with the new state on every pause/resume transition. */
|
|
22
|
+
export type AgentPauseListener = (paused: boolean) => void;
|
|
23
|
+
|
|
24
|
+
/** Freeze switch shared by every agent loop in the process. See module docs. */
|
|
25
|
+
export class AgentPauseGate {
|
|
26
|
+
/** Pending while paused; resolved and cleared on resume. */
|
|
27
|
+
#gate: PromiseWithResolvers<void> | undefined;
|
|
28
|
+
#pausedAt = 0;
|
|
29
|
+
#listeners = new Set<AgentPauseListener>();
|
|
30
|
+
|
|
31
|
+
/** True while the gate is engaged. */
|
|
32
|
+
get paused(): boolean {
|
|
33
|
+
return this.#gate !== undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Epoch ms when the current pause began; undefined when running. */
|
|
37
|
+
get pausedAt(): number | undefined {
|
|
38
|
+
return this.#gate ? this.#pausedAt : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Engage the gate. Returns false (and does nothing) when already paused. */
|
|
42
|
+
pause(): boolean {
|
|
43
|
+
if (this.#gate) return false;
|
|
44
|
+
this.#gate = Promise.withResolvers<void>();
|
|
45
|
+
this.#pausedAt = Date.now();
|
|
46
|
+
this.#notify(true);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Release the gate, waking every parked loop. Returns the pause duration in
|
|
52
|
+
* ms, or undefined when the gate was not engaged.
|
|
53
|
+
*/
|
|
54
|
+
resume(): number | undefined {
|
|
55
|
+
const gate = this.#gate;
|
|
56
|
+
if (!gate) return undefined;
|
|
57
|
+
this.#gate = undefined;
|
|
58
|
+
gate.resolve();
|
|
59
|
+
this.#notify(false);
|
|
60
|
+
return Date.now() - this.#pausedAt;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Subscribe to pause/resume transitions. Returns an unsubscribe function. */
|
|
64
|
+
onChange(listener: AgentPauseListener): () => void {
|
|
65
|
+
this.#listeners.add(listener);
|
|
66
|
+
return () => this.#listeners.delete(listener);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Park until the gate is released. Resolves immediately when not paused.
|
|
71
|
+
* An abort on `signal` releases only this wait — the gate stays engaged —
|
|
72
|
+
* so a cancelled run unwinds while the rest of the process stays frozen.
|
|
73
|
+
*/
|
|
74
|
+
async waitUntilResumed(signal?: AbortSignal): Promise<void> {
|
|
75
|
+
// Loop: resume() swaps the gate promise, so a pause re-engaged while a
|
|
76
|
+
// waiter is between awaits must re-park instead of slipping through.
|
|
77
|
+
while (this.#gate) {
|
|
78
|
+
if (signal?.aborted) return;
|
|
79
|
+
const gate = this.#gate.promise;
|
|
80
|
+
if (!signal) {
|
|
81
|
+
await gate;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const abort = Promise.withResolvers<void>();
|
|
85
|
+
const onAbort = () => abort.resolve();
|
|
86
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
87
|
+
try {
|
|
88
|
+
await Promise.race([gate, abort.promise]);
|
|
89
|
+
} finally {
|
|
90
|
+
signal.removeEventListener("abort", onAbort);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#notify(paused: boolean): void {
|
|
96
|
+
for (const listener of this.#listeners) {
|
|
97
|
+
try {
|
|
98
|
+
listener(paused);
|
|
99
|
+
} catch {
|
|
100
|
+
// Host UI listeners must never break the gate.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The process-wide gate polled by the agent loop. */
|
|
107
|
+
export const agentPauseGate = new AgentPauseGate();
|