@oh-my-pi/pi-agent-core 18.13.1 → 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 +6 -0
- package/dist/types/types.d.ts +8 -1
- package/package.json +8 -8
- package/src/agent-loop.ts +32 -18
- package/src/types.ts +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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
|
+
|
|
5
11
|
## [18.13.1] - 2026-10-27
|
|
6
12
|
|
|
7
13
|
### Added
|
package/dist/types/types.d.ts
CHANGED
|
@@ -223,7 +223,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
223
223
|
*
|
|
224
224
|
* Polled while a tool batch runs (in "wait" mode, only when the batch holds an
|
|
225
225
|
* interruptible tool) to decide whether to abort in-flight and skip
|
|
226
|
-
* not-yet-started *interruptible* waits;
|
|
226
|
+
* not-yet-started *interruptible* waits, unless they handle queued interrupts;
|
|
227
227
|
* every other already-emitted call still executes and the message injects
|
|
228
228
|
* at the batch boundary. The queue keeps
|
|
229
229
|
* owning its messages until the loop reaches the next injection boundary and
|
|
@@ -1056,6 +1056,13 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
|
|
1056
1056
|
* batch boundary. Honored in both `interruptMode`s.
|
|
1057
1057
|
*/
|
|
1058
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;
|
|
1059
1066
|
/**
|
|
1060
1067
|
* Controls how the INTENT_FIELD (`i`) is handled for this tool.
|
|
1061
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
|
@@ -3361,6 +3361,7 @@ async function executeToolCalls(
|
|
|
3361
3361
|
tool,
|
|
3362
3362
|
args,
|
|
3363
3363
|
interruptible,
|
|
3364
|
+
handlesToolInterrupt: interruptible && tool?.handlesToolInterrupt === true,
|
|
3364
3365
|
signal: interruptible ? interruptibleSignal : nonInterruptibleSignal,
|
|
3365
3366
|
started: false,
|
|
3366
3367
|
result: undefined as AgentToolResult<any> | undefined,
|
|
@@ -3391,6 +3392,10 @@ async function executeToolCalls(
|
|
|
3391
3392
|
};
|
|
3392
3393
|
if (speculationCoordinator) speculationCoordinator.onIdle = refreshPauseState;
|
|
3393
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
|
+
|
|
3394
3399
|
const checkAsideInterrupts = async (): Promise<void> => {
|
|
3395
3400
|
// Asides only fire once: an interrupt already recorded on interruptState
|
|
3396
3401
|
// must not re-abort, and (unlike steering) never re-consumes a queue.
|
|
@@ -3453,9 +3458,9 @@ async function executeToolCalls(
|
|
|
3453
3458
|
// interruptMode is "wait") raises the cooperative soft signal for
|
|
3454
3459
|
// everything else: the boundary dequeue
|
|
3455
3460
|
// below injects the message as soon as running tools finish (or
|
|
3456
|
-
// background themselves)
|
|
3457
|
-
//
|
|
3458
|
-
// 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.
|
|
3459
3464
|
if (!steeringAbortController.signal.aborted) {
|
|
3460
3465
|
interruptState.triggered = true;
|
|
3461
3466
|
interruptState.source = steeringSource ?? "unknown";
|
|
@@ -3518,8 +3523,8 @@ async function executeToolCalls(
|
|
|
3518
3523
|
};
|
|
3519
3524
|
|
|
3520
3525
|
const runTool = async (record: (typeof records)[number], index: number): Promise<void> => {
|
|
3521
|
-
// A pending interrupt preempts not-yet-started
|
|
3522
|
-
//
|
|
3526
|
+
// A pending interrupt preempts not-yet-started interruptible tools,
|
|
3527
|
+
// except receivers that drain the wake with an already-aborted signal.
|
|
3523
3528
|
// Non-interruptible work is never skipped, whatever the source: the
|
|
3524
3529
|
// expensive part — generating the call — is already paid, the tool
|
|
3525
3530
|
// itself is cheap, and a skip only makes the model re-emit the same
|
|
@@ -3529,7 +3534,7 @@ async function executeToolCalls(
|
|
|
3529
3534
|
// the parent steers mid-stream (#10645). The steer still injects at the
|
|
3530
3535
|
// batch boundary; the cooperative soft signal lets long-running tools
|
|
3531
3536
|
// step aside on their own.
|
|
3532
|
-
if (interruptState.triggered && record.interruptible) {
|
|
3537
|
+
if (interruptState.triggered && record.interruptible && !record.handlesToolInterrupt) {
|
|
3533
3538
|
// Skip both span emission and the collector orphan record here. The
|
|
3534
3539
|
// tail sweep below (after `Promise.allSettled`) is the single path
|
|
3535
3540
|
// that handles "no result message was produced" — it calls
|
|
@@ -3563,14 +3568,14 @@ async function executeToolCalls(
|
|
|
3563
3568
|
return;
|
|
3564
3569
|
}
|
|
3565
3570
|
const effectiveArgs = record.args;
|
|
3566
|
-
if (record
|
|
3571
|
+
if (isToolAborted(record)) {
|
|
3567
3572
|
record.skipped = true;
|
|
3568
3573
|
recordSkippedTool(telemetry, {
|
|
3569
3574
|
toolCallId: toolCall.id,
|
|
3570
3575
|
toolName: toolCall.name,
|
|
3571
3576
|
status: "aborted",
|
|
3572
3577
|
});
|
|
3573
|
-
emitToolResult(record, createToolSignalAbortedResult(record.signal), true);
|
|
3578
|
+
emitToolResult(record, createToolSignalAbortedResult(signal?.aborted ? signal : record.signal), true);
|
|
3574
3579
|
return;
|
|
3575
3580
|
}
|
|
3576
3581
|
record.started = true;
|
|
@@ -3598,17 +3603,19 @@ async function executeToolCalls(
|
|
|
3598
3603
|
let caughtError: unknown;
|
|
3599
3604
|
let completedToolExecution = false;
|
|
3600
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
|
+
};
|
|
3601
3612
|
|
|
3602
3613
|
await runInActiveSpan(toolSpan, async () => {
|
|
3603
3614
|
try {
|
|
3604
3615
|
if (!tool) {
|
|
3605
3616
|
throw new Error(formatToolNotFoundMessage(toolCall.name, tools, suggestFallbackToolNames?.()));
|
|
3606
3617
|
}
|
|
3607
|
-
if (
|
|
3608
|
-
result = createToolSignalAbortedResult(record.signal);
|
|
3609
|
-
isError = true;
|
|
3610
|
-
return;
|
|
3611
|
-
}
|
|
3618
|
+
if (abortBeforeToolExecution()) return;
|
|
3612
3619
|
|
|
3613
3620
|
if (record.prepareError !== undefined) throw record.prepareError;
|
|
3614
3621
|
if (record.blocked) {
|
|
@@ -3619,6 +3626,9 @@ async function executeToolCalls(
|
|
|
3619
3626
|
record.executionArgs ??
|
|
3620
3627
|
(transformToolCallArguments ? transformToolCallArguments(effectiveArgs, toolCall.name) : effectiveArgs);
|
|
3621
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;
|
|
3622
3632
|
|
|
3623
3633
|
const speculativeOutcome = speculationCoordinator
|
|
3624
3634
|
? await speculationCoordinator.claim(tool, toolCall, executionArgs)
|
|
@@ -3635,6 +3645,7 @@ async function executeToolCalls(
|
|
|
3635
3645
|
}
|
|
3636
3646
|
|
|
3637
3647
|
if (!completedToolExecution) {
|
|
3648
|
+
if (abortBeforeToolExecution()) return;
|
|
3638
3649
|
// The cooperative steering signal and the passive-context sink
|
|
3639
3650
|
// ride the loop-owned ToolCallContext (surfacing as
|
|
3640
3651
|
// `ctx.toolCall.*`); the host surfaces the sink on the context it
|
|
@@ -3659,9 +3670,12 @@ async function executeToolCalls(
|
|
|
3659
3670
|
} else if (streamSession && !streamSession.contextIndependent) {
|
|
3660
3671
|
await streamSession.discard("outer tool context cannot carry stream speculation");
|
|
3661
3672
|
}
|
|
3662
|
-
executionStarted = true;
|
|
3663
3673
|
let rawResult: unknown;
|
|
3664
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;
|
|
3665
3679
|
rawResult = await tool.execute(
|
|
3666
3680
|
toolCall.id,
|
|
3667
3681
|
executionArgs,
|
|
@@ -3741,10 +3755,10 @@ async function executeToolCalls(
|
|
|
3741
3755
|
const interrupted = interruptState.triggered;
|
|
3742
3756
|
const perToolAborted = record.signal.aborted;
|
|
3743
3757
|
const abortedDuringExecution = perToolAborted && isError && !completedToolExecution;
|
|
3744
|
-
if (record.interruptible && interrupted && abortedDuringExecution) {
|
|
3758
|
+
if (record.interruptible && interrupted && abortedDuringExecution && !signal?.aborted) {
|
|
3745
3759
|
// Queued steering/asides abort only interruptible tools. An external
|
|
3746
|
-
// user abort
|
|
3747
|
-
//
|
|
3760
|
+
// user abort takes priority even if a queued interrupt fixed the
|
|
3761
|
+
// combined signal's reason first; keep the actual cancellation error.
|
|
3748
3762
|
record.skipped = true;
|
|
3749
3763
|
emitToolResult(record, createSkippedToolResult(interruptState.source, executionStarted), true);
|
|
3750
3764
|
} else {
|
|
@@ -3801,7 +3815,7 @@ async function executeToolCalls(
|
|
|
3801
3815
|
(softInterrupts || records.some(record => record.interruptible)) &&
|
|
3802
3816
|
(hasSteeringMessages !== undefined || hasAsidePeek);
|
|
3803
3817
|
// Live-taken steering is already pending: interrupt before any call starts
|
|
3804
|
-
// so
|
|
3818
|
+
// so interruptible tools skip or drain the queued wake without blocking.
|
|
3805
3819
|
if (liveSteering.length > 0) await checkSteering();
|
|
3806
3820
|
const eventDrivenSteeringWatch =
|
|
3807
3821
|
watchSteeringWhileRunning && config.waitForSteeringMessages !== undefined && hasSteeringMessages !== undefined;
|
package/src/types.ts
CHANGED
|
@@ -283,7 +283,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
283
283
|
*
|
|
284
284
|
* Polled while a tool batch runs (in "wait" mode, only when the batch holds an
|
|
285
285
|
* interruptible tool) to decide whether to abort in-flight and skip
|
|
286
|
-
* not-yet-started *interruptible* waits;
|
|
286
|
+
* not-yet-started *interruptible* waits, unless they handle queued interrupts;
|
|
287
287
|
* every other already-emitted call still executes and the message injects
|
|
288
288
|
* at the batch boundary. The queue keeps
|
|
289
289
|
* owning its messages until the loop reaches the next injection boundary and
|
|
@@ -1194,6 +1194,13 @@ export interface AgentTool<
|
|
|
1194
1194
|
* batch boundary. Honored in both `interruptMode`s.
|
|
1195
1195
|
*/
|
|
1196
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;
|
|
1197
1204
|
/**
|
|
1198
1205
|
* Controls how the INTENT_FIELD (`i`) is handled for this tool.
|
|
1199
1206
|
* - `"require"` (default): `i` is injected and required in the parameter schema.
|