From f0dd0464d8da35acba775afbaf091b26a7b4e709 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 22 Apr 2026 12:29:00 +0900 Subject: [PATCH] refactor(librarian): teach proper ast_grep_search discipline ast_grep_search was mentioned exactly once in the librarian prompt (bundled as 'grep/ast_grep_search for function/class') with no syntax guidance. When the librarian cloned a repo and tried to match code shape, it fell into the same regex-in-AST trap as the main agent. Two targeted edits, no rewrite of the surrounding request-classification flow: - Phase 1 TYPE B 'Find the implementation' now separates ast_grep_search (code shape) from grep (text/literals) and reminds the LLM that AST patterns use $VAR and $$$ and are not regex. - TOOL REFERENCE adds a dedicated ast_grep_search row with valid examples and the explicit regex anti-pattern list, alongside tightened guidance for grep_app and grep so the LLM picks the right tool for cross-repo vs single-repo, text vs shape. --- .../librarian-ast-grep-discipline.test.ts | 84 +++++++++++++++++++ src/agents/librarian.ts | 16 ++-- 2 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 src/agents/librarian-ast-grep-discipline.test.ts diff --git a/src/agents/librarian-ast-grep-discipline.test.ts b/src/agents/librarian-ast-grep-discipline.test.ts new file mode 100644 index 000000000..482a3c174 --- /dev/null +++ b/src/agents/librarian-ast-grep-discipline.test.ts @@ -0,0 +1,84 @@ +/// + +import { describe, expect, it } from "bun:test" +import { createLibrarianAgent } from "./librarian" + +describe("librarian agent ast-grep discipline", () => { + const model = "openai/gpt-5.4-mini-fast" + + it("#given the prompt #when inspecting TOOL REFERENCE #then documents ast_grep_search", () => { + // given + const agent = createLibrarianAgent(model) + + // when + const prompt = agent.prompt ?? "" + + // then + expect(prompt).toContain("ast_grep_search") + expect(prompt).toContain("$$$") + expect(prompt).toContain("function $NAME($$$) { $$$ }") + }) + + it("#given the prompt #when inspecting #then warns against regex inside ast_grep_search", () => { + // given + const agent = createLibrarianAgent(model) + + // when + const prompt = agent.prompt ?? "" + + // then + expect(prompt.toLowerCase()).toContain("not regex") + expect(prompt).toContain("|") + expect(prompt).toContain(".*") + expect(prompt).toContain("\\w") + }) + + it("#given the prompt #when inspecting #then directs LLM to grep/grep_app for text search", () => { + // given + const agent = createLibrarianAgent(model) + + // when + const prompt = agent.prompt ?? "" + + // then + expect(prompt.toLowerCase()).toContain("for text") + expect(prompt).toContain("grep_app") + }) + + it("#given the prompt #when inspecting Implementation phase #then recommends ast_grep_search for code shape", () => { + // given + const agent = createLibrarianAgent(model) + + // when + const prompt = agent.prompt ?? "" + + // then + expect(prompt).toContain("ast_grep_search for code shape") + }) + + it("#given the prompt #when inspecting #then preserves the evidence + permalink contract", () => { + // given + const agent = createLibrarianAgent(model) + + // when + const prompt = agent.prompt ?? "" + + // then + expect(prompt).toContain("GitHub permalinks") + expect(prompt).toContain("MANDATORY CITATION FORMAT") + }) + + it("#given the prompt #when inspecting #then preserves request classification phases", () => { + // given + const agent = createLibrarianAgent(model) + + // when + const prompt = agent.prompt ?? "" + + // then + expect(prompt).toContain("TYPE A: CONCEPTUAL") + expect(prompt).toContain("TYPE B: IMPLEMENTATION") + expect(prompt).toContain("TYPE C: CONTEXT") + expect(prompt).toContain("TYPE D: COMPREHENSIVE") + }) +}) diff --git a/src/agents/librarian.ts b/src/agents/librarian.ts index 6d02c6cef..2c7bad882 100644 --- a/src/agents/librarian.ts +++ b/src/agents/librarian.ts @@ -141,9 +141,11 @@ Step 2: Get commit SHA for permalinks cd \${TMPDIR:-/tmp}/repo-name && git rev-parse HEAD Step 3: Find the implementation - - grep/ast_grep_search for function/class - - read the specific file - - git blame for context if needed + - ast_grep_search for code shape (function/class/impl declarations) + pattern must be valid source code using $VAR and $$$ - NOT regex + - grep for string literals, log tags, config keys, comments + - read the specific file for surrounding context + - git blame for history if needed Step 4: Construct permalink https://github.com/owner/repo/blob//path/to/file#L10-L20 @@ -246,8 +248,12 @@ https://github.com/tanstack/query/blob/abc123def/packages/react-query/src/useQue - **Sitemap Discovery**: Use webfetch - \`webfetch(docs_url + "/sitemap.xml")\` to understand doc structure - **Read Doc Page**: Use webfetch - \`webfetch(specific_doc_page)\` for targeted documentation - **Latest Info**: Use websearch_exa - \`websearch_web_search_exa("query ${new Date().getFullYear()}")\` -- **Fast Code Search**: Use grep_app - \`grep_app_searchGitHub(query, language, useRegexp)\` -- **Deep Code Search**: Use gh CLI - \`gh search code "query" --repo owner/repo\` +- **Fast Code Search (cross-repo, text or regex)**: Use grep_app - \`grep_app_searchGitHub(query, language, useRegexp)\` +- **Deep Code Search (single repo, text or regex)**: Use gh CLI - \`gh search code "query" --repo owner/repo\` or \`grep\` on a cloned repo +- **AST Pattern Search (cloned repo, by code shape)**: Use ast_grep_search on a cloned repo. Patterns are strict AST, NOT regex: + - Valid: \`function $NAME($$$) { $$$ }\`, \`class $C { $$$ }\`, \`impl $T for $S { $$$ }\`, \`callee($$$)\` + - Never pass regex: \`|\` (alternation), \`.*\` (wildcards), \`\\w\` / \`\\d\` (escapes), \`[a-z]\` (character classes) + - For text search or cross-repo search prefer grep_app or grep - **Clone Repo**: Use gh CLI - \`gh repo clone owner/repo \${TMPDIR:-/tmp}/name -- --depth 1\` - **Issues/PRs**: Use gh CLI - \`gh search issues/prs "query" --repo owner/repo\` - **View Issue/PR**: Use gh CLI - \`gh issue/pr view --repo owner/repo --comments\`