package tools import ( "context" "encoding/json" "fmt" "strings" "time" "gitea.stevedudenhoeffer.com/steve/executus/tool" ) // nowParams is the LLM-facing param struct for current_time / now. // // Why optional `timezone`: most agent prompts know the user's local // timezone (it's in the chatbot's system prompt) but the agent has // no way to override on a per-call basis. An explicit arg lets a // research skill ask "what time is it in NYC for the user reading // this report?" without needing access to a member-config lookup // tool. type nowParams struct { Timezone string `json:"timezone,omitempty" description:"Optional IANA timezone name (e.g. 'America/Chicago', 'Europe/London'). Defaults to the calling user's configured timezone, falling back to UTC."` } // nowResponse is the JSON envelope returned to the agent. // // Why a structured shape: the v1 tool returned a markdown blob. // Agents that needed just the year had to substring-parse, which // fails on locale variations. JSON lets the agent pick the field // it cares about. type nowResponse struct { NowISO string `json:"now_iso"` NowHuman string `json:"now_human"` Timezone string `json:"timezone"` Weekday string `json:"weekday"` Year int `json:"year"` Month int `json:"month"` Day int `json:"day"` Hour int `json:"hour"` Minute int `json:"minute"` Second int `json:"second"` Warning string `json:"warning,omitempty"` } // NewNow constructs the v11 current_time / now tool. The provider // supplies the calling member's configured timezone (per-user // localisation). nil falls back to UTC. // // V11 keeps the registered tool name "now" for back-compat with the // existing tool catalog tests AND adds the same tool surface under // the agent-facing description "current time". The design spec // called the tool "current_time" but the v1 registry already used // "now" — switching the registry name would break stored skills' // `tools` lists. Same name, expanded behaviour. func NewNow(provider CurrentTimeProvider) tool.Tool { return tool.NewGatedTool[nowParams]( "now", "Return the current time. Optional 'timezone' (IANA name e.g. 'America/Chicago'); defaults to the calling user's configured timezone or UTC. Returns ISO + human-readable formats plus structured year/month/day/weekday for time-relative reasoning. Use this BEFORE assuming a year — the agent's knowledge cut-off may differ from real time.", tool.Permission{ AuthoringRequirement: tool.RequirementAnyone, OperatesOn: tool.ScopeGlobal, SafeForShare: true, Categories: []string{"utility"}, }, func(ctx context.Context, inv tool.Invocation, p nowParams) (string, error) { tzName := strings.TrimSpace(p.Timezone) warning := "" if tzName == "" && provider != nil { tzName = provider.UserTimezone(ctx, inv.CallerID) } if tzName == "" { tzName = "UTC" } loc, err := time.LoadLocation(tzName) if err != nil { warning = fmt.Sprintf("unknown timezone %q; falling back to UTC", tzName) tzName = "UTC" loc = time.UTC } t := time.Now().In(loc) out := nowResponse{ NowISO: t.Format(time.RFC3339), NowHuman: t.Format("Monday, January 2, 2006 at 3:04 PM MST"), Timezone: tzName, Weekday: t.Weekday().String(), Year: t.Year(), Month: int(t.Month()), Day: t.Day(), Hour: t.Hour(), Minute: t.Minute(), Second: t.Second(), Warning: warning, } b, mErr := json.Marshal(out) if mErr != nil { return "", fmt.Errorf("now: marshal: %w", mErr) } return string(b), nil }, ) }