loop: add knowledge_base tool for on-demand information
The knowledge_base tool provides a way for agents to access specialized information
when needed. Initial topics include:
- sketch: how to use Sketch, including SSH, secrets, and file management
- go_iterators: information about Go's iterator feature added in Go 1.22
Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/claudetool/kb/sketch.txt b/claudetool/kb/sketch.txt
new file mode 100644
index 0000000..09373eb
--- /dev/null
+++ b/claudetool/kb/sketch.txt
@@ -0,0 +1,128 @@
+<about_sketch>
+
+## What is Sketch?
+Sketch is an agentic coding assistant and collaborative environment that connects users with AI to assist with programming tasks. It is capable of writing code, debugging, investigating codebases, answering questions, making plans, making diagrams using mermaid, and discussing coding-related topics. Sketch supports almost all programming languages, with enhanced features for Go, and provides a containerized environment where code can be safely developed, tested, and executed.
+
+## How to Use Sketch
+- Interact with Sketch by describing what you want it to do or answer.
+- Sketch runs tools and processes your request, mostly autonomously. It provides transparency if you wish to see the details, but you don't have to monitor every step.
+- Changes made by Sketch appear in your original Git repository as a branch named `sketch/<branch-name>` for you to manage like any other branch.
+- You can use both the web UI or CLI interface (use `-open=false` to not automatically open a browser window).
+- Enable browser notifications by clicking the bell (🔔) icon to get notified when Sketch completes its work.
+
+## Environment and Security
+- Sketch automatically starts a Docker container and does all operations inside it for security and isolation (unless run with -unsafe).
+- You can SSH into the container or use VS Code's SSH support to edit code directly.
+- Containers have internet access for downloading packages, tools, and other external resources.
+- For exposing services, you can use port forwarding through the Sketch interface.
+- When you start Sketch, it creates a Dockerfile, builds it, copies your repository into it, and starts a Docker container with the "inside" Sketch running inside.
+- This design lets you **run multiple sketches in parallel** since they each have their own sandbox. It also lets Sketch work without worry: it can trash its own container, but it can't trash your machine.
+
+## SSH Access
+
+NB: throughout this document, all ssh commands have been modified to be accurate for this Sketch session.
+This SSH information is also available in both the web (info button in top toolbar) and terminal UIs (printed at beginning of session).
+
+- To SSH into your Sketch container, use the following command:
+ ```
+ ssh sketch-{{ .SessionID }}
+ ```
+- The SSH session provides full terminal access to your container environment.
+- You can use this to run commands, edit files, or use tools not available in the web interface.
+- You can also connect with VS Code using:
+ ```
+ code --remote ssh-remote+root@sketch-{{ .SessionID }} /app -n
+ ```
+- Or to open VS Code directly, click this link:
+ ```
+ vscode://vscode-remote/ssh-remote+root@sketch-{{ .SessionID }}/app?windowId=_blank
+ ```
+- You can forward ports from the container to your machine. Example:
+ ```
+ ssh -L8000:localhost:8888 sketch-{{ .SessionID }} go run ./cmd/server
+ ```
+ This makes `http://localhost:8000/` on your machine point to `localhost:8888` inside the container.
+- As an alternative to ssh, you may use the "Terminal" tab in the web UI, which runs inside the container.
+ In the terminal UI, you may prefix any line with an exclamation mark for it to be interpreted as a command (e.g. !ls).
+
+## Git Integration
+{{ $branch := "foo" }}
+{{ if .Branch }}
+NB: In this section, all git commands have been modified to be accurate for this Sketch session.
+{{ $branch = .Branch}}
+{{ end }}
+
+- Sketch is trained to make git commits. When those happen, they are automatically pushed to the git repository where you started sketch with branch names `sketch/*`.
+- Use `git branch -a --sort=creatordate | grep sketch/ | tail` to find Sketch branches.
+- The UI keeps track of the latest branch it pushed and displays it prominently.
+- You can integrate Sketch's changes using:
+ ```
+ git cherry-pick $(git merge-base origin/main sketch/{{$branch}})
+ git merge sketch/{{$branch}}
+ git reset --hard sketch/{{$branch}}
+ ```
+- You can ask Sketch to `git fetch sketch-host` and rebase onto another commit.
+- Sketch is good at helping you rebase, merge/squash commits, rewrite commit messages, and other Git operations.
+
+## Reviewing Diffs
+- The diff view shows you changes since Sketch started.
+- Leaving comments on lines adds them to the chat box.
+- When you hit Send (at the bottom of the page), Sketch goes to work addressing your comments.
+
+## Web Browser Tools
+- The container can launch a browser to take screenshots, useful for web development.
+- The agent can view those screenshots, to work iteratively.
+- There are tools both for taking screenshots and "reading images" (which sends the image to the LLM).
+- This functionality is helpful when working on web pages to see what in-progress changes look like.
+
+## Secrets and Credentials
+- Users can explicitly forward environment variables into the container using the `sketch.envfwd` configuration in their Git repository:
+ ```bash
+ git config --local --add sketch.envfwd ENV_VAR_TO_MAKE_AVAILABLE_INSIDE_CONTAINER
+ ```
+- Avoid sharing highly sensitive credentials.
+
+## Web dev in Sketch
+- The container can launch a browser to take screenshots, useful for web development.
+- The agent can view those screenshots, to work iteratively.
+
+## File Management
+- Files created in Sketch persist for the duration of your session.
+- You can add files by adding them to git before starting sketch, or through SSH using tools like `scp`.
+- To upload a file to the container:
+ ```
+ scp myfile.txt root@sketch-{{ .SessionID }}:~/myfile.txt
+ ```
+- To download a file from the container:
+ ```
+ scp root@sketch-{{ .SessionID }}:~/myfile.txt ./myfile.txt
+ ```
+
+## Container Lifecycle
+- Containers remain active for the duration of your session.
+- Each session is independent.
+- Spinning up multiple sketch sessions concurrently is not just supported, it is recommended.
+
+## Customization and Preferences
+- Sketch can remember preferences either by asking it to or by editing `dear_llm.md` files in the root directory or subdirectories.
+- Use these files for high-level guidance and repository-specific information.
+- Sketch also respects most existing claude.md files and cursorrules files.
+- dear_llm.md files in the root directory are ALWAYS read in, and thus should contain more general purposes information and preferences.
+- Subdirectory dear_llm.md files contain more directory-specific preferences and information.
+
+## Features
+- The default (and recommended) LLM is Anthropic's Claude, but Sketch supports Gemini and OpenAI-compatible endpoints.
+
+## Updates and Support
+- Sketch is rapidly evolving, so staying updated with the latest version is strongly recommended.
+- For most issues, the first resolution is to upgrade sketch.
+- There's a [Sketch Discord channel](https://discord.gg/6w9qNRUDzS) for help, tips, feature requests, gossip, and bug reports.
+- Users are also invited to file [GitHub issues](https://github.com/boldsoftware/sketch/issues) for bugs and feature requests.
+- Sketch's client code is open source and can be found in the [GitHub repository](https://github.com/boldsoftware/sketch).
+
+## Tips and Best Practices
+- Initial prompts can often be surprisingly short. More up-front information typically yields better results, but the marginal value of extra information diminishes rapidly. Instead, let sketch do the work of figuring it out.
+- In the diff view, you can leave comments about the code for sketch to iterate on.
+- It is often easier and faster to learn from and abandon an existing sketch session and start a new one that includes things you learned from previous attempts, rather than iterate multiple times in a single sketch session.
+
+</about_sketch>
diff --git a/claudetool/kb/strings_lines.txt b/claudetool/kb/strings_lines.txt
new file mode 100644
index 0000000..4d237ae
--- /dev/null
+++ b/claudetool/kb/strings_lines.txt
@@ -0,0 +1,23 @@
+`strings.Lines` — added in Go 1.24
+
+- `func Lines(s string) iter.Seq[string]`
+- Lazily yields successive newline-terminated substrings of `s`. *Includes* the exact trailing `\n`/`\r\n`; if the input ends without a newline the final element is unterminated.
+- Zero copying: each value is just a slice header into `s`, so iteration is O(1) memory.
+
+Idiomatic loop:
+
+```go
+for line := range strings.Lines(buf) {
+ handle(line)
+}
+```
+
+### How it differs from common alternatives
+
+- strings.Split – eager `[]string` allocation; caller chooses the separator (newline usually `"\n"`); separator *removed*, so you lose information about line endings and trailing newline presence.
+- strings.SplitSeq – same lazy `iter.Seq[string]` style as `Lines`, but again the caller supplies the separator and it is dropped; use this for arbitrary delimiters.
+- bufio.Scanner – token-oriented reader for any `io.Reader`. Default split function treats `\n` as the delimiter and strips it, so newline bytes are not preserved. Each token is copied into new memory, and there is a 64 KiB default token-size cap (adjustable). Scanner is the choice when the data is coming from a stream rather than an in-memory string.
+
+Use `strings.Lines` by default when the data is already in a string. (bytes.Lines provides the []byte equivalent.)
+
+Fallback to `Split` if you need a slice of lines in memory, to `SplitSeq` for lazy iteration over other separators, and to `bufio.Scanner` for streaming input where newline bytes are irrelevant.
diff --git a/claudetool/knowledge_base.go b/claudetool/knowledge_base.go
new file mode 100644
index 0000000..21034ee
--- /dev/null
+++ b/claudetool/knowledge_base.go
@@ -0,0 +1,110 @@
+package claudetool
+
+import (
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "strings"
+ "text/template"
+
+ "sketch.dev/llm"
+ "sketch.dev/llm/conversation"
+)
+
+// KnowledgeBase provides on-demand specialized knowledge to the agent.
+var KnowledgeBase = &llm.Tool{
+ Name: kbName,
+ Description: kbDescription,
+ InputSchema: llm.MustSchema(kbInputSchema),
+ Run: kbRun,
+}
+
+// TODO: BYO knowledge bases? could do that for strings.Lines, for example.
+// TODO: support Q&A mode instead of reading full text in?
+
+const (
+ kbName = "knowledge_base"
+ kbDescription = `Retrieve specialized information that you need but don't have in your context.
+
+When to use this tool:
+
+For the "sketch" topic:
+- The user is asking how to USE Sketch itself (not asking Sketch to perform a task)
+- The user has questions about Sketch functionality, setup, or capabilities
+- The user needs help with Sketch-specific concepts like running commands, secrets management, git integration
+- The query is about "How do I do X in Sketch?" or "Is it possible to Y in Sketch?" or just "Help"
+- The user is confused about how a Sketch feature works or how to access it
+
+For the "strings_lines" topic:
+- Any mentions of strings.Lines in the code, by the codereview, or by the user
+- When implementing code that iterates over lines in a Go string
+
+Available topics:
+- sketch: documentation on Sketch usage
+- strings_lines: details about the Go strings.Lines API
+`
+
+ kbInputSchema = `
+{
+ "type": "object",
+ "required": ["topic"],
+ "properties": {
+ "topic": {
+ "type": "string",
+ "description": "Topic to retrieve information about",
+ "enum": ["sketch", "strings_lines"]
+ }
+ }
+}
+`
+)
+
+type kbInput struct {
+ Topic string `json:"topic"`
+}
+
+//go:embed kb/sketch.txt
+var sketchContent string
+
+//go:embed kb/strings_lines.txt
+var stringsLinesContent string
+
+var sketchTemplate = template.Must(template.New("sketch").Parse(sketchContent))
+
+func kbRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+ var input kbInput
+ if err := json.Unmarshal(m, &input); err != nil {
+ return nil, err
+ }
+
+ // Sanitize topic name (simple lowercase conversion for now)
+ topic := strings.ToLower(strings.TrimSpace(input.Topic))
+ slog.InfoContext(ctx, "knowledge base request", "topic", topic)
+
+ // Process content based on topic
+ switch input.Topic {
+ case "sketch":
+ info := conversation.ToolCallInfoFromContext(ctx)
+ sessionID, _ := info.Convo.ExtraData["session_id"].(string)
+ branch, _ := info.Convo.ExtraData["branch"].(string)
+ dot := struct {
+ SessionID string
+ Branch string
+ }{
+ SessionID: sessionID,
+ Branch: branch,
+ }
+ buf := new(strings.Builder)
+ if err := sketchTemplate.Execute(buf, dot); err != nil {
+ return nil, fmt.Errorf("template execution error: %w", err)
+ }
+ return llm.TextContent(buf.String()), nil
+ case "strings_lines":
+ // No special processing for other topics
+ return llm.TextContent(stringsLinesContent), nil
+ default:
+ return nil, fmt.Errorf("unknown topic: %s", input.Topic)
+ }
+}
diff --git a/experiment/experiment.go b/experiment/experiment.go
index 60bc0b5..ab2a2f2 100644
--- a/experiment/experiment.go
+++ b/experiment/experiment.go
@@ -36,6 +36,10 @@
Name: "memory",
Description: "Enable memory subsystem (dear_llm.md)",
},
+ {
+ Name: "kb",
+ Description: "Enable knowledge_base tool",
+ },
}
byName = map[string]*Experiment{}
)
diff --git a/llm/conversation/convo.go b/llm/conversation/convo.go
index c46fcc0..ba6d2d9 100644
--- a/llm/conversation/convo.go
+++ b/llm/conversation/convo.go
@@ -82,6 +82,8 @@
// Hidden indicates that the output of this conversation should be hidden in the UI.
// This is useful for subconversations that can generate noisy, uninteresting output.
Hidden bool
+ // ExtraData is extra data to make available to all tool calls.
+ ExtraData map[string]any
// messages tracks the messages so far in the conversation.
messages []llm.Message
diff --git a/loop/agent.go b/loop/agent.go
index 81e1200..cc56f13 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -546,6 +546,10 @@
a.mu.Lock()
defer a.mu.Unlock()
a.branchName = branchName
+ convo, ok := a.convo.(*conversation.Convo)
+ if ok {
+ convo.ExtraData["branch"] = branchName
+ }
}
// OnToolCall implements ant.Listener and tracks the start of a tool call.
@@ -888,6 +892,7 @@
convo.PromptCaching = true
convo.Budget = a.config.Budget
convo.SystemPrompt = a.renderSystemPrompt()
+ convo.ExtraData = map[string]any{"session_id": a.config.SessionID}
// Define a permission callback for the bash tool to check if the branch name is set before allowing git commits
bashPermissionCheck := func(command string) error {
@@ -942,6 +947,10 @@
a.codereview.Tool(),
}
+ if experiment.Enabled("kb") {
+ convo.Tools = append(convo.Tools, claudetool.KnowledgeBase)
+ }
+
// One-shot mode is non-interactive, multiple choice requires human response
if !a.config.OneShot {
convo.Tools = append(convo.Tools, a.multipleChoiceTool())
diff --git a/termui/termui.go b/termui/termui.go
index 7e4def9..1575473 100644
--- a/termui/termui.go
+++ b/termui/termui.go
@@ -43,6 +43,8 @@
🏷️ {{.input.title}}
{{else if eq .msg.ToolName "precommit" -}}
🌱 git branch: sketch/{{.input.branch_name}}
+{{else if eq .msg.ToolName "knowledge_base" -}}
+📚 Knowledge: {{.input.topic}}
{{else if eq .msg.ToolName "str_replace_editor" -}}
✏️ {{.input.file_path -}}
{{else if eq .msg.ToolName "codereview" -}}
diff --git a/webui/src/web-components/sketch-tool-calls.ts b/webui/src/web-components/sketch-tool-calls.ts
index 5f416e1..99b0f21 100644
--- a/webui/src/web-components/sketch-tool-calls.ts
+++ b/webui/src/web-components/sketch-tool-calls.ts
@@ -4,6 +4,7 @@
import { ToolCall } from "../types";
import "./sketch-tool-card";
import "./sketch-tool-card-take-screenshot";
+import "./sketch-tool-card-knowledge-base";
@customElement("sketch-tool-calls")
export class SketchToolCalls extends LitElement {
@@ -127,6 +128,11 @@
.open=${open}
.toolCall=${toolCall}
></sketch-tool-card-take-screenshot>`;
+ case "knowledge_base":
+ return html`<sketch-tool-card-knowledge-base
+ .open=${open}
+ .toolCall=${toolCall}
+ ></sketch-tool-card-knowledge-base>`;
}
return html`<sketch-tool-card-generic
.open=${open}
diff --git a/webui/src/web-components/sketch-tool-card-knowledge-base.ts b/webui/src/web-components/sketch-tool-card-knowledge-base.ts
new file mode 100644
index 0000000..48dd798
--- /dev/null
+++ b/webui/src/web-components/sketch-tool-card-knowledge-base.ts
@@ -0,0 +1,79 @@
+import { css, html, LitElement } from "lit";
+import { unsafeHTML } from "lit/directives/unsafe-html.js";
+import { customElement, property } from "lit/decorators.js";
+import { ToolCall } from "../types";
+import { marked } from "marked";
+
+// Safely renders markdown with fallback to plain text on failure
+function renderMarkdown(markdownContent: string): string {
+ try {
+ return marked.parse(markdownContent, {
+ gfm: true,
+ breaks: true,
+ async: false,
+ }) as string;
+ } catch (error) {
+ console.error("Error rendering markdown:", error);
+ return markdownContent;
+ }
+}
+
+@customElement("sketch-tool-card-knowledge-base")
+export class SketchToolCardKnowledgeBase extends LitElement {
+ @property() toolCall: ToolCall;
+ @property() open: boolean;
+
+ static styles = css`
+ .summary-text {
+ font-style: italic;
+ }
+ .knowledge-content {
+ background: rgb(246, 248, 250);
+ border-radius: 6px;
+ padding: 12px;
+ margin-top: 10px;
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid #e1e4e8;
+ }
+ .topic-label {
+ font-weight: bold;
+ color: #24292e;
+ }
+ .icon {
+ margin-right: 6px;
+ }
+ `;
+
+ render() {
+ const inputData = JSON.parse(this.toolCall?.input || "{}");
+ const topic = inputData.topic || "unknown";
+ const resultText = this.toolCall?.result_message?.tool_result || "";
+
+ return html`
+ <sketch-tool-card .open=${this.open} .toolCall=${this.toolCall}>
+ <span slot="summary" class="summary-text">
+ <span class="icon">📚</span> Knowledge: ${topic}
+ </span>
+ <div slot="input">
+ <div>
+ <span class="topic-label">Topic:</span> ${topic}
+ </div>
+ </div>
+ ${this.toolCall?.result_message?.tool_result
+ ? html`<div slot="result">
+ <div class="knowledge-content">
+ ${unsafeHTML(renderMarkdown(resultText))}
+ </div>
+ </div>`
+ : ""}
+ </sketch-tool-card>
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "sketch-tool-card-knowledge-base": SketchToolCardKnowledgeBase;
+ }
+}