@oh-my-pi/pi-agent-core 18.13.0 → 18.13.2
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 +12 -0
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/speculative-execution.d.ts +3 -0
- package/dist/types/types.d.ts +10 -1
- package/package.json +8 -8
- package/src/agent-loop.ts +71 -21
- package/src/agent.ts +12 -0
- package/src/speculative-execution.ts +7 -0
- package/src/types.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.13.2] - 2026-10-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `AgentTool` gained the optional `handlesToolInterrupt` flag: an interruptible tool that sets it (such as `wait`) still runs, without blocking, when a loop-owned interrupt is already queued, so it can drain the queued wake event instead of being skipped; user cancellation still skips it ([#17174](https://github.com/can1357/oh-my-pi/pull/17174) by [@alphastorm](https://github.com/alphastorm))
|
|
10
|
+
|
|
11
|
+
## [18.13.1] - 2026-10-27
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- 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)).
|
|
16
|
+
|
|
5
17
|
## [18.13.0] - 2026-10-26
|
|
6
18
|
|
|
7
19
|
### 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
|
|
@@ -221,7 +223,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
221
223
|
*
|
|
222
224
|
* Polled while a tool batch runs (in "wait" mode, only when the batch holds an
|
|
223
225
|
* interruptible tool) to decide whether to abort in-flight and skip
|
|
224
|
-
* not-yet-started *interruptible* waits;
|
|
226
|
+
* not-yet-started *interruptible* waits, unless they handle queued interrupts;
|
|
225
227
|
* every other already-emitted call still executes and the message injects
|
|
226
228
|
* at the batch boundary. The queue keeps
|
|
227
229
|
* owning its messages until the loop reaches the next injection boundary and
|
|
@@ -1054,6 +1056,13 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
|
|
1054
1056
|
* batch boundary. Honored in both `interruptMode`s.
|
|
1055
1057
|
*/
|
|
1056
1058
|
interruptible?: boolean | ((args: Partial<Static<TParameters>>) => boolean);
|
|
1059
|
+
/**
|
|
1060
|
+
* An interruptible tool can execute with an already-aborted
|
|
1061
|
+
* `TOOL_INTERRUPT_ABORT_REASON` signal to drain caller-owned wake events.
|
|
1062
|
+
* It must not block when that signal is aborted. External/user cancellation
|
|
1063
|
+
* still prevents execution. Omitted tools keep the usual pre-start skip.
|
|
1064
|
+
*/
|
|
1065
|
+
handlesToolInterrupt?: boolean;
|
|
1057
1066
|
/**
|
|
1058
1067
|
* Controls how the INTENT_FIELD (`i`) is handled for this tool.
|
|
1059
1068
|
* - `"require"` (default): `i` is injected and required in the parameter schema.
|
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.2",
|
|
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.2",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "18.13.2",
|
|
43
|
+
"@oh-my-pi/pi-natives": "18.13.2",
|
|
44
|
+
"@oh-my-pi/pi-utils": "18.13.2",
|
|
45
|
+
"@oh-my-pi/pi-wire": "18.13.2",
|
|
46
|
+
"@oh-my-pi/snapcompact": "18.13.2",
|
|
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.2",
|
|
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,
|
|
@@ -3349,6 +3361,7 @@ async function executeToolCalls(
|
|
|
3349
3361
|
tool,
|
|
3350
3362
|
args,
|
|
3351
3363
|
interruptible,
|
|
3364
|
+
handlesToolInterrupt: interruptible && tool?.handlesToolInterrupt === true,
|
|
3352
3365
|
signal: interruptible ? interruptibleSignal : nonInterruptibleSignal,
|
|
3353
3366
|
started: false,
|
|
3354
3367
|
result: undefined as AgentToolResult<any> | undefined,
|
|
@@ -3367,6 +3380,22 @@ async function executeToolCalls(
|
|
|
3367
3380
|
};
|
|
3368
3381
|
});
|
|
3369
3382
|
|
|
3383
|
+
// Derive batch readiness from existing dispatch records, not extra participants.
|
|
3384
|
+
const refreshPauseState = (): void => {
|
|
3385
|
+
config.onPauseChange?.(
|
|
3386
|
+
agentPauseGate.paused &&
|
|
3387
|
+
!signal?.aborted &&
|
|
3388
|
+
!speculationCoordinator?.isRunning &&
|
|
3389
|
+
records.some(record => !record.started && !record.skipped && !record.toolResultMessage) &&
|
|
3390
|
+
!records.some(record => record.started && !record.toolResultMessage),
|
|
3391
|
+
);
|
|
3392
|
+
};
|
|
3393
|
+
if (speculationCoordinator) speculationCoordinator.onIdle = refreshPauseState;
|
|
3394
|
+
|
|
3395
|
+
const isToolAborted = (record: (typeof records)[number]): boolean =>
|
|
3396
|
+
signal?.aborted === true ||
|
|
3397
|
+
(record.signal.aborted && !(record.handlesToolInterrupt && record.signal.reason === TOOL_INTERRUPT_ABORT_REASON));
|
|
3398
|
+
|
|
3370
3399
|
const checkAsideInterrupts = async (): Promise<void> => {
|
|
3371
3400
|
// Asides only fire once: an interrupt already recorded on interruptState
|
|
3372
3401
|
// must not re-abort, and (unlike steering) never re-consumes a queue.
|
|
@@ -3429,9 +3458,9 @@ async function executeToolCalls(
|
|
|
3429
3458
|
// interruptMode is "wait") raises the cooperative soft signal for
|
|
3430
3459
|
// everything else: the boundary dequeue
|
|
3431
3460
|
// below injects the message as soon as running tools finish (or
|
|
3432
|
-
// background themselves)
|
|
3433
|
-
//
|
|
3434
|
-
// a no-op.
|
|
3461
|
+
// background themselves). Not-yet-started interruptible tools are
|
|
3462
|
+
// skipped unless they can drain the queued wake. Idempotent — a
|
|
3463
|
+
// second steer poll after the abort is a no-op.
|
|
3435
3464
|
if (!steeringAbortController.signal.aborted) {
|
|
3436
3465
|
interruptState.triggered = true;
|
|
3437
3466
|
interruptState.source = steeringSource ?? "unknown";
|
|
@@ -3494,8 +3523,8 @@ async function executeToolCalls(
|
|
|
3494
3523
|
};
|
|
3495
3524
|
|
|
3496
3525
|
const runTool = async (record: (typeof records)[number], index: number): Promise<void> => {
|
|
3497
|
-
// A pending interrupt preempts not-yet-started
|
|
3498
|
-
//
|
|
3526
|
+
// A pending interrupt preempts not-yet-started interruptible tools,
|
|
3527
|
+
// except receivers that drain the wake with an already-aborted signal.
|
|
3499
3528
|
// Non-interruptible work is never skipped, whatever the source: the
|
|
3500
3529
|
// expensive part — generating the call — is already paid, the tool
|
|
3501
3530
|
// itself is cheap, and a skip only makes the model re-emit the same
|
|
@@ -3505,7 +3534,7 @@ async function executeToolCalls(
|
|
|
3505
3534
|
// the parent steers mid-stream (#10645). The steer still injects at the
|
|
3506
3535
|
// batch boundary; the cooperative soft signal lets long-running tools
|
|
3507
3536
|
// step aside on their own.
|
|
3508
|
-
if (interruptState.triggered && record.interruptible) {
|
|
3537
|
+
if (interruptState.triggered && record.interruptible && !record.handlesToolInterrupt) {
|
|
3509
3538
|
// Skip both span emission and the collector orphan record here. The
|
|
3510
3539
|
// tail sweep below (after `Promise.allSettled`) is the single path
|
|
3511
3540
|
// that handles "no result message was produced" — it calls
|
|
@@ -3517,7 +3546,11 @@ async function executeToolCalls(
|
|
|
3517
3546
|
// Park before starting this tool while the process-wide pause gate is
|
|
3518
3547
|
// engaged. Tools already executing are unaffected (pausing never aborts);
|
|
3519
3548
|
// a batch interrupted mid-pause unwinds via the signal checks below.
|
|
3520
|
-
|
|
3549
|
+
while (agentPauseGate.paused && !record.signal.aborted) {
|
|
3550
|
+
refreshPauseState();
|
|
3551
|
+
await agentPauseGate.waitUntilResumed(record.signal);
|
|
3552
|
+
}
|
|
3553
|
+
config.onPauseChange?.(false);
|
|
3521
3554
|
|
|
3522
3555
|
const { toolCall, tool } = record;
|
|
3523
3556
|
// Validation (and the beforeToolCall hook) ran in the prepare phase; a
|
|
@@ -3535,14 +3568,14 @@ async function executeToolCalls(
|
|
|
3535
3568
|
return;
|
|
3536
3569
|
}
|
|
3537
3570
|
const effectiveArgs = record.args;
|
|
3538
|
-
if (record
|
|
3571
|
+
if (isToolAborted(record)) {
|
|
3539
3572
|
record.skipped = true;
|
|
3540
3573
|
recordSkippedTool(telemetry, {
|
|
3541
3574
|
toolCallId: toolCall.id,
|
|
3542
3575
|
toolName: toolCall.name,
|
|
3543
3576
|
status: "aborted",
|
|
3544
3577
|
});
|
|
3545
|
-
emitToolResult(record, createToolSignalAbortedResult(record.signal), true);
|
|
3578
|
+
emitToolResult(record, createToolSignalAbortedResult(signal?.aborted ? signal : record.signal), true);
|
|
3546
3579
|
return;
|
|
3547
3580
|
}
|
|
3548
3581
|
record.started = true;
|
|
@@ -3570,17 +3603,19 @@ async function executeToolCalls(
|
|
|
3570
3603
|
let caughtError: unknown;
|
|
3571
3604
|
let completedToolExecution = false;
|
|
3572
3605
|
let executionStarted = false;
|
|
3606
|
+
const abortBeforeToolExecution = (): boolean => {
|
|
3607
|
+
if (!isToolAborted(record)) return false;
|
|
3608
|
+
result = createToolSignalAbortedResult(signal?.aborted ? signal : record.signal);
|
|
3609
|
+
isError = true;
|
|
3610
|
+
return true;
|
|
3611
|
+
};
|
|
3573
3612
|
|
|
3574
3613
|
await runInActiveSpan(toolSpan, async () => {
|
|
3575
3614
|
try {
|
|
3576
3615
|
if (!tool) {
|
|
3577
3616
|
throw new Error(formatToolNotFoundMessage(toolCall.name, tools, suggestFallbackToolNames?.()));
|
|
3578
3617
|
}
|
|
3579
|
-
if (
|
|
3580
|
-
result = createToolSignalAbortedResult(record.signal);
|
|
3581
|
-
isError = true;
|
|
3582
|
-
return;
|
|
3583
|
-
}
|
|
3618
|
+
if (abortBeforeToolExecution()) return;
|
|
3584
3619
|
|
|
3585
3620
|
if (record.prepareError !== undefined) throw record.prepareError;
|
|
3586
3621
|
if (record.blocked) {
|
|
@@ -3591,6 +3626,9 @@ async function executeToolCalls(
|
|
|
3591
3626
|
record.executionArgs ??
|
|
3592
3627
|
(transformToolCallArguments ? transformToolCallArguments(effectiveArgs, toolCall.name) : effectiveArgs);
|
|
3593
3628
|
record.args = executionArgs;
|
|
3629
|
+
// A transform or awaited claim can cancel the external run after
|
|
3630
|
+
// a queued wake already fixed the combined signal's reason.
|
|
3631
|
+
if (abortBeforeToolExecution()) return;
|
|
3594
3632
|
|
|
3595
3633
|
const speculativeOutcome = speculationCoordinator
|
|
3596
3634
|
? await speculationCoordinator.claim(tool, toolCall, executionArgs)
|
|
@@ -3607,6 +3645,7 @@ async function executeToolCalls(
|
|
|
3607
3645
|
}
|
|
3608
3646
|
|
|
3609
3647
|
if (!completedToolExecution) {
|
|
3648
|
+
if (abortBeforeToolExecution()) return;
|
|
3610
3649
|
// The cooperative steering signal and the passive-context sink
|
|
3611
3650
|
// ride the loop-owned ToolCallContext (surfacing as
|
|
3612
3651
|
// `ctx.toolCall.*`); the host surfaces the sink on the context it
|
|
@@ -3631,9 +3670,12 @@ async function executeToolCalls(
|
|
|
3631
3670
|
} else if (streamSession && !streamSession.contextIndependent) {
|
|
3632
3671
|
await streamSession.discard("outer tool context cannot carry stream speculation");
|
|
3633
3672
|
}
|
|
3634
|
-
executionStarted = true;
|
|
3635
3673
|
let rawResult: unknown;
|
|
3636
3674
|
try {
|
|
3675
|
+
// Context construction and stream cleanup can also cancel;
|
|
3676
|
+
// nothing may await or call host code after this final check.
|
|
3677
|
+
if (abortBeforeToolExecution()) return;
|
|
3678
|
+
executionStarted = true;
|
|
3637
3679
|
rawResult = await tool.execute(
|
|
3638
3680
|
toolCall.id,
|
|
3639
3681
|
executionArgs,
|
|
@@ -3713,10 +3755,10 @@ async function executeToolCalls(
|
|
|
3713
3755
|
const interrupted = interruptState.triggered;
|
|
3714
3756
|
const perToolAborted = record.signal.aborted;
|
|
3715
3757
|
const abortedDuringExecution = perToolAborted && isError && !completedToolExecution;
|
|
3716
|
-
if (record.interruptible && interrupted && abortedDuringExecution) {
|
|
3758
|
+
if (record.interruptible && interrupted && abortedDuringExecution && !signal?.aborted) {
|
|
3717
3759
|
// Queued steering/asides abort only interruptible tools. An external
|
|
3718
|
-
// user abort
|
|
3719
|
-
//
|
|
3760
|
+
// user abort takes priority even if a queued interrupt fixed the
|
|
3761
|
+
// combined signal's reason first; keep the actual cancellation error.
|
|
3720
3762
|
record.skipped = true;
|
|
3721
3763
|
emitToolResult(record, createSkippedToolResult(interruptState.source, executionStarted), true);
|
|
3722
3764
|
} else {
|
|
@@ -3773,7 +3815,7 @@ async function executeToolCalls(
|
|
|
3773
3815
|
(softInterrupts || records.some(record => record.interruptible)) &&
|
|
3774
3816
|
(hasSteeringMessages !== undefined || hasAsidePeek);
|
|
3775
3817
|
// Live-taken steering is already pending: interrupt before any call starts
|
|
3776
|
-
// so
|
|
3818
|
+
// so interruptible tools skip or drain the queued wake without blocking.
|
|
3777
3819
|
if (liveSteering.length > 0) await checkSteering();
|
|
3778
3820
|
const eventDrivenSteeringWatch =
|
|
3779
3821
|
watchSteeringWhileRunning && config.waitForSteeringMessages !== undefined && hasSteeringMessages !== undefined;
|
|
@@ -3851,7 +3893,13 @@ async function executeToolCalls(
|
|
|
3851
3893
|
concurrency = concurrencyMode ?? "shared";
|
|
3852
3894
|
}
|
|
3853
3895
|
const start = concurrency === "exclusive" ? Promise.all([lastExclusive, ...sharedTasks]) : lastExclusive;
|
|
3854
|
-
const task = start.then(() =>
|
|
3896
|
+
const task = start.then(async () => {
|
|
3897
|
+
try {
|
|
3898
|
+
await runTool(record, index);
|
|
3899
|
+
} finally {
|
|
3900
|
+
refreshPauseState();
|
|
3901
|
+
}
|
|
3902
|
+
});
|
|
3855
3903
|
tasks.push(task);
|
|
3856
3904
|
if (concurrency === "exclusive") {
|
|
3857
3905
|
lastExclusive = task;
|
|
@@ -3863,6 +3911,8 @@ async function executeToolCalls(
|
|
|
3863
3911
|
try {
|
|
3864
3912
|
await Promise.allSettled(tasks);
|
|
3865
3913
|
} finally {
|
|
3914
|
+
if (speculationCoordinator) speculationCoordinator.onIdle = undefined;
|
|
3915
|
+
config.onPauseChange?.(false);
|
|
3866
3916
|
steeringWatchAbortController.abort();
|
|
3867
3917
|
await steeringWatchPromise?.catch(() => undefined);
|
|
3868
3918
|
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
|
|
@@ -280,7 +283,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
280
283
|
*
|
|
281
284
|
* Polled while a tool batch runs (in "wait" mode, only when the batch holds an
|
|
282
285
|
* interruptible tool) to decide whether to abort in-flight and skip
|
|
283
|
-
* not-yet-started *interruptible* waits;
|
|
286
|
+
* not-yet-started *interruptible* waits, unless they handle queued interrupts;
|
|
284
287
|
* every other already-emitted call still executes and the message injects
|
|
285
288
|
* at the batch boundary. The queue keeps
|
|
286
289
|
* owning its messages until the loop reaches the next injection boundary and
|
|
@@ -1191,6 +1194,13 @@ export interface AgentTool<
|
|
|
1191
1194
|
* batch boundary. Honored in both `interruptMode`s.
|
|
1192
1195
|
*/
|
|
1193
1196
|
interruptible?: boolean | ((args: Partial<Static<TParameters>>) => boolean);
|
|
1197
|
+
/**
|
|
1198
|
+
* An interruptible tool can execute with an already-aborted
|
|
1199
|
+
* `TOOL_INTERRUPT_ABORT_REASON` signal to drain caller-owned wake events.
|
|
1200
|
+
* It must not block when that signal is aborted. External/user cancellation
|
|
1201
|
+
* still prevents execution. Omitted tools keep the usual pre-start skip.
|
|
1202
|
+
*/
|
|
1203
|
+
handlesToolInterrupt?: boolean;
|
|
1194
1204
|
/**
|
|
1195
1205
|
* Controls how the INTENT_FIELD (`i`) is handled for this tool.
|
|
1196
1206
|
* - `"require"` (default): `i` is injected and required in the parameter schema.
|