Documentation
¶
Overview ¶
Package agentprofile implements the persistent agent profile (gm-v5z2.2, work-planning.md §4 Layer 1.2).
Sister to the session profile in internal/planner. Same shape (concepts {tag:weight} + files {path:weight}), keyed on AgentRef.ID, but with two key differences:
Survives `gt handoff`. A new session inherits its agent's profile as a warm starting point, then accumulates session- specific weight on top.
Different question. Session profile answers "what is this session warm on right now?" Agent profile answers "what is this agent good at over weeks?" An agent who has been deep in the planner across multiple sessions stays primed on planner concepts even on its first bead of a fresh session.
Decay: per-day half-life (default 14d), distinct from the session profile's per-bead-event half-life (default 5).
Writes: the retrospective hook (§7) writes both profiles on bead completion. Session row gets the bead's actual concepts/ files at full weight; agent row gets the same contribution scaled by 1 / (lifetime bead count) so a single bead doesn't dominate a long-running agent's profile.
Reads: §4 Layer 3.2 Affinity reads BOTH profiles, weighted (default 0.7 session + 0.3 agent — tunable). The mix lives in the scoring layer; this package only owns persistence + decay.
Index ¶
- Constants
- func AgeByDays(in map[planner.ConceptTag]float64, days, halfLifeDays float64) map[planner.ConceptTag]float64
- func AgeFilesByDays(in map[string]float64, days, halfLifeDays float64) map[string]float64
- type AgentProfile
- type CompletionEvent
- type Store
- func (s *Store) EnsureSchema(ctx context.Context) error
- func (s *Store) Get(ctx context.Context, agentID core.AgentID) (*AgentProfile, error)
- func (s *Store) List(ctx context.Context) ([]AgentProfile, error)
- func (s *Store) RecordCompletion(ctx context.Context, ev CompletionEvent) (*AgentProfile, error)
- func (s *Store) Upsert(ctx context.Context, p *AgentProfile) error
Constants ¶
const DefaultDecayHalfLifeDays = 14.0
DefaultDecayHalfLifeDays is the per-day half-life of agent concept / file weights. 14 days matches the spec rule of thumb — a hand-off after a couple weeks of focused work still gives the new session a warm starting point, but a long quiet period fades the priming back toward neutral.
Variables ¶
This section is empty.
Functions ¶
func AgeByDays ¶
func AgeByDays(in map[planner.ConceptTag]float64, days, halfLifeDays float64) map[planner.ConceptTag]float64
AgeByDays returns a copy of `in` with every weight multiplied by 0.5 ^ (days / halfLifeDays). Pure: input is not mutated.
halfLifeDays <= 0 falls back to DefaultDecayHalfLifeDays. days <= 0 is a no-op (returns a copy with weights unchanged) so the same-day re-read path stays free of arithmetic noise.
nil `in` returns nil so callers can chain without nil-checking.
Types ¶
type AgentProfile ¶
type AgentProfile struct {
AgentID core.AgentID `json:"agent_id"`
// Concepts and Files are recency-decayed weight maps. Decay is
// per-day (vs per-event for the session profile) so an idle
// agent doesn't accumulate inflated priming just from absence
// of new bead events.
Concepts map[planner.ConceptTag]float64 `json:"concepts,omitempty"`
Files map[string]float64 `json:"files,omitempty"`
// LifetimeBeadCount is the number of bead-completion events
// this agent has seen across every session. Used by
// RecordCompletion to scale a new bead's contribution by
// 1 / count so a single bead doesn't dominate the profile.
LifetimeBeadCount int64 `json:"lifetime_bead_count"`
// LastActivityAt is the wall-clock of the most recent bead
// completion. AgeByDays uses (now - last_activity_at) as the
// decay input; a fresh profile (no completions yet) carries a
// zero-value LastActivityAt and AgeByDays is a no-op.
LastActivityAt time.Time `json:"last_activity_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
AgentProfile is the per-agent persistent state. Mirrors the shape of planner.SessionProfile so the affinity scorer can read both with the same map types.
type CompletionEvent ¶
type CompletionEvent struct {
AgentID core.AgentID
BeadID core.WorkItemID
Concepts []planner.ConceptTag
Files []string
// HalfLifeDays overrides DefaultDecayHalfLifeDays per call.
HalfLifeDays float64
// ContributionScale, when set > 0, overrides the default
// 1/(lifetime+1) scaling. Tests pass 1.0 for predictable
// arithmetic; production leaves it 0 and accepts the default.
ContributionScale float64
// Now is injected for deterministic test runs. nil → time.Now.
Now func() time.Time
}
CompletionEvent is the input to RecordCompletion — what the retrospective hook knows when a bead lands. Concepts/Files come from the bead's actual (post-merge) values; ContributionScale, when zero, defaults to 1 / max(LifetimeBeadCount+1, 1) so a single bead doesn't dominate a long-running agent's profile.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists AgentProfile rows. Wraps a *sql.DB the caller owns; nil db disables every method (callers branch on nil-store rather than feature-flagging).
func NewStore ¶
NewStore wraps the given *sql.DB. Returns nil when db is nil so the wiring stays branch-free at the call site.
func (*Store) EnsureSchema ¶
EnsureSchema applies planner.SchemaSQL — the same DDL bundle the rest of the planner uses — so a process that brings up the agent-profile store also brings up session_profiles, scorer_ grades, etc. Safe to call multiple times.
func (*Store) Get ¶
Get reads the profile for agentID. Returns (nil, nil) when no row exists — matches the planner readers' "absent ≠ error" convention.
func (*Store) List ¶
func (s *Store) List(ctx context.Context) ([]AgentProfile, error)
List returns every profile, sorted by agent id. The selection layer doesn't fan over this — it joins per-session — but the CLI's `gemba agent profile --all` and operator audits read it.
func (*Store) RecordCompletion ¶
func (s *Store) RecordCompletion(ctx context.Context, ev CompletionEvent) (*AgentProfile, error)
RecordCompletion ages the existing profile by the days elapsed since LastActivityAt, scales the bead's contribution by 1/(lifetime+1), merges, bumps the lifetime counter, and stamps LastActivityAt = Now. Returns the resulting profile so callers can echo / publish without an extra read.