reflection

package
v0.31.160 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 4, 2026 License: MIT Imports: 9 Imported by: 0

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

func (c Config) EffectiveEmptySessionAge() time.Duration

EffectiveEmptySessionAge returns EmptySessionAge with the 24h default.

func (Config) EffectiveMaxTurnsPerSession

func (c Config) EffectiveMaxTurnsPerSession() int

EffectiveMaxTurnsPerSession returns MaxTurnsPerSession clamped to a reasonable range.

func (Config) EffectiveMemoryLookback

func (c Config) EffectiveMemoryLookback() time.Duration

EffectiveMemoryLookback returns the lookback window as a Duration.

func (Config) EffectiveSleepWindow

func (c Config) EffectiveSleepWindow() string

EffectiveSleepWindow returns SleepWindow with the default applied when the caller passed an empty string.

func (Config) EffectiveTickInterval

func (c Config) EffectiveTickInterval() time.Duration

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.

func (*KBCleanupJob) Name

func (k *KBCleanupJob) Name() string

Name implements Job.

func (*KBCleanupJob) Run

func (k *KBCleanupJob) Run(ctx context.Context) (JobResult, error)

Run implements Job.

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:

  1. Reads the last MaxTurnsPerSession transcript messages;
  2. Pairs consecutive user/assistant messages into turns;
  3. For each turn, derives 0..N auto memory candidates via keyword rules;
  4. 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 candidate level: memory inbox appends dedupe against existing candidates by stable ID and summary.

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.

func (*MemoryJob) Name

func (m *MemoryJob) Name() string

Name implements Job.

func (*MemoryJob) Run

func (m *MemoryJob) Run(ctx context.Context) (JobResult, error)

Run implements Job. Errors accumulate into result.Details["errors"]; the job only returns a non-nil error when it cannot even read the session list.

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

func NewRuntime(cfg Config, jobs []Job, state *State) *Runtime

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) Snapshot

func (r *Runtime) Snapshot() Snapshot

Snapshot is a convenience wrapper around state.Snapshot.

func (*Runtime) Start

func (r *Runtime) Start(ctx context.Context)

Start begins the tick loop in a goroutine. Idempotent. A disabled runtime returns immediately without launching a goroutine.

func (*Runtime) State

func (r *Runtime) State() *State

State returns the state handle used by the runtime. Pulse reads this directly to implement its reflection-failure signal.

func (*Runtime) Stop

func (r *Runtime) Stop()

Stop signals the tick loop to exit and waits for it to drain. Safe to call multiple times; the second call is a no-op.

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.

const (
	SeverityInfo Severity = iota
	SeverityWarn
	SeverityError
)

func (Severity) String

func (s Severity) String() string

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

func NewState(capacity int) *State

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

func (s *State) ConsecutiveFailures() int

ConsecutiveFailures implements pulse.ReflectionHealthSource — pulse's signal scanner reads this to decide whether to emit a reflection- failure signal.

func (*State) LastRunAt

func (s *State) LastRunAt() time.Time

LastRunAt implements pulse.ReflectionHealthSource.

func (*State) MarkAttemptStarted

func (s *State) MarkAttemptStarted(now time.Time)

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.

func (*State) Snapshot

func (s *State) Snapshot() Snapshot

Snapshot returns a chronologically ordered copy of current state.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL