mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 04:20:16 +00:00
129 lines
3.0 KiB
Go
129 lines
3.0 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultWaitMinMs = 100
|
|
defaultWaitMaxMs = 300000
|
|
)
|
|
|
|
// WaitTool pauses the current agent tool sequence for a bounded duration.
|
|
type WaitTool struct{}
|
|
|
|
func NewWaitTool() *WaitTool { return &WaitTool{} }
|
|
|
|
func (t *WaitTool) Name() string { return "wait" }
|
|
|
|
func (t *WaitTool) Description() string {
|
|
return "Pause execution before the next tool call. Use for rate-limit spacing or waiting for async work to complete."
|
|
}
|
|
|
|
func (t *WaitTool) Parameters() map[string]any {
|
|
return map[string]any{
|
|
"type": "object",
|
|
"required": []string{"timeMs"},
|
|
"properties": map[string]any{
|
|
"timeMs": map[string]any{
|
|
"type": "integer",
|
|
"description": "Duration to wait in milliseconds.",
|
|
"minimum": defaultWaitMinMs,
|
|
"maximum": defaultWaitMaxMs,
|
|
},
|
|
"reason": map[string]any{
|
|
"type": "string",
|
|
"description": "Optional reason for logging and debugging.",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (t *WaitTool) Execute(ctx context.Context, args map[string]any) *Result {
|
|
timeMs, err := parseWaitMillis(args["timeMs"])
|
|
if err != nil {
|
|
return ErrorResult(err.Error())
|
|
}
|
|
|
|
minMs, maxMs := waitLimits(ctx)
|
|
if timeMs < minMs {
|
|
return ErrorResult(fmt.Sprintf("timeMs must be at least %dms", minMs))
|
|
}
|
|
if timeMs > maxMs {
|
|
return ErrorResult(fmt.Sprintf("timeMs must be at most %dms", maxMs))
|
|
}
|
|
|
|
timer := time.NewTimer(time.Duration(timeMs) * time.Millisecond)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case <-timer.C:
|
|
reason, _ := args["reason"].(string)
|
|
reason = strings.TrimSpace(reason)
|
|
if reason != "" {
|
|
return SilentResult(fmt.Sprintf("Waited %dms. Reason: %s", timeMs, reason))
|
|
}
|
|
return SilentResult(fmt.Sprintf("Waited %dms.", timeMs))
|
|
case <-ctx.Done():
|
|
return ErrorResult("wait cancelled: " + ctx.Err().Error())
|
|
}
|
|
}
|
|
|
|
func parseWaitMillis(value any) (int, error) {
|
|
if value == nil {
|
|
return 0, fmt.Errorf("timeMs is required")
|
|
}
|
|
switch v := value.(type) {
|
|
case int:
|
|
return v, nil
|
|
case int64:
|
|
return int(v), nil
|
|
case float64:
|
|
if math.IsNaN(v) || math.IsInf(v, 0) || math.Trunc(v) != v {
|
|
return 0, fmt.Errorf("timeMs must be an integer number of milliseconds")
|
|
}
|
|
return int(v), nil
|
|
case json.Number:
|
|
i, err := strconv.Atoi(v.String())
|
|
if err != nil {
|
|
return 0, fmt.Errorf("timeMs must be an integer number of milliseconds")
|
|
}
|
|
return i, nil
|
|
default:
|
|
return 0, fmt.Errorf("timeMs must be an integer number of milliseconds")
|
|
}
|
|
}
|
|
|
|
func waitLimits(ctx context.Context) (int, int) {
|
|
minMs := defaultWaitMinMs
|
|
maxMs := defaultWaitMaxMs
|
|
if cfg := WaitToolConfigFromCtx(ctx); cfg != nil {
|
|
if cfg.MinMs > 0 {
|
|
minMs = clampWaitLimit(cfg.MinMs)
|
|
}
|
|
if cfg.MaxMs > 0 {
|
|
maxMs = clampWaitLimit(cfg.MaxMs)
|
|
}
|
|
}
|
|
if maxMs < minMs {
|
|
maxMs = minMs
|
|
}
|
|
return minMs, maxMs
|
|
}
|
|
|
|
func clampWaitLimit(v int) int {
|
|
if v < defaultWaitMinMs {
|
|
return defaultWaitMinMs
|
|
}
|
|
if v > defaultWaitMaxMs {
|
|
return defaultWaitMaxMs
|
|
}
|
|
return v
|
|
}
|