diff --git a/lib/fetch-wrapper/formats/bedrock.ts b/lib/fetch-wrapper/formats/bedrock.ts index 405e9d7..ea1396b 100644 --- a/lib/fetch-wrapper/formats/bedrock.ts +++ b/lib/fetch-wrapper/formats/bedrock.ts @@ -1,7 +1,5 @@ import type { FormatDescriptor, ToolOutput, ToolTracker } from "../types" import type { PluginState } from "../../state" -import type { Logger } from "../../logger" -import { cacheToolParametersFromMessages } from "../../state/tool-cache" function isNudgeMessage(msg: any, nudgeText: string): boolean { if (typeof msg.content === 'string') { @@ -88,28 +86,6 @@ export const bedrockFormat: FormatDescriptor = { return body.messages }, - cacheToolParameters(data: any[], state: PluginState, logger?: Logger): void { - // Extract toolUseId and tool name from assistant toolUse blocks - for (const m of data) { - if (m.role === 'assistant' && Array.isArray(m.content)) { - for (const block of m.content) { - if (block.toolUse && block.toolUse.toolUseId) { - const toolUseId = block.toolUse.toolUseId.toLowerCase() - state.toolParameters.set(toolUseId, { - tool: block.toolUse.name, - parameters: block.toolUse.input - }) - logger?.debug("bedrock", "Cached tool parameters", { - toolUseId, - toolName: block.toolUse.name - }) - } - } - } - } - cacheToolParametersFromMessages(data, state, logger) - }, - injectSynth(data: any[], instruction: string, nudgeText: string): boolean { return injectSynth(data, instruction, nudgeText) }, diff --git a/lib/fetch-wrapper/formats/gemini.ts b/lib/fetch-wrapper/formats/gemini.ts index 4c0508b..8e2f569 100644 --- a/lib/fetch-wrapper/formats/gemini.ts +++ b/lib/fetch-wrapper/formats/gemini.ts @@ -1,6 +1,5 @@ import type { FormatDescriptor, ToolOutput, ToolTracker } from "../types" import type { PluginState } from "../../state" -import type { Logger } from "../../logger" function isNudgeContent(content: any, nudgeText: string): boolean { if (Array.isArray(content.parts) && content.parts.length === 1) { @@ -72,10 +71,6 @@ export const geminiFormat: FormatDescriptor = { return body.contents }, - cacheToolParameters(_data: any[], _state: PluginState, _logger?: Logger): void { - // No-op: Gemini tool parameters are captured via message events in hooks.ts - }, - injectSynth(data: any[], instruction: string, nudgeText: string): boolean { return injectSynth(data, instruction, nudgeText) }, diff --git a/lib/fetch-wrapper/formats/openai-chat.ts b/lib/fetch-wrapper/formats/openai-chat.ts index 481da4d..141f03f 100644 --- a/lib/fetch-wrapper/formats/openai-chat.ts +++ b/lib/fetch-wrapper/formats/openai-chat.ts @@ -1,7 +1,5 @@ import type { FormatDescriptor, ToolOutput, ToolTracker } from "../types" import type { PluginState } from "../../state" -import type { Logger } from "../../logger" -import { cacheToolParametersFromMessages } from "../../state/tool-cache" function isNudgeMessage(msg: any, nudgeText: string): boolean { if (typeof msg.content === 'string') { @@ -79,10 +77,6 @@ export const openaiChatFormat: FormatDescriptor = { return body.messages }, - cacheToolParameters(data: any[], state: PluginState, logger?: Logger): void { - cacheToolParametersFromMessages(data, state, logger) - }, - injectSynth(data: any[], instruction: string, nudgeText: string): boolean { return injectSynth(data, instruction, nudgeText) }, diff --git a/lib/fetch-wrapper/formats/openai-responses.ts b/lib/fetch-wrapper/formats/openai-responses.ts index 96ee858..549c56b 100644 --- a/lib/fetch-wrapper/formats/openai-responses.ts +++ b/lib/fetch-wrapper/formats/openai-responses.ts @@ -1,7 +1,5 @@ import type { FormatDescriptor, ToolOutput, ToolTracker } from "../types" import type { PluginState } from "../../state" -import type { Logger } from "../../logger" -import { cacheToolParametersFromInput } from "../../state/tool-cache" function isNudgeItem(item: any, nudgeText: string): boolean { if (typeof item.content === 'string') { @@ -66,10 +64,6 @@ export const openaiResponsesFormat: FormatDescriptor = { return body.input }, - cacheToolParameters(data: any[], state: PluginState, logger?: Logger): void { - cacheToolParametersFromInput(data, state, logger) - }, - injectSynth(data: any[], instruction: string, nudgeText: string): boolean { return injectSynth(data, instruction, nudgeText) }, diff --git a/lib/fetch-wrapper/handler.ts b/lib/fetch-wrapper/handler.ts index 1231608..cd9b683 100644 --- a/lib/fetch-wrapper/handler.ts +++ b/lib/fetch-wrapper/handler.ts @@ -2,6 +2,7 @@ import type { FetchHandlerContext, FetchHandlerResult, FormatDescriptor, PrunedI import { type PluginState, ensureSessionRestored } from "../state" import type { Logger } from "../logger" import { buildPrunableToolsList, buildEndInjection } from "./prunable-list" +import { syncToolParametersFromOpenCode } from "../state/tool-cache" const PRUNED_CONTENT_MESSAGE = '[Output removed to save context - information superseded or no longer needed]' @@ -65,14 +66,17 @@ export async function handleFormat( let modified = false - format.cacheToolParameters(data, ctx.state, ctx.logger) + // Sync tool parameters from OpenCode's session API (single source of truth) + const sessionId = ctx.state.lastSeenSessionId + if (sessionId) { + await syncToolParametersFromOpenCode(ctx.client, sessionId, ctx.state, ctx.logger) + } if (ctx.config.strategies.onTool.length > 0) { if (format.injectSynth(data, ctx.prompts.synthInstruction, ctx.prompts.nudgeInstruction)) { modified = true } - const sessionId = ctx.state.lastSeenSessionId if (sessionId) { const toolIds = Array.from(ctx.state.toolParameters.keys()) const alreadyPruned = ctx.state.prunedIds.get(sessionId) ?? [] diff --git a/lib/fetch-wrapper/types.ts b/lib/fetch-wrapper/types.ts index bde8f18..c7ebc68 100644 --- a/lib/fetch-wrapper/types.ts +++ b/lib/fetch-wrapper/types.ts @@ -13,7 +13,6 @@ export interface FormatDescriptor { name: string detect(body: any): boolean getDataArray(body: any): any[] | undefined - cacheToolParameters(data: any[], state: PluginState, logger?: Logger): void injectSynth(data: any[], instruction: string, nudgeText: string): boolean trackNewToolResults(data: any[], tracker: ToolTracker, protectedTools: Set): number injectPrunableList(data: any[], injection: string): boolean diff --git a/lib/hooks.ts b/lib/hooks.ts index 1f54f34..5ee36b7 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -121,13 +121,6 @@ export function createChatParamsHandler( toolCallsByName.set(toolName, []) } toolCallsByName.get(toolName)!.push(callId) - - if (!state.toolParameters.has(callId)) { - state.toolParameters.set(callId, { - tool: part.tool, - parameters: part.input ?? {} - }) - } } } } diff --git a/lib/state/index.ts b/lib/state/index.ts index 2808cb4..a3c2584 100644 --- a/lib/state/index.ts +++ b/lib/state/index.ts @@ -15,9 +15,13 @@ export interface PluginState { lastSeenSessionId: string | null } +export type ToolStatus = "pending" | "running" | "completed" | "error" + export interface ToolParameterEntry { tool: string parameters: any + status?: ToolStatus + error?: string } export interface ModelInfo { diff --git a/lib/state/tool-cache.ts b/lib/state/tool-cache.ts index eadaba3..8d2f8b2 100644 --- a/lib/state/tool-cache.ts +++ b/lib/state/tool-cache.ts @@ -1,111 +1,72 @@ -import type { PluginState } from "./index" +import type { PluginState, ToolStatus } from "./index" import type { Logger } from "../logger" +/** Maximum number of entries to keep in the tool parameters cache */ +const MAX_TOOL_CACHE_SIZE = 500 + /** - * Cache tool parameters from OpenAI Chat Completions and Anthropic style messages. - * Extracts tool call IDs and their parameters from assistant messages. - * - * Supports: - * - OpenAI format: message.tool_calls[] with id, function.name, function.arguments - * - Anthropic format: message.content[] with type='tool_use', id, name, input + * Sync tool parameters from OpenCode's session.messages() API. + * This is the single source of truth for tool parameters, replacing + * format-specific parsing from LLM API requests. */ -export function cacheToolParametersFromMessages( - messages: any[], +export async function syncToolParametersFromOpenCode( + client: any, + sessionId: string, state: PluginState, logger?: Logger -): void { - let openaiCached = 0 - let anthropicCached = 0 +): Promise { + try { + const messagesResponse = await client.session.messages({ + path: { id: sessionId }, + query: { limit: 100 } + }) + const messages = messagesResponse.data || messagesResponse - for (const message of messages) { - if (message.role !== 'assistant') { - continue + if (!Array.isArray(messages)) { + return } - if (Array.isArray(message.tool_calls)) { - for (const toolCall of message.tool_calls) { - if (!toolCall.id || !toolCall.function) { - continue - } + let synced = 0 - try { - const params = typeof toolCall.function.arguments === 'string' - ? JSON.parse(toolCall.function.arguments) - : toolCall.function.arguments - state.toolParameters.set(toolCall.id.toLowerCase(), { - tool: toolCall.function.name, - parameters: params - }) - openaiCached++ - } catch (error) { - } - } - } + for (const msg of messages) { + if (!msg.parts) continue - if (Array.isArray(message.content)) { - for (const part of message.content) { - if (part.type !== 'tool_use' || !part.id || !part.name) { - continue - } + for (const part of msg.parts) { + if (part.type !== "tool" || !part.callID) continue - state.toolParameters.set(part.id.toLowerCase(), { - tool: part.name, - parameters: part.input ?? {} + const id = part.callID.toLowerCase() + + // Skip if already cached (optimization) + if (state.toolParameters.has(id)) continue + + const status = part.state?.status as ToolStatus | undefined + state.toolParameters.set(id, { + tool: part.tool, + parameters: part.state?.input ?? {}, + status, + error: status === "error" ? part.state?.error : undefined, }) - anthropicCached++ + synced++ } } - } - - if (logger && (openaiCached > 0 || anthropicCached > 0)) { - logger.debug("tool-cache", "Cached tool parameters from messages", { - openaiFormat: openaiCached, - anthropicFormat: anthropicCached, - totalCached: state.toolParameters.size - }) - } -} - -/** - * Cache tool parameters from OpenAI Responses API format. - * Extracts from input array items with type='function_call'. - */ -export function cacheToolParametersFromInput( - input: any[], - state: PluginState, - logger?: Logger -): void { - let cached = 0 - for (const item of input) { - if (item.type !== 'function_call' || !item.call_id || !item.name) { - continue - } + trimToolParametersCache(state) - try { - const params = typeof item.arguments === 'string' - ? JSON.parse(item.arguments) - : item.arguments - state.toolParameters.set(item.call_id.toLowerCase(), { - tool: item.name, - parameters: params + if (logger && synced > 0) { + logger.debug("tool-cache", "Synced tool parameters from OpenCode", { + sessionId: sessionId.slice(0, 8), + synced, + totalCached: state.toolParameters.size }) - cached++ - } catch (error) { } - } - - if (logger && cached > 0) { - logger.debug("tool-cache", "Cached tool parameters from input", { - responsesApiFormat: cached, - totalCached: state.toolParameters.size + } catch (error) { + logger?.warn("tool-cache", "Failed to sync tool parameters from OpenCode", { + sessionId: sessionId.slice(0, 8), + error: error instanceof Error ? error.message : String(error) }) } } -/** Maximum number of entries to keep in the tool parameters cache */ -const MAX_TOOL_CACHE_SIZE = 500 - /** * Trim the tool parameters cache to prevent unbounded memory growth. * Uses FIFO eviction - removes oldest entries first.