From bf601628db3c187478ff853fe33b91cec652355e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 10 Apr 2026 11:49:20 -0400 Subject: [PATCH] refactor(tool): convert codesearch tool internals to Effect (#21811) --- packages/opencode/src/tool/codesearch.ts | 179 +++++++---------------- packages/opencode/src/tool/registry.ts | 3 +- 2 files changed, 58 insertions(+), 124 deletions(-) diff --git a/packages/opencode/src/tool/codesearch.ts b/packages/opencode/src/tool/codesearch.ts index 28dd4eb491..7e167df558 100644 --- a/packages/opencode/src/tool/codesearch.ts +++ b/packages/opencode/src/tool/codesearch.ts @@ -1,132 +1,65 @@ import z from "zod" +import { Effect } from "effect" +import { HttpClient } from "effect/unstable/http" import { Tool } from "./tool" +import * as McpExa from "./mcp-exa" import DESCRIPTION from "./codesearch.txt" -import { abortAfterAny } from "../util/abort" -const API_CONFIG = { - BASE_URL: "https://mcp.exa.ai", - ENDPOINTS: { - CONTEXT: "/mcp", - }, -} as const +export const CodeSearchTool = Tool.defineEffect( + "codesearch", + Effect.gen(function* () { + const http = yield* HttpClient.HttpClient -interface McpCodeRequest { - jsonrpc: string - id: number - method: string - params: { - name: string - arguments: { - query: string - tokensNum: number - } - } -} + return { + description: DESCRIPTION, + parameters: z.object({ + query: z + .string() + .describe( + "Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'", + ), + tokensNum: z + .number() + .min(1000) + .max(50000) + .default(5000) + .describe( + "Number of tokens to return (1000-50000). Default is 5000 tokens. Adjust this value based on how much context you need - use lower values for focused queries and higher values for comprehensive documentation.", + ), + }), + execute: (params: { query: string; tokensNum: number }, ctx: Tool.Context) => + Effect.gen(function* () { + yield* Effect.promise(() => + ctx.ask({ + permission: "codesearch", + patterns: [params.query], + always: ["*"], + metadata: { + query: params.query, + tokensNum: params.tokensNum, + }, + }), + ) -interface McpCodeResponse { - jsonrpc: string - result: { - content: Array<{ - type: string - text: string - }> - } -} + const result = yield* McpExa.call( + http, + "get_code_context_exa", + McpExa.CodeArgs, + { + query: params.query, + tokensNum: params.tokensNum || 5000, + }, + "30 seconds", + ) -export const CodeSearchTool = Tool.define("codesearch", { - description: DESCRIPTION, - parameters: z.object({ - query: z - .string() - .describe( - "Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'", - ), - tokensNum: z - .number() - .min(1000) - .max(50000) - .default(5000) - .describe( - "Number of tokens to return (1000-50000). Default is 5000 tokens. Adjust this value based on how much context you need - use lower values for focused queries and higher values for comprehensive documentation.", - ), - }), - async execute(params, ctx) { - await ctx.ask({ - permission: "codesearch", - patterns: [params.query], - always: ["*"], - metadata: { - query: params.query, - tokensNum: params.tokensNum, - }, - }) - - const codeRequest: McpCodeRequest = { - jsonrpc: "2.0", - id: 1, - method: "tools/call", - params: { - name: "get_code_context_exa", - arguments: { - query: params.query, - tokensNum: params.tokensNum || 5000, - }, - }, - } - - const { signal, clearTimeout } = abortAfterAny(30000, ctx.abort) - - try { - const headers: Record = { - accept: "application/json, text/event-stream", - "content-type": "application/json", - } - - const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.CONTEXT}`, { - method: "POST", - headers, - body: JSON.stringify(codeRequest), - signal, - }) - - clearTimeout() - - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Code search error (${response.status}): ${errorText}`) - } - - const responseText = await response.text() - - // Parse SSE response - const lines = responseText.split("\n") - for (const line of lines) { - if (line.startsWith("data: ")) { - const data: McpCodeResponse = JSON.parse(line.substring(6)) - if (data.result && data.result.content && data.result.content.length > 0) { - return { - output: data.result.content[0].text, - title: `Code search: ${params.query}`, - metadata: {}, - } + return { + output: + result ?? + "No code snippets or documentation found. Please try a different query, be more specific about the library or programming concept, or check the spelling of framework names.", + title: `Code search: ${params.query}`, + metadata: {}, } - } - } - - return { - output: - "No code snippets or documentation found. Please try a different query, be more specific about the library or programming concept, or check the spelling of framework names.", - title: `Code search: ${params.query}`, - metadata: {}, - } - } catch (error) { - clearTimeout() - - if (error instanceof Error && error.name === "AbortError") { - throw new Error("Code search request timed out") - } - - throw error + }).pipe(Effect.runPromise), } - }, -}) + }), +) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 389d3d6cdf..7a70c77298 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -102,6 +102,7 @@ export namespace ToolRegistry { const plan = yield* PlanExitTool const webfetch = yield* WebFetchTool const websearch = yield* WebSearchTool + const codesearch = yield* CodeSearchTool const state = yield* InstanceState.make( Effect.fn("ToolRegistry.state")(function* (ctx) { @@ -170,7 +171,7 @@ export namespace ToolRegistry { fetch: Tool.init(webfetch), todo: Tool.init(todo), search: Tool.init(websearch), - code: Tool.init(CodeSearchTool), + code: Tool.init(codesearch), skill: Tool.init(SkillTool), patch: Tool.init(ApplyPatchTool), question: Tool.init(question),