@oh-my-pi/pi-agent-core 17.2.5 → 17.2.7
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 +7 -0
- package/dist/types/types.d.ts +14 -3
- package/package.json +8 -7
- package/src/agent-loop.ts +35 -6
- package/src/agent.ts +11 -3
- package/src/types.ts +16 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.2.6] - 2026-08-03
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed an issue where peer-IRC interrupts (such as subagent messages) incorrectly skipped non-interruptible tool calls queued in the same batch.
|
|
10
|
+
- Improved interruption messaging to clearly distinguish between parent-agent steering and system-advisory interruptions.
|
|
11
|
+
|
|
5
12
|
## [17.2.5] - 2026-08-03
|
|
6
13
|
|
|
7
14
|
### Breaking Changes
|
package/dist/types/types.d.ts
CHANGED
|
@@ -6,13 +6,21 @@ import type { AgentRunCoverage, AgentRunSummary } from "./run-collector.js";
|
|
|
6
6
|
import type { AgentTelemetryConfig } from "./telemetry.js";
|
|
7
7
|
/** Stream function - can return sync or Promise for async config lookup */
|
|
8
8
|
export type StreamFn = (...args: Parameters<typeof streamSimple>) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
|
9
|
+
/** Called once an aside has been inserted into the agent's live context. */
|
|
10
|
+
export declare const ASIDE_MESSAGE_COMMIT: unique symbol;
|
|
11
|
+
/** Called when an aside was drained but the agent loop ended before inserting it. */
|
|
12
|
+
export declare const ASIDE_MESSAGE_DISCARD: unique symbol;
|
|
13
|
+
export type CommittableAsideMessage = AgentMessage & {
|
|
14
|
+
[ASIDE_MESSAGE_COMMIT]?: () => void;
|
|
15
|
+
[ASIDE_MESSAGE_DISCARD]?: (error: Error) => void;
|
|
16
|
+
};
|
|
9
17
|
/**
|
|
10
18
|
* An aside entry: a ready {@link AgentMessage}, or a sync thunk evaluated at
|
|
11
19
|
* injection time that returns the message to inject or `null` to skip it. Thunks
|
|
12
20
|
* let the producer make the final inject-or-drop decision against current state
|
|
13
21
|
* (e.g. dropping late diagnostics a newer edit superseded).
|
|
14
22
|
*/
|
|
15
|
-
export type AsideMessage =
|
|
23
|
+
export type AsideMessage = CommittableAsideMessage | (() => CommittableAsideMessage | null);
|
|
16
24
|
export interface AgentTurnEndContext {
|
|
17
25
|
/** Assistant/user message that just completed this turn boundary. */
|
|
18
26
|
message: AgentMessage;
|
|
@@ -81,8 +89,11 @@ export interface SoftToolRequirementState {
|
|
|
81
89
|
}
|
|
82
90
|
/** True when a {@link ToolChoiceDirective} is a soft requirement, not a hard choice. */
|
|
83
91
|
export declare function isSoftToolRequirement(directive: ToolChoiceDirective | undefined): directive is SoftToolRequirement;
|
|
84
|
-
/**
|
|
85
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Source category for a queued steering interrupt observed without consuming the queue.
|
|
94
|
+
* Distinguishes real-user, agent-authored, system/advisor, and unknown steering.
|
|
95
|
+
*/
|
|
96
|
+
export type SteeringInterruptSource = "user" | "agent" | "system" | "unknown";
|
|
86
97
|
/** Non-consuming summary of whether queued steering should interrupt a tool batch. */
|
|
87
98
|
export interface SteeringQueueState {
|
|
88
99
|
/** True when at least one steering message is queued. */
|
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": "17.2.
|
|
4
|
+
"version": "17.2.7",
|
|
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,15 +35,16 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "17.2.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "17.2.
|
|
40
|
-
"@oh-my-pi/pi-natives": "17.2.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
42
|
-
"@oh-my-pi/pi-wire": "17.2.
|
|
43
|
-
"@oh-my-pi/snapcompact": "17.2.
|
|
38
|
+
"@oh-my-pi/pi-ai": "17.2.7",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "17.2.7",
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.2.7",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.2.7",
|
|
42
|
+
"@oh-my-pi/pi-wire": "17.2.7",
|
|
43
|
+
"@oh-my-pi/snapcompact": "17.2.7",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
+
"@oh-my-pi/omptype": "17.2.7",
|
|
47
48
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
48
49
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
49
50
|
"@types/bun": "^1.3.14"
|
package/src/agent-loop.ts
CHANGED
|
@@ -74,12 +74,13 @@ import type {
|
|
|
74
74
|
AgentTurnEndContext,
|
|
75
75
|
AsideMessage,
|
|
76
76
|
BeforeToolCallResult,
|
|
77
|
+
CommittableAsideMessage,
|
|
77
78
|
SoftToolRequirement,
|
|
78
79
|
SteeringInterruptSource,
|
|
79
80
|
SteeringQueueState,
|
|
80
81
|
StreamFn,
|
|
81
82
|
} from "./types";
|
|
82
|
-
import { isSoftToolRequirement } from "./types";
|
|
83
|
+
import { ASIDE_MESSAGE_COMMIT, ASIDE_MESSAGE_DISCARD, isSoftToolRequirement } from "./types";
|
|
83
84
|
import { yieldIfDue } from "./utils/yield";
|
|
84
85
|
|
|
85
86
|
/** Stop-details marker for a provider error after assistant content/tool args already streamed. */
|
|
@@ -527,6 +528,9 @@ export function agentLoop(
|
|
|
527
528
|
...context,
|
|
528
529
|
messages: [...context.messages, ...prompts],
|
|
529
530
|
};
|
|
531
|
+
for (const prompt of prompts) {
|
|
532
|
+
(prompt as CommittableAsideMessage)[ASIDE_MESSAGE_COMMIT]?.();
|
|
533
|
+
}
|
|
530
534
|
|
|
531
535
|
stream.push({ type: "agent_start" });
|
|
532
536
|
|
|
@@ -952,13 +956,24 @@ function emitInputMessages(stream: EventStream<AgentEvent, AgentMessage[]>, mess
|
|
|
952
956
|
function resolveAsides(entries: AsideMessage[] | undefined): AgentMessage[] {
|
|
953
957
|
if (!entries || entries.length === 0) return [];
|
|
954
958
|
const out: AgentMessage[] = [];
|
|
955
|
-
|
|
956
|
-
const
|
|
957
|
-
|
|
959
|
+
try {
|
|
960
|
+
for (const entry of entries) {
|
|
961
|
+
const message = typeof entry === "function" ? entry() : entry;
|
|
962
|
+
if (message) out.push(message);
|
|
963
|
+
}
|
|
964
|
+
} catch (error) {
|
|
965
|
+
discardAsides(out, error instanceof Error ? error : new Error(String(error)));
|
|
966
|
+
throw error;
|
|
958
967
|
}
|
|
959
968
|
return out;
|
|
960
969
|
}
|
|
961
970
|
|
|
971
|
+
function discardAsides(messages: readonly AgentMessage[], error: Error): void {
|
|
972
|
+
for (const message of messages) {
|
|
973
|
+
(message as CommittableAsideMessage)[ASIDE_MESSAGE_DISCARD]?.(error);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
962
977
|
async function runLoopBody(
|
|
963
978
|
currentContext: AgentContext,
|
|
964
979
|
newMessages: AgentMessage[],
|
|
@@ -989,6 +1004,7 @@ async function runLoopBody(
|
|
|
989
1004
|
const softRequirementState = config.softToolRequirementState ?? { escalations: 0 };
|
|
990
1005
|
let preserveSoftRequirementState = false;
|
|
991
1006
|
|
|
1007
|
+
let pendingMessages: AgentMessage[] = [];
|
|
992
1008
|
try {
|
|
993
1009
|
let messagesToEmit = [...initialMessages];
|
|
994
1010
|
if (isDeadlineExceeded(config.deadline)) {
|
|
@@ -999,7 +1015,6 @@ async function runLoopBody(
|
|
|
999
1015
|
// Check for steering messages at start (user may have typed while waiting).
|
|
1000
1016
|
// Skip when the run is already externally aborted — dequeuing would strand
|
|
1001
1017
|
// the messages in a run that is about to die.
|
|
1002
|
-
let pendingMessages: AgentMessage[];
|
|
1003
1018
|
try {
|
|
1004
1019
|
pendingMessages = signal?.aborted ? [] : (await config.getSteeringMessages?.()) || [];
|
|
1005
1020
|
} catch (error) {
|
|
@@ -1051,6 +1066,7 @@ async function runLoopBody(
|
|
|
1051
1066
|
currentContext.messages.push(message);
|
|
1052
1067
|
newMessages.push(message);
|
|
1053
1068
|
turnMessages.push(message);
|
|
1069
|
+
(message as CommittableAsideMessage)[ASIDE_MESSAGE_COMMIT]?.();
|
|
1054
1070
|
}
|
|
1055
1071
|
pendingMessages = [];
|
|
1056
1072
|
}
|
|
@@ -1449,6 +1465,7 @@ async function runLoopBody(
|
|
|
1449
1465
|
|
|
1450
1466
|
endAgentStream(stream, newMessages, telemetry, stepCounter.count);
|
|
1451
1467
|
} finally {
|
|
1468
|
+
discardAsides(pendingMessages, new Error("Aside message was not committed before the agent loop ended"));
|
|
1452
1469
|
if (!preserveSoftRequirementState) {
|
|
1453
1470
|
softRequirementState.id = undefined;
|
|
1454
1471
|
softRequirementState.forcedToolChoice = undefined;
|
|
@@ -2393,7 +2410,16 @@ async function executeToolCalls(
|
|
|
2393
2410
|
};
|
|
2394
2411
|
|
|
2395
2412
|
const runTool = async (record: (typeof records)[number], index: number): Promise<void> => {
|
|
2396
|
-
|
|
2413
|
+
// A pending interrupt preempts not-yet-started tools so the message
|
|
2414
|
+
// injects promptly. A peer-IRC interrupt is the exception: it aborts
|
|
2415
|
+
// interruptible waits only and leaves non-interruptible foreground work
|
|
2416
|
+
// untouched (see the emit branch below and the `does not abort a
|
|
2417
|
+
// non-interruptible foreground tool` case). That guarantee must hold for
|
|
2418
|
+
// work still queued behind the aborted wait too — otherwise a batched
|
|
2419
|
+
// `todo`/`write` gets dropped as "Skipped due to pending peer interrupt"
|
|
2420
|
+
// purely for being ordered after the wait (#7493). User/system steering
|
|
2421
|
+
// still preempts everything queued.
|
|
2422
|
+
if (interruptState.triggered && (record.interruptible || interruptState.source !== "irc")) {
|
|
2397
2423
|
// Skip both span emission and the collector orphan record here. The
|
|
2398
2424
|
// tail sweep below (after `Promise.allSettled`) is the single path
|
|
2399
2425
|
// that handles "no result message was produced" — it calls
|
|
@@ -2875,6 +2901,9 @@ function createSkippedToolResult(
|
|
|
2875
2901
|
if (source === "user") {
|
|
2876
2902
|
reason = "queued user message";
|
|
2877
2903
|
blocker = "queued message";
|
|
2904
|
+
} else if (source === "agent") {
|
|
2905
|
+
reason = "pending parent steering message";
|
|
2906
|
+
blocker = "steering message";
|
|
2878
2907
|
} else if (source === "system") {
|
|
2879
2908
|
reason = "pending system advisory";
|
|
2880
2909
|
blocker = "advisory";
|
package/src/agent.ts
CHANGED
|
@@ -1373,14 +1373,22 @@ export class Agent {
|
|
|
1373
1373
|
if (this.#steeringQueue.length === 0) {
|
|
1374
1374
|
return { queued: false };
|
|
1375
1375
|
}
|
|
1376
|
-
|
|
1376
|
+
const messageCount = this.#steeringMode === "one-at-a-time" ? 1 : this.#steeringQueue.length;
|
|
1377
|
+
let hasAgentSteering = false;
|
|
1378
|
+
for (let i = 0; i < messageCount; i++) {
|
|
1379
|
+
const message = this.#steeringQueue[i];
|
|
1377
1380
|
const role = "role" in message ? message.role : undefined;
|
|
1378
1381
|
const attribution = "attribution" in message ? message.attribution : undefined;
|
|
1379
|
-
if (
|
|
1382
|
+
if (attribution === "user") {
|
|
1380
1383
|
return { queued: true, source: "user" };
|
|
1381
1384
|
}
|
|
1385
|
+
if (role !== "user") continue;
|
|
1386
|
+
if (attribution !== "agent") {
|
|
1387
|
+
return { queued: true, source: "user" };
|
|
1388
|
+
}
|
|
1389
|
+
hasAgentSteering = true;
|
|
1382
1390
|
}
|
|
1383
|
-
return { queued: true, source: "system" };
|
|
1391
|
+
return { queued: true, source: hasAgentSteering ? "agent" : "system" };
|
|
1384
1392
|
},
|
|
1385
1393
|
waitForSteeringMessages: signal => this.#waitForSteeringMessages(signal),
|
|
1386
1394
|
hasIrcInterrupts: this.hasIrcInterrupts,
|
package/src/types.ts
CHANGED
|
@@ -31,13 +31,23 @@ export type StreamFn = (
|
|
|
31
31
|
...args: Parameters<typeof streamSimple>
|
|
32
32
|
) => AssistantMessageEventStream | Promise<AssistantMessageEventStream>;
|
|
33
33
|
|
|
34
|
+
/** Called once an aside has been inserted into the agent's live context. */
|
|
35
|
+
export const ASIDE_MESSAGE_COMMIT = Symbol("aside-message-commit");
|
|
36
|
+
/** Called when an aside was drained but the agent loop ended before inserting it. */
|
|
37
|
+
export const ASIDE_MESSAGE_DISCARD = Symbol("aside-message-discard");
|
|
38
|
+
|
|
39
|
+
export type CommittableAsideMessage = AgentMessage & {
|
|
40
|
+
[ASIDE_MESSAGE_COMMIT]?: () => void;
|
|
41
|
+
[ASIDE_MESSAGE_DISCARD]?: (error: Error) => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* An aside entry: a ready {@link AgentMessage}, or a sync thunk evaluated at
|
|
36
46
|
* injection time that returns the message to inject or `null` to skip it. Thunks
|
|
37
47
|
* let the producer make the final inject-or-drop decision against current state
|
|
38
48
|
* (e.g. dropping late diagnostics a newer edit superseded).
|
|
39
49
|
*/
|
|
40
|
-
export type AsideMessage =
|
|
50
|
+
export type AsideMessage = CommittableAsideMessage | (() => CommittableAsideMessage | null);
|
|
41
51
|
|
|
42
52
|
export interface AgentTurnEndContext {
|
|
43
53
|
/** Assistant/user message that just completed this turn boundary. */
|
|
@@ -117,8 +127,11 @@ export function isSoftToolRequirement(directive: ToolChoiceDirective | undefined
|
|
|
117
127
|
return typeof directive === "object" && directive !== null && (directive as SoftToolRequirement).soft === true;
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
/**
|
|
121
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Source category for a queued steering interrupt observed without consuming the queue.
|
|
132
|
+
* Distinguishes real-user, agent-authored, system/advisor, and unknown steering.
|
|
133
|
+
*/
|
|
134
|
+
export type SteeringInterruptSource = "user" | "agent" | "system" | "unknown";
|
|
122
135
|
|
|
123
136
|
/** Non-consuming summary of whether queued steering should interrupt a tool batch. */
|
|
124
137
|
export interface SteeringQueueState {
|