@oh-my-pi/pi-agent-core 16.1.14 → 16.1.16

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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.1.16] - 2026-06-23
6
+
7
+ ### Added
8
+
9
+ - Added `generateHandoffFromContext(context, model, options)` to `@oh-my-pi/pi-agent-core/compaction`: runs the handoff oneshot against a fully-built provider `Context` (system prompt, normalized tools, transformed history, trailing handoff prompt) with `streamOptions` mirroring the live turn's cache routing, so a host that owns the transform pipeline can make the handoff request share the prompt cache the main turn populated. `generateHandoff(messages, …)` is unchanged and now delegates to it.
10
+ - Added an optional `systemPrompt` argument to `Agent.buildSideRequestContext(llmMessages, systemPrompt?)`, defaulting to the live agent prompt; callers can pin a different prompt (e.g. handoff generation, which uses the base prompt rather than a per-turn `before_agent_start` hook override).
11
+
12
+ ### Changed
13
+
14
+ - Updated `buildSideRequestContext` to allow pinning custom system prompts
15
+
5
16
  ## [16.1.10] - 2026-06-21
6
17
 
7
18
  ### Fixed
@@ -329,8 +329,13 @@ export declare class Agent {
329
329
  * in-band dialect sessions stay tools-less (matching their no-native-tools wire
330
330
  * shape and avoiding tool-markup leakage). `llmMessages` is already converted
331
331
  * (and, in production, obfuscated) by the caller.
332
+ *
333
+ * `systemPrompt` defaults to the live agent prompt so the side request hits the
334
+ * same cached prefix as the main loop. Callers that must pin a different prompt
335
+ * (e.g. handoff generation, which uses the base prompt rather than a per-turn
336
+ * `before_agent_start` hook override) pass it explicitly.
332
337
  */
333
- buildSideRequestContext(llmMessages: Message[]): Promise<Context>;
338
+ buildSideRequestContext(llmMessages: Message[], systemPrompt?: string[]): Promise<Context>;
334
339
  subscribe(fn: (e: AgentEvent) => void): () => void;
335
340
  setProviderResponseInterceptor(fn: SimpleStreamOptions["onResponse"] | undefined): void;
336
341
  setRawSseEventInterceptor(fn: SimpleStreamOptions["onSseEvent"] | undefined): void;
@@ -4,7 +4,7 @@
4
4
  * Pure functions for compaction logic. The session manager handles I/O,
5
5
  * and after compaction the session is reloaded.
6
6
  */
7
- import { type ApiKey, type FetchImpl, type MessageAttribution, type Model, type Tool, type Usage } from "@oh-my-pi/pi-ai";
7
+ import { type ApiKey, type Context, type FetchImpl, type MessageAttribution, type Model, type SimpleStreamOptions, type Tool, type Usage } from "@oh-my-pi/pi-ai";
8
8
  import { type AgentTelemetry } from "../telemetry";
9
9
  import { ThinkingLevel } from "../thinking";
10
10
  import type { AgentMessage } from "../types";
@@ -177,6 +177,35 @@ export interface HandoffOptions {
177
177
  thinkingLevel?: ThinkingLevel;
178
178
  }
179
179
  export declare function renderHandoffPrompt(customInstructions?: string): string;
180
+ export interface HandoffFromContextOptions {
181
+ /**
182
+ * Stream options mirrored from the live agent turn: `apiKey`, `signal`, the
183
+ * `sessionId`/`promptCacheKey` cache-routing pair, `serviceTier`, and the
184
+ * session's payload/response hooks. Sending the same routing + payload shape
185
+ * the main loop uses is what lets the handoff oneshot READ the provider
186
+ * prompt cache the live turn populated instead of cold-missing the whole
187
+ * prefix. `reasoning` and `toolChoice` are set internally and override
188
+ * anything provided here.
189
+ */
190
+ streamOptions: SimpleStreamOptions;
191
+ /** See {@link HandoffOptions.telemetry}. */
192
+ telemetry?: AgentTelemetry;
193
+ /** See {@link HandoffOptions.thinkingLevel}. */
194
+ thinkingLevel?: ThinkingLevel;
195
+ }
196
+ /**
197
+ * Run the handoff oneshot against a fully-built provider {@link Context}.
198
+ *
199
+ * The caller assembles `context` exactly like a live agent turn — same system
200
+ * prompt, normalized tools, transformed + obfuscated message history, with the
201
+ * trailing handoff-prompt message already appended — and supplies
202
+ * `streamOptions` that mirror the live turn's cache routing. That keeps the
203
+ * cache-preserving context construction in the host (which owns the transform
204
+ * pipeline) while this function centralizes the handoff request contract:
205
+ * `toolChoice: "none"`, clamped reasoning effort, oneshot telemetry, text-only
206
+ * extraction, and provider-error mapping.
207
+ */
208
+ export declare function generateHandoffFromContext(context: Context, model: Model, options: HandoffFromContextOptions): Promise<string>;
180
209
  export declare function generateHandoff(messages: AgentMessage[], model: Model, apiKey: ApiKey, options: HandoffOptions, signal?: AbortSignal): Promise<string>;
181
210
  export interface CompactionPreparation {
182
211
  /** UUID of first entry to keep */
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.1.14",
4
+ "version": "16.1.16",
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.1.14",
39
- "@oh-my-pi/pi-catalog": "16.1.14",
40
- "@oh-my-pi/pi-natives": "16.1.14",
41
- "@oh-my-pi/pi-utils": "16.1.14",
42
- "@oh-my-pi/pi-wire": "16.1.14",
43
- "@oh-my-pi/snapcompact": "16.1.14",
38
+ "@oh-my-pi/pi-ai": "16.1.16",
39
+ "@oh-my-pi/pi-catalog": "16.1.16",
40
+ "@oh-my-pi/pi-natives": "16.1.16",
41
+ "@oh-my-pi/pi-utils": "16.1.16",
42
+ "@oh-my-pi/pi-wire": "16.1.16",
43
+ "@oh-my-pi/snapcompact": "16.1.16",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
package/src/agent.ts CHANGED
@@ -677,8 +677,16 @@ export class Agent {
677
677
  * in-band dialect sessions stay tools-less (matching their no-native-tools wire
678
678
  * shape and avoiding tool-markup leakage). `llmMessages` is already converted
679
679
  * (and, in production, obfuscated) by the caller.
680
+ *
681
+ * `systemPrompt` defaults to the live agent prompt so the side request hits the
682
+ * same cached prefix as the main loop. Callers that must pin a different prompt
683
+ * (e.g. handoff generation, which uses the base prompt rather than a per-turn
684
+ * `before_agent_start` hook override) pass it explicitly.
680
685
  */
681
- async buildSideRequestContext(llmMessages: Message[]): Promise<Context> {
686
+ async buildSideRequestContext(
687
+ llmMessages: Message[],
688
+ systemPrompt: string[] = this.#state.systemPrompt,
689
+ ): Promise<Context> {
682
690
  const model = this.#state.model;
683
691
  if (!model) throw new Error("No active model on agent");
684
692
  const ownedDialect = this.#dialect ?? resolveOwnedDialectFromEnv(Bun.env.PI_DIALECT);
@@ -691,7 +699,7 @@ export class Agent {
691
699
  preferredDialect(model.id),
692
700
  this.#pruneToolDescriptions,
693
701
  ) ?? []);
694
- let context: Context = { systemPrompt: this.#state.systemPrompt, messages, tools };
702
+ let context: Context = { systemPrompt, messages, tools };
695
703
  if (this.#transformProviderContext) context = await this.#transformProviderContext(context, model);
696
704
  return context;
697
705
  }
@@ -8,12 +8,14 @@
8
8
  import {
9
9
  type ApiKey,
10
10
  type AssistantMessage,
11
+ type Context,
11
12
  Effort,
12
13
  type FetchImpl,
13
14
  type Message,
14
15
  type MessageAttribution,
15
16
  type Model,
16
17
  ProviderHttpError,
18
+ type SimpleStreamOptions,
17
19
  type Tool,
18
20
  type Usage,
19
21
  withAuth,
@@ -771,6 +773,61 @@ export function renderHandoffPrompt(customInstructions?: string): string {
771
773
  });
772
774
  }
773
775
 
776
+ export interface HandoffFromContextOptions {
777
+ /**
778
+ * Stream options mirrored from the live agent turn: `apiKey`, `signal`, the
779
+ * `sessionId`/`promptCacheKey` cache-routing pair, `serviceTier`, and the
780
+ * session's payload/response hooks. Sending the same routing + payload shape
781
+ * the main loop uses is what lets the handoff oneshot READ the provider
782
+ * prompt cache the live turn populated instead of cold-missing the whole
783
+ * prefix. `reasoning` and `toolChoice` are set internally and override
784
+ * anything provided here.
785
+ */
786
+ streamOptions: SimpleStreamOptions;
787
+ /** See {@link HandoffOptions.telemetry}. */
788
+ telemetry?: AgentTelemetry;
789
+ /** See {@link HandoffOptions.thinkingLevel}. */
790
+ thinkingLevel?: ThinkingLevel;
791
+ }
792
+
793
+ /**
794
+ * Run the handoff oneshot against a fully-built provider {@link Context}.
795
+ *
796
+ * The caller assembles `context` exactly like a live agent turn — same system
797
+ * prompt, normalized tools, transformed + obfuscated message history, with the
798
+ * trailing handoff-prompt message already appended — and supplies
799
+ * `streamOptions` that mirror the live turn's cache routing. That keeps the
800
+ * cache-preserving context construction in the host (which owns the transform
801
+ * pipeline) while this function centralizes the handoff request contract:
802
+ * `toolChoice: "none"`, clamped reasoning effort, oneshot telemetry, text-only
803
+ * extraction, and provider-error mapping.
804
+ */
805
+ export async function generateHandoffFromContext(
806
+ context: Context,
807
+ model: Model,
808
+ options: HandoffFromContextOptions,
809
+ ): Promise<string> {
810
+ const response = await instrumentedCompleteSimple(
811
+ model,
812
+ context,
813
+ {
814
+ ...options.streamOptions,
815
+ reasoning: resolveCompactionEffort(model, options.thinkingLevel),
816
+ toolChoice: "none",
817
+ },
818
+ { telemetry: options.telemetry, oneshotKind: "handoff" },
819
+ );
820
+
821
+ if (response.stopReason === "error") {
822
+ throw createSummarizationError("Handoff generation failed", response);
823
+ }
824
+
825
+ return response.content
826
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
827
+ .map(c => c.text)
828
+ .join("\n");
829
+ }
830
+
774
831
  export async function generateHandoff(
775
832
  messages: AgentMessage[],
776
833
  model: Model,
@@ -789,32 +846,20 @@ export async function generateHandoff(
789
846
  },
790
847
  ];
791
848
 
792
- const response = await instrumentedCompleteSimple(
849
+ return generateHandoffFromContext(
850
+ { systemPrompt: options.systemPrompt, messages: requestMessages, tools: options.tools },
793
851
  model,
794
852
  {
795
- systemPrompt: options.systemPrompt,
796
- messages: requestMessages,
797
- tools: options.tools,
798
- },
799
- {
800
- apiKey,
801
- signal,
802
- reasoning: resolveCompactionEffort(model, options.thinkingLevel),
803
- toolChoice: "none",
804
- initiatorOverride: options.initiatorOverride,
805
- metadata: options.metadata,
853
+ streamOptions: {
854
+ apiKey,
855
+ signal,
856
+ initiatorOverride: options.initiatorOverride,
857
+ metadata: options.metadata,
858
+ },
859
+ telemetry: options.telemetry,
860
+ thinkingLevel: options.thinkingLevel,
806
861
  },
807
- { telemetry: options.telemetry, oneshotKind: "handoff" },
808
862
  );
809
-
810
- if (response.stopReason === "error") {
811
- throw createSummarizationError("Handoff generation failed", response);
812
- }
813
-
814
- return response.content
815
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
816
- .map(c => c.text)
817
- .join("\n");
818
863
  }
819
864
 
820
865
  async function generateShortSummary(