// kv_list returns metadata (key, size, expiry) for entries within a // scope, optionally filtered by key prefix. Values are NOT loaded — // listing is a hot path that should stay light, and dumping every // value byte into the LLM context would burn tokens for no benefit. package tools import ( "context" "encoding/json" "fmt" "time" "gitea.stevedudenhoeffer.com/steve/executus/tool" ) const ( kvListDefaultLimit = 100 kvListMaxLimit = 1000 ) type kvListArgs struct { Scope string `json:"scope" description:"Storage scope: 'skill', 'user:', 'run:', or 'root_run:'."` Prefix string `json:"prefix,omitempty" description:"Optional key-prefix filter. Empty matches all keys in the scope."` Limit int `json:"limit,omitempty" description:"Max entries to return. Default 100, hard cap 1000."` } type kvListEntry struct { Key string `json:"key"` SizeBytes int `json:"size_bytes"` // ExpiresAt is RFC3339 when set, "" otherwise. JSON serialised this // way so the LLM can reason about it as a string field consistently // (rather than null vs. missing key). ExpiresAt string `json:"expires_at,omitempty"` } // NewKVList constructs the kv_list tool. storage nil → "not configured" // at execute time. func NewKVList(storage KVStorage) tool.Tool { return tool.NewGatedTool[kvListArgs]( "kv_list", "List keys + sizes + expiries in a scope (optionally filtered by key prefix). Returns a JSON array. Does NOT include values — call kv_get to fetch a specific value.", tool.Permission{ AuthoringRequirement: tool.RequirementAnyone, OperatesOn: tool.ScopeCaller, SafeForShare: true, Categories: []string{"storage", "read"}, }, func(ctx context.Context, inv tool.Invocation, args kvListArgs) (string, error) { if storage == nil { return "", fmt.Errorf("kv_list: not configured") } if err := ValidateScope(inv, args.Scope, inv.CallerIsAdmin); err != nil { return "", fmt.Errorf("kv_list: %w", err) } limit := args.Limit if limit <= 0 { limit = kvListDefaultLimit } if limit > kvListMaxLimit { limit = kvListMaxLimit } rows, err := storage.KVList(ctx, kvPartition(inv, args.Scope), args.Scope, args.Prefix, limit) if err != nil { return "", fmt.Errorf("kv_list: %w", err) } out := make([]kvListEntry, 0, len(rows)) for _, r := range rows { e := kvListEntry{ Key: r.Key, SizeBytes: len(r.Value), } if r.ExpiresAt != nil { e.ExpiresAt = r.ExpiresAt.Format(time.RFC3339) } out = append(out, e) } b, err := json.Marshal(out) if err != nil { return "", fmt.Errorf("kv_list: marshal: %w", err) } return string(b), nil }, ) }