refactor(compaction): moved compaction APIs to @oh-my-pi/pi-agent-core
- Relocated compaction, branch-summarization, pruning, and utils from coding-agent to packages/agent/src/compaction. - Moved OpenAI remote compaction helpers from packages/ai to the new compaction module. - Added handoff.ts with extractHandoffDocument, createHandoffContext, and renderHandoffPrompt helpers. - Exposed new entries.ts with standalone SessionEntry types so coding-agent no longer owns them.
This commit is contained in:
+14
-11
@@ -9,15 +9,17 @@ Both are persisted as session entries and converted back into user-context messa
|
||||
|
||||
## Key implementation files
|
||||
|
||||
- `src/session/compaction/compaction.ts`
|
||||
- `src/session/compaction/branch-summarization.ts`
|
||||
- `src/session/compaction/pruning.ts`
|
||||
- `src/session/compaction/utils.ts`
|
||||
- `src/session/session-manager.ts`
|
||||
- `src/session/agent-session.ts`
|
||||
- `src/session/messages.ts`
|
||||
- `src/extensibility/hooks/types.ts`
|
||||
- `src/config/settings-schema.ts`
|
||||
- `packages/agent/src/compaction/compaction.ts`
|
||||
- `packages/agent/src/compaction/branch-summarization.ts`
|
||||
- `packages/agent/src/compaction/pruning.ts`
|
||||
- `packages/agent/src/compaction/utils.ts`
|
||||
- `packages/agent/src/compaction/openai.ts`
|
||||
- `packages/agent/src/compaction/handoff.ts`
|
||||
- `packages/coding-agent/src/session/session-manager.ts`
|
||||
- `packages/coding-agent/src/session/agent-session.ts`
|
||||
- `packages/coding-agent/src/session/messages.ts`
|
||||
- `packages/coding-agent/src/extensibility/hooks/types.ts`
|
||||
- `packages/coding-agent/src/config/settings-schema.ts`
|
||||
|
||||
## Session entry model
|
||||
|
||||
@@ -44,8 +46,9 @@ When context is rebuilt (`buildSessionContext`):
|
||||
|
||||
Those custom roles are then transformed into LLM-facing user messages in `convertToLlm()` using the static templates:
|
||||
|
||||
- `prompts/compaction/compaction-summary-context.md`
|
||||
- `prompts/compaction/branch-summary-context.md`
|
||||
- `packages/agent/src/compaction/prompts/compaction-summary-context.md`
|
||||
- `packages/agent/src/compaction/prompts/branch-summary-context.md`
|
||||
- `packages/agent/src/compaction/prompts/handoff-document.md`
|
||||
|
||||
## Compaction pipeline
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ Does not cover:
|
||||
- [`../src/modes/controllers/input-controller.ts`](../packages/coding-agent/src/modes/controllers/input-controller.ts)
|
||||
- [`../src/modes/controllers/command-controller.ts`](../packages/coding-agent/src/modes/controllers/command-controller.ts)
|
||||
- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts)
|
||||
- [`packages/agent/src/compaction/handoff.ts`](../packages/agent/src/compaction/handoff.ts)
|
||||
- [`../src/session/session-manager.ts`](../packages/coding-agent/src/session/session-manager.ts)
|
||||
- [`../src/extensibility/slash-commands.ts`](../packages/coding-agent/src/extensibility/slash-commands.ts)
|
||||
|
||||
@@ -45,7 +46,7 @@ The same minimum-content guard exists again inside `AgentSession.handoff()` and
|
||||
- Reads current branch entries (`sessionManager.getBranch()`)
|
||||
- Validates minimum message count (`>= 2`)
|
||||
- Creates `#handoffAbortController`
|
||||
- Renders the fixed prompt template `prompts/system/handoff-document.md` with optional `additionalFocus`
|
||||
- Renders the fixed prompt template `packages/agent/src/compaction/prompts/handoff-document.md` via `renderHandoffPrompt(...)` with optional `additionalFocus`
|
||||
- Appends `Additional focus: ...` if custom instructions are provided
|
||||
|
||||
Prompt is sent as an agent-authored developer message via:
|
||||
@@ -67,7 +68,7 @@ Because handoff bypasses `prompt(...)`, normal slash/prompt-template expansion i
|
||||
|
||||
Before sending prompt, `handoff()` subscribes to session events and waits for `agent_end`.
|
||||
|
||||
On `agent_end`, it extracts handoff text from agent state by scanning backward for the most recent `assistant` message, then concatenating all `content` blocks where `type === "text"` with `\n`.
|
||||
On `agent_end`, it calls `extractHandoffDocument(...)`, which scans agent state backward for the most recent `assistant` message, then concatenates all `content` blocks where `type === "text"` with `\n`.
|
||||
|
||||
Important extraction assumptions:
|
||||
|
||||
@@ -100,7 +101,7 @@ If text was captured and not aborted:
|
||||
|
||||
### 5) Handoff-context injection
|
||||
|
||||
The generated handoff document is wrapped and appended to the new session as a `custom_message` entry:
|
||||
The generated handoff document is wrapped by `createHandoffContext(...)` and appended to the new session as a `custom_message` entry:
|
||||
|
||||
```text
|
||||
<handoff-context>
|
||||
|
||||
@@ -59,7 +59,7 @@ The returned tool result is not the final rewind. `AgentSession` waits until `tu
|
||||
- Session files are named `<ISO-timestamp-with-:-and-.-replaced>_<uuidv7>.jsonl` in the session directory; default directory selection is documented in `SessionManager.create()` as `~/.omp/agent/sessions/<encoded-cwd>/` when no override is passed.
|
||||
- User-visible prompts / interactive UI
|
||||
- The tool result itself is visible.
|
||||
- The persisted `branch_summary` becomes an LLM-visible `branchSummary` message when context is rebuilt from `SessionManager.buildSessionContext()`; `messages.ts` renders it as a user-role text message using `prompts/compaction/branch-summary-context.md`.
|
||||
- The persisted `branch_summary` becomes an LLM-visible `branchSummary` message when context is rebuilt from `SessionManager.buildSessionContext()`; `messages.ts` renders it as a user-role text message using `packages/agent/src/compaction/prompts/branch-summary-context.md`.
|
||||
- The persisted `rewind-report` custom message also participates in rebuilt LLM context because `custom_message` entries are converted through `createCustomMessage()`.
|
||||
- Background work / cancellation
|
||||
- Rewind application is deferred to `turn_end`. There is no separate job object or cancel handle.
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
- Added `AgentTelemetryConfig` hooks (`onSpanStart`, `onSpanEnd`, `costEstimator`), `agent` identity, `attributes` envelope merged onto every span, `captureMessageContent` toggle (defaults to the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` env var) emitting OTEL-shaped `gen_ai.input.messages` / `gen_ai.output.messages` / `gen_ai.system_instructions` / `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result`, and tracer/tracerName override surfaces.
|
||||
- Added `Agent#setTelemetry(config)` so consumers can swap or disable instrumentation between invocations.
|
||||
- Added `@opentelemetry/api` as a runtime dependency; SDK setup (exporters, samplers, processors) remains the host's responsibility per standard OTEL conventions. When no SDK is registered, helpers fall through to no-op spans with zero overhead.
|
||||
- Added compaction APIs under `@oh-my-pi/pi-agent-core/compaction`, including context compaction, branch summarization, handoff prompt/context helpers, pruning, token budgeting, prompt templates, and OpenAI `/responses/compact` helpers.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -58,6 +58,14 @@
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./compaction": {
|
||||
"types": "./src/compaction.ts",
|
||||
"import": "./src/compaction.ts"
|
||||
},
|
||||
"./compaction/*": {
|
||||
"types": "./src/compaction/*.ts",
|
||||
"import": "./src/compaction/*.ts"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./src/*.ts",
|
||||
"import": "./src/*.ts"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./compaction/index";
|
||||
+16
-7
@@ -4,20 +4,22 @@
|
||||
* When navigating to a different point in the session tree, this generates
|
||||
* a summary of the branch being left so context isn't lost.
|
||||
*/
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import { completeSimple } from "@oh-my-pi/pi-ai";
|
||||
import { prompt } from "@oh-my-pi/pi-utils";
|
||||
import branchSummaryPrompt from "../../prompts/compaction/branch-summary.md" with { type: "text" };
|
||||
import branchSummaryPreamble from "../../prompts/compaction/branch-summary-preamble.md" with { type: "text" };
|
||||
import type { AgentMessage } from "../types";
|
||||
import { estimateTokens } from "./compaction";
|
||||
import type { ReadonlySessionManager, SessionEntry } from "./entries";
|
||||
import {
|
||||
type ConvertToLlm,
|
||||
convertToLlm,
|
||||
createBranchSummaryMessage,
|
||||
createCompactionSummaryMessage,
|
||||
createCustomMessage,
|
||||
} from "../../session/messages";
|
||||
import type { ReadonlySessionManager, SessionEntry } from "../../session/session-manager";
|
||||
import { estimateTokens } from "./compaction";
|
||||
} from "./messages";
|
||||
import branchSummaryPrompt from "./prompts/branch-summary.md" with { type: "text" };
|
||||
import branchSummaryPreamble from "./prompts/branch-summary-preamble.md" with { type: "text" };
|
||||
import {
|
||||
computeFileLists,
|
||||
createFileOps,
|
||||
@@ -77,6 +79,8 @@ export interface GenerateBranchSummaryOptions {
|
||||
reserveTokens?: number;
|
||||
/** Optional metadata forwarded to the underlying API request (e.g. user_id for session attribution). */
|
||||
metadata?: Record<string, unknown>;
|
||||
/** Convert app-specific messages before serializing the branch summary prompt. */
|
||||
convertToLlm?: ConvertToLlm;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -171,6 +175,11 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
||||
case "model_change":
|
||||
case "custom":
|
||||
case "label":
|
||||
case "service_tier_change":
|
||||
case "ttsr_injection":
|
||||
case "mcp_tool_selection":
|
||||
case "session_init":
|
||||
case "mode_change":
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -274,7 +283,7 @@ export async function generateBranchSummary(
|
||||
|
||||
// Transform to LLM-compatible messages, then serialize to text
|
||||
// Serialization prevents the model from treating it as a conversation to continue
|
||||
const llmMessages = convertToLlm(messages);
|
||||
const llmMessages = (options.convertToLlm ?? convertToLlm)(messages);
|
||||
const conversationText = serializeConversation(llmMessages);
|
||||
|
||||
// Build prompt
|
||||
+45
-37
@@ -4,29 +4,32 @@
|
||||
* Pure functions for compaction logic. The session manager handles I/O,
|
||||
* and after compaction the session is reloaded.
|
||||
*/
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
|
||||
import {
|
||||
type AssistantMessage,
|
||||
buildOpenAiNativeHistory,
|
||||
completeSimple,
|
||||
Effort,
|
||||
getPreservedOpenAiRemoteCompactionData,
|
||||
type MessageAttribution,
|
||||
type Model,
|
||||
requestOpenAiRemoteCompaction,
|
||||
requestRemoteCompaction,
|
||||
shouldUseOpenAiRemoteCompaction,
|
||||
type Usage,
|
||||
withOpenAiRemoteCompactionPreserveData,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { countTokens } from "@oh-my-pi/pi-natives";
|
||||
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
||||
import compactionShortSummaryPrompt from "../../prompts/compaction/compaction-short-summary.md" with { type: "text" };
|
||||
import compactionSummaryPrompt from "../../prompts/compaction/compaction-summary.md" with { type: "text" };
|
||||
import compactionTurnPrefixPrompt from "../../prompts/compaction/compaction-turn-prefix.md" with { type: "text" };
|
||||
import compactionUpdateSummaryPrompt from "../../prompts/compaction/compaction-update-summary.md" with { type: "text" };
|
||||
import { convertToLlm, createBranchSummaryMessage, createCustomMessage } from "../../session/messages";
|
||||
import type { CompactionEntry, SessionEntry } from "../../session/session-manager";
|
||||
import type { AgentMessage } from "../types";
|
||||
import type { CompactionEntry, SessionEntry } from "./entries";
|
||||
import { type ConvertToLlm, convertToLlm, createBranchSummaryMessage, createCustomMessage } from "./messages";
|
||||
import {
|
||||
buildOpenAiNativeHistory,
|
||||
getPreservedOpenAiRemoteCompactionData,
|
||||
requestOpenAiRemoteCompaction,
|
||||
requestRemoteCompaction,
|
||||
shouldUseOpenAiRemoteCompaction,
|
||||
withOpenAiRemoteCompactionPreserveData,
|
||||
} from "./openai";
|
||||
import compactionShortSummaryPrompt from "./prompts/compaction-short-summary.md" with { type: "text" };
|
||||
import compactionSummaryPrompt from "./prompts/compaction-summary.md" with { type: "text" };
|
||||
import compactionTurnPrefixPrompt from "./prompts/compaction-turn-prefix.md" with { type: "text" };
|
||||
import compactionUpdateSummaryPrompt from "./prompts/compaction-update-summary.md" with { type: "text" };
|
||||
|
||||
import {
|
||||
computeFileLists,
|
||||
@@ -247,6 +250,12 @@ const IMAGE_TOKEN_ESTIMATE = 1200;
|
||||
export function estimateTokens(message: AgentMessage): number {
|
||||
const fragments: string[] = [];
|
||||
let extra = 0;
|
||||
if ((message as { role?: string }).role === "bashExecution") {
|
||||
const bash = message as { command?: unknown; output?: unknown };
|
||||
if (typeof bash.command === "string") fragments.push(bash.command);
|
||||
if (typeof bash.output === "string") fragments.push(bash.output);
|
||||
return fragments.length === 0 ? 0 : countTokens(fragments);
|
||||
}
|
||||
|
||||
switch (message.role) {
|
||||
case "user": {
|
||||
@@ -291,11 +300,6 @@ export function estimateTokens(message: AgentMessage): number {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "bashExecution": {
|
||||
fragments.push(message.command);
|
||||
fragments.push(message.output);
|
||||
break;
|
||||
}
|
||||
case "branchSummary":
|
||||
case "compactionSummary": {
|
||||
fragments.push(message.summary);
|
||||
@@ -333,7 +337,7 @@ function findValidCutPoints(entries: SessionEntry[], startIndex: number, endInde
|
||||
const entry = entries[i];
|
||||
switch (entry.type) {
|
||||
case "message": {
|
||||
const role = entry.message.role;
|
||||
const role = entry.message.role as string;
|
||||
switch (role) {
|
||||
case "bashExecution":
|
||||
case "hookMessage":
|
||||
@@ -377,7 +381,7 @@ export function findTurnStartIndex(entries: SessionEntry[], entryIndex: number,
|
||||
return i;
|
||||
}
|
||||
if (entry.type === "message") {
|
||||
const role = entry.message.role;
|
||||
const role = entry.message.role as string;
|
||||
if (role === "user" || role === "bashExecution") {
|
||||
return i;
|
||||
}
|
||||
@@ -502,6 +506,7 @@ export interface SummaryOptions {
|
||||
remoteInstructions?: string;
|
||||
initiatorOverride?: MessageAttribution;
|
||||
metadata?: Record<string, unknown>;
|
||||
convertToLlm?: ConvertToLlm;
|
||||
}
|
||||
|
||||
export async function generateSummary(
|
||||
@@ -526,8 +531,8 @@ export async function generateSummary(
|
||||
}
|
||||
|
||||
// Serialize conversation to text so model doesn't try to continue it
|
||||
// Convert to LLM messages first (handles custom types like bashExecution, hookMessage, etc.)
|
||||
const llmMessages = convertToLlm(currentMessages);
|
||||
// Convert to LLM messages first (handles custom app messages when caller provides a transformer).
|
||||
const llmMessages = (options?.convertToLlm ?? convertToLlm)(currentMessages);
|
||||
const conversationText = serializeConversation(llmMessages);
|
||||
|
||||
// Build the prompt with conversation wrapped in tags
|
||||
@@ -593,7 +598,7 @@ async function generateShortSummary(
|
||||
options?: SummaryOptions,
|
||||
): Promise<string> {
|
||||
const maxTokens = Math.min(512, Math.floor(0.2 * reserveTokens));
|
||||
const llmMessages = convertToLlm(recentMessages);
|
||||
const llmMessages = (options?.convertToLlm ?? convertToLlm)(recentMessages);
|
||||
const conversationText = serializeConversation(llmMessages);
|
||||
|
||||
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
||||
@@ -809,6 +814,7 @@ export async function compact(
|
||||
remoteInstructions: options?.remoteInstructions,
|
||||
initiatorOverride: options?.initiatorOverride,
|
||||
metadata: options?.metadata,
|
||||
convertToLlm: options?.convertToLlm,
|
||||
};
|
||||
|
||||
let preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
|
||||
@@ -819,7 +825,11 @@ export async function compact(
|
||||
previousRemoteCompaction?.provider === model.provider
|
||||
? previousRemoteCompaction.replacementHistory
|
||||
: undefined;
|
||||
const remoteHistory = buildOpenAiNativeHistory(convertToLlm(remoteMessages), model, previousReplacementHistory);
|
||||
const remoteHistory = buildOpenAiNativeHistory(
|
||||
(summaryOptions.convertToLlm ?? convertToLlm)(remoteMessages),
|
||||
model,
|
||||
previousReplacementHistory,
|
||||
);
|
||||
if (remoteHistory.length > 0) {
|
||||
try {
|
||||
const remote = await requestOpenAiRemoteCompaction(
|
||||
@@ -858,15 +868,7 @@ export async function compact(
|
||||
summaryOptions,
|
||||
)
|
||||
: Promise.resolve("No prior history."),
|
||||
generateTurnPrefixSummary(
|
||||
turnPrefixMessages,
|
||||
model,
|
||||
settings.reserveTokens,
|
||||
apiKey,
|
||||
signal,
|
||||
summaryOptions.initiatorOverride,
|
||||
summaryOptions.metadata,
|
||||
),
|
||||
generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal, summaryOptions),
|
||||
]);
|
||||
// Merge into single summary
|
||||
summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
|
||||
@@ -932,12 +934,11 @@ async function generateTurnPrefixSummary(
|
||||
reserveTokens: number,
|
||||
apiKey: string,
|
||||
signal?: AbortSignal,
|
||||
initiatorOverride?: MessageAttribution,
|
||||
metadata?: Record<string, unknown>,
|
||||
options?: SummaryOptions,
|
||||
): Promise<string> {
|
||||
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
||||
|
||||
const llmMessages = convertToLlm(messages);
|
||||
const llmMessages = (options?.convertToLlm ?? convertToLlm)(messages);
|
||||
const conversationText = serializeConversation(llmMessages);
|
||||
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
||||
const summarizationMessages = [
|
||||
@@ -951,7 +952,14 @@ async function generateTurnPrefixSummary(
|
||||
const response = await completeSimple(
|
||||
model,
|
||||
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
||||
{ maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride, metadata },
|
||||
{
|
||||
maxTokens,
|
||||
signal,
|
||||
apiKey,
|
||||
reasoning: Effort.High,
|
||||
initiatorOverride: options?.initiatorOverride,
|
||||
metadata: options?.metadata,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.stopReason === "error") {
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { ImageContent, MessageAttribution, ServiceTier, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import type { AgentMessage } from "../types";
|
||||
|
||||
export interface SessionEntryBase {
|
||||
type: string;
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface SessionMessageEntry extends SessionEntryBase {
|
||||
type: "message";
|
||||
message: AgentMessage;
|
||||
}
|
||||
|
||||
export interface ThinkingLevelChangeEntry extends SessionEntryBase {
|
||||
type: "thinking_level_change";
|
||||
thinkingLevel?: string | null;
|
||||
}
|
||||
|
||||
export interface ModelChangeEntry extends SessionEntryBase {
|
||||
type: "model_change";
|
||||
/** Model in "provider/modelId" format */
|
||||
model: string;
|
||||
/** Role: "default", "smol", "slow", etc. Undefined treated as "default" */
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export interface ServiceTierChangeEntry extends SessionEntryBase {
|
||||
type: "service_tier_change";
|
||||
serviceTier: ServiceTier | null;
|
||||
}
|
||||
|
||||
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
|
||||
type: "compaction";
|
||||
summary: string;
|
||||
shortSummary?: string;
|
||||
firstKeptEntryId: string;
|
||||
tokensBefore: number;
|
||||
/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
|
||||
details?: T;
|
||||
/** Hook-provided data to persist across compaction */
|
||||
preserveData?: Record<string, unknown>;
|
||||
/** True if generated by an extension, undefined/false if pi-generated (backward compatible) */
|
||||
fromExtension?: boolean;
|
||||
}
|
||||
|
||||
export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
|
||||
type: "branch_summary";
|
||||
fromId: string;
|
||||
summary: string;
|
||||
/** Extension-specific data (not sent to LLM) */
|
||||
details?: T;
|
||||
/** True if generated by an extension, false if pi-generated */
|
||||
fromExtension?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
|
||||
type: "custom_message";
|
||||
customType: string;
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
details?: T;
|
||||
display: boolean;
|
||||
/** Who initiated this message for billing/attribution semantics. */
|
||||
attribution?: MessageAttribution;
|
||||
}
|
||||
|
||||
export interface CustomEntry<T = unknown> extends SessionEntryBase {
|
||||
type: "custom";
|
||||
customType: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
export interface LabelEntry extends SessionEntryBase {
|
||||
type: "label";
|
||||
targetId: string;
|
||||
label: string | undefined;
|
||||
}
|
||||
|
||||
export interface TtsrInjectionEntry extends SessionEntryBase {
|
||||
type: "ttsr_injection";
|
||||
/** Names of rules that were injected */
|
||||
injectedRules: string[];
|
||||
}
|
||||
|
||||
export interface MCPToolSelectionEntry extends SessionEntryBase {
|
||||
type: "mcp_tool_selection";
|
||||
/** MCP tool names selected for visibility in discovery mode. */
|
||||
selectedToolNames: string[];
|
||||
}
|
||||
|
||||
export interface SessionInitEntry extends SessionEntryBase {
|
||||
type: "session_init";
|
||||
/** Full system prompt sent to the model */
|
||||
systemPrompt: string;
|
||||
/** Initial task/user message */
|
||||
task: string;
|
||||
/** Tools available to the agent */
|
||||
tools: string[];
|
||||
/** Output schema if structured output was requested */
|
||||
outputSchema?: unknown;
|
||||
}
|
||||
|
||||
export interface ModeChangeEntry extends SessionEntryBase {
|
||||
type: "mode_change";
|
||||
/** Current mode name, or "none" when exiting a mode */
|
||||
mode: string;
|
||||
/** Optional mode-specific data (e.g. plan file path) */
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CustomCompactionSessionEntries {}
|
||||
|
||||
export type SessionEntry =
|
||||
| SessionMessageEntry
|
||||
| ThinkingLevelChangeEntry
|
||||
| ModelChangeEntry
|
||||
| ServiceTierChangeEntry
|
||||
| CompactionEntry
|
||||
| BranchSummaryEntry
|
||||
| CustomEntry
|
||||
| CustomMessageEntry
|
||||
| LabelEntry
|
||||
| TtsrInjectionEntry
|
||||
| MCPToolSelectionEntry
|
||||
| SessionInitEntry
|
||||
| ModeChangeEntry
|
||||
| CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries];
|
||||
|
||||
export interface ReadonlySessionManager {
|
||||
getBranch(leafId?: string | null): SessionEntry[];
|
||||
getEntry(id: string): SessionEntry | undefined;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
||||
import { prompt } from "@oh-my-pi/pi-utils";
|
||||
import type { AgentMessage } from "../types";
|
||||
import autoHandoffThresholdFocusPrompt from "./prompts/auto-handoff-threshold-focus.md" with { type: "text" };
|
||||
import handoffDocumentPrompt from "./prompts/handoff-document.md" with { type: "text" };
|
||||
|
||||
/** Result from a handoff operation. */
|
||||
export interface HandoffResult {
|
||||
document: string;
|
||||
savedPath?: string;
|
||||
}
|
||||
|
||||
export interface HandoffOptions {
|
||||
autoTriggered?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export const AUTO_HANDOFF_THRESHOLD_FOCUS = prompt.render(autoHandoffThresholdFocusPrompt);
|
||||
|
||||
export function renderHandoffPrompt(customInstructions?: string): string {
|
||||
return prompt.render(handoffDocumentPrompt, {
|
||||
additionalFocus: customInstructions,
|
||||
});
|
||||
}
|
||||
|
||||
export function extractHandoffDocument(messages: AgentMessage[]): string | undefined {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message.role !== "assistant") continue;
|
||||
const content = (message as AssistantMessage).content;
|
||||
const textParts = content.filter((c): c is { type: "text"; text: string } => c.type === "text").map(c => c.text);
|
||||
if (textParts.length > 0) return textParts.join("\n");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function createHandoffContext(document: string): string {
|
||||
return `<handoff-context>\n${document}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
|
||||
}
|
||||
|
||||
export function createHandoffFileName(date = new Date()): string {
|
||||
const fileTimestamp = date.toISOString().replace(/[:.]/g, "-");
|
||||
return `handoff-${fileTimestamp}.md`;
|
||||
}
|
||||
+5
@@ -4,5 +4,10 @@
|
||||
|
||||
export * from "./branch-summarization";
|
||||
export * from "./compaction";
|
||||
export * from "./entries";
|
||||
export * from "./errors";
|
||||
export * from "./handoff";
|
||||
export * from "./messages";
|
||||
export * from "./openai";
|
||||
export * from "./pruning";
|
||||
export * from "./utils";
|
||||
@@ -0,0 +1,212 @@
|
||||
import type {
|
||||
AssistantMessage,
|
||||
ImageContent,
|
||||
Message,
|
||||
MessageAttribution,
|
||||
ProviderPayload,
|
||||
TextContent,
|
||||
ToolResultMessage,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { prompt } from "@oh-my-pi/pi-utils";
|
||||
import type { AgentMessage } from "../types";
|
||||
import branchSummaryContextPrompt from "./prompts/branch-summary-context.md" with { type: "text" };
|
||||
import compactionSummaryContextPrompt from "./prompts/compaction-summary-context.md" with { type: "text" };
|
||||
|
||||
const COMPACTION_SUMMARY_TEMPLATE = compactionSummaryContextPrompt;
|
||||
const BRANCH_SUMMARY_TEMPLATE = branchSummaryContextPrompt;
|
||||
|
||||
export interface CustomMessage<T = unknown> {
|
||||
role: "custom";
|
||||
customType: string;
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
display: boolean;
|
||||
details?: T;
|
||||
/** Who initiated this message for billing/attribution semantics. */
|
||||
attribution?: MessageAttribution;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/** Legacy hook message type (pre-extensions). Kept for session migration. */
|
||||
export interface HookMessage<T = unknown> {
|
||||
role: "hookMessage";
|
||||
customType: string;
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
display: boolean;
|
||||
details?: T;
|
||||
/** Who initiated this message for billing/attribution semantics. */
|
||||
attribution?: MessageAttribution;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface BranchSummaryMessage {
|
||||
role: "branchSummary";
|
||||
summary: string;
|
||||
fromId: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface CompactionSummaryMessage {
|
||||
role: "compactionSummary";
|
||||
summary: string;
|
||||
shortSummary?: string;
|
||||
tokensBefore: number;
|
||||
providerPayload?: ProviderPayload;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type CoreCompactionMessage = CustomMessage | HookMessage | BranchSummaryMessage | CompactionSummaryMessage;
|
||||
|
||||
declare module "../types" {
|
||||
interface CustomAgentMessages {
|
||||
custom: CustomMessage;
|
||||
hookMessage: HookMessage;
|
||||
branchSummary: BranchSummaryMessage;
|
||||
compactionSummary: CompactionSummaryMessage;
|
||||
}
|
||||
}
|
||||
export type ConvertToLlm = (messages: AgentMessage[]) => Message[];
|
||||
|
||||
function getPrunedToolResultContent(message: ToolResultMessage): (TextContent | ImageContent)[] {
|
||||
if (message.prunedAt === undefined) {
|
||||
return message.content;
|
||||
}
|
||||
const textBlocks = message.content.filter((content): content is TextContent => content.type === "text");
|
||||
const text = textBlocks.map(block => block.text).join("") || "[Output truncated]";
|
||||
return [{ type: "text", text }];
|
||||
}
|
||||
|
||||
export function renderBranchSummaryContext(summary: string): string {
|
||||
return prompt.render(BRANCH_SUMMARY_TEMPLATE, { summary });
|
||||
}
|
||||
|
||||
export function renderCompactionSummaryContext(summary: string): string {
|
||||
return prompt.render(COMPACTION_SUMMARY_TEMPLATE, { summary });
|
||||
}
|
||||
|
||||
export function createBranchSummaryMessage(summary: string, fromId: string, timestamp: string): BranchSummaryMessage {
|
||||
return {
|
||||
role: "branchSummary",
|
||||
summary,
|
||||
fromId,
|
||||
timestamp: new Date(timestamp).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createCompactionSummaryMessage(
|
||||
summary: string,
|
||||
tokensBefore: number,
|
||||
timestamp: string,
|
||||
shortSummary?: string,
|
||||
providerPayload?: ProviderPayload,
|
||||
): CompactionSummaryMessage {
|
||||
return {
|
||||
role: "compactionSummary",
|
||||
summary,
|
||||
shortSummary,
|
||||
tokensBefore,
|
||||
providerPayload,
|
||||
timestamp: new Date(timestamp).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createCustomMessage(
|
||||
customType: string,
|
||||
content: string | (TextContent | ImageContent)[],
|
||||
display: boolean,
|
||||
details: unknown | undefined,
|
||||
timestamp: string,
|
||||
attribution?: MessageAttribution,
|
||||
): CustomMessage {
|
||||
return {
|
||||
role: "custom",
|
||||
customType,
|
||||
content,
|
||||
display,
|
||||
details,
|
||||
attribution,
|
||||
timestamp: new Date(timestamp).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
function isCoreCompactionMessage(message: AgentMessage): message is AgentMessage & CoreCompactionMessage {
|
||||
return (
|
||||
message.role === "custom" ||
|
||||
message.role === "hookMessage" ||
|
||||
message.role === "branchSummary" ||
|
||||
message.role === "compactionSummary"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default compaction-domain transformer.
|
||||
*
|
||||
* Embedders with their own app messages should pass a richer transformer through
|
||||
* `SummaryOptions.convertToLlm`; this default intentionally preserves only the
|
||||
* core LLM roles and the compaction messages owned by this package.
|
||||
*/
|
||||
export function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
|
||||
return messages
|
||||
.map((message): Message | undefined => {
|
||||
if (isCoreCompactionMessage(message)) {
|
||||
switch (message.role) {
|
||||
case "custom":
|
||||
case "hookMessage": {
|
||||
const content =
|
||||
typeof message.content === "string"
|
||||
? [{ type: "text" as const, text: message.content }]
|
||||
: message.content;
|
||||
return {
|
||||
role: "user",
|
||||
content,
|
||||
attribution: message.attribution,
|
||||
timestamp: message.timestamp,
|
||||
};
|
||||
}
|
||||
case "branchSummary":
|
||||
return {
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: renderBranchSummaryContext(message.summary),
|
||||
},
|
||||
],
|
||||
attribution: "agent",
|
||||
timestamp: message.timestamp,
|
||||
};
|
||||
case "compactionSummary":
|
||||
return {
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: renderCompactionSummaryContext(message.summary),
|
||||
},
|
||||
],
|
||||
attribution: "agent",
|
||||
providerPayload: message.providerPayload,
|
||||
timestamp: message.timestamp,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
switch (message.role) {
|
||||
case "user":
|
||||
return { ...message, attribution: message.attribution ?? "user" };
|
||||
case "developer":
|
||||
return { ...message, attribution: message.attribution ?? "agent" };
|
||||
case "assistant":
|
||||
return message as AssistantMessage;
|
||||
case "toolResult":
|
||||
return {
|
||||
...message,
|
||||
content: getPrunedToolResultContent(message as ToolResultMessage),
|
||||
attribution: message.attribution ?? "agent",
|
||||
};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter(message => message !== undefined);
|
||||
}
|
||||
export const convertToLlm = defaultConvertToLlm;
|
||||
@@ -12,21 +12,21 @@
|
||||
* with `{ summary, shortSummary? }`.
|
||||
*/
|
||||
|
||||
import { logger } from "@oh-my-pi/pi-utils";
|
||||
import {
|
||||
CODEX_BASE_URL,
|
||||
getCodexAccountId,
|
||||
OPENAI_HEADER_VALUES,
|
||||
OPENAI_HEADERS,
|
||||
} from "./providers/openai-codex/constants";
|
||||
import { parseTextSignature } from "./providers/openai-responses-shared";
|
||||
import { transformMessages } from "./providers/transform-messages";
|
||||
import type { AssistantMessage, Message, Model } from "./types";
|
||||
} from "@oh-my-pi/pi-ai/providers/openai-codex/constants";
|
||||
import { parseTextSignature } from "@oh-my-pi/pi-ai/providers/openai-responses-shared";
|
||||
import { transformMessages } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
||||
import type { AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai/types";
|
||||
import {
|
||||
getOpenAIResponsesHistoryItems,
|
||||
getOpenAIResponsesHistoryPayload,
|
||||
normalizeResponsesToolCallId,
|
||||
} from "./utils";
|
||||
} from "@oh-my-pi/pi-ai/utils";
|
||||
import { logger } from "@oh-my-pi/pi-utils";
|
||||
|
||||
// ============================================================================
|
||||
// Public types
|
||||
@@ -200,13 +200,12 @@ function trimOpenAiCompactInput(
|
||||
const trimmed = [...input];
|
||||
while (trimmed.length > 0 && estimateOpenAiCompactInputTokens(trimmed, instructions) > contextWindow) {
|
||||
const last = trimmed[trimmed.length - 1];
|
||||
if (last?.type === "function_call_output") {
|
||||
if (last?.type === "function_call_output" || last?.type === "custom_tool_call_output") {
|
||||
const callId = typeof last.call_id === "string" ? last.call_id : undefined;
|
||||
const callType = last.type === "custom_tool_call_output" ? "custom_tool_call" : "function_call";
|
||||
trimmed.pop();
|
||||
if (callId) {
|
||||
const matchingCallIndex = trimmed.findLastIndex(
|
||||
item => item.type === "function_call" && item.call_id === callId,
|
||||
);
|
||||
const matchingCallIndex = trimmed.findLastIndex(item => item.type === callType && item.call_id === callId);
|
||||
if (matchingCallIndex >= 0) {
|
||||
trimmed.splice(matchingCallIndex, 1);
|
||||
}
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Tool output pruning utilities for compaction.
|
||||
*/
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
|
||||
import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
|
||||
import type { SessionEntry, SessionMessageEntry } from "../session-manager";
|
||||
import type { AgentMessage } from "../types";
|
||||
import { estimateTokens } from "./compaction";
|
||||
import type { SessionEntry, SessionMessageEntry } from "./entries";
|
||||
|
||||
export interface PruneConfig {
|
||||
/** Keep the most recent tool output tokens intact. */
|
||||
+4
-3
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* Shared utilities for compaction and branch summarization.
|
||||
*/
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
|
||||
import type { Message } from "@oh-my-pi/pi-ai";
|
||||
import { prompt } from "@oh-my-pi/pi-utils";
|
||||
import fileOperationsTemplate from "../../prompts/system/file-operations.md" with { type: "text" };
|
||||
import summarizationSystemPrompt from "../../prompts/system/summarization-system.md" with { type: "text" };
|
||||
import type { AgentMessage } from "../types";
|
||||
import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
|
||||
import summarizationSystemPrompt from "./prompts/summarization-system.md" with { type: "text" };
|
||||
|
||||
// ============================================================================
|
||||
// File Operation Tracking
|
||||
@@ -2,6 +2,8 @@
|
||||
export * from "./agent";
|
||||
// Loop functions
|
||||
export * from "./agent-loop";
|
||||
// Compaction
|
||||
export * from "./compaction";
|
||||
export * from "./harmony-leak";
|
||||
// Proxy utilities
|
||||
export * from "./proxy";
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
AUTO_HANDOFF_THRESHOLD_FOCUS,
|
||||
createHandoffContext,
|
||||
createHandoffFileName,
|
||||
extractHandoffDocument,
|
||||
renderHandoffPrompt,
|
||||
} from "@oh-my-pi/pi-agent-core/compaction/handoff";
|
||||
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
||||
|
||||
function assistantText(text: string): AssistantMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text }],
|
||||
timestamp: Date.now(),
|
||||
provider: "mock",
|
||||
model: "mock",
|
||||
api: "mock",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
};
|
||||
}
|
||||
|
||||
describe("handoff helpers", () => {
|
||||
test("renders custom focus into the handoff prompt", () => {
|
||||
const rendered = renderHandoffPrompt("preserve failing test name");
|
||||
expect(rendered).toContain("Write a handoff document");
|
||||
expect(rendered).toContain("Additional focus: preserve failing test name");
|
||||
});
|
||||
|
||||
test("exports the threshold focus text used by auto-handoff", () => {
|
||||
expect(AUTO_HANDOFF_THRESHOLD_FOCUS).toBe(
|
||||
"Threshold-triggered maintenance: preserve critical implementation state and immediate next actions.",
|
||||
);
|
||||
});
|
||||
|
||||
test("extracts the latest assistant text document", () => {
|
||||
const document = extractHandoffDocument([
|
||||
{ role: "user", content: "older", timestamp: 1 },
|
||||
assistantText("old handoff"),
|
||||
{ role: "user", content: "newer", timestamp: 2 },
|
||||
assistantText("new handoff"),
|
||||
]);
|
||||
expect(document).toBe("new handoff");
|
||||
});
|
||||
|
||||
test("creates the persisted handoff context and filename", () => {
|
||||
expect(createHandoffContext("## Goal\nContinue")).toBe(
|
||||
"<handoff-context>\n## Goal\nContinue\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.",
|
||||
);
|
||||
expect(createHandoffFileName(new Date("2026-05-15T12:34:56.789Z"))).toBe("handoff-2026-05-15T12-34-56-789Z.md");
|
||||
});
|
||||
});
|
||||
+35
-9
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { buildOpenAiNativeHistory, requestOpenAiRemoteCompaction } from "@oh-my-pi/pi-ai/remote-compaction";
|
||||
import { buildOpenAiNativeHistory, requestOpenAiRemoteCompaction } from "@oh-my-pi/pi-agent-core/compaction/openai";
|
||||
import type { AssistantMessage, Model, ToolResultMessage } from "@oh-my-pi/pi-ai/types";
|
||||
import { hookFetch } from "@oh-my-pi/pi-utils";
|
||||
|
||||
@@ -105,21 +105,47 @@ describe("buildOpenAiNativeHistory custom tool calls", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("remote compaction input trimming", () => {
|
||||
test("trims custom tool outputs with their matching custom calls", async () => {
|
||||
let requestInput: Array<Record<string, unknown>> | undefined;
|
||||
using _hook = hookFetch(async (_input, init) => {
|
||||
const body = JSON.parse(String(init?.body)) as { input: Array<Record<string, unknown>> };
|
||||
requestInput = body.input;
|
||||
return Response.json({
|
||||
output: [{ type: "compaction_summary", summary: "compact" }],
|
||||
});
|
||||
});
|
||||
|
||||
await requestOpenAiRemoteCompaction(
|
||||
makeOpenAiModel({ contextWindow: 1 }),
|
||||
"test-key",
|
||||
[
|
||||
{ type: "custom_tool_call", call_id: "call_apply_1", name: "apply_patch", input: "x".repeat(10_000) },
|
||||
{ type: "custom_tool_call_output", call_id: "call_apply_1", output: "patch applied".repeat(1_000) },
|
||||
],
|
||||
"compact",
|
||||
);
|
||||
|
||||
expect(requestInput?.some(item => item.type === "custom_tool_call")).toBe(false);
|
||||
expect(requestInput?.some(item => item.type === "custom_tool_call_output")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestOpenAiRemoteCompaction abort", () => {
|
||||
test("rejects when the abort signal is aborted mid-fetch", async () => {
|
||||
const controller = new AbortController();
|
||||
using _hook = hookFetch((_input, init) => {
|
||||
// Honor the provided abort signal: hang until aborted, then reject.
|
||||
const signal = init?.signal as AbortSignal | undefined;
|
||||
return new Promise<Response>((_resolve, reject) => {
|
||||
if (signal?.aborted) {
|
||||
reject(signal.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError"));
|
||||
return;
|
||||
}
|
||||
signal?.addEventListener("abort", () => {
|
||||
reject(signal.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError"));
|
||||
});
|
||||
const { promise, reject } = Promise.withResolvers<Response>();
|
||||
if (signal?.aborted) {
|
||||
reject(signal.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError"));
|
||||
return promise;
|
||||
}
|
||||
signal?.addEventListener("abort", () => {
|
||||
reject(signal.reason instanceof Error ? signal.reason : new DOMException("Aborted", "AbortError"));
|
||||
});
|
||||
return promise;
|
||||
});
|
||||
|
||||
const promise = requestOpenAiRemoteCompaction(
|
||||
@@ -14,9 +14,6 @@
|
||||
- Added `streamMock` and `registerMockApi` so mock responses can be consumed through `stream()` and the global custom API registry without an external model backend
|
||||
- Added async/sync response scripting with optional context-based handlers, and new `push()`/`reset()` controls to drive multi-turn mock interactions and inspect per-call invocation state
|
||||
- Added support in mock responses for simulating tool calls, usage metadata, custom stop reasons, delayed emissions, and terminal error/aborted outcomes
|
||||
- Added `requestOpenAiRemoteCompaction` and `shouldUseOpenAiRemoteCompaction` to call OpenAI-compatible `/responses/compact` endpoints, with replacement-history and preserve-data handling for remote compaction flows
|
||||
- Added `buildOpenAiNativeHistory`, `getPreservedOpenAiRemoteCompactionData`, and `withOpenAiRemoteCompactionPreserveData` to construct OpenAI-native compaction payloads and persist compaction context across turns
|
||||
- Added generic `requestRemoteCompaction` for POSTing `{systemPrompt,prompt}` to self-hosted compaction endpoints and retrieving `{summary, shortSummary}` responses
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -43,8 +40,6 @@
|
||||
- Fixed recursive `$ref` schemas being treated as universally valid: the validator previously short-circuited on the second occurrence of any ref it had already seen, so nested values violating the referenced sub-schema passed. Cycle detection now keys on (ref, value-identity) pairs with a depth cap for primitive values, so genuine sub-tree violations are still caught.
|
||||
- Fixed JSON Schema meta-validator accepting malformed `if`/`then`/`else` and `dependencies` keywords; each conditional sub-schema is now structurally validated and draft-07 `dependencies` accepts either a schema or a string array of dependent keys.
|
||||
- Fixed Zod-emitted wire schemas dropping null-valued unknown root fields before `preserveUnknownRootFields` could snapshot them, so callers like `task.simple` no longer lose a `schema: null` argument and downstream rejection paths fire as intended.
|
||||
- Fixed `buildOpenAiNativeHistory` to serialize freeform tool calls (e.g. `apply_patch`) as `custom_tool_call` / `custom_tool_call_output` items matching the live Responses provider, instead of demoting them to `function_call` and breaking remote-compaction replay
|
||||
- Threaded an `AbortSignal` through `requestOpenAiRemoteCompaction` and `requestRemoteCompaction` so canceled compaction requests no longer keep the underlying `fetch` alive
|
||||
- Fixed mock provider partial `Usage` to recompute `totalTokens` (and `cost.total` when cost components are supplied) when omitted, instead of reporting 0
|
||||
- Fixed mock provider auto-generated tool-call IDs to use a per-instance counter (now reset by `reset()`), so test order no longer affects IDs across `createMockModel()` instances
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ export * from "./providers/openai-completions";
|
||||
export * from "./providers/openai-responses";
|
||||
export * from "./providers/synthetic";
|
||||
export * from "./rate-limit-utils";
|
||||
export * from "./remote-compaction";
|
||||
export * from "./stream";
|
||||
export * from "./types";
|
||||
export * from "./usage";
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
- Added `pr://<N>/diff`, `pr://<N>/diff/<i>`, and `pr://<N>/diff/all` internal-URL shapes covering changed-file listings, per-file slices, and the full unified diff. They share one `pr-diff` SQLite cache row with the same TTL knobs as `pr://<N>` views (`github.cache.softTtlSec` / `github.cache.hardTtlSec` / `github.cache.enabled`). Single PR views now advertise the diff entry point via a `Diff: pr://<owner>/<repo>/<N>/diff` note. Cache schema bumped to `user_version = 3`; older rows are dropped on first open to add credential-scoped keys and relax the `kind` CHECK constraint.
|
||||
- Added `issue://` / `pr://` internal-URL schemes that share a SQLite-backed cache with the rest of the `github` tool. Single-item reads (`issue://<N>`, `issue://<owner>/<repo>/<N>`) return rendered markdown and within `github.cache.softTtlSec` (default 5 minutes) skip the `gh` round-trip entirely; within `github.cache.hardTtlSec` (default 7 days) the cached row is returned and a background refresh is scheduled. Root and repo-scoped reads (`issue://`, `pr://owner/repo`) issue a live `gh issue list` / `gh pr list` for browsing, supporting `?state=open|closed|all` for issues, `?state=open|closed|merged|all` for PRs, and `?limit=`, `?author=`, `?label=` query params. Rendered output lands in `~/.omp/cache/github-cache.db` (override via `OMP_GITHUB_CACHE_DB`); disable the cache entirely with `github.cache.enabled = false`. Cwd→default-repo lookups (`gh repo view`) are memoized per-process.
|
||||
- Added new `Approve and compact context` choice to the ExitPlanMode approval selector. Sits between `Approve and execute` (purge session) and `Approve and keep context` (full transcript) — runs `/compact` on the plan-mode transcript with a planning-specific summarization hint, then dispatches the plan-approved execution turn so it lands on a fresh cache anchor with the summarized rationale carried over. Cancelling the compaction (Esc or any other abort source) defers the execution dispatch and surfaces a warning so the operator can resubmit manually; non-abort failures proceed best-effort.
|
||||
- Added `CompactionCancelledError` typed sentinel and `CompactionOutcome` (`"ok" | "cancelled" | "failed"`) return type to `@oh-my-pi/pi-coding-agent/session/compaction`. `CommandController.executeCompaction` and `handleCompactCommand` now return the outcome instead of `void` so callers can discriminate user-driven aborts from generic failures without inspecting error messages.
|
||||
- Added `CompactionCancelledError` typed sentinel and `CompactionOutcome` (`"ok" | "cancelled" | "failed"`) return type to `@oh-my-pi/pi-agent-core/compaction`. `CommandController.executeCompaction` and `handleCompactCommand` now return the outcome instead of `void` so callers can discriminate user-driven aborts from generic failures without inspecting error messages.
|
||||
- Added a `credential_disabled` extension event so extensions can subscribe via `pi.on("credential_disabled", handler)` and react when `AuthStorage` automatically soft-disables a credential (e.g. OAuth `invalid_grant`). Replaces the current `agent_end` errorMessage regex pattern downstream extensions have to match against. Handler payload is `{ type, provider, disabledCause }`. `createAgentSession()` subscribes the per-session extension runner to the shared `AuthStorage` via `authStorage.onCredentialDisabled(...)` at the very top of session creation — before any startup model probes run — so events fire on every disable regardless of whether the embedder also has a constructor `onCredentialDisabled` handler attached. The SDK forwards through `ExtensionRunner.emitCredentialDisabled(event)`, which buffers events until `runner.initialize(...)` runs in the mode controller and then flushes them through `emit()` so extension handlers see populated UI/runtime context (rather than the constructor's no-op default with `hasUI=false`, an unset model, and no-op runtime actions). On `session.dispose()` the subscription is unsubscribed; the embedder's constructor-attached listener keeps firing through its own permanent subscription. The outer `createAgentSession()` catch also releases the subscription if startup throws before the dispose-wrap is wired, so repeated retries don't accumulate dead listeners.
|
||||
- Added `omp acp` subcommand for launching as an ACP (Agent Client Protocol) server over stdio
|
||||
- Added explicit `type` discriminators to ACP `initialize` auth methods, including a `terminal` setup method gated on `clientCapabilities.auth.terminal`
|
||||
|
||||
@@ -471,14 +471,6 @@
|
||||
"types": "./src/session/*.ts",
|
||||
"import": "./src/session/*.ts"
|
||||
},
|
||||
"./session/compaction": {
|
||||
"types": "./src/session/compaction/index.ts",
|
||||
"import": "./src/session/compaction/index.ts"
|
||||
},
|
||||
"./session/compaction/*": {
|
||||
"types": "./src/session/compaction/*.ts",
|
||||
"import": "./src/session/compaction/*.ts"
|
||||
},
|
||||
"./slash-commands/*": {
|
||||
"types": "./src/slash-commands/*.ts",
|
||||
"import": "./src/slash-commands/*.ts"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* They can provide custom rendering for tool calls and results in the TUI.
|
||||
*/
|
||||
import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import type { Model, Static, TSchema } from "@oh-my-pi/pi-ai";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import type { Rule } from "../../capability/rule";
|
||||
@@ -13,7 +14,6 @@ import type { Settings } from "../../config/settings";
|
||||
import type { ExecOptions, ExecResult } from "../../exec/exec";
|
||||
import type { HookUIContext } from "../../extensibility/hooks/types";
|
||||
import type { Theme } from "../../modes/theme/theme";
|
||||
import type { CompactionResult } from "../../session/compaction";
|
||||
import type { ReadonlySessionManager } from "../../session/session-manager";
|
||||
import type { TodoItem } from "../../tools/todo-write";
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* - Interact with the user via UI primitives
|
||||
*/
|
||||
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessageEvent,
|
||||
@@ -32,7 +33,6 @@ import type { BashResult } from "../../exec/bash-executor";
|
||||
import type { ExecOptions, ExecResult } from "../../exec/exec";
|
||||
import type { CustomEditor } from "../../modes/components/custom-editor";
|
||||
import type { Theme } from "../../modes/theme/theme";
|
||||
import type { CompactionResult } from "../../session/compaction";
|
||||
import type { CustomMessage } from "../../session/messages";
|
||||
import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
|
||||
import type {
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
* `types.ts` files and is documented there.
|
||||
*/
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { CompactionPreparation, CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import type { ImageContent, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
|
||||
import type { Rule } from "../capability/rule";
|
||||
import type { Goal, GoalModeState } from "../goals/state";
|
||||
import type { CompactionPreparation, CompactionResult } from "../session/compaction";
|
||||
import type { BranchSummaryEntry, CompactionEntry, SessionEntry } from "../session/session-manager";
|
||||
import type { TodoItem } from "../tools/todo-write";
|
||||
|
||||
|
||||
@@ -43,8 +43,6 @@ export * from "./sdk";
|
||||
export * from "./session/agent-session";
|
||||
// Auth and model registry
|
||||
export * from "./session/auth-storage";
|
||||
// Compaction
|
||||
export * from "./session/compaction";
|
||||
export * from "./session/messages";
|
||||
export * from "./session/session-dump-format";
|
||||
export * from "./session/session-manager";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { CompactionCancelledError, type CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import {
|
||||
getEnvApiKey,
|
||||
getProviderDetails,
|
||||
@@ -37,7 +38,6 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
||||
import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
|
||||
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
||||
import type { AuthStorage } from "../../session/auth-storage";
|
||||
import { CompactionCancelledError, type CompactionOutcome } from "../../session/compaction";
|
||||
import type { NewSessionOptions } from "../../session/session-manager";
|
||||
import { outputMeta } from "../../tools/output-meta";
|
||||
import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
||||
import { calculatePromptTokens } from "@oh-my-pi/pi-agent-core/compaction/compaction";
|
||||
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
||||
import { type Component, Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
||||
import { settings } from "../../config/settings";
|
||||
@@ -15,7 +16,6 @@ import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
||||
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
||||
import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
|
||||
import type { AgentSessionEvent } from "../../session/agent-session";
|
||||
import { calculatePromptTokens } from "../../session/compaction/compaction";
|
||||
import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
|
||||
import type { ResolveToolDetails } from "../../tools/resolve";
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { type Agent, type AgentMessage, type AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import {
|
||||
type AssistantMessage,
|
||||
type ImageContent,
|
||||
@@ -46,7 +47,6 @@ import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compa
|
||||
type: "text",
|
||||
};
|
||||
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
||||
import type { CompactionOutcome } from "../session/compaction";
|
||||
import { HistoryStorage } from "../session/history-storage";
|
||||
import type { SessionContext, SessionManager } from "../session/session-manager";
|
||||
import { getRecentSessions } from "../session/session-manager";
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* Spawns the agent in RPC mode and provides a typed API for all operations.
|
||||
*/
|
||||
import type { AgentEvent, AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
|
||||
import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
|
||||
import type { BashResult } from "../../exec/bash-executor";
|
||||
import type { SessionStats } from "../../session/agent-session";
|
||||
import type { CompactionResult } from "../../session/compaction";
|
||||
import type {
|
||||
RpcCommand,
|
||||
RpcExtensionUIRequest,
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
* Responses and events are emitted as JSON lines on stdout.
|
||||
*/
|
||||
import type { AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
|
||||
import type { BashResult } from "../../exec/bash-executor";
|
||||
import type { ContextUsage } from "../../extensibility/extensions/types";
|
||||
import type { SessionStats } from "../../session/agent-session";
|
||||
import type { CompactionResult } from "../../session/compaction";
|
||||
import type { TodoPhase } from "../../tools/todo-write";
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
|
||||
import type { Component, Container, EditorTheme, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
||||
import type { KeybindingsManager } from "../config/keybindings";
|
||||
@@ -13,7 +14,6 @@ import type { CompactOptions } from "../extensibility/extensions/types";
|
||||
import type { MCPManager } from "../mcp";
|
||||
import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
|
||||
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
||||
import type { CompactionOutcome } from "../session/compaction";
|
||||
import type { HistoryStorage } from "../session/history-storage";
|
||||
import type { SessionContext, SessionManager } from "../session/session-manager";
|
||||
import type { LspStartupServerInfo } from "../tools";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { CompactionSettings } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import { effectiveReserveTokens, estimateTokens, resolveThresholdTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import type { Model } from "@oh-my-pi/pi-ai";
|
||||
import { countTokens } from "@oh-my-pi/pi-natives";
|
||||
import { formatNumber } from "@oh-my-pi/pi-utils";
|
||||
import type { Skill } from "../../extensibility/skills";
|
||||
import type { AgentSession } from "../../session/agent-session";
|
||||
import type { CompactionSettings } from "../../session/compaction";
|
||||
import { effectiveReserveTokens, estimateTokens, resolveThresholdTokens } from "../../session/compaction";
|
||||
import type { Tool } from "../../tools";
|
||||
import type { theme as Theme } from "../theme/theme";
|
||||
|
||||
|
||||
@@ -26,6 +26,28 @@ import {
|
||||
type AgentTool,
|
||||
ThinkingLevel,
|
||||
} from "@oh-my-pi/pi-agent-core";
|
||||
import {
|
||||
AUTO_HANDOFF_THRESHOLD_FOCUS,
|
||||
CompactionCancelledError,
|
||||
type CompactionPreparation,
|
||||
type CompactionResult,
|
||||
calculateContextTokens,
|
||||
calculatePromptTokens,
|
||||
collectEntriesForBranchSummary,
|
||||
compact,
|
||||
createHandoffContext,
|
||||
createHandoffFileName,
|
||||
estimateTokens,
|
||||
extractHandoffDocument,
|
||||
generateBranchSummary,
|
||||
type HandoffOptions,
|
||||
type HandoffResult,
|
||||
prepareCompaction,
|
||||
renderHandoffPrompt,
|
||||
type SummaryOptions,
|
||||
shouldCompact,
|
||||
} from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "@oh-my-pi/pi-agent-core/compaction/pruning";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Context,
|
||||
@@ -126,9 +148,7 @@ import { resolveMemoryBackend } from "../memory-backend";
|
||||
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
||||
import type { PlanModeState } from "../plan-mode/state";
|
||||
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
||||
import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
|
||||
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
||||
import handoffDocumentPrompt from "../prompts/system/handoff-document.md" with { type: "text" };
|
||||
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
||||
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
||||
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
||||
@@ -160,21 +180,6 @@ import { extractFileMentions, generateFileMentionMessages } from "../utils/file-
|
||||
import { buildNamedToolChoice } from "../utils/tool-choice";
|
||||
import type { AuthStorage } from "./auth-storage";
|
||||
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
||||
import {
|
||||
CompactionCancelledError,
|
||||
type CompactionPreparation,
|
||||
type CompactionResult,
|
||||
calculateContextTokens,
|
||||
calculatePromptTokens,
|
||||
collectEntriesForBranchSummary,
|
||||
compact,
|
||||
estimateTokens,
|
||||
generateBranchSummary,
|
||||
prepareCompaction,
|
||||
type SummaryOptions,
|
||||
shouldCompact,
|
||||
} from "./compaction";
|
||||
import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "./compaction/pruning";
|
||||
import {
|
||||
type BashExecutionMessage,
|
||||
type CompactionSummaryMessage,
|
||||
@@ -369,17 +374,6 @@ export interface SessionStats {
|
||||
cost: number;
|
||||
}
|
||||
|
||||
/** Result from handoff() */
|
||||
export interface HandoffResult {
|
||||
document: string;
|
||||
savedPath?: string;
|
||||
}
|
||||
|
||||
interface HandoffOptions {
|
||||
autoTriggered?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/** Internal marker for hook messages queued through the agent loop */
|
||||
// ============================================================================
|
||||
// Constants
|
||||
@@ -387,8 +381,6 @@ interface HandoffOptions {
|
||||
|
||||
/** Standard thinking levels */
|
||||
|
||||
const AUTO_HANDOFF_THRESHOLD_FOCUS = prompt.render(autoHandoffThresholdFocusPrompt);
|
||||
|
||||
type RetryFallbackChains = Record<string, string[]>;
|
||||
|
||||
type RetryFallbackRevertPolicy = "never" | "cooldown-expiry";
|
||||
@@ -4999,6 +4991,7 @@ export class AgentSession {
|
||||
promptOverride: compactionPrep.hookPrompt,
|
||||
extraContext: compactionPrep.hookContext,
|
||||
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
||||
convertToLlm,
|
||||
},
|
||||
);
|
||||
summary = result.summary;
|
||||
@@ -5175,9 +5168,7 @@ export class AgentSession {
|
||||
}
|
||||
|
||||
// Build the handoff prompt
|
||||
const handoffPrompt = prompt.render(handoffDocumentPrompt, {
|
||||
additionalFocus: customInstructions,
|
||||
});
|
||||
const handoffPrompt = renderHandoffPrompt(customInstructions);
|
||||
|
||||
// Create a promise that resolves when the agent completes
|
||||
let handoffText: string | undefined;
|
||||
@@ -5198,21 +5189,7 @@ export class AgentSession {
|
||||
if (event.type === "agent_end") {
|
||||
unsubscribe?.();
|
||||
handoffSignal.removeEventListener("abort", onCompletionAbort);
|
||||
// Extract text from the last assistant message
|
||||
const messages = this.agent.state.messages;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg.role === "assistant") {
|
||||
const content = (msg as AssistantMessage).content;
|
||||
const textParts = content
|
||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||
.map(c => c.text);
|
||||
if (textParts.length > 0) {
|
||||
handoffText = textParts.join("\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
handoffText = extractHandoffDocument(this.agent.state.messages);
|
||||
resolveCompletion();
|
||||
}
|
||||
});
|
||||
@@ -5261,15 +5238,14 @@ export class AgentSession {
|
||||
this.#todoReminderCount = 0;
|
||||
|
||||
// Inject the handoff document as a custom message
|
||||
const handoffContent = `<handoff-context>\n${handoffText}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
|
||||
const handoffContent = createHandoffContext(handoffText);
|
||||
this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
|
||||
await this.sessionManager.ensureOnDisk();
|
||||
let savedPath: string | undefined;
|
||||
if (options?.autoTriggered && this.settings.get("compaction.handoffSaveToDisk")) {
|
||||
const artifactsDir = this.sessionManager.getArtifactsDir();
|
||||
if (artifactsDir) {
|
||||
const fileTimestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const handoffFilePath = path.join(artifactsDir, `handoff-${fileTimestamp}.md`);
|
||||
const handoffFilePath = path.join(artifactsDir, createHandoffFileName());
|
||||
try {
|
||||
await Bun.write(handoffFilePath, `${handoffText}\n`);
|
||||
savedPath = handoffFilePath;
|
||||
@@ -5954,6 +5930,7 @@ export class AgentSession {
|
||||
return await compact(preparation, candidate, apiKey, customInstructions, signal, {
|
||||
...options,
|
||||
metadata: this.agent.metadataForProvider(candidate.provider),
|
||||
convertToLlm,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!this.#isCompactionAuthFailure(error)) {
|
||||
@@ -6206,6 +6183,7 @@ export class AgentSession {
|
||||
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
||||
metadata: this.agent.metadataForProvider(candidate.provider),
|
||||
initiatorOverride: "agent",
|
||||
convertToLlm,
|
||||
});
|
||||
break;
|
||||
} catch (error) {
|
||||
@@ -7809,6 +7787,7 @@ export class AgentSession {
|
||||
customInstructions: options.customInstructions,
|
||||
reserveTokens: branchSummarySettings.reserveTokens,
|
||||
metadata: this.agent.metadataForProvider(model.provider),
|
||||
convertToLlm,
|
||||
});
|
||||
this.#branchSummaryAbortController = undefined;
|
||||
if (result.aborted) {
|
||||
|
||||
@@ -5,24 +5,31 @@
|
||||
* and provides a transformer to convert them to LLM-compatible messages.
|
||||
*/
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import {
|
||||
type BranchSummaryMessage,
|
||||
type CompactionSummaryMessage,
|
||||
renderBranchSummaryContext,
|
||||
renderCompactionSummaryContext,
|
||||
} from "@oh-my-pi/pi-agent-core/compaction/messages";
|
||||
import type {
|
||||
AssistantMessage,
|
||||
ImageContent,
|
||||
Message,
|
||||
MessageAttribution,
|
||||
ProviderPayload,
|
||||
TextContent,
|
||||
ToolResultMessage,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { prompt } from "@oh-my-pi/pi-utils";
|
||||
import branchSummaryContextPrompt from "../prompts/compaction/branch-summary-context.md" with { type: "text" };
|
||||
import compactionSummaryContextPrompt from "../prompts/compaction/compaction-summary-context.md" with { type: "text" };
|
||||
|
||||
export {
|
||||
type BranchSummaryMessage,
|
||||
type CompactionSummaryMessage,
|
||||
createBranchSummaryMessage,
|
||||
createCompactionSummaryMessage,
|
||||
} from "@oh-my-pi/pi-agent-core/compaction/messages";
|
||||
|
||||
import type { OutputMeta } from "../tools/output-meta";
|
||||
import { formatOutputNotice } from "../tools/output-meta";
|
||||
|
||||
const COMPACTION_SUMMARY_TEMPLATE = compactionSummaryContextPrompt;
|
||||
const BRANCH_SUMMARY_TEMPLATE = branchSummaryContextPrompt;
|
||||
|
||||
export const SKILL_PROMPT_MESSAGE_TYPE = "skill-prompt";
|
||||
|
||||
export interface SkillPromptDetails {
|
||||
@@ -168,22 +175,6 @@ export interface HookMessage<T = unknown> {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface BranchSummaryMessage {
|
||||
role: "branchSummary";
|
||||
summary: string;
|
||||
fromId: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface CompactionSummaryMessage {
|
||||
role: "compactionSummary";
|
||||
summary: string;
|
||||
shortSummary?: string;
|
||||
tokensBefore: number;
|
||||
providerPayload?: ProviderPayload;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message type for auto-read file mentions via @filepath syntax.
|
||||
*/
|
||||
@@ -254,32 +245,6 @@ export function pythonExecutionToText(msg: PythonExecutionMessage): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
export function createBranchSummaryMessage(summary: string, fromId: string, timestamp: string): BranchSummaryMessage {
|
||||
return {
|
||||
role: "branchSummary",
|
||||
summary,
|
||||
fromId,
|
||||
timestamp: new Date(timestamp).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createCompactionSummaryMessage(
|
||||
summary: string,
|
||||
tokensBefore: number,
|
||||
timestamp: string,
|
||||
shortSummary?: string,
|
||||
providerPayload?: ProviderPayload,
|
||||
): CompactionSummaryMessage {
|
||||
return {
|
||||
role: "compactionSummary",
|
||||
summary,
|
||||
shortSummary,
|
||||
tokensBefore,
|
||||
providerPayload,
|
||||
timestamp: new Date(timestamp).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizeRehydratedOpenAIResponsesAssistantMessage(message: AssistantMessage): AssistantMessage {
|
||||
if (message.providerPayload?.type !== "openaiResponsesHistory") {
|
||||
return message;
|
||||
@@ -376,7 +341,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: prompt.render(BRANCH_SUMMARY_TEMPLATE, { summary: m.summary }),
|
||||
text: renderBranchSummaryContext(m.summary),
|
||||
},
|
||||
],
|
||||
attribution: "agent",
|
||||
@@ -388,7 +353,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: prompt.render(COMPACTION_SUMMARY_TEMPLATE, { summary: m.summary }),
|
||||
text: renderCompactionSummaryContext(m.summary),
|
||||
},
|
||||
],
|
||||
attribution: "agent",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { serializeConversation } from "@oh-my-pi/pi-agent-core/compaction/utils";
|
||||
import type { Message } from "@oh-my-pi/pi-ai";
|
||||
import { serializeConversation } from "../src/session/compaction/utils";
|
||||
|
||||
describe("serializeConversation", () => {
|
||||
it("truncates long tool results in serialized summaries", () => {
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
||||
import * as path from "node:path";
|
||||
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
||||
import * as ai from "@oh-my-pi/pi-ai";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-ai/models";
|
||||
import { encodeTextSignatureV1 } from "@oh-my-pi/pi-ai/providers/openai-responses-shared";
|
||||
import type { AssistantMessage, Model, ProviderPayload, Usage } from "@oh-my-pi/pi-ai/types";
|
||||
import { hookFetch } from "@oh-my-pi/pi-utils";
|
||||
|
||||
import {
|
||||
type CompactionSettings,
|
||||
calculateContextTokens,
|
||||
@@ -16,7 +10,12 @@ import {
|
||||
getLastAssistantUsage,
|
||||
prepareCompaction,
|
||||
shouldCompact,
|
||||
} from "../src/session/compaction/compaction";
|
||||
} from "@oh-my-pi/pi-agent-core/compaction/compaction";
|
||||
import * as ai from "@oh-my-pi/pi-ai";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-ai/models";
|
||||
import { encodeTextSignatureV1 } from "@oh-my-pi/pi-ai/providers/openai-responses-shared";
|
||||
import type { AssistantMessage, Model, ProviderPayload, Usage } from "@oh-my-pi/pi-ai/types";
|
||||
import { hookFetch } from "@oh-my-pi/pi-utils";
|
||||
import {
|
||||
buildSessionContext,
|
||||
type CompactionEntry,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
|
||||
import * as path from "node:path";
|
||||
import { Agent } from "@oh-my-pi/pi-agent-core";
|
||||
import * as compactionModule from "@oh-my-pi/pi-agent-core/compaction";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-ai";
|
||||
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
@@ -8,7 +9,6 @@ import { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
||||
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
||||
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
||||
import { TempDir } from "@oh-my-pi/pi-utils";
|
||||
import * as compactionModule from "../src/session/compaction/compaction";
|
||||
import { assistantMsg, userMsg } from "./utilities";
|
||||
|
||||
describe("issue #986 compaction auth fallback", () => {
|
||||
|
||||
Reference in New Issue
Block a user