@oh-my-pi/pi-agent-core 16.4.2 → 16.4.4
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 +14 -1
- package/package.json +7 -7
- package/src/agent-loop.ts +32 -7
- package/src/agent.ts +13 -1
- package/src/types.ts +16 -1
package/CHANGELOG.md
CHANGED
package/dist/types/types.d.ts
CHANGED
|
@@ -53,6 +53,15 @@ export interface SoftToolRequirement {
|
|
|
53
53
|
export type ToolChoiceDirective = ToolChoice | SoftToolRequirement;
|
|
54
54
|
/** True when a {@link ToolChoiceDirective} is a soft requirement, not a hard choice. */
|
|
55
55
|
export declare function isSoftToolRequirement(directive: ToolChoiceDirective | undefined): directive is SoftToolRequirement;
|
|
56
|
+
/** Source category for a queued steering interrupt observed without consuming the queue. */
|
|
57
|
+
export type SteeringInterruptSource = "user" | "system" | "unknown";
|
|
58
|
+
/** Non-consuming summary of whether queued steering should interrupt a tool batch. */
|
|
59
|
+
export interface SteeringQueueState {
|
|
60
|
+
/** True when at least one steering message is queued. */
|
|
61
|
+
queued: boolean;
|
|
62
|
+
/** Best-effort origin used only to word synthetic skipped-tool results. */
|
|
63
|
+
source?: SteeringInterruptSource;
|
|
64
|
+
}
|
|
56
65
|
/**
|
|
57
66
|
* Configuration for the agent loop.
|
|
58
67
|
*/
|
|
@@ -154,10 +163,14 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
154
163
|
* restore queued messages while in-flight tools settle, and an external
|
|
155
164
|
* abort in that window leaves the queue intact for a post-abort continue.
|
|
156
165
|
*
|
|
166
|
+
* Returning `true` is treated as user-originated steering for compatibility.
|
|
167
|
+
* Return a {@link SteeringQueueState} when the queue can distinguish system
|
|
168
|
+
* advisories from real user messages.
|
|
169
|
+
*
|
|
157
170
|
* When omitted, steering never interrupts a running tool batch; queued
|
|
158
171
|
* messages are still delivered at the next injection boundary.
|
|
159
172
|
*/
|
|
160
|
-
hasSteeringMessages?: () => boolean | Promise<boolean>;
|
|
173
|
+
hasSteeringMessages?: () => boolean | SteeringQueueState | Promise<boolean | SteeringQueueState>;
|
|
161
174
|
/**
|
|
162
175
|
* Peeks whether IRC messages should interrupt an interruptible waiting tool.
|
|
163
176
|
*
|
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.4",
|
|
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.4",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "16.4.4",
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.4.4",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.4.4",
|
|
42
|
+
"@oh-my-pi/pi-wire": "16.4.4",
|
|
43
|
+
"@oh-my-pi/snapcompact": "16.4.4",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -66,6 +66,8 @@ import type {
|
|
|
66
66
|
AgentToolResult,
|
|
67
67
|
AgentTurnEndContext,
|
|
68
68
|
AsideMessage,
|
|
69
|
+
SteeringInterruptSource,
|
|
70
|
+
SteeringQueueState,
|
|
69
71
|
StreamFn,
|
|
70
72
|
} from "./types";
|
|
71
73
|
import { isSoftToolRequirement } from "./types";
|
|
@@ -1800,7 +1802,7 @@ async function executeToolCalls(
|
|
|
1800
1802
|
const interruptibleSignal: AbortSignal = signal
|
|
1801
1803
|
? AbortSignal.any([signal, steeringAbortController.signal, ircAbortController.signal])
|
|
1802
1804
|
: AbortSignal.any([steeringAbortController.signal, ircAbortController.signal]);
|
|
1803
|
-
const interruptState = { triggered: false };
|
|
1805
|
+
const interruptState: { triggered: boolean; source?: SteeringInterruptSource | "irc" } = { triggered: false };
|
|
1804
1806
|
|
|
1805
1807
|
const records = toolCalls.map(toolCall => {
|
|
1806
1808
|
// Tools emitted via OpenAI's custom-tool path (e.g. `apply_patch` on GPT-5)
|
|
@@ -1835,15 +1837,25 @@ async function executeToolCalls(
|
|
|
1835
1837
|
// integration only provides getSteeringMessages(), the queue drains at the
|
|
1836
1838
|
// injection boundary below; polling it here would strand or drop messages.
|
|
1837
1839
|
let steeringQueued = false;
|
|
1840
|
+
let steeringSource: SteeringInterruptSource | undefined;
|
|
1838
1841
|
if (hasSteeringMessages) {
|
|
1839
|
-
|
|
1842
|
+
const queuedState = await hasSteeringMessages();
|
|
1843
|
+
if (typeof queuedState === "boolean") {
|
|
1844
|
+
steeringQueued = queuedState;
|
|
1845
|
+
steeringSource = queuedState ? "user" : undefined;
|
|
1846
|
+
} else {
|
|
1847
|
+
const state: SteeringQueueState = queuedState;
|
|
1848
|
+
steeringQueued = state.queued;
|
|
1849
|
+
steeringSource = state.source ?? (state.queued ? "unknown" : undefined);
|
|
1850
|
+
}
|
|
1840
1851
|
}
|
|
1841
1852
|
if (steeringQueued) {
|
|
1842
|
-
//
|
|
1853
|
+
// Queued steering upgrades an in-flight IRC interrupt: it aborts the
|
|
1843
1854
|
// shared signal so foreground tools stop as they do for a user Esc.
|
|
1844
1855
|
// Idempotent — a second steer poll after the abort is a no-op.
|
|
1845
1856
|
if (!steeringAbortController.signal.aborted) {
|
|
1846
1857
|
interruptState.triggered = true;
|
|
1858
|
+
interruptState.source = steeringSource ?? "unknown";
|
|
1847
1859
|
steeringAbortController.abort();
|
|
1848
1860
|
}
|
|
1849
1861
|
return;
|
|
@@ -1855,6 +1867,7 @@ async function executeToolCalls(
|
|
|
1855
1867
|
// Peer IRC only aborts interruptible waits: a foreground bash / write
|
|
1856
1868
|
// mid-execution keeps running so we never leave partial side effects.
|
|
1857
1869
|
interruptState.triggered = true;
|
|
1870
|
+
interruptState.source = "irc";
|
|
1858
1871
|
ircAbortController.abort();
|
|
1859
1872
|
}
|
|
1860
1873
|
};
|
|
@@ -2115,7 +2128,7 @@ async function executeToolCalls(
|
|
|
2115
2128
|
// This tool's own signal fired AND it failed — it was cut off before producing
|
|
2116
2129
|
// a usable result, so report it as skipped.
|
|
2117
2130
|
record.skipped = true;
|
|
2118
|
-
emitToolResult(record, createSkippedToolResult(), true);
|
|
2131
|
+
emitToolResult(record, createSkippedToolResult(interruptState.source), true);
|
|
2119
2132
|
} else {
|
|
2120
2133
|
// No interrupt on this signal, or the tool finished (successfully or with a
|
|
2121
2134
|
// genuine error) before the interrupt landed. Keep its real result: a completed
|
|
@@ -2209,7 +2222,7 @@ async function executeToolCalls(
|
|
|
2209
2222
|
toolName: record.toolCall.name,
|
|
2210
2223
|
status: "skipped",
|
|
2211
2224
|
});
|
|
2212
|
-
emitToolResult(record, createSkippedToolResult(), true);
|
|
2225
|
+
emitToolResult(record, createSkippedToolResult(interruptState.source), true);
|
|
2213
2226
|
}
|
|
2214
2227
|
}
|
|
2215
2228
|
|
|
@@ -2326,12 +2339,24 @@ function createToolSignalAbortedResult(signal: AbortSignal): AgentToolResult<unk
|
|
|
2326
2339
|
};
|
|
2327
2340
|
}
|
|
2328
2341
|
|
|
2329
|
-
function createSkippedToolResult(): AgentToolResult<any> {
|
|
2342
|
+
function createSkippedToolResult(source: SteeringInterruptSource | "irc" | undefined): AgentToolResult<any> {
|
|
2343
|
+
let reason = "pending steering message";
|
|
2344
|
+
let blocker = "queued message";
|
|
2345
|
+
if (source === "user") {
|
|
2346
|
+
reason = "queued user message";
|
|
2347
|
+
blocker = "queued message";
|
|
2348
|
+
} else if (source === "system") {
|
|
2349
|
+
reason = "pending system advisory";
|
|
2350
|
+
blocker = "advisory";
|
|
2351
|
+
} else if (source === "irc") {
|
|
2352
|
+
reason = "pending peer interrupt";
|
|
2353
|
+
blocker = "interrupt";
|
|
2354
|
+
}
|
|
2330
2355
|
return {
|
|
2331
2356
|
content: [
|
|
2332
2357
|
{
|
|
2333
2358
|
type: "text",
|
|
2334
|
-
text:
|
|
2359
|
+
text: `Skipped due to ${reason}. Do not count this skipped result as completed work or verification. After the ${blocker} is handled on the next step, retry the skipped tool if it is still needed.`,
|
|
2335
2360
|
},
|
|
2336
2361
|
],
|
|
2337
2362
|
details: {},
|
package/src/agent.ts
CHANGED
|
@@ -1184,7 +1184,19 @@ export class Agent {
|
|
|
1184
1184
|
}
|
|
1185
1185
|
return this.#dequeueSteeringMessages();
|
|
1186
1186
|
},
|
|
1187
|
-
hasSteeringMessages: () =>
|
|
1187
|
+
hasSteeringMessages: () => {
|
|
1188
|
+
if (this.#steeringQueue.length === 0) {
|
|
1189
|
+
return { queued: false };
|
|
1190
|
+
}
|
|
1191
|
+
for (const message of this.#steeringQueue) {
|
|
1192
|
+
const role = "role" in message ? message.role : undefined;
|
|
1193
|
+
const attribution = "attribution" in message ? message.attribution : undefined;
|
|
1194
|
+
if (role === "user" && attribution !== "agent") {
|
|
1195
|
+
return { queued: true, source: "user" };
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return { queued: true, source: "system" };
|
|
1199
|
+
},
|
|
1188
1200
|
hasIrcInterrupts: this.hasIrcInterrupts,
|
|
1189
1201
|
getFollowUpMessages: async () => this.#dequeueFollowUpMessages(),
|
|
1190
1202
|
getAsideMessages: async () => (await this.#asideMessageProvider?.()) ?? [],
|
package/src/types.ts
CHANGED
|
@@ -83,6 +83,17 @@ export function isSoftToolRequirement(directive: ToolChoiceDirective | undefined
|
|
|
83
83
|
return typeof directive === "object" && directive !== null && (directive as SoftToolRequirement).soft === true;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** Source category for a queued steering interrupt observed without consuming the queue. */
|
|
87
|
+
export type SteeringInterruptSource = "user" | "system" | "unknown";
|
|
88
|
+
|
|
89
|
+
/** Non-consuming summary of whether queued steering should interrupt a tool batch. */
|
|
90
|
+
export interface SteeringQueueState {
|
|
91
|
+
/** True when at least one steering message is queued. */
|
|
92
|
+
queued: boolean;
|
|
93
|
+
/** Best-effort origin used only to word synthetic skipped-tool results. */
|
|
94
|
+
source?: SteeringInterruptSource;
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
/**
|
|
87
98
|
* Configuration for the agent loop.
|
|
88
99
|
*/
|
|
@@ -194,10 +205,14 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
194
205
|
* restore queued messages while in-flight tools settle, and an external
|
|
195
206
|
* abort in that window leaves the queue intact for a post-abort continue.
|
|
196
207
|
*
|
|
208
|
+
* Returning `true` is treated as user-originated steering for compatibility.
|
|
209
|
+
* Return a {@link SteeringQueueState} when the queue can distinguish system
|
|
210
|
+
* advisories from real user messages.
|
|
211
|
+
*
|
|
197
212
|
* When omitted, steering never interrupts a running tool batch; queued
|
|
198
213
|
* messages are still delivered at the next injection boundary.
|
|
199
214
|
*/
|
|
200
|
-
hasSteeringMessages?: () => boolean | Promise<boolean>;
|
|
215
|
+
hasSteeringMessages?: () => boolean | SteeringQueueState | Promise<boolean | SteeringQueueState>;
|
|
201
216
|
|
|
202
217
|
/**
|
|
203
218
|
* Peeks whether IRC messages should interrupt an interruptible waiting tool.
|