remove built-in memory and task tool servers
Delegate memory and task to MCP servers (denote-mcp, 9beads-mcp or equivalents). When no MCP server provides these tools, the model degrades gracefully — files in ollie/m and ollie/pl remain accessible via file_* and execute_code. reasoning_think and file_* stay built-in. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bb66b526ba
commit
ac77e11afa
|
|
@ -1,256 +0,0 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ollie/pkg/tools"
|
||||
)
|
||||
|
||||
// Decl returns a factory for a memory Server.
|
||||
func Decl() func() tools.Server {
|
||||
return func() tools.Server { return &Server{dir: flatDirPath()} }
|
||||
}
|
||||
|
||||
// Server implements tools.Server for memory_remember and memory_recall.
|
||||
type Server struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
// ListTools implements tools.Server.
|
||||
func (s *Server) ListTools() ([]tools.ToolInfo, error) {
|
||||
return []tools.ToolInfo{ToolRemember, ToolRecall}, nil
|
||||
}
|
||||
|
||||
// CallTool implements tools.Server.
|
||||
func (s *Server) CallTool(_ context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
|
||||
var text string
|
||||
var err error
|
||||
switch tool {
|
||||
case ToolRemember.Name:
|
||||
text, err = s.remember(args)
|
||||
case ToolRecall.Name:
|
||||
text, err = s.recall(args)
|
||||
default:
|
||||
text = "error: unknown tool: " + tool
|
||||
}
|
||||
if err != nil {
|
||||
text = "error: " + err.Error()
|
||||
}
|
||||
result, _ := json.Marshal(map[string]any{
|
||||
"content": []map[string]string{{"type": "text", "text": text}},
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Close implements tools.Server (no-op).
|
||||
func (s *Server) Close() {}
|
||||
|
||||
func (s *Server) remember(args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Title string `json:"title"`
|
||||
Tags []string `json:"tags"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("bad args: %w", err)
|
||||
}
|
||||
if a.Title == "" {
|
||||
return "", fmt.Errorf("'title' is required")
|
||||
}
|
||||
if len(a.Tags) == 0 {
|
||||
return "", fmt.Errorf("'tags' must be non-empty")
|
||||
}
|
||||
if a.Body == "" {
|
||||
return "", fmt.Errorf("'body' is required")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(s.dir, 0700); err != nil {
|
||||
return "", fmt.Errorf("memory dir: %w", err)
|
||||
}
|
||||
|
||||
slug := slugify(a.Title)
|
||||
ts := time.Now().Format("20060102T150405")
|
||||
name := ts + "--" + slug + "__" + strings.Join(a.Tags, "_") + ".md"
|
||||
|
||||
content := buildNote(a.Title, a.Tags, a.Body)
|
||||
if err := os.WriteFile(filepath.Join(s.dir, name), []byte(content), 0600); err != nil {
|
||||
return "", fmt.Errorf("write memory: %w", err)
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (s *Server) recall(args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("bad args: %w", err)
|
||||
}
|
||||
if a.Query == "" {
|
||||
return "", fmt.Errorf("'query' is required")
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(s.dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "no memories found matching: " + a.Query, nil
|
||||
}
|
||||
return "", fmt.Errorf("read memory dir: %w", err)
|
||||
}
|
||||
|
||||
q := strings.ToLower(a.Query)
|
||||
var results []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(e.Name()), q) {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(s.dir, e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
results = append(results, fmt.Sprintf("file: %s\n\n%s", e.Name(), strings.TrimSpace(string(data))))
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return "no memories found matching: " + a.Query, nil
|
||||
}
|
||||
return strings.Join(results, "\n\n---\n\n"), nil
|
||||
}
|
||||
|
||||
// flatDirPath resolves the memory directory: OLLIE_MEMORY_PATH env var or
|
||||
// ~/.local/share/ollie/memory/.
|
||||
func flatDirPath() string {
|
||||
if d := os.Getenv("OLLIE_MEMORY_PATH"); d != "" {
|
||||
return d
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".local", "share", "ollie", "memory")
|
||||
}
|
||||
|
||||
var nonAlnum = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
func slugify(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
s = nonAlnum.ReplaceAllString(s, "-")
|
||||
return strings.Trim(s, "-")
|
||||
}
|
||||
|
||||
func buildNote(title string, tags []string, body string) string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "# %s\n\n", title)
|
||||
if len(tags) > 0 {
|
||||
fmt.Fprintf(&sb, "tags: %s\n\n", strings.Join(tags, ", "))
|
||||
}
|
||||
sb.WriteString(strings.TrimSpace(body))
|
||||
sb.WriteByte('\n')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
var ToolRemember = tools.ToolInfo{
|
||||
Name: "memory_remember",
|
||||
Description: `Save a persistent memory across sessions.
|
||||
|
||||
Usage:
|
||||
- Store key facts, decisions, preferences, or context
|
||||
- Memories persist for future recall
|
||||
- Use descriptive titles and specific tags
|
||||
- Keep body concise and actionable
|
||||
|
||||
Title Requirements:
|
||||
- Descriptive, stands alone
|
||||
- Answers "what is this about?"
|
||||
- Minimum 3 characters
|
||||
|
||||
Tag Guidelines:
|
||||
- Specific, stable terms (not generic)
|
||||
- Concrete topics, people, projects
|
||||
- Influences recall accuracy
|
||||
- Minimum 2 characters per tag
|
||||
- Unique within memory
|
||||
|
||||
Body Content:
|
||||
- Key facts and essential context
|
||||
- Decisions made or preferences
|
||||
- Not raw transcripts or chatter
|
||||
- Minimum 8 characters
|
||||
|
||||
Examples:
|
||||
- Project structure decisions
|
||||
- User preferences or constraints
|
||||
- Code patterns or conventions
|
||||
- Important discoveries or insights`,
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["title", "tags", "body"],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"description": "Descriptive title that can stand alone and answer 'what is this about?'"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 2
|
||||
},
|
||||
"description": "Specific, stable recall tags you would search for later. Prefer concrete topics, people, projects, decisions, or preferences."
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"minLength": 8,
|
||||
"description": "Concise memory content: key facts, decisions, preferences, and essential context. Not a transcript."
|
||||
}
|
||||
}
|
||||
}`),
|
||||
}
|
||||
|
||||
var ToolRecall = tools.ToolInfo{
|
||||
Name: "memory_recall",
|
||||
Description: `Search previously saved memories.
|
||||
|
||||
Usage:
|
||||
- Find memories by keyword search
|
||||
- Searches titles and tags only (not body content)
|
||||
- Use specific, literal terms from original tags/titles
|
||||
- Returns matching memories with full details
|
||||
|
||||
Search Tips:
|
||||
- Use exact tags or project names
|
||||
- Prefer specific terms over vague descriptions
|
||||
- Minimum query length: 2 characters
|
||||
- Case-insensitive matching
|
||||
|
||||
Examples:
|
||||
- Project name: "ollie" or "core"
|
||||
- Topic: "authentication" or "database"
|
||||
- Person: "user" or specific username
|
||||
- Decision type: "architecture" or "api-design"
|
||||
|
||||
Note: Body content is not searched - only titles and tags.`,
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["query"],
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"minLength": 2,
|
||||
"description": "Short keyword or phrase to search for in memory titles and tags. Prefer literal terms such as names, projects, topics, or exact tags."
|
||||
}
|
||||
}
|
||||
}`),
|
||||
}
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ollie/pkg/tools"
|
||||
)
|
||||
|
||||
// Decl returns a factory for a task Server rooted at planDir.
|
||||
func Decl(planDir string) func() tools.Server {
|
||||
return func() tools.Server { return &Server{planDir: planDir} }
|
||||
}
|
||||
|
||||
// Server implements tools.Server for task_plan and task_complete.
|
||||
type Server struct {
|
||||
planDir string
|
||||
}
|
||||
|
||||
// ListTools implements tools.Server.
|
||||
func (s *Server) ListTools() ([]tools.ToolInfo, error) {
|
||||
return []tools.ToolInfo{ToolPlan, ToolComplete}, nil
|
||||
}
|
||||
|
||||
// CallTool implements tools.Server.
|
||||
func (s *Server) CallTool(_ context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
|
||||
var text string
|
||||
var err error
|
||||
switch tool {
|
||||
case ToolPlan.Name:
|
||||
text, err = s.plan(args)
|
||||
case ToolComplete.Name:
|
||||
text, err = s.complete(args)
|
||||
default:
|
||||
text = "error: unknown tool: " + tool
|
||||
}
|
||||
if err != nil {
|
||||
text = "error: " + err.Error()
|
||||
}
|
||||
result, _ := json.Marshal(map[string]any{
|
||||
"content": []map[string]string{{"type": "text", "text": text}},
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Close implements tools.Server (no-op).
|
||||
func (s *Server) Close() {}
|
||||
|
||||
type planStep struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"description"`
|
||||
After []int `json:"depends_on"`
|
||||
}
|
||||
|
||||
func (s *Server) plan(args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Goal string `json:"goal"`
|
||||
Steps []planStep `json:"steps"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("bad args: %w", err)
|
||||
}
|
||||
if a.Goal == "" {
|
||||
return "", fmt.Errorf("'goal' is required")
|
||||
}
|
||||
if len(a.Steps) == 0 {
|
||||
return "", fmt.Errorf("'steps' must not be empty")
|
||||
}
|
||||
|
||||
order, err := topoSort(a.Steps)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
slug := slugify(a.Goal)
|
||||
uid := make([]byte, 4)
|
||||
rand.Read(uid) //nolint:errcheck
|
||||
ts := time.Now().Format("20060102T150405")
|
||||
filename := ts + "_" + fmt.Sprintf("%08x", uid) + "--" + slug + "__todo.md"
|
||||
|
||||
var md strings.Builder
|
||||
fmt.Fprintf(&md, "# %s\n\n", a.Goal)
|
||||
for _, idx := range order {
|
||||
step := a.Steps[idx]
|
||||
fmt.Fprintf(&md, "- [ ] %s\n", step.Title)
|
||||
if step.Body != "" {
|
||||
for line := range strings.SplitSeq(step.Body, "\n") {
|
||||
fmt.Fprintf(&md, " %s\n", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(s.planDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("plan dir: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(s.planDir, filename), []byte(md.String()), 0644); err != nil {
|
||||
return "", fmt.Errorf("write plan: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"Plan saved to pl/%s (%d steps). Rename __todo → __wip when you start work, use task_complete to mark steps done, rename __wip → __done when the goal is realized.",
|
||||
filename, len(a.Steps),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (s *Server) complete(args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
File string `json:"file"`
|
||||
Step string `json:"step"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("bad args: %w", err)
|
||||
}
|
||||
if a.File == "" {
|
||||
return "", fmt.Errorf("'file' is required")
|
||||
}
|
||||
if a.Step == "" {
|
||||
return "", fmt.Errorf("'step' is required")
|
||||
}
|
||||
|
||||
path := filepath.Join(s.planDir, a.File)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read plan: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
needle := strings.ToLower(strings.TrimSpace(a.Step))
|
||||
matched := false
|
||||
for i, line := range lines {
|
||||
if !strings.HasPrefix(line, "- [ ] ") {
|
||||
continue
|
||||
}
|
||||
title := strings.ToLower(strings.TrimPrefix(line, "- [ ] "))
|
||||
if strings.Contains(title, needle) {
|
||||
lines[i] = "- [x] " + strings.TrimPrefix(line, "- [ ] ")
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return "", fmt.Errorf("no unchecked step matching %q in %s", a.Step, a.File)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil {
|
||||
return "", fmt.Errorf("write plan: %w", err)
|
||||
}
|
||||
|
||||
var remaining []string
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "- [ ] ") {
|
||||
remaining = append(remaining, strings.TrimPrefix(line, "- [ ] "))
|
||||
}
|
||||
}
|
||||
|
||||
if len(remaining) == 0 {
|
||||
return "step marked complete. all steps done.", nil
|
||||
}
|
||||
return fmt.Sprintf("step marked complete. remaining steps:\n- %s", strings.Join(remaining, "\n- ")), nil
|
||||
}
|
||||
|
||||
func topoSort(steps []planStep) ([]int, error) {
|
||||
n := len(steps)
|
||||
visited := make([]bool, n)
|
||||
result := make([]int, 0, n)
|
||||
|
||||
var visit func(i int, path []bool) error
|
||||
visit = func(i int, path []bool) error {
|
||||
if path[i] {
|
||||
return fmt.Errorf("dependency cycle at step %d", i)
|
||||
}
|
||||
if visited[i] {
|
||||
return nil
|
||||
}
|
||||
path[i] = true
|
||||
for _, dep := range steps[i].After {
|
||||
if dep < 0 || dep >= n {
|
||||
continue
|
||||
}
|
||||
if err := visit(dep, path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
path[i] = false
|
||||
visited[i] = true
|
||||
result = append(result, i)
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range steps {
|
||||
if err := visit(i, make([]bool, n)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var slugRe = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
func slugify(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
s = slugRe.ReplaceAllString(s, "-")
|
||||
s = strings.Trim(s, "-")
|
||||
if len(s) > 60 {
|
||||
s = s[:60]
|
||||
if i := strings.LastIndex(s, "-"); i > 20 {
|
||||
s = s[:i]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var ToolPlan = tools.ToolInfo{
|
||||
Name: "task_plan",
|
||||
Description: `Create an execution plan for multi-step tasks.
|
||||
|
||||
Usage:
|
||||
- Use for non-trivial tasks requiring multiple steps
|
||||
- Plan before acting when order matters or there are dependencies
|
||||
- Saves a markdown checklist to the plan directory (pl/)
|
||||
- Execute steps sequentially based on dependencies
|
||||
|
||||
Plan Structure:
|
||||
- goal: top-level objective
|
||||
- steps: array of ordered steps
|
||||
- Each step: title, description, depends_on, acceptance_criteria
|
||||
- depends_on: earlier step indexes (0-based)
|
||||
- Execute steps only after dependencies complete
|
||||
|
||||
Plan Lifecycle:
|
||||
- Plans are saved as __todo.md files in pl/
|
||||
- Rename __todo → __wip when you begin work
|
||||
- Use task_complete after each step finishes
|
||||
- Rename __wip → __done when the goal is realized
|
||||
|
||||
When to Plan:
|
||||
- Multiple steps required
|
||||
- Steps have dependencies
|
||||
- Execution order is not obvious
|
||||
|
||||
Re-plan If:
|
||||
- A step fails
|
||||
- New information changes constraints
|
||||
- Better decomposition is possible`,
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["goal", "steps"],
|
||||
"properties": {
|
||||
"goal": {
|
||||
"type": "string",
|
||||
"description": "Top-level goal or objective."
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"description": "Ordered plan steps. Dependencies may only reference earlier 0-based step indexes.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["title"],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Short, outcome-oriented step title."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional implementation detail for the step."
|
||||
},
|
||||
"depends_on": {
|
||||
"type": "array",
|
||||
"description": "0-based indexes of earlier steps that must complete before this one.",
|
||||
"items": {"type": "integer", "minimum": 0},
|
||||
"default": []
|
||||
},
|
||||
"acceptance_criteria": {
|
||||
"type": "string",
|
||||
"description": "How to tell the step is complete or successful."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
}
|
||||
|
||||
var ToolComplete = tools.ToolInfo{
|
||||
Name: "task_complete",
|
||||
Description: `Mark a plan step as complete.
|
||||
|
||||
Finds the first unchecked step whose title contains the given string
|
||||
(case-insensitive) and marks it [x]. Returns the remaining unchecked steps
|
||||
so you always know what's left.
|
||||
|
||||
Call this immediately after finishing each step — before starting the next.`,
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["file", "step"],
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "Plan filename as returned by task_plan (e.g. '20240101T120000_ab12cd34--my-goal__wip.md')."
|
||||
},
|
||||
"step": {
|
||||
"type": "string",
|
||||
"description": "Title (or substring) of the step to mark complete. Matched case-insensitively against unchecked steps."
|
||||
}
|
||||
}
|
||||
}`),
|
||||
}
|
||||
Reference in New Issue