filename:
assistant/tools/scratchpad.go
branch:
main
back to repo
package tools
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"unicode/utf8"
)
const scratchpadMaxReadRunes = 120_000
var scratchpadMu sync.Mutex
var scratchpadReadParams = json.RawMessage(`{
"type": "object",
"properties": {},
"additionalProperties": false
}`)
var scratchpadWriteParams = json.RawMessage(`{
"type": "object",
"properties": {
"content": { "type": "string", "description": "Markdown to append: a newline is written first, then this text at the end of the file." }
},
"required": ["content"],
"additionalProperties": false
}`)
type scratchpadWriteArgs struct {
Content string `json:"content"`
}
func readScratchpadFile(path string) (string, error) {
b, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
if !utf8.Valid(b) {
return "", fmt.Errorf("scratchpad is not valid UTF-8")
}
return string(b), nil
}
func writeScratchpadFile(path string, content string) error {
return os.WriteFile(path, []byte(content), 0o644)
}
// ReadScratchpadLocked reads the scratchpad file using the same lock as other scratchpad I/O.
func ReadScratchpadLocked(absPath string) (string, error) {
scratchpadMu.Lock()
defer scratchpadMu.Unlock()
return readScratchpadFile(absPath)
}
// WriteScratchpadLocked overwrites the entire scratchpad file (e.g. web UI save).
func WriteScratchpadLocked(absPath string, content string) error {
scratchpadMu.Lock()
defer scratchpadMu.Unlock()
return writeScratchpadFile(absPath, content)
}
func appendNewlineThenContent(prev, content string) string {
if strings.TrimSpace(prev) == "" {
return content
}
return strings.TrimRight(prev, "\r\n") + "\n" + content
}
func scratchpadTools(absPath string) []Tool {
descRead := "Read the shared Markdown scratchpad file. Call this when the user refers to the scratchpad or you need the latest text."
descWrite := "Append Markdown to the end of the shared scratchpad. The server adds a single newline, then your content (it does not replace the file)."
return []Tool{
{
Name: "scratchpad_read",
Description: descRead,
Parameters: scratchpadReadParams,
Run: func(ctx context.Context, args json.RawMessage) (string, error) {
scratchpadMu.Lock()
defer scratchpadMu.Unlock()
text, err := readScratchpadFile(absPath)
if err != nil {
return "", err
}
if strings.TrimSpace(text) == "" {
return "(Scratchpad is empty.)", nil
}
if utf8.RuneCountInString(text) > scratchpadMaxReadRunes {
runes := []rune(text)
head := string(runes[:scratchpadMaxReadRunes])
return fmt.Sprintf("%s\n\n[Truncated: file is longer than %d characters; ask the user to split content or edit in the UI.]",
head, scratchpadMaxReadRunes), nil
}
return text, nil
},
},
{
Name: "scratchpad_write",
Description: descWrite,
Parameters: scratchpadWriteParams,
Run: func(ctx context.Context, args json.RawMessage) (string, error) {
var a scratchpadWriteArgs
if err := json.Unmarshal(args, &a); err != nil {
return "", err
}
scratchpadMu.Lock()
defer scratchpadMu.Unlock()
prev, err := readScratchpadFile(absPath)
if err != nil {
return "", err
}
out := appendNewlineThenContent(prev, a.Content)
if err := writeScratchpadFile(absPath, out); err != nil {
return "", err
}
return fmt.Sprintf("Appended to scratchpad (file now %d characters).", utf8.RuneCountInString(out)), nil
},
},
}
}