summaryrefslogtreecommitdiff
path: root/internal/executor/localtools.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-06-03 23:01:39 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-06-03 23:01:39 +0000
commit68cf1e24dd2ca2612b72babc4b35280cd3512f92 (patch)
tree14b73f21570a2f42ae729ca7e9676de6628d6819 /internal/executor/localtools.go
parentb28cfc6ff288d083f6c8e9c055b69bfcadbceccc (diff)
parent7388337d3be5cc7f65ef547e30b2e39884dd165b (diff)
merge: integrate oss branch — budget gating, MCP back-channel, events, loopback-only bind
Key features from OSS branch: - Budget gating: rolling per-provider spend caps with BUDGET_EXCEEDED task state - Agent MCP back-channel: runners mint tokens; /mcp endpoint resolves them - Chatbot MCP server at /chatbot/mcp (requires api_token in config) - Event timeline: unified event stream replacing ad-hoc question/summary flows - Loopback-only default bind (127.0.0.1:8484); external_bind_allowed=true to expose - repository_url required on task creation (enforces traceability) - Auto-checker: spawns verification task after each execution Conflict resolutions: - serve.go: keep cfg.Runners.XEnabled() conditional registration from main + agentRegistry from oss - config.go: keep RunnersConfig from main + BudgetConfig/ExternalBindAllowed from oss - server.go: handleStaticFiles (base-path rewrite) kept; deduplicated duplicate from both sides - executor/claude.go: accepted oss deletion (ClaudeRunner replaced by ContainerRunner) - container_test.go: added noopChannel + AgentChannel parameter to Run call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/executor/localtools.go')
-rw-r--r--internal/executor/localtools.go135
1 files changed, 135 insertions, 0 deletions
diff --git a/internal/executor/localtools.go b/internal/executor/localtools.go
new file mode 100644
index 0000000..48b862f
--- /dev/null
+++ b/internal/executor/localtools.go
@@ -0,0 +1,135 @@
+package executor
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/thepeterstone/claudomator/internal/llm"
+)
+
+// agentToolDefs returns the four agent back-channel tools as OpenAI
+// function-calling definitions, for runners that talk to an OpenAI-compatible
+// endpoint (LocalRunner). They mirror the MCP tools the container runners expose.
+func agentToolDefs() []llm.Tool {
+ strProp := func(desc string) map[string]any {
+ return map[string]any{"type": "string", "description": desc}
+ }
+ return []llm.Tool{
+ {Type: "function", Function: llm.ToolFunction{
+ 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.",
+ Parameters: 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"},
+ },
+ }},
+ {Type: "function", Function: llm.ToolFunction{
+ Name: "report_summary",
+ Description: "Record a concise 2-5 sentence summary of what you accomplished. Call this before finishing.",
+ Parameters: map[string]any{
+ "type": "object",
+ "properties": map[string]any{"summary": strProp("the summary text")},
+ "required": []string{"summary"},
+ },
+ }},
+ {Type: "function", Function: llm.ToolFunction{
+ Name: "spawn_subtask",
+ Description: "Create a child task to be executed separately. Use this to break large work into focused pieces.",
+ Parameters: 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"},
+ },
+ }},
+ {Type: "function", Function: llm.ToolFunction{
+ Name: "record_progress",
+ Description: "Record a short progress note that appears in the task timeline.",
+ Parameters: map[string]any{
+ "type": "object",
+ "properties": map[string]any{"message": strProp("a short progress note")},
+ "required": []string{"message"},
+ },
+ }},
+ }
+}
+
+// dispatchAgentTool invokes one model-requested tool against the AgentChannel.
+// 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).
+func dispatchAgentTool(ctx context.Context, ch AgentChannel, 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 := ch.AskUser(ctx, string(payload))
+ if errors.Is(askErr, 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 := ch.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 := ch.SpawnSubtask(ctx, 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 := ch.RecordProgress(ctx, a.Message); rpErr != nil {
+ return "", false, rpErr
+ }
+ return "Noted.", false, nil
+
+ default:
+ return "", false, fmt.Errorf("unknown tool %q", name)
+ }
+}