mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 12:16:54 +00:00
feat(gateway/methods): add cache-aware resolveAgentUUIDCached helper
Three-part addition so WS handlers can accept agent_key input without a DB roundtrip on every request: 1. Router.GetCached — lock-free TTL cache read, no resolver fallback. 2. Loop.UUID() — canonical UUID accessor (agent_key stays on ID()). 3. resolveAgentUUIDCached — fast-path checks router cache via an agentUUIDProvider interface, falls back to resolveAgentUUID on miss or when input is a UUID string (un-cached post-canonicalization). Helper delegates to pure DB lookup when router is nil, so handlers with no router dependency still work. Tests cover nil-router, cache-miss, and UUID-input paths — cache-hit coverage is exercised via downstream handler integration tests. Phase 3 foundation (TD-2).
This commit is contained in:
1 parent
82dc4d3b94
commit
6c17aad8d7
4 files changed
+136
-1
No files matched your search
@@ -22,9 +22,16 @@ func (l *Loop) emit(event AgentEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns the agent's identifier.
|
||||
// ID returns the agent's identifier (agent_key, e.g. "goctech-leader").
|
||||
// Use for logs, UI, filesystem paths. NEVER for DB FK or DomainEvent.AgentID.
|
||||
// See docs/agent-identity-conventions.md (Phase 6).
|
||||
func (l *Loop) ID() string { return l.id }
|
||||
|
||||
// UUID returns the agent's canonical UUID (DB primary key).
|
||||
// Use for SQL WHERE/JOIN, DomainEvent.AgentID, context propagation.
|
||||
// See docs/agent-identity-conventions.md (Phase 6).
|
||||
func (l *Loop) UUID() uuid.UUID { return l.agentUUID }
|
||||
|
||||
// Model returns the model identifier for this agent loop.
|
||||
func (l *Loop) Model() string { return l.model }
|
||||
|
||||
|
||||
@@ -208,6 +208,24 @@ func (r *Router) IsRunning(ctx context.Context, agentID string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCached returns a cached agent without invoking the resolver. Used by
|
||||
// cache-aware helpers that want to avoid a DB roundtrip on the hot path.
|
||||
// Returns (nil, false) on miss or when the entry has exceeded TTL.
|
||||
// Does NOT trigger resolver fallback — call Router.Get() for that path.
|
||||
func (r *Router) GetCached(ctx context.Context, agentID string) (Agent, bool) {
|
||||
cacheKey := agentCacheKey(ctx, agentID)
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
entry, ok := r.agents[cacheKey]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if r.ttl > 0 && time.Since(entry.cachedAt) >= r.ttl {
|
||||
return nil, false
|
||||
}
|
||||
return entry.agent, true
|
||||
}
|
||||
|
||||
// --- Active Run Tracking (matching TS chat-abort.ts) ---
|
||||
|
||||
// ActiveRun tracks a running agent invocation so it can be aborted via chat.abort
|
||||
|
||||
@@ -408,6 +408,11 @@ func (m *AgentLinksMethods) invalidateLinkAgentsByID(ctx context.Context, source
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
// resolveAgentUUID resolves an agent identifier (either UUID or agent_key) to its
|
||||
// canonical UUID via a DB lookup. Tenant-aware via store.TenantIDFromContext(ctx)
|
||||
// inside agentStore.GetByID/GetByKey. Prefer resolveAgentUUIDCached for hot-path
|
||||
// handlers to avoid an unnecessary DB roundtrip.
|
||||
// See docs/agent-identity-conventions.md trap zone 5.
|
||||
func resolveAgentUUID(ctx context.Context, agentStore store.AgentStore, keyOrID string) (uuid.UUID, error) {
|
||||
if id, err := uuid.Parse(keyOrID); err == nil {
|
||||
ag, err := agentStore.GetByID(ctx, id)
|
||||
@@ -423,6 +428,36 @@ func resolveAgentUUID(ctx context.Context, agentStore store.AgentStore, keyOrID
|
||||
return ag.ID, nil
|
||||
}
|
||||
|
||||
// agentUUIDProvider is satisfied by *agent.Loop and by test stubs; lets the
|
||||
// cache-aware helper extract a canonical UUID from any cached Agent without
|
||||
// a direct dependency on the concrete Loop type.
|
||||
type agentUUIDProvider interface {
|
||||
UUID() uuid.UUID
|
||||
}
|
||||
|
||||
// resolveAgentUUIDCached is the cache-aware variant of resolveAgentUUID.
|
||||
// Checks the router cache first when the input is an agent_key and the Loop is
|
||||
// cached — avoids a DB roundtrip on the hot path. Falls back to DB lookup on
|
||||
// cache miss or when the input is a UUID string (not cached under the raw UUID
|
||||
// key post-Phase-2 canonicalization). If router is nil, delegates straight to
|
||||
// resolveAgentUUID. See docs/agent-identity-conventions.md trap zone 5.
|
||||
func resolveAgentUUIDCached(ctx context.Context, router *agent.Router, agentStore store.AgentStore, keyOrID string) (uuid.UUID, error) {
|
||||
// Fast path: input is agent_key and the agent is cached in the router.
|
||||
if router != nil {
|
||||
if _, err := uuid.Parse(keyOrID); err != nil {
|
||||
if ag, ok := router.GetCached(ctx, keyOrID); ok {
|
||||
if up, ok := ag.(agentUUIDProvider); ok {
|
||||
if uid := up.UUID(); uid != uuid.Nil {
|
||||
return uid, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Slow path: DB lookup.
|
||||
return resolveAgentUUID(ctx, agentStore, keyOrID)
|
||||
}
|
||||
|
||||
// resolveAgentInfo returns full agent data for validation and cache invalidation.
|
||||
func resolveAgentInfo(ctx context.Context, agentStore store.AgentStore, keyOrID string) (*store.AgentData, error) {
|
||||
if id, err := uuid.Parse(keyOrID); err == nil {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package methods
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
// errSentinelMiss is a sentinel error used to verify DB fallback in cache-miss
|
||||
// paths. The value is stable across tests so equality checks are reliable.
|
||||
var errSentinelMiss = errors.New("sentinel: agent miss")
|
||||
|
||||
// errorAgentStore embeds store.AgentStore and overrides only GetByID and
|
||||
// GetByKey to return a sentinel error. All other methods retain the nil embed,
|
||||
// so any unexpected call path panics — loud failure is intentional.
|
||||
type errorAgentStore struct {
|
||||
store.AgentStore
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *errorAgentStore) GetByID(context.Context, uuid.UUID) (*store.AgentData, error) {
|
||||
return nil, s.err
|
||||
}
|
||||
|
||||
func (s *errorAgentStore) GetByKey(context.Context, string) (*store.AgentData, error) {
|
||||
return nil, s.err
|
||||
}
|
||||
|
||||
// TestResolveAgentUUIDCached_NilRouterCallsResolveAgentUUID verifies that when
|
||||
// router is nil the helper delegates directly to resolveAgentUUID — exercising
|
||||
// the slow DB path. With a stub store returning a sentinel error, the helper
|
||||
// must propagate that error unchanged.
|
||||
func TestResolveAgentUUIDCached_NilRouterCallsResolveAgentUUID(t *testing.T) {
|
||||
stub := &errorAgentStore{err: errSentinelMiss}
|
||||
|
||||
_, err := resolveAgentUUIDCached(context.Background(), nil, stub, uuid.New().String())
|
||||
|
||||
if !errors.Is(err, errSentinelMiss) {
|
||||
t.Errorf("expected sentinel miss error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveAgentUUIDCached_CacheMissFallsBack — when the router is set but
|
||||
// has no cached entry for the given agent_key, the helper must fall back to
|
||||
// the DB path. The stub store's sentinel error confirms we took the fallback.
|
||||
func TestResolveAgentUUIDCached_CacheMissFallsBack(t *testing.T) {
|
||||
r := agent.NewRouter()
|
||||
stub := &errorAgentStore{err: errSentinelMiss}
|
||||
|
||||
_, err := resolveAgentUUIDCached(context.Background(), r, stub, "missing-agent-key")
|
||||
|
||||
if !errors.Is(err, errSentinelMiss) {
|
||||
t.Errorf("expected sentinel miss error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveAgentUUIDCached_UUIDInputTakesDBPath — per Phase 2 H1, a caller
|
||||
// passing the UUID form is never cached under the raw UUID key, so the helper
|
||||
// must fall through to the DB path. Verifies this by checking the stub's
|
||||
// sentinel error surfaces.
|
||||
func TestResolveAgentUUIDCached_UUIDInputTakesDBPath(t *testing.T) {
|
||||
r := agent.NewRouter()
|
||||
stub := &errorAgentStore{err: errSentinelMiss}
|
||||
|
||||
_, err := resolveAgentUUIDCached(context.Background(), r, stub, uuid.New().String())
|
||||
|
||||
if !errors.Is(err, errSentinelMiss) {
|
||||
t.Errorf("expected sentinel miss error, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user