Documentation
¶
Overview ¶
Package session persists agent conversation history across runs.
Sessions enable multi-turn conversations: a user runs a task, the agent responds, and the user continues the conversation with "odek continue", picking up the full message history from the previous turn.
Storage: ~/.odek/sessions/<id>.json. Each file is a full conversation transcript including system messages, user turns, assistant responses, tool calls, and tool results. Sessions are loaded by ID for continuation or by listing metadata for browsing.
The Store is intentionally minimal — it's a JSON file manager, not a database. Session struct fields are all public, so callers can mutate the session directly and call Save(). This makes advanced operations (editing, truncating, merging sessions) trivial at the CLI layer.
Index ¶
- func BuildConversationText(messages []llm.Message) string
- func NovelResources(userText string, toolText string) []string
- func ResourcesIn(text string) []string
- func ValidateSessionID(id string) error
- type AuditIngest
- type AuditLog
- type AuditStore
- type AuditTurn
- type IndexEntry
- type SearchResult
- type Session
- type Store
- func (s *Store) Append(id string, newMsgs []llm.Message) error
- func (s *Store) Cleanup(before time.Time) (int, error)
- func (s *Store) Create(messages []llm.Message, model, task string) (*Session, error)
- func (s *Store) Delete(id string) error
- func (s *Store) Dir() string
- func (s *Store) InitVectorIndex(cfg *embedding.Config) error
- func (s *Store) Latest() (*Session, error)
- func (s *Store) List(limit int) ([]Session, error)
- func (s *Store) Load(id string) (*Session, error)
- func (s *Store) Path(id string) string
- func (s *Store) Save(sess *Session) error
- type VectorIndex
- func (vi *VectorIndex) Add(sessionID string, messages []llm.Message) error
- func (vi *VectorIndex) Init(dir string) error
- func (vi *VectorIndex) InitWithConfig(dir string, cfg *embedding.Config) error
- func (vi *VectorIndex) Ready() bool
- func (vi *VectorIndex) Remove(sessionID string) error
- func (vi *VectorIndex) Save() error
- func (vi *VectorIndex) Search(query string, k int) ([]SearchResult, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildConversationText ¶ added in v0.58.0
BuildConversationText extracts user and assistant text from messages for embedding. Tool calls and results are excluded — they add noise.
func NovelResources ¶ added in v1.0.0
NovelResources returns resources from toolText that do not appear in userText. Order preserved; case-insensitive comparison.
func ResourcesIn ¶ added in v1.0.0
ResourcesIn returns the set of resource-like tokens found in text.
func ValidateSessionID ¶
ValidateSessionID validates that a session ID is safe for filesystem use. Rejects empty strings, path separators, traversal patterns, and dot names.
Types ¶
type AuditIngest ¶ added in v1.0.0
type AuditIngest struct {
Turn int `json:"turn"`
Source string `json:"source"` // e.g. "https://x", "/abs/path", "mcp:server:tool"
ContentHash string `json:"content_hash"` // sha256 of the ingested body (first 16 hex)
At time.Time `json:"at"`
}
AuditIngest records that the agent ingested an untrusted-source content blob at the given turn.
type AuditLog ¶ added in v1.0.0
type AuditLog struct {
SessionID string `json:"session_id"`
Ingests []AuditIngest `json:"ingests"`
Turns []AuditTurn `json:"turns"`
}
AuditLog is the per-session aggregate.
type AuditStore ¶ added in v1.0.0
type AuditStore struct {
// contains filtered or unexported fields
}
AuditStore manages per-session audit logs under <dir>/audit/.
func NewAuditStore ¶ added in v1.0.0
func NewAuditStore(dir string) *AuditStore
NewAuditStore returns a store rooted at dir; the audit subdir is created on first write.
func (*AuditStore) Load ¶ added in v1.0.0
func (s *AuditStore) Load(sessionID string) (AuditLog, error)
Load returns the audit log for a session, or empty if not present.
func (*AuditStore) RecordIngest ¶ added in v1.0.0
func (s *AuditStore) RecordIngest(sessionID string, turn int, source, content string) error
RecordIngest appends an ingest entry for a session.
func (*AuditStore) RecordTurn ¶ added in v1.0.0
func (s *AuditStore) RecordTurn(sessionID string, turn AuditTurn) error
RecordTurn appends a turn assessment for a session.
type AuditTurn ¶ added in v1.0.0
type AuditTurn struct {
Turn int `json:"turn"`
UserMessage string `json:"user_message"`
ToolCalls []string `json:"tool_calls"` // names of tools called this turn
NovelResources []string `json:"novel_resources,omitempty"` // resources referenced by tools but not by user
IngestedUntrusted bool `json:"ingested_untrusted"`
SuspiciousDivergence bool `json:"suspicious_divergence"`
}
AuditTurn records the per-turn divergence assessment.
type IndexEntry ¶
type IndexEntry struct {
ID string `json:"id"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Turns int `json:"turns"`
}
IndexEntry holds minimal session metadata for the session index. This avoids loading every session file just to list or find the latest.
type SearchResult ¶ added in v0.58.0
type SearchResult struct {
SessionID string `json:"session_id"`
Score float32 `json:"score"` // cosine similarity, higher = more relevant
}
SearchResult holds a single session search result.
type Session ¶
type Session struct {
ID string `json:"id"` // e.g. "20260518-abc123"
CreatedAt time.Time `json:"created_at"` // first message time
UpdatedAt time.Time `json:"updated_at"` // last append time
Model string `json:"model"` // model name used
Turns int `json:"turns"` // number of user turns
Task string `json:"task"` // first user message (label)
Sandbox bool `json:"sandbox"` // was sandboxed — auto-apply on resume
Messages []llm.Message `json:"messages"` // full conversation history
Buffer []string `json:"buffer,omitempty"` // last N turn summaries (memory tier 2)
}
Session represents a single multi-turn conversation with the agent. All fields are exported for direct manipulation at the CLI layer.
func (*Session) GetMessages ¶
GetMessages returns the session's message slice. Nil-safe. Returns an empty (non-nil) slice for a session with no messages.
type Store ¶
type Store struct {
// Vec is the optional semantic search index. When non-nil, every
// Save/Delete/Cleanup call updates the vector index automatically.
// Call InitVectorIndex() to initialize.
Vec *VectorIndex
// contains filtered or unexported fields
}
Store manages session files in a directory on disk. Operations are simple file reads/writes — no locking, no caching.
func NewStore ¶
NewStore creates a session store rooted at ~/.odek/sessions/. The directory is created if it doesn't exist.
func (*Store) Append ¶
Append adds new messages to an existing session, updates timestamps and turn counts, and saves the result atomically. The full read-modify-write is serialized by s.mu to prevent both concurrent-write data loss and symlink-swap TOCTOU attacks.
func (*Store) Cleanup ¶
Cleanup deletes all sessions whose UpdatedAt is before the given time. Returns the count of deleted sessions. Idempotent — nonexistent files are skipped silently. Uses the session index for efficient batch operations. Falls back to scanning individual session files when no index exists (backward compat).
func (*Store) Create ¶
Create persists a new session with the given messages and metadata. It generates an ID, sets timestamps, counts user turns, and saves.
func (*Store) Delete ¶
Delete removes a session file from disk and removes its entry from the session index. Returns nil if the file doesn't exist (idempotent delete).
func (*Store) Dir ¶
Dir returns the session store directory path. Exported for testing and debugging.
func (*Store) InitVectorIndex ¶ added in v0.58.0
InitVectorIndex initializes the semantic search index using the embedding backend selected by cfg (nil = default RandomProjections). Must be called after NewStore, before the first Save. Safe to call multiple times — subsequent calls are no-ops once the index is ready.
func (*Store) Latest ¶
Latest returns the most recently updated session, or nil if no sessions exist. Returns an error when no sessions exist. Uses the session index for O(1) lookups. Falls back to scanning individual session files when no index exists (backward compat).
func (*Store) List ¶
List returns session summaries ordered by UpdatedAt descending (most recent first). limit caps the number returned (0 = all). Only metadata fields are populated — Messages is nil to keep listings lightweight. Uses the session index for O(n) reads (n = session count, but no JSON parsing per session). Falls back to loading each session file when no index exists (backward compat).
func (*Store) Load ¶
Load reads a session from disk by ID. Returns an error if the file doesn't exist or can't be parsed.
func (*Store) Path ¶
Path returns the absolute filesystem path for a session file. Exported for testing and debugging.
func (*Store) Save ¶
Save writes a session to disk atomically and durably via fsatomic.WriteFile (temp-file → fsync → rename → dir fsync). This prevents:
- Partial writes from crashes (rename is atomic on POSIX)
- Data loss on power failure (the fsync flushes bytes before the rename)
- Symlink-following TOCTOU attacks (os.Rename replaces the directory entry itself — it does NOT follow symlinks)
type VectorIndex ¶ added in v0.58.0
type VectorIndex struct {
// contains filtered or unexported fields
}
VectorIndex provides semantic session search over the shared embedding backend (internal/embedding): RandomProjections by default, or any OpenAI-compatible HTTP embeddings API when configured.
Lifecycle:
- Init / InitWithConfig loads persisted state (when the fingerprint matches), or fits+embeds from all sessions.
- On Add: embed conversation text and insert into the store.
- On Search: embed query, k-NN search, return ranked results.
- On Remove: delete from the store.
- Save persists store + embedder state + meta atomically.
Resilience: with a remote backend, a down server never fails a session save or surfaces an error to search — Add/Search degrade silently and a 30s cool-down (failedAt) keeps the backend from being hammered. The keyword fallback in the session_search tool is the safety net while the embedder is unavailable.
Thread-safe: all exported methods hold the RWMutex.
func (*VectorIndex) Add ¶ added in v0.58.0
func (vi *VectorIndex) Add(sessionID string, messages []llm.Message) error
Add embeds the conversation text and adds the session to the index, replacing any existing entry. Best-effort: a missing/failing embedding backend never fails the caller's session save — the entry is simply skipped and a retry cool-down starts. If the index was not ready (e.g. the backend was down at init), a rebuild is attempted first; it already picks up the just-saved session from disk.
func (*VectorIndex) Init ¶ added in v0.58.0
func (vi *VectorIndex) Init(dir string) error
Init creates or loads the vector index using the default RandomProjections backend. Equivalent to InitWithConfig(dir, nil).
func (*VectorIndex) InitWithConfig ¶ added in v1.6.0
func (vi *VectorIndex) InitWithConfig(dir string, cfg *embedding.Config) error
InitWithConfig creates or loads the vector index using the embedding backend selected by cfg (nil = default RandomProjections). If persisted state in the same embedding space exists it is loaded directly; otherwise the index is rebuilt from all session files. A backend that is down at init time is not fatal — the index stays empty (search falls back to keyword) and retries after the cool-down.
func (*VectorIndex) Ready ¶ added in v0.58.0
func (vi *VectorIndex) Ready() bool
Ready returns true if the index has been initialized.
func (*VectorIndex) Remove ¶ added in v0.58.0
func (vi *VectorIndex) Remove(sessionID string) error
Remove deletes a session from the index. Idempotent.
func (*VectorIndex) Save ¶ added in v0.58.0
func (vi *VectorIndex) Save() error
Save persists the store, embedder state, and meta to disk.
func (*VectorIndex) Search ¶ added in v0.58.0
func (vi *VectorIndex) Search(query string, k int) ([]SearchResult, error)
Search embeds the query and returns the k most similar sessions ranked by cosine similarity. Returns nil (no error) when the index is unavailable — not ready, empty, or the embedding backend failed — so the caller falls back to keyword search. If the index was not ready, one rebuild is attempted (subject to the cool-down).