Skip to content

Commit bf60162

Browse files
authored
refactor(tool): convert codesearch tool internals to Effect (#21811)
1 parent 00e39d2 commit bf60162

2 files changed

Lines changed: 58 additions & 124 deletions

File tree

Lines changed: 56 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,65 @@
11
import z from "zod"
2+
import { Effect } from "effect"
3+
import { HttpClient } from "effect/unstable/http"
24
import { Tool } from "./tool"
5+
import * as McpExa from "./mcp-exa"
36
import DESCRIPTION from "./codesearch.txt"
4-
import { abortAfterAny } from "../util/abort"
57

6-
const API_CONFIG = {
7-
BASE_URL: "https://mcp.exa.ai",
8-
ENDPOINTS: {
9-
CONTEXT: "/mcp",
10-
},
11-
} as const
8+
export const CodeSearchTool = Tool.defineEffect(
9+
"codesearch",
10+
Effect.gen(function* () {
11+
const http = yield* HttpClient.HttpClient
1212

13-
interface McpCodeRequest {
14-
jsonrpc: string
15-
id: number
16-
method: string
17-
params: {
18-
name: string
19-
arguments: {
20-
query: string
21-
tokensNum: number
22-
}
23-
}
24-
}
25-
26-
interface McpCodeResponse {
27-
jsonrpc: string
28-
result: {
29-
content: Array<{
30-
type: string
31-
text: string
32-
}>
33-
}
34-
}
35-
36-
export const CodeSearchTool = Tool.define("codesearch", {
37-
description: DESCRIPTION,
38-
parameters: z.object({
39-
query: z
40-
.string()
41-
.describe(
42-
"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'",
43-
),
44-
tokensNum: z
45-
.number()
46-
.min(1000)
47-
.max(50000)
48-
.default(5000)
49-
.describe(
50-
"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.",
51-
),
52-
}),
53-
async execute(params, ctx) {
54-
await ctx.ask({
55-
permission: "codesearch",
56-
patterns: [params.query],
57-
always: ["*"],
58-
metadata: {
59-
query: params.query,
60-
tokensNum: params.tokensNum,
61-
},
62-
})
63-
64-
const codeRequest: McpCodeRequest = {
65-
jsonrpc: "2.0",
66-
id: 1,
67-
method: "tools/call",
68-
params: {
69-
name: "get_code_context_exa",
70-
arguments: {
71-
query: params.query,
72-
tokensNum: params.tokensNum || 5000,
73-
},
74-
},
75-
}
76-
77-
const { signal, clearTimeout } = abortAfterAny(30000, ctx.abort)
78-
79-
try {
80-
const headers: Record<string, string> = {
81-
accept: "application/json, text/event-stream",
82-
"content-type": "application/json",
83-
}
13+
return {
14+
description: DESCRIPTION,
15+
parameters: z.object({
16+
query: z
17+
.string()
18+
.describe(
19+
"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'",
20+
),
21+
tokensNum: z
22+
.number()
23+
.min(1000)
24+
.max(50000)
25+
.default(5000)
26+
.describe(
27+
"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.",
28+
),
29+
}),
30+
execute: (params: { query: string; tokensNum: number }, ctx: Tool.Context) =>
31+
Effect.gen(function* () {
32+
yield* Effect.promise(() =>
33+
ctx.ask({
34+
permission: "codesearch",
35+
patterns: [params.query],
36+
always: ["*"],
37+
metadata: {
38+
query: params.query,
39+
tokensNum: params.tokensNum,
40+
},
41+
}),
42+
)
8443

85-
const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.CONTEXT}`, {
86-
method: "POST",
87-
headers,
88-
body: JSON.stringify(codeRequest),
89-
signal,
90-
})
44+
const result = yield* McpExa.call(
45+
http,
46+
"get_code_context_exa",
47+
McpExa.CodeArgs,
48+
{
49+
query: params.query,
50+
tokensNum: params.tokensNum || 5000,
51+
},
52+
"30 seconds",
53+
)
9154

92-
clearTimeout()
93-
94-
if (!response.ok) {
95-
const errorText = await response.text()
96-
throw new Error(`Code search error (${response.status}): ${errorText}`)
97-
}
98-
99-
const responseText = await response.text()
100-
101-
// Parse SSE response
102-
const lines = responseText.split("\n")
103-
for (const line of lines) {
104-
if (line.startsWith("data: ")) {
105-
const data: McpCodeResponse = JSON.parse(line.substring(6))
106-
if (data.result && data.result.content && data.result.content.length > 0) {
107-
return {
108-
output: data.result.content[0].text,
109-
title: `Code search: ${params.query}`,
110-
metadata: {},
111-
}
55+
return {
56+
output:
57+
result ??
58+
"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.",
59+
title: `Code search: ${params.query}`,
60+
metadata: {},
11261
}
113-
}
114-
}
115-
116-
return {
117-
output:
118-
"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.",
119-
title: `Code search: ${params.query}`,
120-
metadata: {},
121-
}
122-
} catch (error) {
123-
clearTimeout()
124-
125-
if (error instanceof Error && error.name === "AbortError") {
126-
throw new Error("Code search request timed out")
127-
}
128-
129-
throw error
62+
}).pipe(Effect.runPromise),
13063
}
131-
},
132-
})
64+
}),
65+
)

packages/opencode/src/tool/registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export namespace ToolRegistry {
102102
const plan = yield* PlanExitTool
103103
const webfetch = yield* WebFetchTool
104104
const websearch = yield* WebSearchTool
105+
const codesearch = yield* CodeSearchTool
105106

106107
const state = yield* InstanceState.make<State>(
107108
Effect.fn("ToolRegistry.state")(function* (ctx) {
@@ -170,7 +171,7 @@ export namespace ToolRegistry {
170171
fetch: Tool.init(webfetch),
171172
todo: Tool.init(todo),
172173
search: Tool.init(websearch),
173-
code: Tool.init(CodeSearchTool),
174+
code: Tool.init(codesearch),
174175
skill: Tool.init(SkillTool),
175176
patch: Tool.init(ApplyPatchTool),
176177
question: Tool.init(question),

0 commit comments

Comments
 (0)