// kv_get is the v4 KV-storage read tool. It looks up a single value by // (scope, key) within the calling skill's KV namespace and returns the // stored JSON value, or `null` when no row matches. // // Why "null" on miss (vs an error): the LLM's most natural use is // "fetch this if cached, otherwise compute and store". Miss-as-error // would force the agent to wrap every call in error handling; miss-as- // null collapses the happy path. package tools import ( "context" "errors" "fmt" "gitea.stevedudenhoeffer.com/steve/executus/tool" ) type kvGetArgs struct { Scope string `json:"scope" description:"Storage scope: 'skill' (shared across all callers of this skill), 'user:' (per-caller), 'run:' (this run's scratchpad), or 'root_run:' (shared scratchpad of this whole dispatch tree — use to coordinate with parallel sibling workers)."` Key string `json:"key" description:"Key within the scope."` } // NewKVGet constructs the kv_get tool. storage may be nil — the tool // then surfaces "not configured" at execute time instead of failing // registration. // // Permission: anyone may author; safe for share. The scope check at // handler entry makes share-safety meaningful — a shared skill cannot // read another caller's `user:` bucket because ValidateScope // rejects that. func NewKVGet(storage KVStorage) tool.Tool { return tool.NewGatedTool[kvGetArgs]( "kv_get", "Look up a value by key in this skill's storage. Returns the stored JSON value, or `null` if no row matches the (scope, key) tuple.", tool.Permission{ AuthoringRequirement: tool.RequirementAnyone, OperatesOn: tool.ScopeCaller, SafeForShare: true, Categories: []string{"storage", "read"}, }, func(ctx context.Context, inv tool.Invocation, args kvGetArgs) (string, error) { if storage == nil { return "", fmt.Errorf("kv_get: not configured") } if err := ValidateScope(inv, args.Scope, inv.CallerIsAdmin); err != nil { return "", fmt.Errorf("kv_get: %w", err) } if args.Key == "" { return "", fmt.Errorf("kv_get: key required") } entry, err := storage.KVGet(ctx, kvPartition(inv, args.Scope), args.Scope, args.Key) if err != nil { if errors.Is(err, ErrKVNotFound) { return "null", nil } return "", fmt.Errorf("kv_get: %w", err) } return string(entry.Value), nil }, ) }