@oh-my-pi/pi-agent-core 15.12.3 → 15.12.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 +5 -0
- package/dist/types/agent.d.ts +10 -0
- package/dist/types/compaction/openai.d.ts +11 -0
- package/package.json +6 -6
- package/src/agent.ts +30 -2
- package/src/compaction/compaction.ts +6 -0
- package/src/compaction/openai.ts +39 -17
package/CHANGELOG.md
CHANGED
package/dist/types/agent.d.ts
CHANGED
|
@@ -308,6 +308,7 @@ export declare class Agent {
|
|
|
308
308
|
getInterruptMode(): "immediate" | "wait";
|
|
309
309
|
setTools(t: AgentTool<any>[]): void;
|
|
310
310
|
replaceMessages(ms: AgentMessage[]): void;
|
|
311
|
+
replaceQueues(steering: AgentMessage[], followUp: AgentMessage[]): void;
|
|
311
312
|
appendMessage(m: AgentMessage): void;
|
|
312
313
|
popMessage(): AgentMessage | undefined;
|
|
313
314
|
/**
|
|
@@ -324,6 +325,15 @@ export declare class Agent {
|
|
|
324
325
|
clearFollowUpQueue(): void;
|
|
325
326
|
clearAllQueues(): void;
|
|
326
327
|
hasQueuedMessages(): boolean;
|
|
328
|
+
/** Non-consuming view of the pending steering queue (insertion order, newest
|
|
329
|
+
* last). The session layer derives its queued-message display/count from
|
|
330
|
+
* this live view instead of a mirror, so the agent-core queue stays the
|
|
331
|
+
* single source of truth. */
|
|
332
|
+
peekSteeringQueue(): readonly AgentMessage[];
|
|
333
|
+
/** Non-consuming view of the pending follow-up queue. See
|
|
334
|
+
* {@link peekSteeringQueue}. */
|
|
335
|
+
peekFollowUpQueue(): readonly AgentMessage[];
|
|
336
|
+
get isAborting(): boolean;
|
|
327
337
|
/**
|
|
328
338
|
* Remove and return the last steering message from the queue (LIFO).
|
|
329
339
|
* Used by dequeue keybinding.
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { FetchImpl, Message, Model } from "@oh-my-pi/pi-ai/types";
|
|
15
15
|
export declare const OPENAI_REMOTE_COMPACTION_PRESERVE_KEY = "openaiRemoteCompaction";
|
|
16
|
+
/**
|
|
17
|
+
* Hard ceiling on remote compaction HTTP requests. Unlike every provider
|
|
18
|
+
* stream (guarded by first-event/idle watchdogs in pi-ai), these are raw
|
|
19
|
+
* fetches awaiting one non-streamed JSON body — a connection silently dropped
|
|
20
|
+
* by a middlebox would otherwise hang the whole compaction pipeline forever
|
|
21
|
+
* (frozen "Auto context-full maintenance…" spinner, manual /compact queueing
|
|
22
|
+
* behind it). On timeout the caller falls back to local summarization.
|
|
23
|
+
*/
|
|
24
|
+
export declare const REMOTE_COMPACTION_TIMEOUT_MS = 180000;
|
|
16
25
|
export type OpenAiRemoteCompactionItem = {
|
|
17
26
|
type: "compaction" | "compaction_summary";
|
|
18
27
|
encrypted_content?: string;
|
|
@@ -56,7 +65,9 @@ export declare function withOpenAiRemoteCompactionPreserveData(preserveData: Rec
|
|
|
56
65
|
export declare function buildOpenAiNativeHistory(messages: Message[], model: Model, previousReplacementHistory?: Array<Record<string, unknown>>): Array<Record<string, unknown>>;
|
|
57
66
|
export declare function requestOpenAiRemoteCompaction(model: Model, apiKey: string, compactInput: Array<Record<string, unknown>>, instructions: string, signal?: AbortSignal, opts?: {
|
|
58
67
|
fetch?: FetchImpl;
|
|
68
|
+
timeoutMs?: number;
|
|
59
69
|
}): Promise<OpenAiRemoteCompactionResponse>;
|
|
60
70
|
export declare function requestRemoteCompaction(endpoint: string, request: RemoteCompactionRequest, signal?: AbortSignal, opts?: {
|
|
61
71
|
fetch?: FetchImpl;
|
|
72
|
+
timeoutMs?: number;
|
|
62
73
|
}): Promise<RemoteCompactionResponse>;
|
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": "15.12.
|
|
4
|
+
"version": "15.12.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,11 +35,11 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "15.12.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "15.12.
|
|
40
|
-
"@oh-my-pi/pi-natives": "15.12.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.12.
|
|
42
|
-
"@oh-my-pi/snapcompact": "15.12.
|
|
38
|
+
"@oh-my-pi/pi-ai": "15.12.4",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "15.12.4",
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.12.4",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.12.4",
|
|
42
|
+
"@oh-my-pi/snapcompact": "15.12.4",
|
|
43
43
|
"@opentelemetry/api": "^1.9.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
package/src/agent.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type ToolResultMessage,
|
|
24
24
|
} from "@oh-my-pi/pi-ai";
|
|
25
25
|
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
|
26
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
26
27
|
import { abortReasonText, agentLoop, agentLoopContinue } from "./agent-loop";
|
|
27
28
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
28
29
|
import type { HarmonyAuditEvent } from "./harmony-leak";
|
|
@@ -706,6 +707,11 @@ export class Agent {
|
|
|
706
707
|
this.#state.messages = ms.slice();
|
|
707
708
|
}
|
|
708
709
|
|
|
710
|
+
replaceQueues(steering: AgentMessage[], followUp: AgentMessage[]) {
|
|
711
|
+
this.#steeringQueue = steering.slice();
|
|
712
|
+
this.#followUpQueue = followUp.slice();
|
|
713
|
+
}
|
|
714
|
+
|
|
709
715
|
appendMessage(m: AgentMessage) {
|
|
710
716
|
this.#state.messages.push(m);
|
|
711
717
|
}
|
|
@@ -751,6 +757,24 @@ export class Agent {
|
|
|
751
757
|
return this.#steeringQueue.length > 0 || this.#followUpQueue.length > 0;
|
|
752
758
|
}
|
|
753
759
|
|
|
760
|
+
/** Non-consuming view of the pending steering queue (insertion order, newest
|
|
761
|
+
* last). The session layer derives its queued-message display/count from
|
|
762
|
+
* this live view instead of a mirror, so the agent-core queue stays the
|
|
763
|
+
* single source of truth. */
|
|
764
|
+
peekSteeringQueue(): readonly AgentMessage[] {
|
|
765
|
+
return this.#steeringQueue;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Non-consuming view of the pending follow-up queue. See
|
|
769
|
+
* {@link peekSteeringQueue}. */
|
|
770
|
+
peekFollowUpQueue(): readonly AgentMessage[] {
|
|
771
|
+
return this.#followUpQueue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
get isAborting(): boolean {
|
|
775
|
+
return this.#abortController?.signal.aborted === true && this.#state.isStreaming;
|
|
776
|
+
}
|
|
777
|
+
|
|
754
778
|
#dequeueSteeringMessages(): AgentMessage[] {
|
|
755
779
|
if (this.#steeringMode === "one-at-a-time") {
|
|
756
780
|
if (this.#steeringQueue.length > 0) {
|
|
@@ -1153,11 +1177,15 @@ export class Agent {
|
|
|
1153
1177
|
const result = listener(e) as unknown;
|
|
1154
1178
|
if (isPromise(result)) {
|
|
1155
1179
|
result.catch(err => {
|
|
1156
|
-
|
|
1180
|
+
logger.warn("Agent listener rejected", {
|
|
1181
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1182
|
+
});
|
|
1157
1183
|
});
|
|
1158
1184
|
}
|
|
1159
1185
|
} catch (err) {
|
|
1160
|
-
|
|
1186
|
+
logger.warn("Agent listener threw", {
|
|
1187
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1188
|
+
});
|
|
1161
1189
|
}
|
|
1162
1190
|
}
|
|
1163
1191
|
}
|
|
@@ -807,6 +807,7 @@ async function generateShortSummary(
|
|
|
807
807
|
prompt: promptText,
|
|
808
808
|
},
|
|
809
809
|
signal,
|
|
810
|
+
{ fetch: options?.fetch },
|
|
810
811
|
);
|
|
811
812
|
return remote.summary;
|
|
812
813
|
}
|
|
@@ -1047,6 +1048,10 @@ export async function compact(
|
|
|
1047
1048
|
);
|
|
1048
1049
|
preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, remote);
|
|
1049
1050
|
} catch (err) {
|
|
1051
|
+
// A user/session abort is a cancellation, not a remote failure —
|
|
1052
|
+
// swallowing it here would downgrade Esc into "fall back to local
|
|
1053
|
+
// summarization" and keep compaction running on an aborted signal.
|
|
1054
|
+
if (signal?.aborted) throw err;
|
|
1050
1055
|
logger.warn("OpenAI remote compaction failed, falling back to local summarization", {
|
|
1051
1056
|
error: err instanceof Error ? err.message : String(err),
|
|
1052
1057
|
model: model.id,
|
|
@@ -1114,6 +1119,7 @@ export async function compact(
|
|
|
1114
1119
|
// Same propagation as summaryOptions above — generateShortSummary
|
|
1115
1120
|
// resolves its own reasoning via resolveCompactionEffort.
|
|
1116
1121
|
thinkingLevel: options?.thinkingLevel,
|
|
1122
|
+
fetch: summaryOptions.fetch,
|
|
1117
1123
|
},
|
|
1118
1124
|
);
|
|
1119
1125
|
|
package/src/compaction/openai.ts
CHANGED
|
@@ -35,6 +35,23 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
35
35
|
|
|
36
36
|
export const OPENAI_REMOTE_COMPACTION_PRESERVE_KEY = "openaiRemoteCompaction";
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Hard ceiling on remote compaction HTTP requests. Unlike every provider
|
|
40
|
+
* stream (guarded by first-event/idle watchdogs in pi-ai), these are raw
|
|
41
|
+
* fetches awaiting one non-streamed JSON body — a connection silently dropped
|
|
42
|
+
* by a middlebox would otherwise hang the whole compaction pipeline forever
|
|
43
|
+
* (frozen "Auto context-full maintenance…" spinner, manual /compact queueing
|
|
44
|
+
* behind it). On timeout the caller falls back to local summarization.
|
|
45
|
+
*/
|
|
46
|
+
export const REMOTE_COMPACTION_TIMEOUT_MS = 180_000;
|
|
47
|
+
|
|
48
|
+
/** Race the caller's signal against the request timeout; `timeoutMs <= 0` disables the watchdog. */
|
|
49
|
+
function withRequestTimeout(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal | undefined {
|
|
50
|
+
if (timeoutMs <= 0) return signal;
|
|
51
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
52
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
export type OpenAiRemoteCompactionItem = {
|
|
39
56
|
type: "compaction" | "compaction_summary";
|
|
40
57
|
encrypted_content?: string;
|
|
@@ -147,14 +164,6 @@ export function withOpenAiRemoteCompactionPreserveData(
|
|
|
147
164
|
// Input/output filtering for OpenAI compact endpoint
|
|
148
165
|
// ============================================================================
|
|
149
166
|
|
|
150
|
-
function estimateOpenAiCompactInputTokens(input: Array<Record<string, unknown>>, instructions: string): number {
|
|
151
|
-
let chars = instructions.length;
|
|
152
|
-
for (const item of input) {
|
|
153
|
-
chars += JSON.stringify(item).length;
|
|
154
|
-
}
|
|
155
|
-
return Math.ceil(chars / 4);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
167
|
function shouldTrimOpenAiCompactInputItem(item: Record<string, unknown>): boolean {
|
|
159
168
|
return item.type === "function_call_output" || (item.type === "message" && item.role === "developer");
|
|
160
169
|
}
|
|
@@ -171,16 +180,29 @@ function trimOpenAiCompactInput(
|
|
|
171
180
|
instructions: string,
|
|
172
181
|
): Array<Record<string, unknown>> {
|
|
173
182
|
const trimmed = [...input];
|
|
174
|
-
|
|
183
|
+
// Per-item serialized sizes are cached and decremented on removal.
|
|
184
|
+
// Re-stringifying the whole input per popped item was O(N²) in total chars
|
|
185
|
+
// — hundreds of MB of stringify churn on a 200k-token codex history,
|
|
186
|
+
// blocking the event loop for seconds (same class as the addOpenAiCallIds
|
|
187
|
+
// fix above).
|
|
188
|
+
const sizes = trimmed.map(item => JSON.stringify(item).length);
|
|
189
|
+
let chars = instructions.length;
|
|
190
|
+
for (const size of sizes) chars += size;
|
|
191
|
+
const removeAt = (index: number): void => {
|
|
192
|
+
chars -= sizes[index] ?? 0;
|
|
193
|
+
trimmed.splice(index, 1);
|
|
194
|
+
sizes.splice(index, 1);
|
|
195
|
+
};
|
|
196
|
+
while (trimmed.length > 0 && Math.ceil(chars / 4) > contextWindow) {
|
|
175
197
|
const last = trimmed[trimmed.length - 1];
|
|
176
198
|
if (last?.type === "function_call_output" || last?.type === "custom_tool_call_output") {
|
|
177
199
|
const callId = typeof last.call_id === "string" ? last.call_id : undefined;
|
|
178
200
|
const callType = last.type === "custom_tool_call_output" ? "custom_tool_call" : "function_call";
|
|
179
|
-
trimmed.
|
|
201
|
+
removeAt(trimmed.length - 1);
|
|
180
202
|
if (callId) {
|
|
181
203
|
const matchingCallIndex = trimmed.findLastIndex(item => item.type === callType && item.call_id === callId);
|
|
182
204
|
if (matchingCallIndex >= 0) {
|
|
183
|
-
|
|
205
|
+
removeAt(matchingCallIndex);
|
|
184
206
|
}
|
|
185
207
|
}
|
|
186
208
|
continue;
|
|
@@ -188,7 +210,7 @@ function trimOpenAiCompactInput(
|
|
|
188
210
|
if (!last || !shouldTrimOpenAiCompactInputItem(last)) {
|
|
189
211
|
break;
|
|
190
212
|
}
|
|
191
|
-
trimmed.
|
|
213
|
+
removeAt(trimmed.length - 1);
|
|
192
214
|
}
|
|
193
215
|
return trimmed;
|
|
194
216
|
}
|
|
@@ -429,12 +451,12 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
429
451
|
compactInput: Array<Record<string, unknown>>,
|
|
430
452
|
instructions: string,
|
|
431
453
|
signal?: AbortSignal,
|
|
432
|
-
opts?: { fetch?: FetchImpl },
|
|
454
|
+
opts?: { fetch?: FetchImpl; timeoutMs?: number },
|
|
433
455
|
): Promise<OpenAiRemoteCompactionResponse> {
|
|
434
456
|
const endpoint = resolveOpenAiCompactEndpoint(model);
|
|
435
457
|
const request: OpenAiRemoteCompactionRequest = {
|
|
436
458
|
model: model.id,
|
|
437
|
-
input: trimOpenAiCompactInput(compactInput, model.contextWindow, instructions),
|
|
459
|
+
input: trimOpenAiCompactInput(compactInput, model.contextWindow ?? Number.POSITIVE_INFINITY, instructions),
|
|
438
460
|
instructions,
|
|
439
461
|
};
|
|
440
462
|
const headers: Record<string, string> = {
|
|
@@ -457,7 +479,7 @@ export async function requestOpenAiRemoteCompaction(
|
|
|
457
479
|
method: "POST",
|
|
458
480
|
headers,
|
|
459
481
|
body: JSON.stringify(request),
|
|
460
|
-
signal,
|
|
482
|
+
signal: withRequestTimeout(signal, opts?.timeoutMs ?? REMOTE_COMPACTION_TIMEOUT_MS),
|
|
461
483
|
});
|
|
462
484
|
|
|
463
485
|
if (!response.ok) {
|
|
@@ -509,13 +531,13 @@ export async function requestRemoteCompaction(
|
|
|
509
531
|
endpoint: string,
|
|
510
532
|
request: RemoteCompactionRequest,
|
|
511
533
|
signal?: AbortSignal,
|
|
512
|
-
opts?: { fetch?: FetchImpl },
|
|
534
|
+
opts?: { fetch?: FetchImpl; timeoutMs?: number },
|
|
513
535
|
): Promise<RemoteCompactionResponse> {
|
|
514
536
|
const response = await (opts?.fetch ?? fetch)(endpoint, {
|
|
515
537
|
method: "POST",
|
|
516
538
|
headers: { "content-type": "application/json" },
|
|
517
539
|
body: JSON.stringify(request),
|
|
518
|
-
signal,
|
|
540
|
+
signal: withRequestTimeout(signal, opts?.timeoutMs ?? REMOTE_COMPACTION_TIMEOUT_MS),
|
|
519
541
|
});
|
|
520
542
|
|
|
521
543
|
if (!response.ok) {
|