@oh-my-pi/pi-agent-core 15.5.4 → 15.5.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/dist/types/compaction/compaction.d.ts +17 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/utils/yield.d.ts +38 -13
- package/package.json +4 -4
- package/src/agent.ts +12 -1
- package/src/compaction/compaction.ts +77 -4
- package/src/index.ts +2 -0
- package/src/utils/yield.ts +58 -12
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type MessageAttribution, type Model, type Usage } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import { type AgentTelemetry } from "../telemetry";
|
|
9
|
+
import { ThinkingLevel } from "../thinking";
|
|
9
10
|
import type { AgentMessage, AgentTool } from "../types";
|
|
10
11
|
import type { SessionEntry } from "./entries";
|
|
11
12
|
import { type ConvertToLlm } from "./messages";
|
|
@@ -115,6 +116,15 @@ export interface SummaryOptions {
|
|
|
115
116
|
* or `compaction_turn_prefix`). `undefined` keeps the call paths zero-cost.
|
|
116
117
|
*/
|
|
117
118
|
telemetry?: AgentTelemetry;
|
|
119
|
+
/**
|
|
120
|
+
* Active session thinking level. Threaded from `agent-session.ts` so
|
|
121
|
+
* compaction honors the user's `/model` thinking selection instead of
|
|
122
|
+
* silently overriding it with `Effort.High` (the historical default).
|
|
123
|
+
* `undefined` / `ThinkingLevel.Inherit` falls back to that historical
|
|
124
|
+
* default; `ThinkingLevel.Off` omits reasoning entirely. See
|
|
125
|
+
* `resolveCompactionEffort` for the conversion contract.
|
|
126
|
+
*/
|
|
127
|
+
thinkingLevel?: ThinkingLevel;
|
|
118
128
|
}
|
|
119
129
|
export declare function generateSummary(currentMessages: AgentMessage[], model: Model, reserveTokens: number, apiKey: string, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, options?: SummaryOptions): Promise<string>;
|
|
120
130
|
export interface HandoffOptions {
|
|
@@ -131,6 +141,13 @@ export interface HandoffOptions {
|
|
|
131
141
|
* wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "handoff"`.
|
|
132
142
|
*/
|
|
133
143
|
telemetry?: AgentTelemetry;
|
|
144
|
+
/**
|
|
145
|
+
* Active session thinking level. Threaded from `agent-session.ts` so
|
|
146
|
+
* handoff generation honors the user's `/model` thinking selection
|
|
147
|
+
* instead of silently overriding it with `Effort.High`. See
|
|
148
|
+
* `resolveCompactionEffort` for the conversion contract.
|
|
149
|
+
*/
|
|
150
|
+
thinkingLevel?: ThinkingLevel;
|
|
134
151
|
}
|
|
135
152
|
export declare function renderHandoffPrompt(customInstructions?: string): string;
|
|
136
153
|
export declare function generateHandoff(messages: AgentMessage[], model: Model, apiKey: string, options: HandoffOptions, signal?: AbortSignal): Promise<string>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cooperative yield utility for preventing Bun event-loop busy-wait.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
4
|
+
* ## Root Cause
|
|
5
|
+
*
|
|
6
|
+
* Bun 1.3.x (JavaScriptCore) event loop busy-waits (spins in userspace)
|
|
7
|
+
* when the only pending work is an unresolved Promise — even if there are
|
|
8
|
+
* active I/O watchers (stdin, child process pipes, etc.). The event loop
|
|
9
|
+
* continuously polls for microtask resolution instead of blocking in
|
|
10
|
+
* `epoll_wait`, consuming ~100% of a CPU core.
|
|
11
|
+
*
|
|
12
|
+
* This affects any `await` on a never-resolved Promise, including:
|
|
13
|
+
* - `Promise.withResolvers()` used for user input callbacks
|
|
14
|
+
* - `await proc.exited` for long-running child processes
|
|
15
|
+
* - Agent loop iterations waiting for the next tool call
|
|
16
|
+
*
|
|
17
|
+
* ## Fix
|
|
18
|
+
*
|
|
19
|
+
* A recurring `setInterval` keeps the event loop sleeping in `epoll_wait`.
|
|
20
|
+
* The `EventLoopKeepalive` class and `keepaliveWhile()` wrapper provide a
|
|
21
|
+
* clean way to install and clean up this keepalive timer.
|
|
22
|
+
*
|
|
23
|
+
* The older `yieldIfDue()` and `ExponentialYield` approaches (compensated
|
|
24
|
+
* sleep loops) are retained for the agent-loop hot-path where Promises
|
|
25
|
+
* resolve frequently and the keepalive alone is insufficient.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Await a Promise with an event-loop keepalive active.
|
|
29
|
+
*
|
|
30
|
+
* This is the primary fix for Bun's busy-wait on unresolved Promises.
|
|
31
|
+
* Use it wherever you `await` a Promise that may remain unresolved for
|
|
32
|
+
* an extended period (user input, long-running subprocess, etc.).
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* // Before (100% CPU while waiting):
|
|
36
|
+
* const input = await mode.getUserInput();
|
|
37
|
+
*
|
|
38
|
+
* // After (proper epoll_wait sleep):
|
|
39
|
+
* const input = await keepaliveWhile(mode.getUserInput());
|
|
40
|
+
* ```
|
|
17
41
|
*/
|
|
42
|
+
export declare function keepaliveWhile<T>(promise: Promise<T>): Promise<T>;
|
|
18
43
|
/**
|
|
19
44
|
* Yield to the Bun event loop, sleeping for at least 20 ms — but at most
|
|
20
45
|
* once every {@link YIELD_INTERVAL_MS}. Callers in hot paths can invoke
|
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.5.
|
|
4
|
+
"version": "15.5.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,9 +35,9 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "15.5.
|
|
39
|
-
"@oh-my-pi/pi-natives": "15.5.
|
|
40
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
38
|
+
"@oh-my-pi/pi-ai": "15.5.7",
|
|
39
|
+
"@oh-my-pi/pi-natives": "15.5.7",
|
|
40
|
+
"@oh-my-pi/pi-utils": "15.5.7",
|
|
41
41
|
"@opentelemetry/api": "^1.9.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
package/src/agent.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Agent class that uses the agent-loop directly.
|
|
2
2
|
* No transport abstraction - calls streamSimple via the loop.
|
|
3
3
|
*/
|
|
4
|
+
|
|
5
|
+
import { isPromise } from "node:util/types";
|
|
4
6
|
import {
|
|
5
7
|
type AssistantMessage,
|
|
6
8
|
type AssistantMessageEvent,
|
|
@@ -1074,7 +1076,16 @@ export class Agent {
|
|
|
1074
1076
|
|
|
1075
1077
|
#emit(e: AgentEvent) {
|
|
1076
1078
|
for (const listener of this.#listeners) {
|
|
1077
|
-
|
|
1079
|
+
try {
|
|
1080
|
+
const result = listener(e) as unknown;
|
|
1081
|
+
if (isPromise(result)) {
|
|
1082
|
+
result.catch(err => {
|
|
1083
|
+
console.error("Agent listener rejected:", err instanceof Error ? err.message : err);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
console.error("Agent listener threw:", err instanceof Error ? err.message : err);
|
|
1088
|
+
}
|
|
1078
1089
|
}
|
|
1079
1090
|
}
|
|
1080
1091
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
type AssistantMessage,
|
|
10
|
+
clampThinkingLevelForModel,
|
|
10
11
|
Effort,
|
|
11
12
|
type Message,
|
|
12
13
|
type MessageAttribution,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
import { countTokens } from "@oh-my-pi/pi-natives";
|
|
17
18
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
18
19
|
import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
20
|
+
import { ThinkingLevel } from "../thinking";
|
|
19
21
|
import type { AgentMessage, AgentTool } from "../types";
|
|
20
22
|
import type { CompactionEntry, SessionEntry } from "./entries";
|
|
21
23
|
import { type ConvertToLlm, convertToLlm, createBranchSummaryMessage, createCustomMessage } from "./messages";
|
|
@@ -502,6 +504,52 @@ function formatAdditionalContext(context: string[] | undefined): string {
|
|
|
502
504
|
return `<additional-context>\n${lines}\n</additional-context>\n\n`;
|
|
503
505
|
}
|
|
504
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Maps the non-special `ThinkingLevel` values to their `Effort` counterparts.
|
|
509
|
+
* Exhaustive over the union; throws for `Off`/`Inherit` to surface logic
|
|
510
|
+
* errors in callers that forgot to filter those out. Never use a TS cast for
|
|
511
|
+
* this — `ThinkingLevel` is a string-union over distinct concepts (Off /
|
|
512
|
+
* Inherit are not Efforts), and a cast hides the contract.
|
|
513
|
+
*/
|
|
514
|
+
function effortFromThinkingLevel(level: ThinkingLevel): Effort {
|
|
515
|
+
switch (level) {
|
|
516
|
+
case ThinkingLevel.Minimal:
|
|
517
|
+
return Effort.Minimal;
|
|
518
|
+
case ThinkingLevel.Low:
|
|
519
|
+
return Effort.Low;
|
|
520
|
+
case ThinkingLevel.Medium:
|
|
521
|
+
return Effort.Medium;
|
|
522
|
+
case ThinkingLevel.High:
|
|
523
|
+
return Effort.High;
|
|
524
|
+
case ThinkingLevel.XHigh:
|
|
525
|
+
return Effort.XHigh;
|
|
526
|
+
case ThinkingLevel.Off:
|
|
527
|
+
case ThinkingLevel.Inherit:
|
|
528
|
+
throw new Error(`effortFromThinkingLevel: ${level} must be handled by caller`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Resolves the reasoning effort to send on a compaction LLM call.
|
|
534
|
+
*
|
|
535
|
+
* - Explicit `Off` → `undefined` (omit reasoning entirely; the user said no thinking).
|
|
536
|
+
* - `undefined` / `Inherit` → historical `Effort.High` default → clamped per model
|
|
537
|
+
* (preserves current behavior for users who never touched the dial).
|
|
538
|
+
* - Explicit effort → respect user choice → clamped per model.
|
|
539
|
+
*
|
|
540
|
+
* The clamp routes through `clampThinkingLevelForModel`, which returns
|
|
541
|
+
* `undefined` for models with `compat.supportsReasoningEffort: false`
|
|
542
|
+
* (e.g. `xai-oauth/grok-build`). That `undefined` then flows through to the
|
|
543
|
+
* openai-responses mapper where `modelOmitsReasoningEffort` short-circuits
|
|
544
|
+
* the wire param — no `requireSupportedEffort` throw.
|
|
545
|
+
*/
|
|
546
|
+
function resolveCompactionEffort(model: Model, level: ThinkingLevel | undefined): Effort | undefined {
|
|
547
|
+
if (level === ThinkingLevel.Off) return undefined;
|
|
548
|
+
const requested: Effort =
|
|
549
|
+
level === undefined || level === ThinkingLevel.Inherit ? Effort.High : effortFromThinkingLevel(level);
|
|
550
|
+
return clampThinkingLevelForModel(model, requested);
|
|
551
|
+
}
|
|
552
|
+
|
|
505
553
|
/**
|
|
506
554
|
* Generate a summary of the conversation using the LLM.
|
|
507
555
|
* If previousSummary is provided, uses the update prompt to merge.
|
|
@@ -521,6 +569,15 @@ export interface SummaryOptions {
|
|
|
521
569
|
* or `compaction_turn_prefix`). `undefined` keeps the call paths zero-cost.
|
|
522
570
|
*/
|
|
523
571
|
telemetry?: AgentTelemetry;
|
|
572
|
+
/**
|
|
573
|
+
* Active session thinking level. Threaded from `agent-session.ts` so
|
|
574
|
+
* compaction honors the user's `/model` thinking selection instead of
|
|
575
|
+
* silently overriding it with `Effort.High` (the historical default).
|
|
576
|
+
* `undefined` / `ThinkingLevel.Inherit` falls back to that historical
|
|
577
|
+
* default; `ThinkingLevel.Off` omits reasoning entirely. See
|
|
578
|
+
* `resolveCompactionEffort` for the conversion contract.
|
|
579
|
+
*/
|
|
580
|
+
thinkingLevel?: ThinkingLevel;
|
|
524
581
|
}
|
|
525
582
|
|
|
526
583
|
export async function generateSummary(
|
|
@@ -584,7 +641,7 @@ export async function generateSummary(
|
|
|
584
641
|
maxTokens,
|
|
585
642
|
signal,
|
|
586
643
|
apiKey,
|
|
587
|
-
reasoning:
|
|
644
|
+
reasoning: resolveCompactionEffort(model, options?.thinkingLevel),
|
|
588
645
|
initiatorOverride: options?.initiatorOverride,
|
|
589
646
|
metadata: options?.metadata,
|
|
590
647
|
},
|
|
@@ -621,6 +678,13 @@ export interface HandoffOptions {
|
|
|
621
678
|
* wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "handoff"`.
|
|
622
679
|
*/
|
|
623
680
|
telemetry?: AgentTelemetry;
|
|
681
|
+
/**
|
|
682
|
+
* Active session thinking level. Threaded from `agent-session.ts` so
|
|
683
|
+
* handoff generation honors the user's `/model` thinking selection
|
|
684
|
+
* instead of silently overriding it with `Effort.High`. See
|
|
685
|
+
* `resolveCompactionEffort` for the conversion contract.
|
|
686
|
+
*/
|
|
687
|
+
thinkingLevel?: ThinkingLevel;
|
|
624
688
|
}
|
|
625
689
|
|
|
626
690
|
export function renderHandoffPrompt(customInstructions?: string): string {
|
|
@@ -658,7 +722,7 @@ export async function generateHandoff(
|
|
|
658
722
|
{
|
|
659
723
|
apiKey,
|
|
660
724
|
signal,
|
|
661
|
-
reasoning:
|
|
725
|
+
reasoning: resolveCompactionEffort(model, options.thinkingLevel),
|
|
662
726
|
toolChoice: "none",
|
|
663
727
|
initiatorOverride: options.initiatorOverride,
|
|
664
728
|
metadata: options.metadata,
|
|
@@ -718,7 +782,7 @@ async function generateShortSummary(
|
|
|
718
782
|
maxTokens,
|
|
719
783
|
signal,
|
|
720
784
|
apiKey,
|
|
721
|
-
reasoning:
|
|
785
|
+
reasoning: resolveCompactionEffort(model, options?.thinkingLevel),
|
|
722
786
|
initiatorOverride: options?.initiatorOverride,
|
|
723
787
|
metadata: options?.metadata,
|
|
724
788
|
},
|
|
@@ -905,6 +969,12 @@ export async function compact(
|
|
|
905
969
|
metadata: options?.metadata,
|
|
906
970
|
convertToLlm: options?.convertToLlm,
|
|
907
971
|
telemetry: options?.telemetry,
|
|
972
|
+
// Honor /model thinking selection on every fan-out summarizer.
|
|
973
|
+
// Without this propagation, generateSummary / generateTurnPrefixSummary
|
|
974
|
+
// see options?.thinkingLevel === undefined and resolveCompactionEffort
|
|
975
|
+
// silently falls back to Effort.High — the same defect e07b47ee4 fixed
|
|
976
|
+
// at the call sites, leaked back in here. See resolveCompactionEffort.
|
|
977
|
+
thinkingLevel: options?.thinkingLevel,
|
|
908
978
|
};
|
|
909
979
|
|
|
910
980
|
let preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
|
|
@@ -995,6 +1065,9 @@ export async function compact(
|
|
|
995
1065
|
initiatorOverride: summaryOptions.initiatorOverride,
|
|
996
1066
|
metadata: summaryOptions.metadata,
|
|
997
1067
|
telemetry: summaryOptions.telemetry,
|
|
1068
|
+
// Same propagation as summaryOptions above — generateShortSummary
|
|
1069
|
+
// resolves its own reasoning via resolveCompactionEffort.
|
|
1070
|
+
thinkingLevel: options?.thinkingLevel,
|
|
998
1071
|
},
|
|
999
1072
|
);
|
|
1000
1073
|
|
|
@@ -1047,7 +1120,7 @@ async function generateTurnPrefixSummary(
|
|
|
1047
1120
|
maxTokens,
|
|
1048
1121
|
signal,
|
|
1049
1122
|
apiKey,
|
|
1050
|
-
reasoning:
|
|
1123
|
+
reasoning: resolveCompactionEffort(model, options?.thinkingLevel),
|
|
1051
1124
|
initiatorOverride: options?.initiatorOverride,
|
|
1052
1125
|
metadata: options?.metadata,
|
|
1053
1126
|
},
|
package/src/index.ts
CHANGED
package/src/utils/yield.ts
CHANGED
|
@@ -1,23 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cooperative yield utility for preventing Bun event-loop busy-wait.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the microtask queue is continuously non-empty. In long-running agent loops
|
|
6
|
-
* (LLM streaming, tool execution) this causes ~100% CPU usage even when the
|
|
7
|
-
* process is simply waiting for I/O.
|
|
4
|
+
* ## Root Cause
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* Bun 1.3.x (JavaScriptCore) event loop busy-waits (spins in userspace)
|
|
7
|
+
* when the only pending work is an unresolved Promise — even if there are
|
|
8
|
+
* active I/O watchers (stdin, child process pipes, etc.). The event loop
|
|
9
|
+
* continuously polls for microtask resolution instead of blocking in
|
|
10
|
+
* `epoll_wait`, consuming ~100% of a CPU core.
|
|
14
11
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* This affects any `await` on a never-resolved Promise, including:
|
|
13
|
+
* - `Promise.withResolvers()` used for user input callbacks
|
|
14
|
+
* - `await proc.exited` for long-running child processes
|
|
15
|
+
* - Agent loop iterations waiting for the next tool call
|
|
16
|
+
*
|
|
17
|
+
* ## Fix
|
|
18
|
+
*
|
|
19
|
+
* A recurring `setInterval` keeps the event loop sleeping in `epoll_wait`.
|
|
20
|
+
* The `EventLoopKeepalive` class and `keepaliveWhile()` wrapper provide a
|
|
21
|
+
* clean way to install and clean up this keepalive timer.
|
|
22
|
+
*
|
|
23
|
+
* The older `yieldIfDue()` and `ExponentialYield` approaches (compensated
|
|
24
|
+
* sleep loops) are retained for the agent-loop hot-path where Promises
|
|
25
|
+
* resolve frequently and the keepalive alone is insufficient.
|
|
17
26
|
*/
|
|
18
27
|
|
|
19
28
|
import { scheduler } from "node:timers/promises";
|
|
20
29
|
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// EventLoopKeepalive — the primary fix for idle-state busy-wait
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const KEEPALIVE_INTERVAL_MS = 86_400_000; // 24 hours — re-armed each interval
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Await a Promise with an event-loop keepalive active.
|
|
38
|
+
*
|
|
39
|
+
* This is the primary fix for Bun's busy-wait on unresolved Promises.
|
|
40
|
+
* Use it wherever you `await` a Promise that may remain unresolved for
|
|
41
|
+
* an extended period (user input, long-running subprocess, etc.).
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* // Before (100% CPU while waiting):
|
|
45
|
+
* const input = await mode.getUserInput();
|
|
46
|
+
*
|
|
47
|
+
* // After (proper epoll_wait sleep):
|
|
48
|
+
* const input = await keepaliveWhile(mode.getUserInput());
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export async function keepaliveWhile<T>(promise: Promise<T>): Promise<T> {
|
|
52
|
+
const ka = setInterval(() => {}, KEEPALIVE_INTERVAL_MS);
|
|
53
|
+
try {
|
|
54
|
+
ka.unref();
|
|
55
|
+
return await promise;
|
|
56
|
+
} finally {
|
|
57
|
+
clearInterval(ka);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// yieldIfDue — retained for agent-loop hot-path
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
21
65
|
const YIELD_SLEEP_MS = 20;
|
|
22
66
|
const YIELD_INTERVAL_MS = 50;
|
|
23
67
|
|
|
@@ -63,7 +107,9 @@ export async function yieldIfDue(): Promise<void> {
|
|
|
63
107
|
lastYieldAt = Date.now();
|
|
64
108
|
}
|
|
65
109
|
|
|
66
|
-
//
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// ExponentialYield — retained for bash-executor long waits
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
67
113
|
|
|
68
114
|
const EXP_DEFAULT_MIN_MS = 20;
|
|
69
115
|
const EXP_DEFAULT_MAX_MS = 10_000;
|