@oh-my-pi/pi-agent-core 16.4.1 → 16.4.3
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/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/compaction/compaction-v2-streaming.ts +2 -2
- package/src/compaction/compaction.ts +2 -2
- package/src/compaction/openai.ts +4 -4
- package/src/compaction/utils.ts +2 -2
- package/src/types.ts +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.4.3] - 2026-07-11
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed an issue where skipped sibling tool results incorrectly reported that a queued user message caused the skip.
|
|
10
|
+
|
|
11
|
+
## [16.4.2] - 2026-07-10
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Fixed serialization of BigInt tool arguments to prevent data loss during remote compaction.
|
|
16
|
+
|
|
5
17
|
## [16.4.1] - 2026-07-10
|
|
6
18
|
|
|
7
19
|
### Fixed
|
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.3",
|
|
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.3",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "16.4.3",
|
|
40
|
+
"@oh-my-pi/pi-natives": "16.4.3",
|
|
41
|
+
"@oh-my-pi/pi-utils": "16.4.3",
|
|
42
|
+
"@oh-my-pi/pi-wire": "16.4.3",
|
|
43
|
+
"@oh-my-pi/snapcompact": "16.4.3",
|
|
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?.()) ?? [],
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
OPENAI_HEADER_VALUES,
|
|
28
28
|
OPENAI_HEADERS,
|
|
29
29
|
} from "@oh-my-pi/pi-catalog/wire/codex";
|
|
30
|
-
import { $env, logger } from "@oh-my-pi/pi-utils";
|
|
30
|
+
import { $env, logger, stringifyJson } from "@oh-my-pi/pi-utils";
|
|
31
31
|
|
|
32
32
|
// ============================================================================
|
|
33
33
|
// Types & Configuration
|
|
@@ -329,7 +329,7 @@ async function attemptCompactionV2Streaming(
|
|
|
329
329
|
const response = await fetchImpl(endpoint, {
|
|
330
330
|
method: "POST",
|
|
331
331
|
headers: buildCompactionV2Headers(model, apiKey, request, codexMetadata),
|
|
332
|
-
body:
|
|
332
|
+
body: stringifyJson(body),
|
|
333
333
|
signal,
|
|
334
334
|
});
|
|
335
335
|
|
|
@@ -28,7 +28,7 @@ import { convertTools } from "@oh-my-pi/pi-ai/providers/openai-responses";
|
|
|
28
28
|
import { buildResponsesInput, resolveOpenAICompatPolicy } from "@oh-my-pi/pi-ai/providers/openai-shared";
|
|
29
29
|
import { preferredDialect } from "@oh-my-pi/pi-catalog/identity";
|
|
30
30
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
31
|
-
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
31
|
+
import { logger, prompt, stringifyJson } from "@oh-my-pi/pi-utils";
|
|
32
32
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
33
33
|
import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
34
34
|
import { ThinkingLevel } from "../thinking";
|
|
@@ -406,7 +406,7 @@ export function estimateTokens(message: AgentMessage, options?: { excludeEncrypt
|
|
|
406
406
|
}
|
|
407
407
|
} else if (block.type === "toolCall") {
|
|
408
408
|
fragments.push(block.name);
|
|
409
|
-
fragments.push(
|
|
409
|
+
fragments.push(stringifyJson(block.arguments) ?? "null");
|
|
410
410
|
} else if (block.type === "redactedThinking") {
|
|
411
411
|
// Encrypted reasoning blob the provider still bills for on replay;
|
|
412
412
|
// excluded from the compaction floor for the same reason as above.
|
package/src/compaction/openai.ts
CHANGED
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
OPENAI_HEADER_VALUES,
|
|
44
44
|
OPENAI_HEADERS,
|
|
45
45
|
} from "@oh-my-pi/pi-catalog/wire/codex";
|
|
46
|
-
import { $env, logger } from "@oh-my-pi/pi-utils";
|
|
46
|
+
import { $env, logger, stringifyJson } from "@oh-my-pi/pi-utils";
|
|
47
47
|
|
|
48
48
|
export * from "./compaction-v2-streaming";
|
|
49
49
|
|
|
@@ -419,7 +419,7 @@ export function buildOpenAiNativeHistory(
|
|
|
419
419
|
id: itemId,
|
|
420
420
|
call_id: normalized.callId,
|
|
421
421
|
name: block.name,
|
|
422
|
-
arguments:
|
|
422
|
+
arguments: stringifyJson(block.arguments) ?? "null",
|
|
423
423
|
});
|
|
424
424
|
}
|
|
425
425
|
}
|
|
@@ -549,7 +549,7 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
549
549
|
const response = await (opts?.fetch ?? fetch)(endpoint, {
|
|
550
550
|
method: "POST",
|
|
551
551
|
headers,
|
|
552
|
-
body:
|
|
552
|
+
body: stringifyJson(request),
|
|
553
553
|
signal: withRequestTimeout(signal, opts?.timeoutMs ?? REMOTE_COMPACTION_TIMEOUT_MS),
|
|
554
554
|
});
|
|
555
555
|
|
|
@@ -646,7 +646,7 @@ export async function requestRemoteCompaction(
|
|
|
646
646
|
const response = await (opts?.fetch ?? fetch)(endpoint, {
|
|
647
647
|
method: "POST",
|
|
648
648
|
headers,
|
|
649
|
-
body:
|
|
649
|
+
body: stringifyJson(body),
|
|
650
650
|
signal: withRequestTimeout(signal, opts?.timeoutMs ?? REMOTE_COMPACTION_TIMEOUT_MS),
|
|
651
651
|
});
|
|
652
652
|
|
package/src/compaction/utils.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Message, ToolCall } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import { type Dialect, getDialectDefinition } from "@oh-my-pi/pi-ai/dialect";
|
|
7
|
-
import { formatGroupedPaths, prompt } from "@oh-my-pi/pi-utils";
|
|
7
|
+
import { formatGroupedPaths, prompt, stringifyJson } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import type { AgentMessage } from "../types";
|
|
9
9
|
import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
|
|
10
10
|
import summarizationSystemPrompt from "./prompts/summarization-system.md" with { type: "text" };
|
|
@@ -309,7 +309,7 @@ function renderToolCalls(calls: ToolCall[]): string {
|
|
|
309
309
|
return calls
|
|
310
310
|
.map(call => {
|
|
311
311
|
const argsStr = Object.entries(call.arguments as Record<string, unknown>)
|
|
312
|
-
.map(([k, v]) => `${k}=${
|
|
312
|
+
.map(([k, v]) => `${k}=${stringifyJson(v) ?? "null"}`)
|
|
313
313
|
.join(", ");
|
|
314
314
|
return `${call.name}(${argsStr})`;
|
|
315
315
|
})
|
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.
|