| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "strings" |
| 8 | |
| 9 | "reasonix/internal/evidence" |
| 10 | "reasonix/internal/tool" |
| 11 | ) |
| 12 | |
| 13 | // parseToolCall resolves the canonical tool and rejects ambiguity/unknown tools. |
| 14 | func (a *Agent) parseToolCall(ctx context.Context, turn *turnRuntime, plan *toolCallPlan) (toolOutcome, bool) { |
| 15 | if retiredTool(plan.call.Name) { |
| 16 | msg := fmt.Sprintf("tool_retired: %s is no longer part of the execution protocol; use file tools, todo_write, or provide the final answer directly", plan.call.Name) |
| 17 | return toolOutcome{output: msg, errMsg: "tool_retired"}, true |
| 18 | } |
| 19 | t, canonicalName, ambiguous := a.svc.tools.ResolveCall(plan.call.Name) |
| 20 | if len(ambiguous) > 0 { |
| 21 | msg := fmt.Sprintf("ambiguous MCP tool reference %q; use one of: %s", plan.call.Name, strings.Join(ambiguous, ", ")) |
| 22 | return toolOutcome{ |
| 23 | output: "error: " + msg, |
| 24 | errMsg: msg, |
| 25 | }, true |
| 26 | } |
| 27 | if t == nil { |
| 28 | if server, ok := completedMCPConnect(a.svc.tools, plan.call.Name); ok { |
| 29 | return toolOutcome{ |
| 30 | output: fmt.Sprintf("MCP server %q is connected; its real tools are now available", server), |
| 31 | }, true |
| 32 | } |
| 33 | return toolOutcome{ |
| 34 | output: fmt.Sprintf("error: unknown tool %q", plan.call.Name), |
| 35 | errMsg: fmt.Sprintf("unknown tool %q", plan.call.Name), |
| 36 | }, true |
| 37 | } |
| 38 | plan.tool = t |
| 39 | plan.canonicalName = canonicalName |
| 40 | plan.permName = canonicalName |
| 41 | plan.permArgs = json.RawMessage(plan.call.Arguments) |
| 42 | plan.execTool = t |
| 43 | plan.execArgs = json.RawMessage(plan.call.Arguments) |
| 44 | plan.evidenceName = canonicalName |
| 45 | plan.evidenceArgs = json.RawMessage(plan.call.Arguments) |
| 46 | plan.readOnly = t.ReadOnly() |
| 47 | if canonicalName == "read_file" { |
| 48 | if out, blocked := a.resolveReadCursor(plan); blocked { |
| 49 | return out, true |
| 50 | } |
| 51 | } |
| 52 | if tool.IsShellToolName(canonicalName) { |
| 53 | var permissionReader bool |
| 54 | plan.effects, permissionReader = evidence.ClassifyBashToolCall(plan.execArgs) |
| 55 | if permissionReader { |
| 56 | // Carry the resolved read-only classification without changing the schema. |
| 57 | plan.readOnly = true |
| 58 | plan.resolvedMeta = &tool.ResolvedCall{TargetName: canonicalName, ReadOnly: true} |
| 59 | } |
| 60 | } else { |
| 61 | plan.effects = evidence.ClassifyToolCall(plan.evidenceName, plan.evidenceArgs, plan.readOnly) |
| 62 | } |
| 63 | return toolOutcome{}, false |
| 64 | } |
| 65 | |
| 66 | func retiredTool(name string) bool { |
| 67 | switch strings.ToLower(strings.TrimSpace(name)) { |
| 68 | case "complete_step", "review_report", "read_policy_receipt", "session_read_strategy_receipt": |
| 69 | return true |
| 70 | default: |
| 71 | return false |
| 72 | } |
| 73 | } |
| 74 |