summaryrefslogtreecommitdiffhomepage
path: root/packages/api
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-02 16:33:02 +0900
committerAdam Malczewski <[email protected]>2026-06-02 16:33:02 +0900
commit0f99a0c92707b44aef7c627012c4711bad5a8efd (patch)
tree4b9fdc0895b30339d6501ed6387b02bc364bcdb6 /packages/api
parentc0c08720cceb75b5e635e71190ae1f956f535133 (diff)
downloaddispatch-0f99a0c92707b44aef7c627012c4711bad5a8efd.tar.gz
dispatch-0f99a0c92707b44aef7c627012c4711bad5a8efd.zip
feat: add search_code tool wrapping the cs code-search engine
Add a dedicated, permission-gated search_code tool that wraps boyter/cs (code spelunker) — a fast, relevance-ranked, structure-aware code search engine — giving agents a better default than grep/find for exploratory 'where is X / how does Y work' searches (ranked results, snippets, ~5x smaller payloads). - packages/core/src/tools/search-code.ts: createSearchCodeTool factory; -f json invocation, workdir path containment, graceful missing-binary handling (DISPATCH_CS_BIN override), readable per-file formatted output. - Wire-up: export from core; register in agent-manager (both child-whitelist and parent perm paths) behind new perm_search_code; add to summon catalog + tools enum; frontend ToolPermissions + settings. - Docker: build a patched, statically-linked cs (pinned v3.1.0 commit) in a golang builder stage and bundle at /usr/local/bin/cs. - docker/cs/luau-declarations.patch: additive Luau declaration table so --only-declarations / definition ranking works for Roblox .luau files (upstream has Lua but not Luau). Applied during the Docker build. - Tests: new search-code.test.ts (stubbed JSON formatting + live-cs integration, skipped when cs absent); agent-manager/routes mocks + perm-gating assertions; loader pass-through. All tests (596), biome, and tsc (core/api/frontend) pass. cs-builder Docker stage verified to build and produce a working patched binary.
Diffstat (limited to 'packages/api')
-rw-r--r--packages/api/src/agent-manager.ts18
-rw-r--r--packages/api/tests/agent-manager.test.ts29
-rw-r--r--packages/api/tests/routes.test.ts8
3 files changed, 54 insertions, 1 deletions
diff --git a/packages/api/src/agent-manager.ts b/packages/api/src/agent-manager.ts
index 85dd160..48b869e 100644
--- a/packages/api/src/agent-manager.ts
+++ b/packages/api/src/agent-manager.ts
@@ -19,6 +19,7 @@ import {
createReadTabTool,
createRetrieveTool,
createRunShellTool,
+ createSearchCodeTool,
createSendToTabTool,
createSkillsWatcher,
createSummonTool,
@@ -75,6 +76,8 @@ const TOOL_DESCRIPTIONS: Record<string, string> = {
write_file: "Write content to a file (creates parent directories if needed)",
run_shell:
"Execute shell commands in the working directory (bash). Returns stdout, stderr, and exit code. Set background=true to run in the background and get a job_id for later retrieval. Do NOT run destructive or irreversible commands unless the user explicitly requests them.",
+ search_code:
+ "Search the codebase by query using the 'cs' code search engine (relevance-ranked, structure-aware). Returns the most relevant files first with matching snippets and line numbers. Better than grep/find for exploratory 'where is X / how does Y work' searches; use run_shell with rg for exhaustive exact-match lists.",
todo: "Create/maintain a todo list to plan and track work. Declarative whole-list write: send the entire list in `todos` each call (it replaces the previous list). Statuses: pending, in_progress, completed, cancelled.",
summon:
"Spawn a child agent to work on a task independently. By default blocks until the child finishes. Set background=true to return immediately with an agent_id for later retrieval.",
@@ -403,9 +406,10 @@ export class AgentManager {
const permSendToTab = getSetting("perm_send_to_tab") === "allow";
const permReadTab = getSetting("perm_read_tab") === "allow";
const permWebSearch = getSetting("perm_web_search") === "allow";
+ const permSearchCode = getSetting("perm_search_code") === "allow";
const permYoutubeTranscribe = getSetting("perm_youtube_transcribe") === "allow";
const sysPrompt = getSetting("system_prompt") ?? "";
- const permKey = `${permRead}:${permEdit}:${permBash}:${permSummon}:${permUserAgent}:${permSendToTab}:${permReadTab}:${permWebSearch}:${permYoutubeTranscribe}:${sysPrompt}`;
+ const permKey = `${permRead}:${permEdit}:${permBash}:${permSummon}:${permUserAgent}:${permSendToTab}:${permReadTab}:${permWebSearch}:${permYoutubeTranscribe}:${permSearchCode}:${sysPrompt}`;
// If the override differs or permissions changed, invalidate the cached agent
if (
@@ -479,6 +483,12 @@ export class AgentManager {
tool: createRunShellTool(workingDirectory, tabAgent.shellStore),
});
}
+ if (allowed.has("search_code")) {
+ toolEntries.push({
+ name: "search_code",
+ tool: createSearchCodeTool(workingDirectory),
+ });
+ }
if (allowed.has("web_search")) {
toolEntries.push({ name: "web_search", tool: createWebSearchTool() });
}
@@ -565,6 +575,12 @@ export class AgentManager {
tool: createRunShellTool(workingDirectory, tabAgent.shellStore),
});
}
+ if (permSearchCode) {
+ toolEntries.push({
+ name: "search_code",
+ tool: createSearchCodeTool(workingDirectory),
+ });
+ }
if (permWebSearch) {
toolEntries.push({ name: "web_search", tool: createWebSearchTool() });
}
diff --git a/packages/api/tests/agent-manager.test.ts b/packages/api/tests/agent-manager.test.ts
index 014022a..970ac1d 100644
--- a/packages/api/tests/agent-manager.test.ts
+++ b/packages/api/tests/agent-manager.test.ts
@@ -419,6 +419,14 @@ vi.mock("@dispatch/core", () => ({
execute: async () => "mock",
};
},
+ createSearchCodeTool(_wd: string) {
+ return {
+ name: "search_code",
+ description: "search code",
+ parameters: { _type: "z.ZodObject", shape: {} },
+ execute: async () => "mock",
+ };
+ },
createYoutubeTranscribeTool() {
return {
name: "youtube_transcribe",
@@ -1441,6 +1449,27 @@ describe("AgentManager", () => {
});
});
+ describe("search_code permission gating", () => {
+ // Reuses the parent-path tool construction to confirm the perm flag wires
+ // the search_code tool on/off correctly.
+ async function toolsForPerms(tabId: string, perms: Record<string, string>): Promise<string[]> {
+ for (const [k, v] of Object.entries(perms)) setFakeSetting(k, v);
+ const manager = new AgentManager();
+ await manager.processMessage(tabId, "go");
+ return constructedAgents.at(-1)?.toolNames ?? [];
+ }
+
+ it("grants search_code when perm_search_code is allowed", async () => {
+ const tools = await toolsForPerms("tab-cs-on", { perm_search_code: "allow" });
+ expect(tools).toContain("search_code");
+ });
+
+ it("omits search_code when perm_search_code is not allowed", async () => {
+ const tools = await toolsForPerms("tab-cs-off", {});
+ expect(tools).not.toContain("search_code");
+ });
+ });
+
// ─── Usage side-channel persistence ──────────────────────────────
//
// `usage` AgentEvents (one per LLM round-trip) are persisted as invisible
diff --git a/packages/api/tests/routes.test.ts b/packages/api/tests/routes.test.ts
index a8db5ce..c85d43d 100644
--- a/packages/api/tests/routes.test.ts
+++ b/packages/api/tests/routes.test.ts
@@ -283,6 +283,14 @@ vi.mock("@dispatch/core", () => ({
execute: async () => "mock",
};
},
+ createSearchCodeTool(_wd: string) {
+ return {
+ name: "search_code",
+ description: "search code",
+ parameters: { _type: "z.ZodObject", shape: {} },
+ execute: async () => "mock",
+ };
+ },
createYoutubeTranscribeTool() {
return {
name: "youtube_transcribe",