P3: store group — kv_* + file_* tools (agent memory)
RegisterStore(reg, StoreDeps) registers the persistent-memory tools over the host's KV and/or File backends: - kv_get/set/list/delete (KVStorage seam) - file_save/get/get_text/get_metadata/list/delete (FileStorage seam), plus file_search (FileSearcher) and create_file_url (FileTokenMinter) when wired. Near-zero-config: Quota defaults to a generous static cap (staticQuota), the per-value/per-file caps default, and the kv vs file groups register independently (a host can take just one). Seams moved clean (interface-only): kv_storage.go, quota_provider.go, file_descendant_grant.go. The default in-memory KV/File backends come with contrib/store at P4. Core go.sum still free of gorm/redis/discordgo/sqlite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// kv_storage.go declares the narrow KV-storage interface that the four
|
||||
// KV tools (kv_get, kv_set, kv_list, kv_delete) need at execute time.
|
||||
//
|
||||
// Why a narrow interface (vs importing pkg/logic/skills directly):
|
||||
// pkg/logic/skills imports pkg/skilltools (for Invocation + Tool), so
|
||||
// importing skills back here would form an import cycle. Production
|
||||
// wiring (pkg/logic/mort.go, deferred) will supply a concrete adapter
|
||||
// that wraps `*skills.System.Storage()` and translates between
|
||||
// skills.KVEntry and the local KVDomainEntry shape.
|
||||
//
|
||||
// Why a *separate* domain shape (KVDomainEntry) vs reusing skills.KVEntry:
|
||||
// the cycle break has to be complete — even importing the type would
|
||||
// pull skills into skilltools/tools' import graph. The two shapes mirror
|
||||
// each other field-for-field; the adapter is a trivial struct copy.
|
||||
//
|
||||
// The same pattern is used by skill_invoke.go (SkillInvokerProvider).
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.stevedudenhoeffer.com/steve/executus/tool"
|
||||
)
|
||||
|
||||
// kvPartition picks the skill_id partition for a KV operation. KV rows
|
||||
// are keyed (skill_id, scope, key); for the shared `root_run:<id>`
|
||||
// scope, every run in a dispatch tree — including ephemeral workers
|
||||
// with distinct agent IDs — must land in ONE partition or siblings
|
||||
// could never see each other's writes. The sentinel
|
||||
// tool.RootRunKVPartition is that shared partition; isolation
|
||||
// between trees is preserved because the scope string embeds the root
|
||||
// run id, which ValidateScope checks against inv.RootRunID.
|
||||
func kvPartition(inv tool.Invocation, scope string) string {
|
||||
if strings.HasPrefix(scope, "root_run:") {
|
||||
return tool.RootRunKVPartition
|
||||
}
|
||||
return inv.SkillID
|
||||
}
|
||||
|
||||
// KVStorage is the narrow surface KV tools need from the skills package.
|
||||
// nil-safe: tools constructed against a nil KVStorage surface a clean
|
||||
// "not configured" error at the first call rather than crashing.
|
||||
type KVStorage interface {
|
||||
KVGet(ctx context.Context, skillID, scope, key string) (*KVDomainEntry, error)
|
||||
KVSet(ctx context.Context, e KVDomainEntry) error
|
||||
KVList(ctx context.Context, skillID, scope, prefix string, limit int) ([]KVDomainEntry, error)
|
||||
KVDelete(ctx context.Context, skillID, scope, key string) error
|
||||
KVUsageBytes(ctx context.Context, skillID string) (int64, error)
|
||||
}
|
||||
|
||||
// KVHistoryRecorder is the OPTIONAL post-write hook for the v7
|
||||
// versioned KV history. The kv_set tool checks for this interface via
|
||||
// type assertion; production storage adapters that satisfy it write a
|
||||
// history row AFTER a successful KVSet.
|
||||
//
|
||||
// Why optional (vs adding to KVStorage): existing test fakes don't
|
||||
// need to grow a method. Production wires the real adapter which
|
||||
// satisfies the interface; tests that don't care about history skip
|
||||
// the implementation entirely.
|
||||
//
|
||||
// Why only on success: a failed KVSet leaves no skill_kv row to refer
|
||||
// to; appending a history entry would create an orphan record of a
|
||||
// change that didn't happen.
|
||||
type KVHistoryRecorder interface {
|
||||
RecordKVHistory(ctx context.Context, skillID, scope, key string, value []byte, changedBy string) error
|
||||
}
|
||||
|
||||
// KVDomainEntry mirrors skills.KVEntry without pulling in the cycle.
|
||||
// Field-for-field with the skills package's KVEntry; the production
|
||||
// adapter is a struct copy.
|
||||
type KVDomainEntry struct {
|
||||
SkillID string
|
||||
Scope string // "skill" | "user:<id>" | "run:<id>"
|
||||
Key string
|
||||
Value json.RawMessage
|
||||
ExpiresAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// ErrKVNotFound mirrors skills.ErrKVNotFound. The production adapter
|
||||
// returns this sentinel when wrapping a skills.ErrKVNotFound; tools
|
||||
// detect it with errors.Is to surface "not_found" to the LLM rather
|
||||
// than a generic error.
|
||||
var ErrKVNotFound = errors.New("kv: not found")
|
||||
Reference in New Issue
Block a user