Documentation
¶
Overview ¶
Package reflection implements the nightly batch runner that owns memory cleanup and knowledge-base cleanup for the TARS workspace.
Reflection is one half of the system surface (the other being pulse). It is called reflection because it runs during a configured sleep window and consolidates work that should not block the per-turn chat hot path: experience extraction, knowledge compilation, and session hygiene.
Reflection has no LLM tool surface. Its jobs are pure Go functions that may call llm.Client.Chat directly for knowledge compilation, but they never expose tools to the model. If a future job needs a tool it must register it on a tool.Registry constructed with RegistryScopeReflection.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
// Enabled globally controls whether reflection runs at all. When
// false, the runtime still answers HTTP status queries but its tick
// loop is a no-op.
Enabled bool
// SleepWindow is the "HH:MM-HH:MM" range (in Timezone) during which
// reflection is allowed to run its nightly jobs. Defaults to
// "02:00-05:00". Wrap-around windows (e.g. "22:00-02:00") are
// supported.
SleepWindow string
// Timezone is the IANA zone name used for SleepWindow evaluation.
// Empty or "Local" uses the host's local time.
Timezone string
// TickInterval is how often the scheduler wakes up to check whether
// the current moment is inside SleepWindow and whether today's run
// has happened. Defaults to 5 minutes. Keeping it short is cheap
// (the inner check has no LLM calls) and ensures jobs start within
// a few minutes of the window opening even after system sleep.
TickInterval time.Duration
// EmptySessionAge is the minimum age a zero-message session must
// reach before the KB cleanup job will delete it. Defaults to 24h
// so fresh sessions that happen to be empty don't disappear under
// the user while they're still composing a first turn.
EmptySessionAge time.Duration
// MemoryLookbackHours controls how far back the memory cleanup job
// reads session history when extracting experiences and compiling
// knowledge. Defaults to 24 hours — one reflection run per night
// with a 24-hour window covers everything between runs.
MemoryLookbackHours int
// MaxTurnsPerSession caps how many turns the memory job processes
// per session to keep nightly runs bounded even for extremely
// chatty sessions.
MaxTurnsPerSession int
}
Config is the runtime configuration for a reflection Runtime. It is populated from the main server config at startup; restarting the server is required to pick up changes.
func (Config) EffectiveEmptySessionAge ¶
EffectiveEmptySessionAge returns EmptySessionAge with the 24h default.
func (Config) EffectiveMaxTurnsPerSession ¶
EffectiveMaxTurnsPerSession returns MaxTurnsPerSession clamped to a reasonable range.
func (Config) EffectiveMemoryLookback ¶
EffectiveMemoryLookback returns the lookback window as a Duration.
func (Config) EffectiveSleepWindow ¶
EffectiveSleepWindow returns SleepWindow with the default applied when the caller passed an empty string.
func (Config) EffectiveTickInterval ¶
EffectiveTickInterval returns TickInterval with sensible defaults.
type Job ¶
type Job interface {
// Name returns the job's stable identifier, used in logs, state
// snapshots, and HTTP responses.
Name() string
// Run performs the job. It must not panic; any error becomes a
// JobResult with Success=false. The context is the run context
// derived from the runtime's parent ctx; jobs should respect
// ctx.Done() for cooperative cancellation.
Run(ctx context.Context) (JobResult, error)
}
Job is the interface each reflection task implements. Jobs are registered in order on the Runtime and executed sequentially during a reflection run. A failing job does not stop subsequent jobs from running — each job is expected to be independent so that partial success is still meaningful.
type JobResult ¶
type JobResult struct {
Name string `json:"name"`
Success bool `json:"success"`
Changed bool `json:"changed,omitempty"`
Summary string `json:"summary,omitempty"`
Details map[string]any `json:"details,omitempty"`
Err string `json:"err,omitempty"`
// Duration is the wall-clock time the job took to execute. Helpful
// for spotting creeping regressions in nightly performance.
Duration time.Duration `json:"duration_ms"`
}
JobResult captures the outcome of a single reflection job execution. Jobs never panic; any error becomes Err and Success is false. Changed is true when the job modified workspace state (so the HTTP view can highlight meaningful runs vs. no-op days).
type KBCleanupJob ¶
type KBCleanupJob struct {
Sessions SessionDeleter
EmptySessionAge time.Duration
Now func() time.Time
}
KBCleanupJob runs the "knowledge base cleanup" half of reflection. Phase 1 scope is intentionally minimal: remove sessions whose transcript contains zero messages AND whose UpdatedAt is older than EmptySessionAge. This is safe because:
- Zero-message sessions have nothing the user would mourn;
- The age threshold protects fresh sessions that the user is in the middle of composing a first turn for;
- Main sessions (kind="main") are never touched, so the always-on main chat is never deleted out from under a running UI;
Session compression (gzip old transcripts) is a deliberate follow-up — it requires read-side decompression changes that widen the blast radius beyond PR2's scope.
type MemoryJob ¶
type MemoryJob struct {
WorkspaceDir string
Backend memory.Backend
Sessions SessionSource
Router llm.Router
Lookback time.Duration
MaxTurnsPerSession int
Now func() time.Time
}
MemoryJob runs the "memory cleanup" half of reflection. It is the batch form of the per-turn derivation+compilation logic that used to live in internal/tarsserver/chat_memory_hook.go. Moving the work here takes LLM calls off the per-turn hot path and lets operators tune lookback windows and turn caps via config.
For each session updated within Lookback, the job:
- Reads the last MaxTurnsPerSession transcript messages;
- Pairs consecutive user/assistant messages into turns;
- For each turn, derives 0..N auto experiences via keyword rules;
- For each turn that clears the knowledge-base gate, calls the LLM to compile structured knowledge and applies the diff.
The job is idempotent at the experience level: appendExperienceIfNew dedupes against existing entries by summary+category match.
The knowledge-compilation call uses the llm.RoleReflectionMemory role, which operators can map to the light tier via llm_role_reflection_memory to keep nightly runs cheap.
type RunSummary ¶
type RunSummary struct {
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at"`
Results []JobResult `json:"results"`
// Success is true only if every job in Results succeeded. One failed
// job marks the whole run as failed, which is what drives pulse's
// reflection-failure signal.
Success bool `json:"success"`
// Err is set when reflection was unable to even start jobs (e.g.
// scheduler error). Individual job errors live on JobResult.
Err string `json:"err,omitempty"`
}
RunSummary is the aggregate of one reflection run (all jobs for a given night). It is stored in state and exposed via HTTP.
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime orchestrates the reflection tick loop. Unlike pulse, which ticks every minute, reflection ticks slowly (default every 5 minutes) because each tick does nothing unless a narrow sleep window is open AND today's run has not yet happened.
The runtime is safe to construct with nil jobs/state — Start becomes a no-op and RunOnce returns a summary describing why it skipped.
func NewRuntime ¶
NewRuntime constructs a Runtime with the given configuration, ordered job list, and shared state. Callers typically build one at server startup and call Start immediately.
func (*Runtime) RunOnce ¶
func (r *Runtime) RunOnce(ctx context.Context) RunSummary
RunOnce forces a reflection run now, bypassing the sleep-window gate. Tests and the HTTP run-once endpoint use this. It still returns an empty summary when reflection is disabled.
func (*Runtime) SetTickHook ¶
func (r *Runtime) SetTickHook(fn func(summary RunSummary))
SetTickHook installs a callback invoked after every tick that actually ran jobs. Intended for tests; the hook must not block.
func (*Runtime) Start ¶
Start begins the tick loop in a goroutine. Idempotent. A disabled runtime returns immediately without launching a goroutine.
type SessionDeleter ¶
type SessionDeleter interface {
SessionSource
Delete(id string) error
}
SessionDeleter is the subset of session.Store that KBCleanupJob needs in addition to the SessionSource interface it inherits from the memory job. Split into its own interface so tests can provide a fake that implements only what's exercised.
type SessionSource ¶
type SessionSource interface {
ListAll() ([]session.Session, error)
TranscriptPath(id string) string
}
SessionSource is the narrow interface the memory job needs from the session store. The real *session.Store satisfies it.
type Severity ¶
type Severity int
Severity categorizes a reflection log entry's urgency. Used only for exposition in state snapshots; reflection does not act on severity.
type Snapshot ¶
type Snapshot struct {
LastRunAt time.Time `json:"last_run_at"`
LastRunSuccess bool `json:"last_run_success"`
LastRunSummary *RunSummary `json:"last_run_summary,omitempty"`
LastSuccessfulRunAt time.Time `json:"last_successful_run_at,omitempty"`
ConsecutiveFailures int `json:"consecutive_failures"`
TotalRuns int `json:"total_runs"`
TotalSuccesses int `json:"total_successes"`
TotalFailures int `json:"total_failures"`
Recent []RunSummary `json:"recent"`
}
Snapshot is a point-in-time view of state safe to serialize.
type State ¶
type State struct {
// contains filtered or unexported fields
}
State is the in-memory runtime state of a reflection Runtime. It tracks the last run, a short history of recent runs, and counters pulse reads via the ReflectionHealthSource interface.
State is safe for concurrent use. The exposed Snapshot is a deep-ish copy suitable for JSON serialization without holding the internal lock.
func NewState ¶
NewState creates a reflection state with the given ring buffer capacity. A non-positive capacity falls back to 14 (roughly two weeks of nightly runs).
func (*State) ConsecutiveFailures ¶
ConsecutiveFailures implements pulse.ReflectionHealthSource — pulse's signal scanner reads this to decide whether to emit a reflection- failure signal.
func (*State) MarkAttemptStarted ¶
MarkAttemptStarted lets the scheduler tag "today" as attempted even before jobs finish, so a crash mid-run doesn't cause the next tick to immediately retry the same day. The returned boolean indicates whether this is the first attempt today.
func (*State) RecordRun ¶
func (s *State) RecordRun(summary RunSummary)
RecordRun appends a summary and updates counters. Called by the runtime at the end of each reflection run.