1package tools
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "io"
8
9 "github.com/bnema/zut/packages/core"
10 "github.com/bnema/zut/packages/provider"
11)
12
13const (
14 UpdatePlanToolName = "update_plan"
15 updatePlanSchema = `{"type":"object","properties":{"explanation":{"type":"string","description":"Optional explanation for this plan update."},"plan":{"type":"array","description":"The list of steps.","items":{"type":"object","properties":{"step":{"type":"string","description":"Task step text."},"status":{"type":"string","enum":["pending","in_progress","completed"],"description":"Step status."}},"required":["step","status"],"additionalProperties":false}}},"required":["plan"],"additionalProperties":false}`
16)
17
18// UpdatePlanTool lets the model maintain a turn-scoped task checklist.
19type UpdatePlanTool struct{}
20
21var _ core.ToolArgumentRewritePolicy = (*UpdatePlanTool)(nil)
22
23type updatePlanArgs struct {
24 Explanation *string `json:"explanation"`
25 Plan *[]updatePlanStep `json:"plan"`
26}
27
28type updatePlanStep struct {
29 Step *string `json:"step"`
30 Status *string `json:"status"`
31}
32
33func (t *UpdatePlanTool) Name() string { return UpdatePlanToolName }
34
35// AllowArgumentRewrite keeps the displayed checklist identical to the plan the
36// model submitted. Guards may still allow or refuse the call.
37func (t *UpdatePlanTool) AllowArgumentRewrite() bool { return false }
38
39func (t *UpdatePlanTool) Description() string {
40 return "Track steps and progress for complex, ambiguous, or multi-phase work. Skip trivial tasks and keep the plan current as work progresses."
41}
42
43func (t *UpdatePlanTool) Schema() json.RawMessage { return json.RawMessage(updatePlanSchema) }
44
45func (t *UpdatePlanTool) Execute(_ context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
46 var args updatePlanArgs
47 decoder := json.NewDecoder(bytes.NewReader(raw))
48 decoder.DisallowUnknownFields()
49 if err := decoder.Decode(&args); err != nil {
50 return updatePlanError(), nil
51 }
52 if err := decoder.Decode(&struct{}{}); err != io.EOF || args.Plan == nil {
53 return updatePlanError(), nil
54 }
55
56 update := core.PlanUpdate{
57 Explanation: args.Explanation,
58 Plan: make([]core.PlanStep, len(*args.Plan)),
59 }
60 for idx, step := range *args.Plan {
61 if step.Step == nil || step.Status == nil {
62 return updatePlanError(), nil
63 }
64 status := core.PlanStepStatus(*step.Status)
65 switch status {
66 case core.PlanPending, core.PlanInProgress, core.PlanCompleted:
67 default:
68 return updatePlanError(), nil
69 }
70 update.Plan[idx] = core.PlanStep{Step: *step.Step, Status: status}
71 }
72
73 return core.ToolResult{
74 Content: []provider.Content{provider.TextBlock{Text: "Plan updated"}},
75 Details: update,
76 }, nil
77}
78
79func updatePlanError() core.ToolResult {
80 return core.ToolResult{
81 Content: []provider.Content{provider.TextBlock{Text: "invalid arguments"}},
82 IsError: true,
83 }
84}