@oh-my-pi/pi-agent-core 15.5.6 → 15.5.8

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.
@@ -1,6 +1,3 @@
1
- /** Agent class that uses the agent-loop directly.
2
- * No transport abstraction - calls streamSimple via the loop.
3
- */
4
1
  import { type AssistantMessage, type AssistantMessageEvent, type CursorExecHandlers, type CursorToolResultHandler, type Effort, type ImageContent, type Message, type Model, type ProviderSessionState, type ServiceTier, type SimpleStreamOptions, type ThinkingBudgets, type ToolChoice } from "@oh-my-pi/pi-ai";
5
2
  import type { AppendOnlyContextManager } from "./append-only-context";
6
3
  import type { HarmonyAuditEvent } from "./harmony-leak";
@@ -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>;
@@ -24,22 +24,10 @@
24
24
  * sleep loops) are retained for the agent-loop hot-path where Promises
25
25
  * resolve frequently and the keepalive alone is insufficient.
26
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
- * ```
41
- */
42
- export declare function keepaliveWhile<T>(promise: Promise<T>): Promise<T>;
27
+ export declare class EventLoopKeepalive {
28
+ #private;
29
+ [Symbol.dispose](): void;
30
+ }
43
31
  /**
44
32
  * Yield to the Bun event loop, sleeping for at least 20 ms — but at most
45
33
  * 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.6",
4
+ "version": "15.5.8",
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.6",
39
- "@oh-my-pi/pi-natives": "15.5.6",
40
- "@oh-my-pi/pi-utils": "15.5.6",
38
+ "@oh-my-pi/pi-ai": "15.5.8",
39
+ "@oh-my-pi/pi-natives": "15.5.8",
40
+ "@oh-my-pi/pi-utils": "15.5.8",
41
41
  "@opentelemetry/api": "^1.9.0"
42
42
  },
43
43
  "devDependencies": {
package/src/agent.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /** Agent class that uses the agent-loop directly.
2
2
  * No transport abstraction - calls streamSimple via the loop.
3
3
  */
4
-
5
4
  import { isPromise } from "node:util/types";
6
5
  import {
7
6
  type AssistantMessage,
@@ -36,6 +35,7 @@ import type {
36
35
  StreamFn,
37
36
  ToolCallContext,
38
37
  } from "./types";
38
+ import { EventLoopKeepalive } from "./utils/yield";
39
39
 
40
40
  /**
41
41
  * Default convertToLlm: Keep only LLM-compatible messages, convert attachments.
@@ -859,7 +859,7 @@ export class Agent {
859
859
  if (!model) throw new Error("No model configured");
860
860
 
861
861
  let skipInitialSteeringPoll = options?.skipInitialSteeringPoll === true;
862
-
862
+ using _ = new EventLoopKeepalive();
863
863
  const { promise, resolve } = Promise.withResolvers<void>();
864
864
  this.#runningPrompt = promise;
865
865
  this.#resolveRunningPrompt = resolve;
@@ -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: Effort.High,
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: Effort.High,
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: Effort.High,
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: Effort.High,
1123
+ reasoning: resolveCompactionEffort(model, options?.thinkingLevel),
1051
1124
  initiatorOverride: options?.initiatorOverride,
1052
1125
  metadata: options?.metadata,
1053
1126
  },
@@ -31,30 +31,10 @@ import { scheduler } from "node:timers/promises";
31
31
  // EventLoopKeepalive — the primary fix for idle-state busy-wait
32
32
  // ---------------------------------------------------------------------------
33
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);
34
+ export class EventLoopKeepalive {
35
+ #tmr = setInterval(() => {}, 86_400_000).unref();
36
+ [Symbol.dispose](): void {
37
+ clearInterval(this.#tmr);
58
38
  }
59
39
  }
60
40