package tools import ( "bytes" "context" "encoding/json" "io" "strings" "github.com/bnema/zut/packages/core" "github.com/bnema/zut/packages/provider" ) const ( UpdateGoalToolName = "update_goal" updateGoalSchema = `{"type":"object","properties":{"status":{"type":"string","enum":["active","complete","blocked"],"description":"Set the next active goal, or mark the active goal complete or blocked."},"objective":{"type":"string","description":"Concrete objective for status active."},"mission_id":{"type":"string","description":"Mission identifier supplied in the active-goal context when setting a next goal."},"reason":{"type":"string","description":"Concise reason when the goal is blocked."}},"required":["status"],"additionalProperties":false}` ) // UpdateGoalTool lets the manager set the next concrete active goal or settle // the current goal. Host-side persistence validates that a transition remains // within the session's active mission. type UpdateGoalTool struct{} type updateGoalArgs struct { Status string `json:"status"` Objective string `json:"objective,omitempty"` MissionID string `json:"mission_id,omitempty"` Reason string `json:"reason,omitempty"` } // GoalUpdate is trusted metadata returned only by UpdateGoalTool. type GoalUpdate struct { Status core.GoalStatus Objective string MissionID string Reason string } func (t *UpdateGoalTool) Name() string { return UpdateGoalToolName } func (t *UpdateGoalTool) Description() string { return "Set the next concrete goal needed for the active user mission, or mark the active goal complete or blocked. Do not broaden the user's mission." } func (t *UpdateGoalTool) Schema() json.RawMessage { return json.RawMessage(updateGoalSchema) } func (t *UpdateGoalTool) Execute(_ context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) { var args updateGoalArgs decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if err := decoder.Decode(&args); err != nil { return goalToolError("invalid arguments"), nil } if err := decoder.Decode(&struct{}{}); err != io.EOF { return goalToolError("invalid arguments"), nil } args.Status = strings.TrimSpace(args.Status) args.Objective = strings.TrimSpace(args.Objective) args.MissionID = strings.TrimSpace(args.MissionID) args.Reason = strings.TrimSpace(args.Reason) var update GoalUpdate switch args.Status { case "active": if args.Objective == "" { return goalToolError("objective is required when setting a goal"), nil } update.Status = core.GoalActive update.Objective = args.Objective update.MissionID = args.MissionID case "complete": update.Status = core.GoalDone case "blocked": if args.Reason == "" { return goalToolError("reason is required when blocking a goal"), nil } update.Status = core.GoalBlocked update.Reason = args.Reason default: return goalToolError("status must be active, complete, or blocked"), nil } text := "autonomous goal marked " + string(update.Status) if update.Reason != "" { text += ": " + update.Reason } return core.ToolResult{ Content: []provider.Content{provider.TextBlock{Text: text}}, Details: update, }, nil } // GoalUpdateFromResult extracts a valid update_goal state transition. func GoalUpdateFromResult(result core.ToolResult) (GoalUpdate, bool) { if result.IsError { return GoalUpdate{}, false } update, ok := result.Details.(GoalUpdate) if !ok { return GoalUpdate{}, false } if update.Status != core.GoalActive && update.Status != core.GoalDone && update.Status != core.GoalBlocked { return GoalUpdate{}, false } if update.Status == core.GoalActive && strings.TrimSpace(update.Objective) == "" { return GoalUpdate{}, false } if update.Status == core.GoalBlocked && strings.TrimSpace(update.Reason) == "" { return GoalUpdate{}, false } return update, true } func goalToolError(message string) core.ToolResult { return core.ToolResult{ Content: []provider.Content{provider.TextBlock{Text: message}}, IsError: true, } }