diff options
Diffstat (limited to 'internal/agentloop/tools.go')
| -rw-r--r-- | internal/agentloop/tools.go | 226 |
1 files changed, 226 insertions, 0 deletions
diff --git a/internal/agentloop/tools.go b/internal/agentloop/tools.go new file mode 100644 index 0000000..e92a12f --- /dev/null +++ b/internal/agentloop/tools.go @@ -0,0 +1,226 @@ +package agentloop + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/thepeterstone/claudomator/internal/agentchannel" + "github.com/thepeterstone/claudomator/internal/provider" +) + +// agentToolSpecs returns the eight tools available to the loop: the four +// agent back-channel tools (mirroring the MCP tools ContainerRunner exposes) +// plus the four sandbox tools, as provider-neutral ToolSpecs. Ported verbatim +// (same names/descriptions/JSON schemas) from the former +// executor.agentToolDefs (internal/executor/localtools.go). +func agentToolSpecs() []provider.ToolSpec { + strProp := func(desc string) map[string]any { + return map[string]any{"type": "string", "description": desc} + } + return []provider.ToolSpec{ + { + Name: "ask_user", + Description: "Ask the user a question when you genuinely need a decision to proceed. The task pauses until the user answers; do not call other tools after this.", + ParametersJSONSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "question": strProp("the question to ask, phrased as a real question"), + "options": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "optional suggested answer choices"}, + }, + "required": []string{"question"}, + }, + }, + { + Name: "report_summary", + Description: "Record a concise 2-5 sentence summary of what you accomplished. Call this before finishing.", + ParametersJSONSchema: map[string]any{ + "type": "object", + "properties": map[string]any{"summary": strProp("the summary text")}, + "required": []string{"summary"}, + }, + }, + { + Name: "spawn_subtask", + Description: "Create a child task to be executed separately. Use this to break large work into focused pieces.", + ParametersJSONSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "name": strProp("short descriptive name for the subtask"), + "instructions": strProp("complete instructions for the subtask agent"), + "model": strProp("optional model override"), + "max_budget_usd": map[string]any{"type": "number", "description": "optional budget cap in USD"}, + }, + "required": []string{"name", "instructions"}, + }, + }, + { + Name: "record_progress", + Description: "Record a short progress note that appears in the task timeline.", + ParametersJSONSchema: map[string]any{ + "type": "object", + "properties": map[string]any{"message": strProp("a short progress note")}, + "required": []string{"message"}, + }, + }, + { + Name: "read_file", + Description: "Read the contents of a file in the sandbox working directory.", + ParametersJSONSchema: map[string]any{ + "type": "object", + "properties": map[string]any{"path": strProp("path to the file, relative or absolute")}, + "required": []string{"path"}, + }, + }, + { + Name: "write_file", + Description: "Write or overwrite a file in the sandbox working directory.", + ParametersJSONSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "path": strProp("path to the file, relative or absolute"), + "content": strProp("content to write"), + }, + "required": []string{"path", "content"}, + }, + }, + { + Name: "run_bash", + Description: "Run a shell command in the sandbox working directory. Returns exit code, stdout, and stderr.", + ParametersJSONSchema: map[string]any{ + "type": "object", + "properties": map[string]any{"command": strProp("shell command to execute")}, + "required": []string{"command"}, + }, + }, + { + Name: "glob", + Description: "List files matching a glob pattern relative to the sandbox working directory.", + ParametersJSONSchema: map[string]any{ + "type": "object", + "properties": map[string]any{"pattern": strProp("glob pattern, e.g. **/*.go")}, + "required": []string{"pattern"}, + }, + }, + } +} + +// dispatchTool invokes one model-requested tool. Back-channel tools +// (ask_user/report_summary/spawn_subtask/record_progress) go to l.Channel; +// sandbox tools (read_file/write_file/run_bash/glob) go to l.Sandbox. It +// returns the text to feed back to the model as the tool result, and a +// blocked flag set when ask_user could not be answered in-session (the run +// must stop and the task block). Ported from the former +// executor.dispatchAgentTool (internal/executor/localtools.go), preserving +// identical result-string formatting. +func (l *Loop) dispatchTool(ctx context.Context, name, argsJSON string) (result string, blocked bool, err error) { + switch name { + case "ask_user": + var a struct { + Question string `json:"question"` + Options []string `json:"options"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + q := map[string]any{"text": a.Question} + if len(a.Options) > 0 { + q["options"] = a.Options + } + payload, _ := json.Marshal(q) + ans, askErr := l.Channel.AskUser(ctx, string(payload)) + if errors.Is(askErr, agentchannel.ErrAgentBlocked) { + return "", true, nil + } + if askErr != nil { + return "", false, askErr + } + return ans, false, nil + + case "report_summary": + var a struct { + Summary string `json:"summary"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + if rsErr := l.Channel.ReportSummary(ctx, a.Summary); rsErr != nil { + return "", false, rsErr + } + return "Summary recorded.", false, nil + + case "spawn_subtask": + var a struct { + Name string `json:"name"` + Instructions string `json:"instructions"` + Model string `json:"model"` + MaxBudgetUSD float64 `json:"max_budget_usd"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + id, ssErr := l.Channel.SpawnSubtask(ctx, agentchannel.SubtaskSpec{ + Name: a.Name, + Instructions: a.Instructions, + Model: a.Model, + MaxBudgetUSD: a.MaxBudgetUSD, + }) + if ssErr != nil { + return "", false, ssErr + } + return "Created subtask " + id, false, nil + + case "record_progress": + var a struct { + Message string `json:"message"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + if rpErr := l.Channel.RecordProgress(ctx, a.Message); rpErr != nil { + return "", false, rpErr + } + return "Noted.", false, nil + + case "read_file": + var a struct { + Path string `json:"path"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + content, readErr := l.Sandbox.ReadFile(ctx, a.Path) + if readErr != nil { + return "", false, readErr + } + return content, false, nil + + case "write_file": + var a struct { + Path string `json:"path"` + Content string `json:"content"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + if writeErr := l.Sandbox.WriteFile(ctx, a.Path, a.Content); writeErr != nil { + return "", false, writeErr + } + return "Written.", false, nil + + case "run_bash": + var a struct { + Command string `json:"command"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + out, errOut, exitCode, runErr := l.Sandbox.RunBash(ctx, a.Command) + if runErr != nil { + return "", false, runErr + } + return fmt.Sprintf("exit_code: %d\nstdout:\n%s\nstderr:\n%s", exitCode, out, errOut), false, nil + + case "glob": + var a struct { + Pattern string `json:"pattern"` + } + _ = json.Unmarshal([]byte(argsJSON), &a) + matches, globErr := l.Sandbox.Glob(ctx, a.Pattern) + if globErr != nil { + return "", false, globErr + } + out, _ := json.Marshal(matches) + return string(out), false, nil + + default: + return "", false, fmt.Errorf("unknown tool %q", name) + } +} |
