fix(runtime): compact context inside worker

This commit is contained in:
ZacharyZhang-NY committed 2026-04-22 14:19:46 -04:00
1 parent 33c6ede381
commit 6dfa1a851d
13 files changed
+214 -92

No files matched your search

+4
View File
@@ -3,6 +3,7 @@ import type { workerMessageSchema } from "./types";
import { handleRunLog } from "../handlers/run-log-handler";
import { handleRunStatus } from "../handlers/run-status-handler";
import { handleRunFinished } from "../handlers/run-finished-handler";
import { handleContextCompaction } from "../handlers/context-compaction-handler";
import type { HandlerContext, HandlerDeps } from "../handlers/handler-context";
export async function handleWorkerMessage(
@@ -67,6 +68,9 @@ export async function handleWorkerMessage(
}
return;
}
case "context-compaction":
handleContextCompaction(event, deps, context);
return;
case "rpc-request": {
const bridge = deps.getRuntimeBridge?.();
const response = bridge
+10 -1
View File
@@ -24,7 +24,6 @@ export const workerRpcMethodSchema = z.enum([
"prepareRun",
"beforeToolCall",
"afterToolCall",
"transformContext",
"invokeInternalTool",
]);
@@ -65,6 +64,16 @@ export const workerMessageSchema = z.union([
emittedAt: z.string(),
event: z.record(z.string(), z.unknown()).and(z.object({ type: z.string() })),
}),
z.object({
type: z.literal("context-compaction"),
runId: z.string().uuid(),
emittedAt: z.string(),
estimatedTokens: z.number(),
originalMessageCount: z.number().int(),
compactedMessageCount: z.number().int(),
compactedOlderMessageCount: z.number().int(),
retainedRecentMessageCount: z.number().int(),
}),
z.object({
type: z.literal("rpc-request"),
rpcId: z.string(),
@@ -0,0 +1,30 @@
import { persistRunEvent } from "../observability/run-event-recorder";
import type { HandlerContext, HandlerDeps, WorkerMessageEvent } from "./handler-context";
export function handleContextCompaction(
event: Extract<WorkerMessageEvent, { type: "context-compaction" }>,
deps: HandlerDeps,
_context: HandlerContext,
): void {
const runState = deps.db.runs.getRunAuthState(event.runId);
if (!runState) {
deps.logger.warn(`[context-compaction] run ${event.runId} disappeared before telemetry could be recorded.`);
return;
}
persistRunEvent(deps.db, deps.logger, {
companyId: runState.companyId,
runId: event.runId,
agentId: runState.agentId,
eventType: "context_compaction",
level: "info",
message: `Compacted ${event.compactedOlderMessageCount} older messages into a summary.`,
payload: {
emittedAt: event.emittedAt,
estimatedTokens: event.estimatedTokens,
originalMessageCount: event.originalMessageCount,
compactedMessageCount: event.compactedMessageCount,
compactedOlderMessageCount: event.compactedOlderMessageCount,
retainedRecentMessageCount: event.retainedRecentMessageCount,
},
});
}
+10
View File
@@ -77,6 +77,16 @@ export type ChildMessage =
emittedAt: string;
event: { type: string } & Record<string, unknown>;
}
| {
type: "context-compaction";
runId: string;
emittedAt: string;
estimatedTokens: number;
originalMessageCount: number;
compactedMessageCount: number;
compactedOlderMessageCount: number;
retainedRecentMessageCount: number;
}
| { type: "rpc-request"; rpcId: string; runId: string; method: string; payload: unknown }
| { type: "run-agent-state"; runId: string; state: RunAgentLifecycleState }
| { type: "steer-ack"; runId: string; correlationId: string; accepted: boolean };
+12
View File
@@ -178,6 +178,18 @@ export class WorkerRunner {
apiKey: prepared.apiKey,
extraHeaders,
rpc,
onContextCompaction: (telemetry) => {
post({
type: "context-compaction",
runId: telemetry.runId,
emittedAt: telemetry.emittedAt,
estimatedTokens: telemetry.estimatedTokens,
originalMessageCount: telemetry.originalMessageCount,
compactedMessageCount: telemetry.compactedMessageCount,
compactedOlderMessageCount: telemetry.compactedOlderMessageCount,
retainedRecentMessageCount: telemetry.retainedRecentMessageCount,
});
},
});
this.activeAgents.set(job.runId, agent);
post({ type: "run-agent-state", runId: job.runId, state: "agent_active" });
@@ -9,7 +9,7 @@
* none of which the worker should replicate.
*
* This is structurally the same pattern as `beforeToolCall`/
* `afterToolCall`/`transformContext` in `hook-rpc.ts`; what's different
* `afterToolCall` in `hook-rpc.ts`; what's different
* is that those are side-effect hooks around pi's own tools, whereas
* this is a primary-effect tool whose business logic lives entirely in
* main. Keeping the payload shape minimal (name + JSON args) mirrors
+9 -3
View File
@@ -9,8 +9,8 @@
* thrown hooks — a thrown `beforeToolCall` produces an error tool
* result, a thrown `afterToolCall` likewise — so RPC failures degrade
* to a deterministic tool-error instead of crashing the run. The
* `transformContext` contract explicitly forbids throwing, so on RPC
* failure we return the original messages unchanged.
* `transformContext` runs the deterministic compactor in the worker
* process and lets compaction failures fail the run explicitly.
*/
import {
@@ -20,6 +20,7 @@ import {
} from "@mariozechner/pi-agent-core";
import { streamSimple, type Api, type Model } from "@mariozechner/pi-ai";
import { buildAgentHooks } from "@main/agent-runtime/agent-hooks";
import type { ContextCompactionTelemetry } from "@main/agent-runtime/agent-hooks";
import type { WorkerRpcClient } from "../entry/rpc";
// Side-effect import: drags the hook signature contract into the typecheck
// graph so any drift in pi-agent-core hook shapes breaks the build here.
@@ -40,6 +41,7 @@ export interface PiAgentFactoryInput {
apiKey: string;
extraHeaders: Record<string, string> | undefined;
rpc: WorkerRpcClient;
onContextCompaction?: (telemetry: ContextCompactionTelemetry) => void;
}
/** Map the UI-facing `thinkingEffort` string onto pi's `ThinkingLevel`. */
@@ -61,7 +63,11 @@ export function resolveThinkingLevel(effort: string | null | undefined): Thinkin
}
export function createPiAgent(input: PiAgentFactoryInput): Agent {
const hooks = buildAgentHooks({ runId: input.runId, rpc: input.rpc });
const hooks = buildAgentHooks({
runId: input.runId,
rpc: input.rpc,
onContextCompaction: input.onContextCompaction,
});
return new Agent({
initialState: {
systemPrompt: input.systemPrompt,