@oh-my-pi/pi-agent-core 18.13.0 → 18.13.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/CHANGELOG.md +6 -0
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/speculative-execution.d.ts +3 -0
- package/dist/types/types.d.ts +2 -0
- package/package.json +8 -8
- package/src/agent-loop.ts +39 -3
- package/src/agent.ts +12 -0
- package/src/speculative-execution.ts +7 -0
- package/src/types.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.13.1] - 2026-10-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Hosts can distinguish safely paused agents from turns still draining with `Agent.isPaused` ([#17073](https://github.com/can1357/oh-my-pi/pull/17073) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy)).
|
|
10
|
+
|
|
5
11
|
## [18.13.0] - 2026-10-26
|
|
6
12
|
|
|
7
13
|
### Added
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -240,6 +240,8 @@ export interface AgentPromptOptions {
|
|
|
240
240
|
}
|
|
241
241
|
export declare class Agent {
|
|
242
242
|
#private;
|
|
243
|
+
/** True while this unfinished turn is safely parked by the process pause gate. */
|
|
244
|
+
get isPaused(): boolean;
|
|
243
245
|
streamFn: StreamFn;
|
|
244
246
|
getApiKey?: (model: Model) => Promise<ApiKey | undefined> | ApiKey | undefined;
|
|
245
247
|
/** Prepare actual queue deliveries after dequeue gates; commit runs only while ownership remains valid. */
|
|
@@ -23,6 +23,9 @@ export declare class SpeculativeOperationCoordinator {
|
|
|
23
23
|
#private;
|
|
24
24
|
readonly config: SpeculativeToolExecutionConfig;
|
|
25
25
|
readonly environment?: CoordinatorEnvironment | undefined;
|
|
26
|
+
/** Observer of existing speculative execution draining; no separate work ledger. */
|
|
27
|
+
onIdle?: () => void;
|
|
28
|
+
get isRunning(): boolean;
|
|
26
29
|
constructor(config: SpeculativeToolExecutionConfig, environment?: CoordinatorEnvironment | undefined);
|
|
27
30
|
get maxInFlight(): number;
|
|
28
31
|
get size(): number;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -126,6 +126,8 @@ export interface SteeringQueueState {
|
|
|
126
126
|
*/
|
|
127
127
|
export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
128
128
|
model: Model;
|
|
129
|
+
/** Reports whether this loop has reached a safe process-pause boundary. */
|
|
130
|
+
onPauseChange?: (paused: boolean) => void;
|
|
129
131
|
/**
|
|
130
132
|
* When to interrupt tool execution for steering messages.
|
|
131
133
|
* - "immediate" = cut interruptible waits short and raise the cooperative
|
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": "18.13.
|
|
4
|
+
"version": "18.13.1",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@oh-my-pi/pi-ai": "18.13.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "18.13.
|
|
43
|
-
"@oh-my-pi/pi-natives": "18.13.
|
|
44
|
-
"@oh-my-pi/pi-utils": "18.13.
|
|
45
|
-
"@oh-my-pi/pi-wire": "18.13.
|
|
46
|
-
"@oh-my-pi/snapcompact": "18.13.
|
|
41
|
+
"@oh-my-pi/pi-ai": "18.13.1",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.13.1",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.13.1",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.13.1",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.13.1",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.13.1",
|
|
47
47
|
"@opentelemetry/api": "^1.9.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@oh-my-pi/omptype": "18.13.
|
|
50
|
+
"@oh-my-pi/omptype": "18.13.1",
|
|
51
51
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
52
52
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
53
53
|
"@types/bun": "^1.3.14"
|
package/src/agent-loop.ts
CHANGED
|
@@ -110,6 +110,17 @@ import {
|
|
|
110
110
|
SPECULATIVE_STREAM_SESSION,
|
|
111
111
|
} from "./types";
|
|
112
112
|
import { yieldIfDue } from "./utils/yield";
|
|
113
|
+
|
|
114
|
+
async function waitForProcessPause(config: AgentLoopConfig, signal?: AbortSignal): Promise<void> {
|
|
115
|
+
try {
|
|
116
|
+
while (agentPauseGate.paused && !signal?.aborted) {
|
|
117
|
+
config.onPauseChange?.(true);
|
|
118
|
+
await agentPauseGate.waitUntilResumed(signal);
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
config.onPauseChange?.(false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
113
124
|
/** Stop-details marker for a provider error after assistant content/tool args already streamed. */
|
|
114
125
|
export const STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL = "stream_interrupted_after_content";
|
|
115
126
|
|
|
@@ -1331,7 +1342,7 @@ async function runLoopBody(
|
|
|
1331
1342
|
// Park at the turn boundary while the process-wide pause gate is
|
|
1332
1343
|
// engaged (host /pause). An external abort releases the park so a
|
|
1333
1344
|
// cancelled run still unwinds while everything else stays frozen.
|
|
1334
|
-
if (agentPauseGate.paused) await
|
|
1345
|
+
if (agentPauseGate.paused) await waitForProcessPause(config, signal);
|
|
1335
1346
|
|
|
1336
1347
|
// Build the provider-bound context before opening the turn. Queue
|
|
1337
1348
|
// messages are added now but their events remain deferred until
|
|
@@ -1443,6 +1454,7 @@ async function runLoopBody(
|
|
|
1443
1454
|
let recovered: HarmonyRecoveredToolCall | undefined;
|
|
1444
1455
|
let message: AssistantMessage;
|
|
1445
1456
|
try {
|
|
1457
|
+
if (agentPauseGate.paused) await waitForProcessPause(config, signal);
|
|
1446
1458
|
message = await streamAssistantResponse(
|
|
1447
1459
|
currentContext,
|
|
1448
1460
|
config,
|
|
@@ -3367,6 +3379,18 @@ async function executeToolCalls(
|
|
|
3367
3379
|
};
|
|
3368
3380
|
});
|
|
3369
3381
|
|
|
3382
|
+
// Derive batch readiness from existing dispatch records, not extra participants.
|
|
3383
|
+
const refreshPauseState = (): void => {
|
|
3384
|
+
config.onPauseChange?.(
|
|
3385
|
+
agentPauseGate.paused &&
|
|
3386
|
+
!signal?.aborted &&
|
|
3387
|
+
!speculationCoordinator?.isRunning &&
|
|
3388
|
+
records.some(record => !record.started && !record.skipped && !record.toolResultMessage) &&
|
|
3389
|
+
!records.some(record => record.started && !record.toolResultMessage),
|
|
3390
|
+
);
|
|
3391
|
+
};
|
|
3392
|
+
if (speculationCoordinator) speculationCoordinator.onIdle = refreshPauseState;
|
|
3393
|
+
|
|
3370
3394
|
const checkAsideInterrupts = async (): Promise<void> => {
|
|
3371
3395
|
// Asides only fire once: an interrupt already recorded on interruptState
|
|
3372
3396
|
// must not re-abort, and (unlike steering) never re-consumes a queue.
|
|
@@ -3517,7 +3541,11 @@ async function executeToolCalls(
|
|
|
3517
3541
|
// Park before starting this tool while the process-wide pause gate is
|
|
3518
3542
|
// engaged. Tools already executing are unaffected (pausing never aborts);
|
|
3519
3543
|
// a batch interrupted mid-pause unwinds via the signal checks below.
|
|
3520
|
-
|
|
3544
|
+
while (agentPauseGate.paused && !record.signal.aborted) {
|
|
3545
|
+
refreshPauseState();
|
|
3546
|
+
await agentPauseGate.waitUntilResumed(record.signal);
|
|
3547
|
+
}
|
|
3548
|
+
config.onPauseChange?.(false);
|
|
3521
3549
|
|
|
3522
3550
|
const { toolCall, tool } = record;
|
|
3523
3551
|
// Validation (and the beforeToolCall hook) ran in the prepare phase; a
|
|
@@ -3851,7 +3879,13 @@ async function executeToolCalls(
|
|
|
3851
3879
|
concurrency = concurrencyMode ?? "shared";
|
|
3852
3880
|
}
|
|
3853
3881
|
const start = concurrency === "exclusive" ? Promise.all([lastExclusive, ...sharedTasks]) : lastExclusive;
|
|
3854
|
-
const task = start.then(() =>
|
|
3882
|
+
const task = start.then(async () => {
|
|
3883
|
+
try {
|
|
3884
|
+
await runTool(record, index);
|
|
3885
|
+
} finally {
|
|
3886
|
+
refreshPauseState();
|
|
3887
|
+
}
|
|
3888
|
+
});
|
|
3855
3889
|
tasks.push(task);
|
|
3856
3890
|
if (concurrency === "exclusive") {
|
|
3857
3891
|
lastExclusive = task;
|
|
@@ -3863,6 +3897,8 @@ async function executeToolCalls(
|
|
|
3863
3897
|
try {
|
|
3864
3898
|
await Promise.allSettled(tasks);
|
|
3865
3899
|
} finally {
|
|
3900
|
+
if (speculationCoordinator) speculationCoordinator.onIdle = undefined;
|
|
3901
|
+
config.onPauseChange?.(false);
|
|
3866
3902
|
steeringWatchAbortController.abort();
|
|
3867
3903
|
await steeringWatchPromise?.catch(() => undefined);
|
|
3868
3904
|
clearInterval(steeringWatchTimer);
|
package/src/agent.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
unpairedToolCallTail,
|
|
40
40
|
} from "./agent-loop";
|
|
41
41
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
42
|
+
import { agentPauseGate } from "./pause";
|
|
42
43
|
import { isProviderRefusalMessage } from "./replay-policy";
|
|
43
44
|
import { SentToolDefinitions } from "./sent-tool-definitions";
|
|
44
45
|
import { Tokenizer } from "./tokenizer";
|
|
@@ -474,6 +475,12 @@ export class Agent {
|
|
|
474
475
|
#cwdResolver?: () => string | undefined;
|
|
475
476
|
|
|
476
477
|
#runningPrompt?: Promise<void>;
|
|
478
|
+
#isPaused = false;
|
|
479
|
+
|
|
480
|
+
/** True while this unfinished turn is safely parked by the process pause gate. */
|
|
481
|
+
get isPaused(): boolean {
|
|
482
|
+
return this.#isPaused && agentPauseGate.paused && !this.#abortController?.signal.aborted;
|
|
483
|
+
}
|
|
477
484
|
#resolveRunningPrompt?: () => void;
|
|
478
485
|
#kimiApiFormat?: "openai" | "anthropic";
|
|
479
486
|
#preferWebsockets?: boolean;
|
|
@@ -1783,6 +1790,7 @@ export class Agent {
|
|
|
1783
1790
|
this.#state.isStreaming = true;
|
|
1784
1791
|
this.#state.streamMessage = null;
|
|
1785
1792
|
this.#state.error = undefined;
|
|
1793
|
+
this.#isPaused = false;
|
|
1786
1794
|
|
|
1787
1795
|
// Clear Cursor tool result buffer at start of each run
|
|
1788
1796
|
this.#cursorToolResultBuffer = [];
|
|
@@ -1955,6 +1963,9 @@ export class Agent {
|
|
|
1955
1963
|
onAssistantMessageEvent: this.#onAssistantMessageEvent,
|
|
1956
1964
|
onHarmonyLeak: this.#onHarmonyLeak,
|
|
1957
1965
|
onTurnEnd: (messages, signal, context) => this.#onTurnEnd?.(messages, signal, context),
|
|
1966
|
+
onPauseChange: paused => {
|
|
1967
|
+
this.#isPaused = paused;
|
|
1968
|
+
},
|
|
1958
1969
|
getToolChoice,
|
|
1959
1970
|
softToolRequirementState: this.#softToolRequirementState,
|
|
1960
1971
|
onToolChoiceRejected: () => {
|
|
@@ -2205,6 +2216,7 @@ export class Agent {
|
|
|
2205
2216
|
this.#emit({ type: "agent_end", messages: [errorMsg] });
|
|
2206
2217
|
}
|
|
2207
2218
|
} finally {
|
|
2219
|
+
this.#isPaused = false;
|
|
2208
2220
|
this.#restoreUndeliveredQueuedMessages(loopAbortController);
|
|
2209
2221
|
resolveRun?.();
|
|
2210
2222
|
if (this.#abortController === loopAbortController) {
|
|
@@ -239,6 +239,12 @@ export class SpeculativeOperationCoordinator {
|
|
|
239
239
|
#closed = false;
|
|
240
240
|
#admissionsFinalized = false;
|
|
241
241
|
#running = 0;
|
|
242
|
+
/** Observer of existing speculative execution draining; no separate work ledger. */
|
|
243
|
+
onIdle?: () => void;
|
|
244
|
+
|
|
245
|
+
get isRunning(): boolean {
|
|
246
|
+
return this.#running > 0;
|
|
247
|
+
}
|
|
242
248
|
|
|
243
249
|
constructor(
|
|
244
250
|
readonly config: SpeculativeToolExecutionConfig,
|
|
@@ -955,6 +961,7 @@ export class SpeculativeOperationCoordinator {
|
|
|
955
961
|
} finally {
|
|
956
962
|
this.#running--;
|
|
957
963
|
this.#drain();
|
|
964
|
+
if (this.#running === 0) this.onIdle?.();
|
|
958
965
|
}
|
|
959
966
|
})();
|
|
960
967
|
}
|
package/src/types.ts
CHANGED
|
@@ -175,6 +175,9 @@ export interface SteeringQueueState {
|
|
|
175
175
|
export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
176
176
|
model: Model;
|
|
177
177
|
|
|
178
|
+
/** Reports whether this loop has reached a safe process-pause boundary. */
|
|
179
|
+
onPauseChange?: (paused: boolean) => void;
|
|
180
|
+
|
|
178
181
|
/**
|
|
179
182
|
* When to interrupt tool execution for steering messages.
|
|
180
183
|
* - "immediate" = cut interruptible waits short and raise the cooperative
|