Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions lib/fetch-wrapper/formats/bedrock.ts
Original file line number Diff line number Diff line change
@@ -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') {
Expand Down Expand Up @@ -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)
},
Expand Down
5 changes: 0 additions & 5 deletions lib/fetch-wrapper/formats/gemini.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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)
},
Expand Down
6 changes: 0 additions & 6 deletions lib/fetch-wrapper/formats/openai-chat.ts
Original file line number Diff line number Diff line change
@@ -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') {
Expand Down Expand Up @@ -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)
},
Expand Down
6 changes: 0 additions & 6 deletions lib/fetch-wrapper/formats/openai-responses.ts
Original file line number Diff line number Diff line change
@@ -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') {
Expand Down Expand Up @@ -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)
},
Expand Down
8 changes: 6 additions & 2 deletions lib/fetch-wrapper/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]'

Expand Down Expand Up @@ -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) ?? []
Expand Down
1 change: 0 additions & 1 deletion lib/fetch-wrapper/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>): number
injectPrunableList(data: any[], injection: string): boolean
Expand Down
7 changes: 0 additions & 7 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? {}
})
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions lib/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
131 changes: 46 additions & 85 deletions lib/state/tool-cache.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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.
Expand Down