From 57bba4e32b9053780040992f4a677f57a256d6b6 Mon Sep 17 00:00:00 2001 From: Helmut Januschka Date: Sat, 3 Jan 2026 09:31:39 +0100 Subject: [PATCH] WIP: Add hook API for dynamic tool control with plan-mode hook example - Add pi.getTools() and pi.setTools(toolNames) to HookAPI - Hooks can now enable/disable tools dynamically - Changes take effect on next agent turn New example hook: plan-mode.ts - Claude Code-style read-only exploration mode - /plan command toggles plan mode on/off - Plan mode tools: read, bash, grep, find, ls - Edit/write tools disabled in plan mode - Injects context telling agent about restrictions - After each response, prompts to execute/stay/refine - State persists across sessions --- packages/coding-agent/CHANGELOG.md | 2 + packages/coding-agent/docs/hooks.md | 23 ++++ .../coding-agent/examples/hooks/README.md | 1 + .../coding-agent/examples/hooks/plan-mode.ts | 119 ++++++++++++++++++ .../coding-agent/src/core/agent-session.ts | 39 +++++- packages/coding-agent/src/core/hooks/index.ts | 2 + .../coding-agent/src/core/hooks/loader.ts | 47 ++++++- .../coding-agent/src/core/hooks/runner.ts | 8 +- packages/coding-agent/src/core/hooks/types.ts | 22 ++++ packages/coding-agent/src/core/sdk.ts | 33 ++++- .../src/modes/interactive/interactive-mode.ts | 2 + packages/coding-agent/src/modes/print-mode.ts | 2 + .../coding-agent/src/modes/rpc/rpc-mode.ts | 2 + .../test/compaction-hooks.test.ts | 10 ++ 14 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 packages/coding-agent/examples/hooks/plan-mode.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3826cb37..299b6f3f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -35,6 +35,8 @@ ### Added - `$ARGUMENTS` syntax for custom slash commands as alternative to `$@` for all arguments joined. Aligns with patterns used by Claude, Codex, and OpenCode. Both syntaxes remain fully supported. ([#418](https://github.com/badlogic/pi-mono/pull/418) by [@skuridin](https://github.com/skuridin)) +- Hook API: `pi.getTools()` and `pi.setTools(toolNames)` for dynamically enabling/disabling tools from hooks +- New example hook: `plan-mode.ts` - Claude Code-style read-only exploration mode with `/plan` command ### Changed diff --git a/packages/coding-agent/docs/hooks.md b/packages/coding-agent/docs/hooks.md index 7313bd48..7f30213c 100644 --- a/packages/coding-agent/docs/hooks.md +++ b/packages/coding-agent/docs/hooks.md @@ -752,6 +752,29 @@ const result = await pi.exec("git", ["status"], { // result.stdout, result.stderr, result.code, result.killed ``` +### pi.getTools() + +Get the names of currently active tools: + +```typescript +const toolNames = pi.getTools(); +// ["read", "bash", "edit", "write"] +``` + +### pi.setTools(toolNames) + +Set the active tools by name. Changes take effect on the next agent turn. + +```typescript +// Switch to read-only mode (plan mode) +pi.setTools(["read", "bash", "grep", "find", "ls"]); + +// Restore full access +pi.setTools(["read", "bash", "edit", "write"]); +``` + +Only built-in tools can be enabled/disabled. Custom tools are always active. Unknown tool names are ignored. + ## Examples ### Permission Gate diff --git a/packages/coding-agent/examples/hooks/README.md b/packages/coding-agent/examples/hooks/README.md index e084206c..cdab51d4 100644 --- a/packages/coding-agent/examples/hooks/README.md +++ b/packages/coding-agent/examples/hooks/README.md @@ -16,6 +16,7 @@ cp permission-gate.ts ~/.pi/agent/hooks/ | Hook | Description | |------|-------------| +| `plan-mode.ts` | Claude Code-style plan mode for read-only exploration with `/plan` command | | `permission-gate.ts` | Prompts for confirmation before dangerous bash commands (rm -rf, sudo, etc.) | | `git-checkpoint.ts` | Creates git stash checkpoints at each turn for code restoration on branch | | `protected-paths.ts` | Blocks writes to protected paths (.env, .git/, node_modules/) | diff --git a/packages/coding-agent/examples/hooks/plan-mode.ts b/packages/coding-agent/examples/hooks/plan-mode.ts new file mode 100644 index 00000000..a8fe073d --- /dev/null +++ b/packages/coding-agent/examples/hooks/plan-mode.ts @@ -0,0 +1,119 @@ +/** + * Plan Mode Hook + * + * Provides a Claude Code-style "plan mode" for safe code exploration. + * When enabled, the agent can only use read-only tools and cannot modify files. + * + * Features: + * - /plan command to toggle plan mode + * - In plan mode: only read, bash (read-only), grep, find, ls are available + * - Injects system context telling the agent about the restrictions + * - After each agent response, prompts to execute the plan or continue planning + * + * Usage: + * 1. Copy this file to ~/.pi/agent/hooks/ or your project's .pi/hooks/ + * 2. Use /plan to toggle plan mode on/off + */ + +import type { HookAPI } from "@mariozechner/pi-coding-agent/hooks"; + +// Read-only tools for plan mode +const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls"]; + +// Full set of tools for normal mode +const NORMAL_MODE_TOOLS = ["read", "bash", "edit", "write"]; + +export default function planModeHook(pi: HookAPI) { + // Track plan mode state + let planModeEnabled = false; + + // Register /plan command + pi.registerCommand("plan", { + description: "Toggle plan mode (read-only exploration)", + handler: async (_args, ctx) => { + planModeEnabled = !planModeEnabled; + + if (planModeEnabled) { + // Switch to read-only tools + pi.setTools(PLAN_MODE_TOOLS); + ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(", ")}`); + } else { + // Switch back to normal tools + pi.setTools(NORMAL_MODE_TOOLS); + ctx.ui.notify("Plan mode disabled. Full access restored."); + } + }, + }); + + // Inject plan mode context at the start of each turn via before_agent_start + pi.on("before_agent_start", async () => { + if (!planModeEnabled) return; + + // Return a message to inject into context + return { + message: { + customType: "plan-mode-context", + content: `[PLAN MODE ACTIVE] +You are in plan mode - a read-only exploration mode for safe code analysis. + +Restrictions: +- You can only use: read, bash (read-only commands), grep, find, ls +- You CANNOT use: edit, write (file modifications are disabled) +- Focus on analysis, planning, and understanding the codebase + +Your task is to explore, analyze, and create a detailed plan. +Do NOT attempt to make changes - just describe what you would do. +When you have a complete plan, I will switch to normal mode to execute it.`, + display: false, // Don't show in TUI, just inject into context + }, + }; + }); + + // After agent finishes, offer to execute the plan + pi.on("agent_end", async (_event, ctx) => { + if (!planModeEnabled) return; + if (!ctx.hasUI) return; + + const choice = await ctx.ui.select("Plan mode - what next?", [ + "Execute the plan", + "Stay in plan mode", + "Refine the plan", + ]); + + if (choice === "Execute the plan") { + // Switch to normal mode + planModeEnabled = false; + pi.setTools(NORMAL_MODE_TOOLS); + ctx.ui.notify("Switched to normal mode. Full access restored."); + + // Set editor text to prompt execution + ctx.ui.setEditorText("Execute the plan you just created. Proceed step by step."); + } else if (choice === "Refine the plan") { + const refinement = await ctx.ui.input("What should be refined?"); + if (refinement) { + ctx.ui.setEditorText(refinement); + } + } + // "Stay in plan mode" - do nothing, just continue + }); + + // Persist plan mode state across sessions + pi.on("session_start", async (_event, ctx) => { + // Check if there's persisted plan mode state + const entries = ctx.sessionManager.getEntries(); + const planModeEntry = entries + .filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "plan-mode") + .pop() as { data?: { enabled: boolean } } | undefined; + + if (planModeEntry?.data?.enabled) { + planModeEnabled = true; + pi.setTools(PLAN_MODE_TOOLS); + } + }); + + // Save state when plan mode changes (via tool_call or other events) + pi.on("turn_start", async () => { + // Persist current state + pi.appendEntry("plan-mode", { enabled: planModeEnabled }); + }); +} diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 5fb852bd..689d6ca7 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -13,7 +13,14 @@ * Modes use this class and add their own I/O layer on top. */ -import type { Agent, AgentEvent, AgentMessage, AgentState, ThinkingLevel } from "@mariozechner/pi-agent-core"; +import type { + Agent, + AgentEvent, + AgentMessage, + AgentState, + AgentTool, + ThinkingLevel, +} from "@mariozechner/pi-agent-core"; import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@mariozechner/pi-ai"; import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai"; import { getAuthPath } from "../config.js"; @@ -75,6 +82,8 @@ export interface AgentSessionConfig { skillsSettings?: Required; /** Model registry for API key resolution and model discovery */ modelRegistry: ModelRegistry; + /** Tool registry for hook getTools/setTools - maps name to tool */ + toolRegistry?: Map; } /** Options for AgentSession.prompt() */ @@ -174,6 +183,9 @@ export class AgentSession { // Model registry for API key resolution private _modelRegistry: ModelRegistry; + // Tool registry for hook getTools/setTools + private _toolRegistry: Map; + constructor(config: AgentSessionConfig) { this.agent = config.agent; this.sessionManager = config.sessionManager; @@ -184,6 +196,7 @@ export class AgentSession { this._customTools = config.customTools ?? []; this._skillsSettings = config.skillsSettings; this._modelRegistry = config.modelRegistry; + this._toolRegistry = config.toolRegistry ?? new Map(); // Always subscribe to agent events for internal handling // (session persistence, hooks, auto-compaction, retry logic) @@ -417,6 +430,30 @@ export class AgentSession { return this.agent.state.isStreaming; } + /** + * Get the names of currently active tools. + * Returns the names of tools currently set on the agent. + */ + getActiveToolNames(): string[] { + return this.agent.state.tools.map((t) => t.name); + } + + /** + * Set active tools by name. + * Only tools in the registry can be enabled. Unknown tool names are ignored. + * Changes take effect on the next agent turn. + */ + setActiveToolsByName(toolNames: string[]): void { + const tools: AgentTool[] = []; + for (const name of toolNames) { + const tool = this._toolRegistry.get(name); + if (tool) { + tools.push(tool); + } + } + this.agent.setTools(tools); + } + /** Whether auto-compaction is currently running */ get isCompacting(): boolean { return this._autoCompactionAbortController !== undefined || this._compactionAbortController !== undefined; diff --git a/packages/coding-agent/src/core/hooks/index.ts b/packages/coding-agent/src/core/hooks/index.ts index 9729ecbc..6d000397 100644 --- a/packages/coding-agent/src/core/hooks/index.ts +++ b/packages/coding-agent/src/core/hooks/index.ts @@ -4,11 +4,13 @@ export { loadHooks, type AppendEntryHandler, type BranchHandler, + type GetToolsHandler, type LoadedHook, type LoadHooksResult, type NavigateTreeHandler, type NewSessionHandler, type SendMessageHandler, + type SetToolsHandler, } from "./loader.js"; export { execCommand, HookRunner, type HookErrorListener } from "./runner.js"; export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper.js"; diff --git a/packages/coding-agent/src/core/hooks/loader.ts b/packages/coding-agent/src/core/hooks/loader.ts index e0bbe9ac..497a6e85 100644 --- a/packages/coding-agent/src/core/hooks/loader.ts +++ b/packages/coding-agent/src/core/hooks/loader.ts @@ -61,6 +61,16 @@ export type SendMessageHandler = ( */ export type AppendEntryHandler = (customType: string, data?: T) => void; +/** + * Get tools handler type for pi.getTools(). + */ +export type GetToolsHandler = () => string[]; + +/** + * Set tools handler type for pi.setTools(). + */ +export type SetToolsHandler = (toolNames: string[]) => void; + /** * New session handler type for ctx.newSession() in HookCommandContext. */ @@ -100,6 +110,10 @@ export interface LoadedHook { setSendMessageHandler: (handler: SendMessageHandler) => void; /** Set the append entry handler for this hook's pi.appendEntry() */ setAppendEntryHandler: (handler: AppendEntryHandler) => void; + /** Set the get tools handler for this hook's pi.getTools() */ + setGetToolsHandler: (handler: GetToolsHandler) => void; + /** Set the set tools handler for this hook's pi.setTools() */ + setSetToolsHandler: (handler: SetToolsHandler) => void; } /** @@ -159,6 +173,8 @@ function createHookAPI( commands: Map; setSendMessageHandler: (handler: SendMessageHandler) => void; setAppendEntryHandler: (handler: AppendEntryHandler) => void; + setGetToolsHandler: (handler: GetToolsHandler) => void; + setSetToolsHandler: (handler: SetToolsHandler) => void; } { let sendMessageHandler: SendMessageHandler = () => { // Default no-op until mode sets the handler @@ -166,6 +182,10 @@ function createHookAPI( let appendEntryHandler: AppendEntryHandler = () => { // Default no-op until mode sets the handler }; + let getToolsHandler: GetToolsHandler = () => []; + let setToolsHandler: SetToolsHandler = () => { + // Default no-op until mode sets the handler + }; const messageRenderers = new Map(); const commands = new Map(); @@ -195,6 +215,12 @@ function createHookAPI( exec(command: string, args: string[], options?: ExecOptions) { return execCommand(command, args, options?.cwd ?? cwd, options); }, + getTools(): string[] { + return getToolsHandler(); + }, + setTools(toolNames: string[]): void { + setToolsHandler(toolNames); + }, } as HookAPI; return { @@ -207,6 +233,12 @@ function createHookAPI( setAppendEntryHandler: (handler: AppendEntryHandler) => { appendEntryHandler = handler; }, + setGetToolsHandler: (handler: GetToolsHandler) => { + getToolsHandler = handler; + }, + setSetToolsHandler: (handler: SetToolsHandler) => { + setToolsHandler = handler; + }, }; } @@ -234,10 +266,15 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo // Create handlers map and API const handlers = new Map(); - const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI( - handlers, - cwd, - ); + const { + api, + messageRenderers, + commands, + setSendMessageHandler, + setAppendEntryHandler, + setGetToolsHandler, + setSetToolsHandler, + } = createHookAPI(handlers, cwd); // Call factory to register handlers factory(api); @@ -251,6 +288,8 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo commands, setSendMessageHandler, setAppendEntryHandler, + setGetToolsHandler, + setSetToolsHandler, }, error: null, }; diff --git a/packages/coding-agent/src/core/hooks/runner.ts b/packages/coding-agent/src/core/hooks/runner.ts index 65dfe3f7..08f0d49f 100644 --- a/packages/coding-agent/src/core/hooks/runner.ts +++ b/packages/coding-agent/src/core/hooks/runner.ts @@ -98,6 +98,10 @@ export class HookRunner { sendMessageHandler: SendMessageHandler; /** Handler for hooks to append entries */ appendEntryHandler: AppendEntryHandler; + /** Handler for getting current tools */ + getToolsHandler: () => string[]; + /** Handler for setting tools */ + setToolsHandler: (toolNames: string[]) => void; /** Handler for creating new sessions (for HookCommandContext) */ newSessionHandler?: NewSessionHandler; /** Handler for branching sessions (for HookCommandContext) */ @@ -132,10 +136,12 @@ export class HookRunner { if (options.navigateTreeHandler) { this.navigateTreeHandler = options.navigateTreeHandler; } - // Set per-hook handlers for pi.sendMessage() and pi.appendEntry() + // Set per-hook handlers for pi.sendMessage(), pi.appendEntry(), pi.getTools(), pi.setTools() for (const hook of this.hooks) { hook.setSendMessageHandler(options.sendMessageHandler); hook.setAppendEntryHandler(options.appendEntryHandler); + hook.setGetToolsHandler(options.getToolsHandler); + hook.setSetToolsHandler(options.setToolsHandler); } this.uiContext = options.uiContext ?? noOpUIContext; this.hasUI = options.hasUI ?? false; diff --git a/packages/coding-agent/src/core/hooks/types.ts b/packages/coding-agent/src/core/hooks/types.ts index 50597c42..9b22eb76 100644 --- a/packages/coding-agent/src/core/hooks/types.ts +++ b/packages/coding-agent/src/core/hooks/types.ts @@ -749,6 +749,28 @@ export interface HookAPI { * Supports timeout and abort signal. */ exec(command: string, args: string[], options?: ExecOptions): Promise; + + /** + * Get the list of currently active tool names. + * @returns Array of tool names (e.g., ["read", "bash", "edit", "write"]) + */ + getTools(): string[]; + + /** + * Set the active tools by name. + * Only built-in tools can be enabled/disabled. Custom tools are always active. + * Changes take effect on the next agent turn. + * + * @param toolNames - Array of tool names to enable (e.g., ["read", "bash", "grep", "find", "ls"]) + * + * @example + * // Switch to read-only mode (plan mode) + * pi.setTools(["read", "bash", "grep", "find", "ls"]); + * + * // Restore full access + * pi.setTools(["read", "bash", "edit", "write"]); + */ + setTools(toolNames: string[]): void; } /** diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 5d0749d2..3870484f 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -29,7 +29,7 @@ * ``` */ -import { Agent, type ThinkingLevel } from "@mariozechner/pi-agent-core"; +import { Agent, type AgentTool, type ThinkingLevel } from "@mariozechner/pi-agent-core"; import type { Model } from "@mariozechner/pi-ai"; import { join } from "path"; import { getAgentDir } from "../config.js"; @@ -349,6 +349,8 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" }, ) => void = () => {}; let appendEntryHandler: (customType: string, data?: any) => void = () => {}; + let getToolsHandler: () => string[] = () => []; + let setToolsHandler: (toolNames: string[]) => void = () => {}; let newSessionHandler: (options?: any) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false }); let branchHandler: (entryId: string) => Promise<{ cancelled: boolean }> = async () => ({ cancelled: false }); let navigateTreeHandler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }> = async () => ({ @@ -376,6 +378,8 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa newSession: (options?: any) => newSessionHandler(options), branch: (entryId: string) => branchHandler(entryId), navigateTree: (targetId: string, options?: any) => navigateTreeHandler(targetId, options), + getTools: () => getToolsHandler(), + setTools: (toolNames: string[]) => setToolsHandler(toolNames), }; def.factory(api as any); @@ -403,6 +407,12 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa setNavigateTreeHandler: (handler: (targetId: string, options?: any) => Promise<{ cancelled: boolean }>) => { navigateTreeHandler = handler; }, + setGetToolsHandler: (handler: () => string[]) => { + getToolsHandler = handler; + }, + setSetToolsHandler: (handler: (toolNames: string[]) => void) => { + setToolsHandler = handler; + }, }; }); } @@ -588,10 +598,28 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} }, })); + // Create tool registry mapping name -> tool (for hook getTools/setTools) + // Cast to AgentTool since createCodingTools actually returns AgentTool[] (type is just Tool[]) + const toolRegistry = new Map(); + for (const tool of builtInTools as AgentTool[]) { + toolRegistry.set(tool.name, tool); + } + for (const tool of wrappedCustomTools as AgentTool[]) { + toolRegistry.set(tool.name, tool); + } + let allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools]; time("combineTools"); + + // Wrap tools with hooks if available + let wrappedToolRegistry: Map | undefined; if (hookRunner) { - allToolsArray = wrapToolsWithHooks(allToolsArray, hookRunner) as Tool[]; + allToolsArray = wrapToolsWithHooks(allToolsArray as AgentTool[], hookRunner); + // Also create a wrapped version of the registry for setTools + wrappedToolRegistry = new Map(); + for (const tool of allToolsArray as AgentTool[]) { + wrappedToolRegistry.set(tool.name, tool); + } } let systemPrompt: string; @@ -670,6 +698,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} customTools: customToolsResult.tools, skillsSettings: settingsManager.getSkillsSettings(), modelRegistry, + toolRegistry: wrappedToolRegistry ?? toolRegistry, }); time("createAgentSession"); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 82bc764b..7effc9f5 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -457,6 +457,8 @@ export class InteractiveMode { appendEntryHandler: (customType, data) => { this.sessionManager.appendCustomEntry(customType, data); }, + getToolsHandler: () => this.session.getActiveToolNames(), + setToolsHandler: (toolNames) => this.session.setActiveToolsByName(toolNames), newSessionHandler: async (options) => { // Stop any loading animation if (this.loadingAnimation) { diff --git a/packages/coding-agent/src/modes/print-mode.ts b/packages/coding-agent/src/modes/print-mode.ts index 56c53420..4da05491 100644 --- a/packages/coding-agent/src/modes/print-mode.ts +++ b/packages/coding-agent/src/modes/print-mode.ts @@ -40,6 +40,8 @@ export async function runPrintMode( appendEntryHandler: (customType, data) => { session.sessionManager.appendCustomEntry(customType, data); }, + getToolsHandler: () => session.getActiveToolNames(), + setToolsHandler: (toolNames) => session.setActiveToolsByName(toolNames), }); hookRunner.onError((err) => { console.error(`Hook error (${err.hookPath}): ${err.error}`); diff --git a/packages/coding-agent/src/modes/rpc/rpc-mode.ts b/packages/coding-agent/src/modes/rpc/rpc-mode.ts index e7392f31..e4d57d9e 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-mode.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-mode.ts @@ -189,6 +189,8 @@ export async function runRpcMode(session: AgentSession): Promise { appendEntryHandler: (customType, data) => { session.sessionManager.appendCustomEntry(customType, data); }, + getToolsHandler: () => session.getActiveToolNames(), + setToolsHandler: (toolNames) => session.setActiveToolsByName(toolNames), uiContext: createHookUIContext(), hasUI: false, }); diff --git a/packages/coding-agent/test/compaction-hooks.test.ts b/packages/coding-agent/test/compaction-hooks.test.ts index 5d5a5130..696e269c 100644 --- a/packages/coding-agent/test/compaction-hooks.test.ts +++ b/packages/coding-agent/test/compaction-hooks.test.ts @@ -80,6 +80,8 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { commands: new Map(), setSendMessageHandler: () => {}, setAppendEntryHandler: () => {}, + setGetToolsHandler: () => {}, + setSetToolsHandler: () => {}, }; } @@ -104,6 +106,8 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { getModel: () => session.model, sendMessageHandler: async () => {}, appendEntryHandler: async () => {}, + getToolsHandler: () => [], + setToolsHandler: () => {}, uiContext: { select: async () => undefined, confirm: async () => false, @@ -267,6 +271,8 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { commands: new Map(), setSendMessageHandler: () => {}, setAppendEntryHandler: () => {}, + setGetToolsHandler: () => {}, + setSetToolsHandler: () => {}, }; createSession([throwingHook]); @@ -314,6 +320,8 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { commands: new Map(), setSendMessageHandler: () => {}, setAppendEntryHandler: () => {}, + setGetToolsHandler: () => {}, + setSetToolsHandler: () => {}, }; const hook2: LoadedHook = { @@ -343,6 +351,8 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => { commands: new Map(), setSendMessageHandler: () => {}, setAppendEntryHandler: () => {}, + setGetToolsHandler: () => {}, + setSetToolsHandler: () => {}, }; createSession([hook1, hook2]);