cleanup tools
This commit is contained in:
parent
1e40a6d573
commit
1b44244f81
|
|
@ -1,136 +0,0 @@
|
|||
package tools
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// ExecuteDefs returns the ToolInfo definitions for the execute_* built-in tools.
|
||||
// toolsPath is embedded in the descriptions.
|
||||
func ExecuteDefs(toolsPath string) []ToolInfo {
|
||||
return []ToolInfo{
|
||||
{
|
||||
Name: "execute_code",
|
||||
Description: "Run inline code in a sandboxed environment.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["code"],
|
||||
"properties": {
|
||||
"code": {"type": "string", "description": "Code to execute."},
|
||||
"language": {"type": "string", "description": "Language interpreter (default: bash)."},
|
||||
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."},
|
||||
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "execute_tool",
|
||||
Description: "Run a named tool script from " + toolsPath + " in a sandboxed environment. Supported languages: bash, python3 (detected from shebang). Use this only for named scripts, not for inline shell commands.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["tool"],
|
||||
"properties": {
|
||||
"tool": {"type": "string", "description": "Name of the tool script in the tools directory."},
|
||||
"args": {"type": "array", "items": {"type": "string"}, "description": "Arguments for the tool script."},
|
||||
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."},
|
||||
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "execute_pipe",
|
||||
Description: "Run a pipeline of steps, piping stdout of each into stdin of the next. Use {code: \"cmd --flags\"} for shell commands; use {tool, args} only for named scripts in " + toolsPath + ".",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["pipe"],
|
||||
"properties": {
|
||||
"pipe": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tool": {"type": "string"},
|
||||
"args": {"type": "array", "items": {"type": "string"}},
|
||||
"code": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."},
|
||||
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MemoryDefs returns the ToolInfo definitions for the memory_* built-in tools.
|
||||
func MemoryDefs() []ToolInfo {
|
||||
return []ToolInfo{
|
||||
{
|
||||
Name: "memory_remember",
|
||||
Description: "Save a memory that persists across sessions. Provide a descriptive title, relevant tags, and a concise body. Choose tags carefully — they are the primary recall mechanism. Prefer specific, stable terms over generic ones.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["title", "tags", "body"],
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "Descriptive title. Should answer 'what is this about' on its own."},
|
||||
"tags": {"type": "array", "items": {"type": "string"}, "minItems": 1, "description": "Tags for recall. Use specific, stable terms you would search for later."},
|
||||
"body": {"type": "string", "description": "Memory content. Write concisely — key facts, decisions, and context. Not a transcript."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "memory_recall",
|
||||
Description: "Search memories by keyword. Matches against titles and tags only — body content is not searched. Use specific terms likely to appear in titles or tags at write time.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["query"],
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Keyword to search for in memory titles and tags."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ReasoningDefs returns the ToolInfo definitions for the reasoning_* built-in tools.
|
||||
func ReasoningDefs() []ToolInfo {
|
||||
return []ToolInfo{
|
||||
{
|
||||
Name: "reasoning_think",
|
||||
Description: "Internal reasoning tool for breaking down problems, planning approaches, analyzing constraints, and reasoning through multi-step solutions before acting. The thought content is not shown to the user.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["thought"],
|
||||
"properties": {
|
||||
"thought": {"type": "string", "description": "A reflective note or intermediate reasoning step."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "reasoning_plan",
|
||||
Description: "Executive planning tool. Before beginning multi-step work, decompose the goal into ordered steps with explicit dependencies. If a task backend is available (task_create), steps are committed to persistent storage and assigned IDs. Otherwise steps are queued for sequential execution. Use reasoning_think for moment-to-moment reflection; use reasoning_plan to lay out a complete work breakdown before acting. Steps may only depend on earlier steps (lower index).",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["goal", "steps"],
|
||||
"properties": {
|
||||
"goal": {"type": "string", "description": "Top-level goal or objective."},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"description": "Ordered work steps. Each step may only list earlier steps (by 0-based index) in 'after'.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["title"],
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "Short step title."},
|
||||
"body": {"type": "string", "description": "Optional detail or acceptance criteria."},
|
||||
"after": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"description": "0-based indices of steps that must complete before this one."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,165 @@
|
|||
package execute
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"ollie/internal/sandbox"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// execute_code is implemented directly by Executor.Execute with trusted=false.
|
||||
// See executor.go for the implementation.
|
||||
|
||||
var dangerousPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s*/(home|var|usr|etc|boot|root|bin|sbin|lib|opt|srv)?`),
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*f[a-z]*\s+)*-[a-z]*r[a-z]*\s*/(home|var|usr|etc|boot|root|bin|sbin|lib|opt|srv)?`),
|
||||
regexp.MustCompile(`rm\s+.*--recursive.*--force`),
|
||||
regexp.MustCompile(`rm\s+.*--force.*--recursive`),
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+\.\.?(/|$)`),
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+~`),
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+\*`),
|
||||
regexp.MustCompile(`:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&`),
|
||||
regexp.MustCompile(`\bmkfs\b`),
|
||||
regexp.MustCompile(`\bdd\b.*\bif=/dev/`),
|
||||
regexp.MustCompile(`>\s*/dev/sd`),
|
||||
regexp.MustCompile(`\beval\s+".*\$`),
|
||||
regexp.MustCompile(`\b(sudo|su)\s`),
|
||||
regexp.MustCompile(`/etc/(shadow|sudoers)`),
|
||||
}
|
||||
|
||||
// Dispatch routes a named execute tool call. Called by tools.BuiltinServer.
|
||||
func (e *Server) Dispatch(ctx context.Context, name string, args json.RawMessage) (string, error) {
|
||||
switch name {
|
||||
case "execute_code":
|
||||
return dispatchExecuteCode(ctx, e, args)
|
||||
case "execute_tool":
|
||||
return dispatchExecuteTool(ctx, e, args)
|
||||
case "execute_pipe":
|
||||
return dispatchExecutePipe(ctx, e, args)
|
||||
default:
|
||||
return "", fmt.Errorf("unknown execute tool: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateCode checks code against dangerous patterns.
|
||||
func (e *Server) ValidateCode(code string) error {
|
||||
if err := e.checkRateLimit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
normalized := strings.ToLower(code)
|
||||
normalized = whitespacePattern.ReplaceAllString(normalized, " ")
|
||||
|
||||
for _, pattern := range dangerousPatterns {
|
||||
if pattern.MatchString(normalized) {
|
||||
e.recordValidationFailure()
|
||||
return fmt.Errorf("dangerous pattern detected")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadLayeredConfig(name string) (*sandbox.Config, error) {
|
||||
baseCfg, err := sandbox.Load()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load global config: %w", err)
|
||||
}
|
||||
|
||||
baseLayer := sandbox.LayeredConfig{
|
||||
Filesystem: baseCfg.Filesystem,
|
||||
Network: baseCfg.Network,
|
||||
Env: baseCfg.Env,
|
||||
}
|
||||
layers := []sandbox.LayeredConfig{baseLayer}
|
||||
|
||||
if name == "" {
|
||||
name = "default"
|
||||
}
|
||||
sbxLayer, err := sandbox.LoadSandbox(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load sandbox %q: %w", name, err)
|
||||
}
|
||||
layers = append(layers, sbxLayer)
|
||||
|
||||
general := sandbox.GeneralConfig{
|
||||
BestEffort: baseCfg.General.BestEffort,
|
||||
LogLevel: baseCfg.General.LogLevel,
|
||||
}
|
||||
advanced := sandbox.AdvancedConfig{
|
||||
LDD: baseCfg.Advanced.LDD,
|
||||
AddExec: baseCfg.Advanced.AddExec,
|
||||
}
|
||||
|
||||
return sandbox.Merge(general, advanced, layers...), nil
|
||||
}
|
||||
|
||||
type limitedWriter struct {
|
||||
w io.Writer
|
||||
written int
|
||||
limit int
|
||||
truncated bool
|
||||
}
|
||||
|
||||
func (lw *limitedWriter) Write(p []byte) (n int, err error) {
|
||||
if lw.written >= lw.limit {
|
||||
lw.truncated = true
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
remaining := lw.limit - lw.written
|
||||
toWrite := p
|
||||
if len(p) > remaining {
|
||||
toWrite = p[:remaining]
|
||||
lw.truncated = true
|
||||
}
|
||||
|
||||
written, err := lw.w.Write(toWrite)
|
||||
lw.written += written
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func dispatchExecuteCode(ctx context.Context, e *Server, args json.RawMessage) (string, error) {
|
||||
code, language, sandbox, timeout, err := execArgs(args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("execute_code: bad args: %w", err)
|
||||
}
|
||||
if code == "" {
|
||||
return "", fmt.Errorf("execute_code: 'code' is required")
|
||||
}
|
||||
if e.Confirm != nil && !e.Confirm(fmt.Sprintf("execute_code: %s", code)) {
|
||||
return "", fmt.Errorf("execute_code: denied by user")
|
||||
}
|
||||
return e.Execute(ctx, code, language, timeout, sandbox, false)
|
||||
}
|
||||
|
||||
func execArgs(args json.RawMessage) (code, language, sandbox string, timeout int, err error) {
|
||||
var a struct {
|
||||
Code string `json:"code"`
|
||||
Language string `json:"language"`
|
||||
Timeout int `json:"timeout"`
|
||||
Sandbox string `json:"sandbox"`
|
||||
}
|
||||
if err = json.Unmarshal(args, &a); err != nil {
|
||||
return
|
||||
}
|
||||
code = a.Code
|
||||
language = a.Language
|
||||
if language == "" {
|
||||
language = "bash"
|
||||
}
|
||||
timeout = a.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
sandbox = a.Sandbox
|
||||
if sandbox == "" {
|
||||
sandbox = "default"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,132 +0,0 @@
|
|||
package execute
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dispatch routes a named execute tool call. Called by tools.BuiltinServer.
|
||||
func (e *Server) Dispatch(ctx context.Context, name string, args json.RawMessage) (string, error) {
|
||||
switch name {
|
||||
case "execute_code":
|
||||
return dispatchExecuteCode(ctx, e, args)
|
||||
case "execute_tool":
|
||||
return dispatchExecuteTool(ctx, e, args)
|
||||
case "execute_pipe":
|
||||
return dispatchExecutePipe(ctx, e, args)
|
||||
default:
|
||||
return "", fmt.Errorf("unknown execute tool: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func execArgs(args json.RawMessage) (code, language, sandbox string, timeout int, err error) {
|
||||
var a struct {
|
||||
Code string `json:"code"`
|
||||
Language string `json:"language"`
|
||||
Timeout int `json:"timeout"`
|
||||
Sandbox string `json:"sandbox"`
|
||||
}
|
||||
if err = json.Unmarshal(args, &a); err != nil {
|
||||
return
|
||||
}
|
||||
code = a.Code
|
||||
language = a.Language
|
||||
if language == "" {
|
||||
language = "bash"
|
||||
}
|
||||
timeout = a.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
sandbox = a.Sandbox
|
||||
if sandbox == "" {
|
||||
sandbox = "default"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func dispatchExecuteCode(ctx context.Context, e *Server, args json.RawMessage) (string, error) {
|
||||
code, language, sandbox, timeout, err := execArgs(args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("execute_code: bad args: %w", err)
|
||||
}
|
||||
if code == "" {
|
||||
return "", fmt.Errorf("execute_code: 'code' is required")
|
||||
}
|
||||
if e.Confirm != nil && !e.Confirm(fmt.Sprintf("execute_code: %s", squashWhitespace(code))) {
|
||||
return "", fmt.Errorf("execute_code: denied by user")
|
||||
}
|
||||
return e.Execute(ctx, code, language, timeout, sandbox, false)
|
||||
}
|
||||
|
||||
func dispatchExecuteTool(ctx context.Context, e *Server, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Tool string `json:"tool"`
|
||||
Args []string `json:"args"`
|
||||
Timeout int `json:"timeout"`
|
||||
Sandbox string `json:"sandbox"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("execute_tool: bad args: %w", err)
|
||||
}
|
||||
if a.Tool == "" {
|
||||
return "", fmt.Errorf("execute_tool: 'tool' is required")
|
||||
}
|
||||
if e.Confirm != nil && !e.Confirm(fmt.Sprintf("execute_tool: %s %s", a.Tool, strings.Join(a.Args, " "))) {
|
||||
return "", fmt.Errorf("execute_tool: denied by user")
|
||||
}
|
||||
toolCode, err := ReadTool(a.Tool)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
language := detectLanguage(toolCode)
|
||||
code := toolCode
|
||||
if len(a.Args) > 0 {
|
||||
code = injectArgs(language, a.Tool, a.Args, toolCode)
|
||||
}
|
||||
timeout := a.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
sandbox := a.Sandbox
|
||||
if sandbox == "" {
|
||||
sandbox = "default"
|
||||
}
|
||||
return e.Execute(ctx, code, language, timeout, sandbox, true)
|
||||
}
|
||||
|
||||
func dispatchExecutePipe(ctx context.Context, e *Server, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Pipe []PipeStep `json:"pipe"`
|
||||
Timeout int `json:"timeout"`
|
||||
Sandbox string `json:"sandbox"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("execute_pipe: bad args: %w", err)
|
||||
}
|
||||
if len(a.Pipe) == 0 {
|
||||
return "", fmt.Errorf("execute_pipe: 'pipe' is required")
|
||||
}
|
||||
code, _, err := BuildPipeline(a.Pipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if e.Confirm != nil && !e.Confirm(fmt.Sprintf("execute_pipe: %s", squashWhitespace(code))) {
|
||||
return "", fmt.Errorf("execute_pipe: denied by user")
|
||||
}
|
||||
timeout := a.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
sandbox := a.Sandbox
|
||||
if sandbox == "" {
|
||||
sandbox = "default"
|
||||
}
|
||||
return e.Execute(ctx, code, "bash", timeout, sandbox, true)
|
||||
}
|
||||
|
||||
func squashWhitespace(s string) string {
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
// Package execute provides the builtin execute_code, execute_tool, and
|
||||
// execute_pipe tools. The Server struct is the shared sandbox runner.
|
||||
package execute
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"ollie/internal/sandbox"
|
||||
)
|
||||
|
||||
// Server runs code in a sandboxed environment.
|
||||
type Server struct {
|
||||
// Confirm is an optional function called before executing sensitive operations.
|
||||
// If it returns false, the operation is denied.
|
||||
Confirm func(string) bool
|
||||
|
||||
// workdir is the working directory for sandboxed commands. If empty,
|
||||
// the process working directory is used.
|
||||
wdMu sync.RWMutex
|
||||
workdir string
|
||||
|
||||
// rate limiting state (per-Server)
|
||||
rateLimitMu sync.Mutex
|
||||
validationFailures int
|
||||
lastFailure time.Time
|
||||
blockedUntil time.Time
|
||||
}
|
||||
|
||||
// New creates a new Server with the given working directory.
|
||||
func New(workdir string) *Server { return &Server{workdir: workdir} }
|
||||
|
||||
// SetWorkDir updates the working directory used for subsequent command executions.
|
||||
func (e *Server) SetWorkDir(dir string) {
|
||||
e.wdMu.Lock()
|
||||
e.workdir = dir
|
||||
e.wdMu.Unlock()
|
||||
}
|
||||
|
||||
var dangerousPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s*/(home|var|usr|etc|boot|root|bin|sbin|lib|opt|srv)?`),
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*f[a-z]*\s+)*-[a-z]*r[a-z]*\s*/(home|var|usr|etc|boot|root|bin|sbin|lib|opt|srv)?`),
|
||||
regexp.MustCompile(`rm\s+.*--recursive.*--force`),
|
||||
regexp.MustCompile(`rm\s+.*--force.*--recursive`),
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+\.\.?(/|$)`),
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+~`),
|
||||
regexp.MustCompile(`rm\s+(-[a-z]*r[a-z]*\s+)*-[a-z]*f[a-z]*\s+\*`),
|
||||
regexp.MustCompile(`:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&`),
|
||||
regexp.MustCompile(`\bmkfs\b`),
|
||||
regexp.MustCompile(`\bdd\b.*\bif=/dev/`),
|
||||
regexp.MustCompile(`>\s*/dev/sd`),
|
||||
regexp.MustCompile(`\beval\s+".*\$`),
|
||||
regexp.MustCompile(`\b(sudo|su)\s`),
|
||||
regexp.MustCompile(`/etc/(shadow|sudoers)`),
|
||||
}
|
||||
|
||||
var whitespacePattern = regexp.MustCompile(`\s+`)
|
||||
|
||||
const (
|
||||
maxFailures = 5
|
||||
blockDuration = 30 * time.Second
|
||||
failureWindow = 60 * time.Second
|
||||
)
|
||||
|
||||
func (e *Server) checkRateLimit() error {
|
||||
e.rateLimitMu.Lock()
|
||||
defer e.rateLimitMu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if now.Before(e.blockedUntil) {
|
||||
remaining := e.blockedUntil.Sub(now).Round(time.Second)
|
||||
return fmt.Errorf("rate limited: too many validation failures, blocked for %v", remaining)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Server) recordValidationFailure() {
|
||||
e.rateLimitMu.Lock()
|
||||
defer e.rateLimitMu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if now.Sub(e.lastFailure) > failureWindow {
|
||||
e.validationFailures = 0
|
||||
}
|
||||
|
||||
e.validationFailures++
|
||||
e.lastFailure = now
|
||||
|
||||
if e.validationFailures >= maxFailures {
|
||||
e.blockedUntil = now.Add(blockDuration)
|
||||
e.validationFailures = 0
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateCode checks code against dangerous patterns.
|
||||
func (e *Server) ValidateCode(code string) error {
|
||||
if err := e.checkRateLimit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
normalized := strings.ToLower(code)
|
||||
normalized = whitespacePattern.ReplaceAllString(normalized, " ")
|
||||
|
||||
for _, pattern := range dangerousPatterns {
|
||||
if pattern.MatchString(normalized) {
|
||||
e.recordValidationFailure()
|
||||
return fmt.Errorf("dangerous pattern detected")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadLayeredConfig(name string) (*sandbox.Config, error) {
|
||||
baseCfg, err := sandbox.Load()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load global config: %w", err)
|
||||
}
|
||||
|
||||
baseLayer := sandbox.LayeredConfig{
|
||||
Filesystem: baseCfg.Filesystem,
|
||||
Network: baseCfg.Network,
|
||||
Env: baseCfg.Env,
|
||||
}
|
||||
layers := []sandbox.LayeredConfig{baseLayer}
|
||||
|
||||
if name == "" {
|
||||
name = "default"
|
||||
}
|
||||
sbxLayer, err := sandbox.LoadSandbox(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load sandbox %q: %w", name, err)
|
||||
}
|
||||
layers = append(layers, sbxLayer)
|
||||
|
||||
general := sandbox.GeneralConfig{
|
||||
BestEffort: baseCfg.General.BestEffort,
|
||||
LogLevel: baseCfg.General.LogLevel,
|
||||
}
|
||||
advanced := sandbox.AdvancedConfig{
|
||||
LDD: baseCfg.Advanced.LDD,
|
||||
AddExec: baseCfg.Advanced.AddExec,
|
||||
}
|
||||
|
||||
return sandbox.Merge(general, advanced, layers...), nil
|
||||
}
|
||||
|
||||
// Execute runs code in a sandbox and returns combined stdout+stderr.
|
||||
func (e *Server) Execute(ctx context.Context, code, language string, timeout int, sandboxName string, trusted bool) (string, error) {
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
|
||||
if !trusted {
|
||||
if err := e.ValidateCode(code); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := loadLayeredConfig(sandboxName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
e.wdMu.RLock()
|
||||
workDir := e.workdir
|
||||
e.wdMu.RUnlock()
|
||||
if workDir == "" {
|
||||
workDir, _ = os.Getwd()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
var interpreter []string
|
||||
switch language {
|
||||
case "bash", "":
|
||||
interpreter = []string{"bash", "-c", code}
|
||||
case "python3", "python":
|
||||
interpreter = []string{"python3", "-c", code}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported language: %s (supported: bash, python3)", language)
|
||||
}
|
||||
wrapped := sandbox.WrapCommand(cfg, interpreter, workDir)
|
||||
cmd = exec.CommandContext(ctx, wrapped[0], wrapped[1:]...)
|
||||
cmd.Dir = workDir
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
cmd.Cancel = func() error {
|
||||
if cmd.Process != nil {
|
||||
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var outputBuf bytes.Buffer
|
||||
lw := &limitedWriter{w: &outputBuf, limit: 10 * 1024 * 1024}
|
||||
cmd.Stdout = lw
|
||||
cmd.Stderr = lw
|
||||
|
||||
err = cmd.Run()
|
||||
output := outputBuf.Bytes()
|
||||
if lw.truncated {
|
||||
output = append(output, []byte("\n[output truncated at 10MB]")...)
|
||||
}
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return "", fmt.Errorf("execution timeout after %d seconds", timeout)
|
||||
}
|
||||
if err != nil {
|
||||
return string(output), fmt.Errorf("execution failed: %v\nOutput: %s", err, string(output))
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
type limitedWriter struct {
|
||||
w io.Writer
|
||||
written int
|
||||
limit int
|
||||
truncated bool
|
||||
}
|
||||
|
||||
func (lw *limitedWriter) Write(p []byte) (n int, err error) {
|
||||
if lw.written >= lw.limit {
|
||||
lw.truncated = true
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
remaining := lw.limit - lw.written
|
||||
toWrite := p
|
||||
if len(p) > remaining {
|
||||
toWrite = p[:remaining]
|
||||
lw.truncated = true
|
||||
}
|
||||
|
||||
written, err := lw.w.Write(toWrite)
|
||||
lw.written += written
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
package execute
|
||||
|
||||
// execute_pipe constructs a pipeline of execute_code and execute_tool calls
|
||||
// to enable the composition of tools and arbitrary code.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -55,3 +60,33 @@ func BuildPipeline(steps []PipeStep) (string, bool, error) {
|
|||
return strings.Join(parts, " |\n"), true, nil
|
||||
|
||||
}
|
||||
|
||||
func dispatchExecutePipe(ctx context.Context, e *Server, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Pipe []PipeStep `json:"pipe"`
|
||||
Timeout int `json:"timeout"`
|
||||
Sandbox string `json:"sandbox"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("execute_pipe: bad args: %w", err)
|
||||
}
|
||||
if len(a.Pipe) == 0 {
|
||||
return "", fmt.Errorf("execute_pipe: 'pipe' is required")
|
||||
}
|
||||
code, _, err := BuildPipeline(a.Pipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if e.Confirm != nil && !e.Confirm(fmt.Sprintf("execute_pipe: %s", code)) {
|
||||
return "", fmt.Errorf("execute_pipe: denied by user")
|
||||
}
|
||||
timeout := a.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
sandbox := a.Sandbox
|
||||
if sandbox == "" {
|
||||
sandbox = "default"
|
||||
}
|
||||
return e.Execute(ctx, code, "bash", timeout, sandbox, true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,99 @@
|
|||
package execute
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"ollie/internal/sandbox"
|
||||
"ollie/pkg/tools"
|
||||
)
|
||||
|
||||
// Server runs code in a sandboxed environment.
|
||||
type Server struct {
|
||||
// Confirm is an optional function called before executing sensitive operations.
|
||||
// If it returns false, the operation is denied.
|
||||
Confirm func(string) bool
|
||||
|
||||
// workdir is the working directory for sandboxed commands. If empty,
|
||||
// the process working directory is used.
|
||||
wdMu sync.RWMutex
|
||||
workdir string
|
||||
|
||||
// rate limiting state (per-Server)
|
||||
rateLimitMu sync.Mutex
|
||||
validationFailures int
|
||||
lastFailure time.Time
|
||||
blockedUntil time.Time
|
||||
}
|
||||
|
||||
// Decl returns a factory for an execute Server with the given working directory.
|
||||
func Decl(workdir string) func() tools.Server {
|
||||
return func() tools.Server { return New(workdir) }
|
||||
}
|
||||
|
||||
// ListTools implements tools.Server.
|
||||
// ListTools implements tools.Server, returning the ToolInfo definitions for the execute_* built-in tools.
|
||||
func (e *Server) ListTools() ([]tools.ToolInfo, error) {
|
||||
return tools.ExecuteDefs(ToolsPath()), nil
|
||||
return []tools.ToolInfo{
|
||||
{
|
||||
Name: "execute_code",
|
||||
Description: "Run inline code in a sandboxed environment.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["code"],
|
||||
"properties": {
|
||||
"code": {"type": "string", "description": "Code to execute."},
|
||||
"language": {"type": "string", "description": "Language interpreter (default: bash)."},
|
||||
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."},
|
||||
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "execute_tool",
|
||||
Description: "Run a named tool script from " + ToolsPath() + " in a sandboxed environment. Supported languages: bash, python3 (detected from shebang). Use this only for named scripts, not for inline shell commands.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["tool"],
|
||||
"properties": {
|
||||
"tool": {"type": "string", "description": "Name of the tool script in the tools directory."},
|
||||
"args": {"type": "array", "items": {"type": "string"}, "description": "Arguments for the tool script."},
|
||||
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."},
|
||||
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
Name: "execute_pipe",
|
||||
Description: "Run a pipeline of steps, piping stdout of each into stdin of the next. Use {code: \"cmd --flags\"} for shell commands; use {tool, args} only for named scripts in " + ToolsPath() + ".",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["pipe"],
|
||||
"properties": {
|
||||
"pipe": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tool": {"type": "string"},
|
||||
"args": {"type": "array", "items": {"type": "string"}},
|
||||
"code": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."},
|
||||
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CallTool implements tools.Server.
|
||||
|
|
@ -28,6 +107,122 @@ func (e *Server) CallTool(ctx context.Context, tool string, args json.RawMessage
|
|||
})
|
||||
}
|
||||
|
||||
// New creates a new Server with the given working directory.
|
||||
func New(workdir string) *Server { return &Server{workdir: workdir} }
|
||||
|
||||
// SetWorkDir updates the working directory used for subsequent command executions.
|
||||
func (e *Server) SetWorkDir(dir string) {
|
||||
e.wdMu.Lock()
|
||||
e.workdir = dir
|
||||
e.wdMu.Unlock()
|
||||
}
|
||||
|
||||
var whitespacePattern = regexp.MustCompile(`\s+`)
|
||||
|
||||
const (
|
||||
maxFailures = 5
|
||||
blockDuration = 30 * time.Second
|
||||
failureWindow = 60 * time.Second
|
||||
)
|
||||
|
||||
func (e *Server) checkRateLimit() error {
|
||||
e.rateLimitMu.Lock()
|
||||
defer e.rateLimitMu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if now.Before(e.blockedUntil) {
|
||||
remaining := e.blockedUntil.Sub(now).Round(time.Second)
|
||||
return fmt.Errorf("rate limited: too many validation failures, blocked for %v", remaining)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Server) recordValidationFailure() {
|
||||
e.rateLimitMu.Lock()
|
||||
defer e.rateLimitMu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if now.Sub(e.lastFailure) > failureWindow {
|
||||
e.validationFailures = 0
|
||||
}
|
||||
|
||||
e.validationFailures++
|
||||
e.lastFailure = now
|
||||
|
||||
if e.validationFailures >= maxFailures {
|
||||
e.blockedUntil = now.Add(blockDuration)
|
||||
e.validationFailures = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs code in a sandbox and returns combined stdout+stderr.
|
||||
func (e *Server) Execute(ctx context.Context, code, language string, timeout int, sandboxName string, trusted bool) (string, error) {
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
|
||||
if !trusted {
|
||||
if err := e.ValidateCode(code); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := loadLayeredConfig(sandboxName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
e.wdMu.RLock()
|
||||
workDir := e.workdir
|
||||
e.wdMu.RUnlock()
|
||||
if workDir == "" {
|
||||
workDir, _ = os.Getwd()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
var interpreter []string
|
||||
switch language {
|
||||
case "bash", "":
|
||||
interpreter = []string{"bash", "-c", code}
|
||||
case "python3", "python":
|
||||
interpreter = []string{"python3", "-c", code}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported language: %s (supported: bash, python3)", language)
|
||||
}
|
||||
wrapped := sandbox.WrapCommand(cfg, interpreter, workDir)
|
||||
cmd = exec.CommandContext(ctx, wrapped[0], wrapped[1:]...)
|
||||
cmd.Dir = workDir
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
cmd.Cancel = func() error {
|
||||
if cmd.Process != nil {
|
||||
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var outputBuf bytes.Buffer
|
||||
lw := &limitedWriter{w: &outputBuf, limit: 10 * 1024 * 1024}
|
||||
cmd.Stdout = lw
|
||||
cmd.Stderr = lw
|
||||
|
||||
err = cmd.Run()
|
||||
output := outputBuf.Bytes()
|
||||
if lw.truncated {
|
||||
output = append(output, []byte("\n[output truncated at 10MB]")...)
|
||||
}
|
||||
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return "", fmt.Errorf("execution timeout after %d seconds", timeout)
|
||||
}
|
||||
if err != nil {
|
||||
return string(output), fmt.Errorf("execution failed: %v\nOutput: %s", err, string(output))
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// Close implements tools.Server (no-op).
|
||||
func (e *Server) Close() {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
package execute
|
||||
|
||||
// execute_tool is a specific case of execute_code, such that the name of the
|
||||
// script passed to `tool` is loaded into memory from `$OLLIE_9MOUNT/t/` and
|
||||
// run with Execute(...).
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -84,3 +90,39 @@ func ReadTool(name string) (string, error) {
|
|||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func dispatchExecuteTool(ctx context.Context, e *Server, args json.RawMessage) (string, error) {
|
||||
var a struct {
|
||||
Tool string `json:"tool"`
|
||||
Args []string `json:"args"`
|
||||
Timeout int `json:"timeout"`
|
||||
Sandbox string `json:"sandbox"`
|
||||
}
|
||||
if err := json.Unmarshal(args, &a); err != nil {
|
||||
return "", fmt.Errorf("execute_tool: bad args: %w", err)
|
||||
}
|
||||
if a.Tool == "" {
|
||||
return "", fmt.Errorf("execute_tool: 'tool' is required")
|
||||
}
|
||||
if e.Confirm != nil && !e.Confirm(fmt.Sprintf("execute_tool: %s %s", a.Tool, strings.Join(a.Args, " "))) {
|
||||
return "", fmt.Errorf("execute_tool: denied by user")
|
||||
}
|
||||
toolCode, err := ReadTool(a.Tool)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
language := detectLanguage(toolCode)
|
||||
code := toolCode
|
||||
if len(a.Args) > 0 {
|
||||
code = injectArgs(language, a.Tool, a.Args, toolCode)
|
||||
}
|
||||
timeout := a.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = 30
|
||||
}
|
||||
sandbox := a.Sandbox
|
||||
if sandbox == "" {
|
||||
sandbox = "default"
|
||||
}
|
||||
return e.Execute(ctx, code, language, timeout, sandbox, true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package reasoning
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ollie/pkg/tools"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
|
@ -10,6 +12,61 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// SetMemoryBackend implements tools.MemoryBackendSetter.
|
||||
func (s *Server) SetMemoryBackend(b tools.MemoryBackend) { s.Memory = b }
|
||||
|
||||
var ToolRemember = tools.ToolInfo{
|
||||
Name: "memory_remember",
|
||||
Description: `Save a memory that persists across sessions.
|
||||
|
||||
Provide a descriptive title, relevant tags, and a concise body. Choose tags carefully because they strongly influence later recall. Prefer specific, stable terms over generic ones. Store durable facts, decisions, preferences, or context — not raw transcripts or temporary chatter.`,
|
||||
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 saved memories by keyword.
|
||||
|
||||
Search matches titles and tags only; body content is not searched. Use short, specific terms likely to appear literally in titles or tags at write time. Prefer names, project identifiers, stable topics, or exact tags over full sentences or vague descriptions.`,
|
||||
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."
|
||||
}
|
||||
}
|
||||
}`),
|
||||
}
|
||||
|
||||
// FlatDirBackend implements tools.MemoryBackend using a plain directory of
|
||||
// markdown files. Filenames follow a denote-like convention:
|
||||
//
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
package reasoning
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"ollie/pkg/tools"
|
||||
)
|
||||
|
||||
var ToolPlan = tools.ToolInfo{
|
||||
Name: "reasoning_plan",
|
||||
Description: `Executive planning tool.
|
||||
|
||||
Use this tool before taking action whenever the task is non-trivial: specifically, when it requires multiple steps, has dependencies, may produce side effects, benefits from persistence/resumption, or the correct order is not obvious.
|
||||
|
||||
First, create a complete ordered plan for the current goal. The plan must:
|
||||
- Break the goal into concrete, outcome-oriented steps
|
||||
- Order steps by execution sequence
|
||||
- Record explicit dependencies
|
||||
- Allow dependencies only on earlier steps (lower index)
|
||||
- Keep steps small enough to execute or verify independently
|
||||
- Avoid redundant or purely decorative steps
|
||||
|
||||
Each step should include:
|
||||
- index
|
||||
- short title
|
||||
- description
|
||||
- depends_on (list of earlier step indexes only)
|
||||
- acceptance_criteria
|
||||
|
||||
If a task backend is available via ` + "`task_create`" + `, persist the plan and steps there and use returned task IDs in subsequent updates. If no backend is available, maintain the same ordered plan in memory and execute it sequentially.
|
||||
|
||||
Use ` + "`reasoning_plan`" + `only to create or revise the overall work breakdown. Use ` + "`reasoning_think`" + ` for short, local reflection during execution. Do not substitute moment-to-moment reasoning for a full plan when planning is required.
|
||||
|
||||
Re-plan only if:
|
||||
- a step fails,
|
||||
- new information changes the goal or constraints,
|
||||
- dependencies were incorrect or incomplete,
|
||||
- or a better decomposition becomes necessary.
|
||||
|
||||
Do not begin executing dependent work until its dependencies are complete.
|
||||
`,
|
||||
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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
}
|
||||
|
||||
// SetPlanBackend implements tools.PlanBackendSetter.
|
||||
func (s *Server) SetPlanBackend(b tools.PlanBackend) { s.Plan = b }
|
||||
|
|
@ -15,7 +15,9 @@ func Decl() func() tools.Server { return func() tools.Server { return &Server{}
|
|||
// Server implements tools.Server for reasoning tools.
|
||||
type Server struct {
|
||||
// Plan is the optional task persistence backend. When nil, reasoning_plan
|
||||
// produces an in-context plan only.
|
||||
// produces an in-context plan only. This in-context plan queues each step
|
||||
// of the plan, in order, as prompts for the model to execute when the
|
||||
// planning turn finishes.
|
||||
Plan tools.PlanBackend
|
||||
|
||||
// Memory is the memory persistence backend. Always non-nil after
|
||||
|
|
@ -23,22 +25,21 @@ type Server struct {
|
|||
Memory tools.MemoryBackend
|
||||
}
|
||||
|
||||
// SetPlanBackend implements tools.PlanBackendSetter.
|
||||
func (s *Server) SetPlanBackend(b tools.PlanBackend) { s.Plan = b }
|
||||
|
||||
// SetMemoryBackend implements tools.MemoryBackendSetter.
|
||||
func (s *Server) SetMemoryBackend(b tools.MemoryBackend) { s.Memory = b }
|
||||
|
||||
// ListTools implements tools.Server.
|
||||
func (s *Server) ListTools() ([]tools.ToolInfo, error) {
|
||||
return append(tools.ReasoningDefs(), tools.MemoryDefs()...), nil
|
||||
return []tools.ToolInfo{
|
||||
ToolThink,
|
||||
ToolPlan,
|
||||
ToolRemember,
|
||||
ToolRecall,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CallTool implements tools.Server.
|
||||
func (s *Server) CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
|
||||
var text string
|
||||
switch tool {
|
||||
case "reasoning_think":
|
||||
case ToolThink.Name:
|
||||
var a struct {
|
||||
Thought string `json:"thought"`
|
||||
}
|
||||
|
|
@ -46,19 +47,19 @@ func (s *Server) CallTool(ctx context.Context, tool string, args json.RawMessage
|
|||
text = "error: missing required field 'thought'"
|
||||
}
|
||||
// No-op: the thought is recorded in conversation history by the loop.
|
||||
case "reasoning_plan":
|
||||
case ToolPlan.Name:
|
||||
var err error
|
||||
text, err = s.plan(ctx, args)
|
||||
if err != nil {
|
||||
text = "error: " + err.Error()
|
||||
}
|
||||
case "memory_remember":
|
||||
case ToolRemember.Name:
|
||||
var err error
|
||||
text, err = s.remember(ctx, args)
|
||||
if err != nil {
|
||||
text = "error: " + err.Error()
|
||||
}
|
||||
case "memory_recall":
|
||||
case ToolRecall.Name:
|
||||
var err error
|
||||
text, err = s.recall(ctx, args)
|
||||
if err != nil {
|
||||
|
|
@ -189,6 +190,7 @@ func (s *Server) recall(ctx context.Context, args json.RawMessage) (string, erro
|
|||
return s.Memory.Recall(ctx, a.Query)
|
||||
}
|
||||
|
||||
// Compile-time interface checks
|
||||
var _ tools.Server = (*Server)(nil)
|
||||
var _ tools.PlanBackendSetter = (*Server)(nil)
|
||||
var _ tools.MemoryBackendSetter = (*Server)(nil)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package reasoning
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"ollie/pkg/tools"
|
||||
)
|
||||
|
||||
// think is a no-op tool for reasoning models. it functions like an in-context "scratchpad".
|
||||
|
||||
var ToolThink = tools.ToolInfo{
|
||||
Name: "reasoning_think",
|
||||
Description: "Internal reasoning tool for breaking down problems, planning approaches, analyzing constraints, and reasoning through multi-step solutions before acting. The thought content is not shown to the user.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"required": ["thought"],
|
||||
"properties": {
|
||||
"thought": {"type": "string", "description": "A reflective note or intermediate reasoning step."}
|
||||
}
|
||||
}`),
|
||||
}
|
||||
Reference in New Issue