import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition, } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { isCodeModeRuntime, resolveCodexRuntimePlanForState } from "../adapter/activation/runtime-plan.ts"; import type { AdapterState } from "../adapter/activation/state.ts"; import { createHistoryNotesTools } from "./history-notes.ts"; import { registerCodeModeExtensionTools } from "../code-mode-extension-tools.ts"; import { toNestedTool } from "../adapter/code-mode/nested-tool-adapter.ts"; import { remoteDeliveryScope, withRemoteContextScope } from "./remote-scope.ts"; import { registerContextSharingService } from "./sharing-service.ts"; import { contextRemainingRenderers, newContextRenderers } from "./rendering.ts"; import { syncAdapter } from "../adapter/activation/activation.ts"; import type { ContextRouter } from "../context-sharing.ts"; import { contextAgentIdentity, contextTargetAgent } from "./agent-identity.ts"; import { hasFreshContextNotes } from "./saved-notes.ts"; const EMPTY_PARAMETERS = Type.Object({}, { additionalProperties: false }); interface NewContextDetails { started: boolean; } export interface ContextRemainingDetails { remainingTokens?: number | undefined; remainingPercent?: number | undefined; windowId?: string | undefined; contextWindow: number; } export function createContextWindowTools( pi: ExtensionAPI, state: AdapterState, ): [ ToolDefinition, ToolDefinition, ] { return [ { name: "new_context", label: "new_context", description: "Start fresh from saved notes; recover earlier conversation through history", parameters: EMPTY_PARAMETERS, ...newContextRenderers, executionMode: "sequential", async execute(_id, _params, signal, _update, ctx) { const plan = assertContextManagementActive(ctx, state); if (plan.newWindowRequests === "off") throw new Error("New window requests are disabled. Continue in this window."); const identity = state.contextWindows.currentIdentity(); if (!identity || !hasFreshContextNotes(ctx.sessionManager.getBranch(), identity.currentWindowId, plan.contextManagementMode, false, _id)) throw new Error("Save your progress with notes, wait for the write to succeed, then call new_context next."); if (state.contextKickoff.hasIdleInput) return { content: [{ type: "text", text: "A new window will open after this checkpoint. Save your notes and finish your reply." }], details: { started: false }, }; const usesPi = plan.compactOnRollover || plan.newWindowRequests === "pi-limits"; const started = usesPi ? state.contextWindows.scheduleRolloverCompaction(plan.newWindowRequests, plan.compactOnRollover) : plan.contextManagementMode === "tree" ? state.contextTree.schedule(ctx) : await state.contextKickoff.startWindow(pi, ctx, { triggerTurn: true, signal, mode: plan.contextManagementMode, trimPreviousWindow: true, }); // Pi's terminate flag only stops a batch when every result terminates. if (started && !usesPi) ctx.abort(); return { ...(started ? { terminate: true } : {}), content: [ { type: "text", text: started ? usesPi ? "Window request scheduled; continuation follows after Pi checks the history." : "A new context window will start without summarizing conversation history." : "A new context window is already scheduled.", }, ], details: { started }, }; }, }, { name: "get_context_remaining", label: "get_context_remaining", description: "Remaining context tokens", parameters: EMPTY_PARAMETERS, ...contextRemainingRenderers, async execute(_id, _params, _signal, _update, ctx) { assertContextManagementActive(ctx, state); const remaining = state.contextWindows.remaining(ctx); return { content: [ { type: "text", text: remaining.remainingTokens === undefined ? "You have unknown tokens left in this context window." : `${remaining.remainingPercent}% remaining (${remaining.remainingTokens} of ${remaining.contextWindow} tokens).`, }, ], details: remaining, }; }, }, ]; } export function registerContextManagementTools( pi: ExtensionAPI, state: AdapterState, ): ContextRouter { const [newContext, getContextRemaining] = createContextWindowTools(pi, state); const plan = (ctx: ExtensionContext) => { const resolved = resolveCodexRuntimePlanForState(ctx, state); return state.externalNotes ? { ...resolved, contextManagementMode: "local" as const, shareSubagentContext: state.config.compaction.shareSubagentContext } : resolved; }; const mode = (ctx: ExtensionContext) => plan(ctx).contextManagementMode; const route = registerContextSharingService(pi, plan, async (ctx, request, signal) => { if (state.externalNotes) { const owner = state.externalNotes.owner; if (!owner.executeShared) throw new Error("Shared notes owner is unavailable"); return owner.executeShared({ namespace: request.namespace, params: request.params }, "shared-context", ctx, signal); } return request.namespace === "history" ? history.execute("shared-context", request.params as Parameters[1], signal, undefined, ctx) : notes.execute("shared-context", request.params as Parameters[1], signal, undefined, ctx); }, () => state.externalNotes?.owner); const [history, notes] = createHistoryNotesTools( pi, mode, (action, path, ctx) => () => state.contextTree.handoff.finishNoteWrite(action, path, ctx), route, ); state.restoreNativeNotesTools = () => { pi.registerTool(newContext); pi.registerTool(getContextRemaining); pi.registerTool(history); pi.registerTool(notes); }; state.restoreNativeNotesTools(); // Reuse the registered routers, including family routing and Tree write completion. const contract = { deferLoading: true, discoverWhenDeferred: true, modelVisibleResult: true, opaqueResultScope: (result: AgentToolResult) => { const details = result.details; return remoteDeliveryScope(details && typeof details === "object" && "codexHistoryNotes" in details ? details.codexHistoryNotes : undefined); } }; const nestedTools = (remote: boolean, protectedResults = remote) => [ toNestedTool(history, "await tools.history()", {}, { ...contract, opaqueResult: protectedResults, allowPlainResult: true }), toNestedTool(notes, "await tools.notes()", {}, { ...contract, prepareInput: prepareNestedNotesInput, opaqueResult: protectedResults, allowPlainResult: true, propagateTermination: true, isContextNoteWrite: (input) => Boolean(input && typeof input === "object" && "action" in input && (input.action === "write_file" || input.action === "append_to_file")), }), ].map((tool) => ({ ...tool, discovery: "help" as const, ...(protectedResults ? { output: "Remote receipts only; contents reach the model automatically, not JavaScript", } : {}), invoke: async (...[input, context, signal]: Parameters) => { if (!context.extensionContext || !plan(context.extensionContext).contextManagementNested) throw new Error("Nested history and notes require active context in Code or Notebook"); if (plan(context.extensionContext).contextManagementRemote !== remote) throw new Error("Context storage changed; start a new exec cell"); if (!protectedResults && route.requiresRemoteScope?.(context.extensionContext)) throw new Error("Shared context changed; start a new exec cell"); return tool.invoke(input, protectedResults ? { ...context, extensionContext: withRemoteContextScope(context.extensionContext, context.opaqueScope) } : context, signal); }, })); const nested = nestedTools(false); const mixedNested = nestedTools(false, true); const remoteNested = nestedTools(true); registerCodeModeExtensionTools(pi, (ctx) => { if (state.externalNotes) return externalNestedTools(state, ctx); return ctx && plan(ctx).contextManagementNested ? plan(ctx).contextManagementRemote ? remoteNested : route.requiresRemoteScope?.(ctx) ? mixedNested : nested : []; }); return route; } export function configureExternalNotesTools( pi: ExtensionAPI, state: AdapterState, tools: readonly ToolDefinition[], ctx: ExtensionContext, contracts: Readonly>, ): void { if (state.externalNotes) { state.externalNotes.tools = tools; state.externalNotes.contracts = contracts; } syncAdapter(pi, ctx, state); } function externalNestedTools(state: AdapterState, ctx?: ExtensionContext) { const external = state.externalNotes; if (!external || !ctx || !isCodeModeRuntime(resolveCodexRuntimePlanForState(ctx, state))) return []; const protectedResults = ctx.model?.api === "openai-codex-responses"; return (external.tools ?? []).filter(tool => tool.name === "notes" || tool.name === "history").map(tool => { const nested = toNestedTool(tool, `await tools.${tool.name}()`, {}, { deferLoading: true, discoverWhenDeferred: true, modelVisibleResult: true, ...(tool.name === "notes" ? { prepareInput: prepareNestedNotesInput } : {}), opaqueResult: protectedResults, allowPlainResult: true, opaqueResultScope: result => { const details = result.details; return remoteDeliveryScope(details && typeof details === "object" && "codexHistoryNotes" in details ? details.codexHistoryNotes : undefined); }, isContextNoteWrite: input => tool.name === "notes" && Boolean(input && typeof input === "object" && "action" in input && (input.action === "write_file" || input.action === "append_to_file") && contextTargetAgent("notes", input as Record, contextAgentIdentity(ctx).agentName) === contextAgentIdentity(ctx).agentName), }); return { ...nested, discovery: "help" as const, discoveryUsage: external.contracts?.[tool.name === "notes" ? "notes" : "history"], invoke: (...[input, context, signal]: Parameters) => nested.invoke(input, protectedResults && !context.resolveOpaqueScope && context.extensionContext ? { ...context, extensionContext: withRemoteContextScope(context.extensionContext, context.opaqueScope) } : context, signal), }; }); } function prepareNestedNotesInput(input: unknown): unknown { if (!input || typeof input !== "object" || Array.isArray(input)) return input; const params = input as Record; const canonical = params["action"] === "search_contents" ? "max_files" : params["action"] === "list_files_by_prefix" ? "max_results" : undefined; if (!canonical) return input; const alias = canonical === "max_files" ? "max_results" : "max_files"; if (!(alias in params)) return input; const prepared = { ...params }; if (!(canonical in prepared)) prepared[canonical] = prepared[alias]; delete prepared[alias]; return prepared; } function assertContextManagementActive( ctx: ExtensionContext, state: AdapterState, ): ReturnType { const plan = resolveCodexRuntimePlanForState(ctx, state); if (!plan.contextManagement) throw new Error( "Context tools require an active Responses adapter with a notes-based continuity strategy", ); return plan; }