Documentation
¶
Overview ¶
Package projectstate implements durable, event-sourced project state for agents: typed tasks, typed long-term memories, session summaries, and a prime-context builder. State is persisted under a state directory as an append-only event log (events.jsonl) with derived JSON indexes, so it survives process restarts and context compaction.
Memory model ¶
Memories are typed by Kind (pinned, semantic, episodic, procedural) and Scope (project, user, task, file). Store them with UpsertMemory, retrieve them with SearchMemories (query required) or ListMemories, and surface a compact session-start summary with PrimeContext.
Hybrid recall ¶
By default SearchMemories ranks memories with a lexical keyword score. When a FilesystemStore is configured with an Embedder, recall becomes hybrid: it fuses the normalized lexical score with cosine similarity over cached embeddings, plus a small pinned boost and recency decay (all tunable via HybridConfig). Candidates are filtered by kind and tags but not by keyword, so semantically relevant memories surface even when they share no exact terms with the query. With no Embedder, recall falls back to the lexical path unchanged, so embeddings are fully optional and backward compatible.
Embeddings are computed on write and cached in indexes/embeddings.json, keyed by content hash and model so the cache self-invalidates when a memory's content or the embedding model changes. Memories written before an embedder was configured are backfilled lazily on the next recall. Embedding failures never block writes and degrade recall to whatever vectors already exist alongside the lexical signal.
Embedders ¶
OpenAIEmbedder implements Embedder against any OpenAI-compatible /v1/embeddings endpoint. Set OpenAIEmbedderOptions.BaseURL to choose the provider:
- OpenAI: "https://api.openai.com/v1" with "text-embedding-3-small".
- Local (Ollama): "http://localhost:11434/v1" with e.g. "bge-m3".
OpenRouter currently exposes chat/completions but not a general-purpose /v1/embeddings endpoint, so it cannot back embeddings today; use OpenAI or a local model for vectors. If OpenRouter adds an embeddings route, point BaseURL at it with no code change. To use a different backend entirely, implement the Embedder interface.
Index ¶
- Constants
- func DefaultStateDir(projectID string) (string, error)
- func DeriveProjectID(workDir string) string
- type CreateTaskInput
- type Embedder
- type Event
- type FilesystemOptions
- type FilesystemStore
- func (s FilesystemStore) AddComment(ctx context.Context, taskID, actor, body string) (*TaskComment, error)
- func (s FilesystemStore) AddDependency(ctx context.Context, taskID, dependsOnID string) error
- func (s FilesystemStore) ClaimTask(ctx context.Context, id, actor string) (*Task, error)
- func (s FilesystemStore) Close() error
- func (s FilesystemStore) CloseTask(ctx context.Context, id, reason string) (*Task, error)
- func (s FilesystemStore) CreateTask(ctx context.Context, in CreateTaskInput) (*Task, error)
- func (s FilesystemStore) DeleteMemory(ctx context.Context, id string) error
- func (s FilesystemStore) GetTask(ctx context.Context, id string) (*Task, error)
- func (s FilesystemStore) ListMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
- func (s FilesystemStore) ListSessionSummaries(ctx context.Context, limit int) ([]SessionSummary, error)
- func (s FilesystemStore) ListTasks(ctx context.Context) ([]Task, error)
- func (s FilesystemStore) PrimeContext(ctx context.Context, opts PrimeOptions) (string, error)
- func (s FilesystemStore) ProjectID() string
- func (s FilesystemStore) ReadyTasks(ctx context.Context, filter TaskFilter) ([]Task, error)
- func (s FilesystemStore) RemoveDependency(ctx context.Context, taskID, dependsOnID string) error
- func (s FilesystemStore) SaveSessionSummary(ctx context.Context, summary SessionSummary) (*SessionSummary, error)
- func (s FilesystemStore) SearchMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
- func (s *FilesystemStore) StateDir() string
- func (s FilesystemStore) UpdateTask(ctx context.Context, id string, patch TaskPatch) (*Task, error)
- func (s FilesystemStore) UpsertMemory(ctx context.Context, in UpsertMemoryInput) (*Memory, error)
- type HybridConfig
- type Memory
- type MemoryFilter
- type MemoryStore
- type OpenAIEmbedder
- type OpenAIEmbedderOptions
- type PrimeOptions
- type PrimeStore
- type Project
- type SQLiteOptions
- type SQLiteStore
- func (s SQLiteStore) AddComment(ctx context.Context, taskID, actor, body string) (*TaskComment, error)
- func (s SQLiteStore) AddDependency(ctx context.Context, taskID, dependsOnID string) error
- func (s SQLiteStore) ClaimTask(ctx context.Context, id, actor string) (*Task, error)
- func (s SQLiteStore) Close() error
- func (s SQLiteStore) CloseTask(ctx context.Context, id, reason string) (*Task, error)
- func (s SQLiteStore) CreateTask(ctx context.Context, in CreateTaskInput) (*Task, error)
- func (s *SQLiteStore) DB() *sql.DB
- func (s SQLiteStore) DeleteMemory(ctx context.Context, id string) error
- func (s SQLiteStore) GetTask(ctx context.Context, id string) (*Task, error)
- func (s SQLiteStore) ListMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
- func (s SQLiteStore) ListSessionSummaries(ctx context.Context, limit int) ([]SessionSummary, error)
- func (s SQLiteStore) ListTasks(ctx context.Context) ([]Task, error)
- func (s SQLiteStore) PrimeContext(ctx context.Context, opts PrimeOptions) (string, error)
- func (s SQLiteStore) ProjectID() string
- func (s SQLiteStore) ReadyTasks(ctx context.Context, filter TaskFilter) ([]Task, error)
- func (s SQLiteStore) RemoveDependency(ctx context.Context, taskID, dependsOnID string) error
- func (s SQLiteStore) SaveSessionSummary(ctx context.Context, summary SessionSummary) (*SessionSummary, error)
- func (s SQLiteStore) SearchMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
- func (s SQLiteStore) UpdateTask(ctx context.Context, id string, patch TaskPatch) (*Task, error)
- func (s SQLiteStore) UpsertMemory(ctx context.Context, in UpsertMemoryInput) (*Memory, error)
- type SessionStore
- type SessionSummary
- type Store
- type Task
- type TaskComment
- type TaskFilter
- type TaskPatch
- type TaskStore
- type UpsertMemoryInput
Constants ¶
const ( TaskStatusOpen = "open" TaskStatusInProgress = "in_progress" TaskStatusBlocked = "blocked" TaskStatusClosed = "closed" TaskStatusDeferred = "deferred" )
const ( TaskTypeTask = "task" TaskTypeBug = "bug" TaskTypeFeature = "feature" TaskTypeChore = "chore" TaskTypeEpic = "epic" )
const ( MemoryKindPinned = "pinned" MemoryKindSemantic = "semantic" MemoryKindEpisodic = "episodic" MemoryKindProcedural = "procedural" )
const ( MemoryScopeProject = "project" MemoryScopeUser = "user" MemoryScopeTask = "task" MemoryScopeFile = "file" )
const SchemaVersion = 1
Variables ¶
This section is empty.
Functions ¶
func DefaultStateDir ¶
func DeriveProjectID ¶
Types ¶
type CreateTaskInput ¶
type CreateTaskInput struct {
Title string `json:"title"`
Description string `json:"description,omitempty"`
Type string `json:"type,omitempty"`
Priority int `json:"priority,omitempty"`
Assignee string `json:"assignee,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
Labels []string `json:"labels,omitempty"`
SourceRun string `json:"source_run,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
type Embedder ¶ added in v0.0.2
type Embedder interface {
// Embed returns one vector per input text, in input order.
Embed(ctx context.Context, texts []string) ([][]float32, error)
// Model identifies the embedding model. It is persisted alongside cached
// vectors so the cache can be invalidated when the model changes.
Model() string
}
Embedder turns text into dense vectors for semantic memory retrieval. It is optional: when a FilesystemStore has no embedder, memory recall falls back to the lexical keyword search and behaves exactly as before.
Implementations should be safe for concurrent use and should embed the input slice as a batch, returning one vector per input in the same order.
type FilesystemOptions ¶
type FilesystemOptions struct {
StateDir string
ProjectID string
WorkDir string
Actor string
RunID string
// Embedder enables embeddings-backed hybrid memory recall. When nil,
// SearchMemories falls back to the lexical keyword search and behaves
// exactly as before.
Embedder Embedder
// Hybrid tunes lexical/semantic fusion. When nil, DefaultHybridConfig is
// used. Ignored when Embedder is nil.
Hybrid *HybridConfig
}
type FilesystemStore ¶
type FilesystemStore struct {
// contains filtered or unexported fields
}
FilesystemStore persists durable project state as an append-only events.jsonl log plus JSON index snapshots under a project state directory.
func NewFilesystemStore ¶
func NewFilesystemStore(opts FilesystemOptions) (*FilesystemStore, error)
func (FilesystemStore) AddComment ¶
func (s FilesystemStore) AddComment(ctx context.Context, taskID, actor, body string) (*TaskComment, error)
func (FilesystemStore) AddDependency ¶
func (FilesystemStore) CreateTask ¶
func (s FilesystemStore) CreateTask(ctx context.Context, in CreateTaskInput) (*Task, error)
func (FilesystemStore) DeleteMemory ¶
func (FilesystemStore) ListMemories ¶
func (s FilesystemStore) ListMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
func (FilesystemStore) ListSessionSummaries ¶
func (s FilesystemStore) ListSessionSummaries(ctx context.Context, limit int) ([]SessionSummary, error)
func (FilesystemStore) PrimeContext ¶
func (s FilesystemStore) PrimeContext(ctx context.Context, opts PrimeOptions) (string, error)
PrimeContext renders the durable project state into a compact briefing block.
func (FilesystemStore) ReadyTasks ¶
func (s FilesystemStore) ReadyTasks(ctx context.Context, filter TaskFilter) ([]Task, error)
func (FilesystemStore) RemoveDependency ¶
func (FilesystemStore) SaveSessionSummary ¶
func (s FilesystemStore) SaveSessionSummary(ctx context.Context, summary SessionSummary) (*SessionSummary, error)
func (FilesystemStore) SearchMemories ¶
func (s FilesystemStore) SearchMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
func (*FilesystemStore) StateDir ¶
func (s *FilesystemStore) StateDir() string
func (FilesystemStore) UpdateTask ¶
func (FilesystemStore) UpsertMemory ¶
func (s FilesystemStore) UpsertMemory(ctx context.Context, in UpsertMemoryInput) (*Memory, error)
type HybridConfig ¶ added in v0.0.2
type HybridConfig struct {
// LexicalWeight weights the normalized lexical (keyword) score.
LexicalWeight float64
// DenseWeight weights the semantic (cosine similarity) score.
DenseWeight float64
// PinnedBoost is added to the score of pinned memories so durable facts
// surface ahead of incidental matches of equal relevance.
PinnedBoost float64
// RecencyWeight weights an exponential recency score in [0,1].
RecencyWeight float64
// RecencyHalfLife is the age at which the recency score decays to 0.5.
RecencyHalfLife time.Duration
// MinScore drops candidates whose fused score is below this threshold.
// Keep at 0 to preserve every filtered candidate.
MinScore float64
}
HybridConfig tunes how lexical and semantic signals are fused during recall. The zero value is not usable on its own; callers should start from DefaultHybridConfig and override individual fields.
func DefaultHybridConfig ¶ added in v0.0.2
func DefaultHybridConfig() HybridConfig
DefaultHybridConfig returns balanced defaults: semantic similarity leads, keyword overlap supports it, pinned memories get a small boost, and recency is a light tie-breaker with a 30 day half-life.
type Memory ¶
type Memory struct {
ID string `json:"id"`
Kind string `json:"kind"`
Scope string `json:"scope"`
Content string `json:"content"`
Tags []string `json:"tags,omitempty"`
TaskIDs []string `json:"task_ids,omitempty"`
FilePaths []string `json:"file_paths,omitempty"`
SourceRun string `json:"source_run,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastReadAt *time.Time `json:"last_read_at,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
type MemoryFilter ¶
type MemoryStore ¶
type MemoryStore interface {
UpsertMemory(ctx context.Context, in UpsertMemoryInput) (*Memory, error)
SearchMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
ListMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
DeleteMemory(ctx context.Context, id string) error
}
type OpenAIEmbedder ¶ added in v0.0.2
type OpenAIEmbedder struct {
// contains filtered or unexported fields
}
OpenAIEmbedder is an Embedder backed by an OpenAI-compatible embeddings API.
func NewOpenAIEmbedder ¶ added in v0.0.2
func NewOpenAIEmbedder(opts OpenAIEmbedderOptions) (*OpenAIEmbedder, error)
NewOpenAIEmbedder builds an OpenAI-compatible embedder. It returns an error only when no model is provided; networking is validated lazily on first use.
func (*OpenAIEmbedder) Embed ¶ added in v0.0.2
Embed implements Embedder, batching inputs to respect provider limits.
func (*OpenAIEmbedder) Model ¶ added in v0.0.2
func (e *OpenAIEmbedder) Model() string
Model implements Embedder.
type OpenAIEmbedderOptions ¶ added in v0.0.2
type OpenAIEmbedderOptions struct {
// BaseURL is the API root, e.g. "https://api.openai.com/v1". A trailing
// "/embeddings" is appended automatically. Defaults to the OpenAI API.
BaseURL string
// APIKey is sent as a Bearer token. Optional for local servers.
APIKey string
// ModelID is the embedding model, e.g. "text-embedding-3-small".
ModelID string
// Dimensions optionally requests a reduced embedding size when the model
// supports it (e.g. OpenAI text-embedding-3-*).
Dimensions int
// BatchSize caps how many inputs are sent per request. Defaults to 96.
BatchSize int
// HTTPClient overrides the default client (30s timeout).
HTTPClient *http.Client
// Headers adds extra request headers (e.g. OpenRouter attribution).
Headers map[string]string
}
OpenAIEmbedderOptions configures an OpenAI-compatible embedding client.
Any provider that implements the OpenAI /v1/embeddings contract works, including OpenAI itself, a local Ollama server (BaseURL "http://localhost:11434/v1"), and other OpenAI-compatible gateways. Point BaseURL at the provider you want and supply the matching model and key.
Note on OpenRouter: OpenRouter focuses on chat/completions and does not currently expose a general-purpose /v1/embeddings endpoint. Use OpenAI or a local model (Ollama) for embeddings, and keep OpenRouter for completions. If OpenRouter adds an embeddings route, set BaseURL to it here with no code changes.
type PrimeOptions ¶
type PrimeStore ¶
type PrimeStore interface {
PrimeContext(ctx context.Context, opts PrimeOptions) (string, error)
}
type SQLiteOptions ¶ added in v0.0.4
type SQLiteOptions struct {
// DB is an existing database handle to use. When set, the store does not
// close it and assumes the caller has configured WAL/pragmas. Table names
// are namespaced (see TablePrefix) so projectstate can coexist with other
// tables in a shared database.
DB *sql.DB
// Path opens (creating if necessary) a dedicated database file. Ignored when
// DB is set.
Path string
// TablePrefix namespaces the store's tables. Defaults to "projectstate_".
TablePrefix string
ProjectID string
WorkDir string
Actor string
RunID string
// Embedder enables embeddings-backed hybrid memory recall. When nil,
// SearchMemories falls back to lexical keyword search.
Embedder Embedder
// Hybrid tunes lexical/semantic fusion. When nil, DefaultHybridConfig is used.
Hybrid *HybridConfig
}
SQLiteOptions configures a SQLiteStore. Exactly one of DB or Path supplies the database: pass DB to share an already-open handle (for example the assistant's shared state.db) or Path to have the store open and own its own file.
type SQLiteStore ¶ added in v0.0.4
type SQLiteStore struct {
// contains filtered or unexported fields
}
SQLiteStore persists durable project state in SQLite. It reuses the same event-sourced engine as FilesystemStore: events live in a table and the in-memory state is rebuilt by replaying them, so behavior is identical to the filesystem store while gaining transactional durability and a shareable DB.
func NewSQLiteStore ¶ added in v0.0.4
func NewSQLiteStore(opts SQLiteOptions) (*SQLiteStore, error)
NewSQLiteStore opens a SQLite-backed project state store.
func (SQLiteStore) AddComment ¶ added in v0.0.4
func (s SQLiteStore) AddComment(ctx context.Context, taskID, actor, body string) (*TaskComment, error)
func (SQLiteStore) AddDependency ¶ added in v0.0.4
func (SQLiteStore) CreateTask ¶ added in v0.0.4
func (s SQLiteStore) CreateTask(ctx context.Context, in CreateTaskInput) (*Task, error)
func (*SQLiteStore) DB ¶ added in v0.0.4
func (s *SQLiteStore) DB() *sql.DB
DB returns the underlying database handle so callers can share it.
func (SQLiteStore) DeleteMemory ¶ added in v0.0.4
func (SQLiteStore) ListMemories ¶ added in v0.0.4
func (s SQLiteStore) ListMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
func (SQLiteStore) ListSessionSummaries ¶ added in v0.0.4
func (s SQLiteStore) ListSessionSummaries(ctx context.Context, limit int) ([]SessionSummary, error)
func (SQLiteStore) PrimeContext ¶ added in v0.0.4
func (s SQLiteStore) PrimeContext(ctx context.Context, opts PrimeOptions) (string, error)
PrimeContext renders the durable project state into a compact briefing block.
func (SQLiteStore) ReadyTasks ¶ added in v0.0.4
func (s SQLiteStore) ReadyTasks(ctx context.Context, filter TaskFilter) ([]Task, error)
func (SQLiteStore) RemoveDependency ¶ added in v0.0.4
func (SQLiteStore) SaveSessionSummary ¶ added in v0.0.4
func (s SQLiteStore) SaveSessionSummary(ctx context.Context, summary SessionSummary) (*SessionSummary, error)
func (SQLiteStore) SearchMemories ¶ added in v0.0.4
func (s SQLiteStore) SearchMemories(ctx context.Context, filter MemoryFilter) ([]Memory, error)
func (SQLiteStore) UpdateTask ¶ added in v0.0.4
func (SQLiteStore) UpsertMemory ¶ added in v0.0.4
func (s SQLiteStore) UpsertMemory(ctx context.Context, in UpsertMemoryInput) (*Memory, error)
type SessionStore ¶
type SessionStore interface {
SaveSessionSummary(ctx context.Context, summary SessionSummary) (*SessionSummary, error)
ListSessionSummaries(ctx context.Context, limit int) ([]SessionSummary, error)
}
type SessionSummary ¶
type Store ¶
type Store interface {
TaskStore
MemoryStore
SessionStore
PrimeStore
Close() error
}
type Task ¶
type Task struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Type string `json:"type"`
Status string `json:"status"`
Priority int `json:"priority"`
Assignee string `json:"assignee,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
Blocks []string `json:"blocks,omitempty"`
Labels []string `json:"labels,omitempty"`
Comments []TaskComment `json:"comments,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ClosedAt *time.Time `json:"closed_at,omitempty"`
SourceRun string `json:"source_run,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}
type TaskComment ¶
type TaskFilter ¶
type TaskPatch ¶
type TaskPatch struct {
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
Type *string `json:"type,omitempty"`
Status *string `json:"status,omitempty"`
Priority *int `json:"priority,omitempty"`
Assignee *string `json:"assignee,omitempty"`
Labels []string `json:"labels,omitempty"`
ReplaceLabels bool `json:"replace_labels,omitempty"`
Metadata *json.RawMessage `json:"metadata,omitempty"`
}
type TaskStore ¶
type TaskStore interface {
CreateTask(ctx context.Context, in CreateTaskInput) (*Task, error)
UpdateTask(ctx context.Context, id string, patch TaskPatch) (*Task, error)
ClaimTask(ctx context.Context, id, actor string) (*Task, error)
CloseTask(ctx context.Context, id, reason string) (*Task, error)
ReadyTasks(ctx context.Context, filter TaskFilter) ([]Task, error)
ListTasks(ctx context.Context) ([]Task, error)
GetTask(ctx context.Context, id string) (*Task, error)
AddDependency(ctx context.Context, taskID, dependsOnID string) error
RemoveDependency(ctx context.Context, taskID, dependsOnID string) error
AddComment(ctx context.Context, taskID, actor, body string) (*TaskComment, error)
}
type UpsertMemoryInput ¶
type UpsertMemoryInput struct {
ID string `json:"id,omitempty"`
Kind string `json:"kind,omitempty"`
Scope string `json:"scope,omitempty"`
Content string `json:"content"`
Tags []string `json:"tags,omitempty"`
TaskIDs []string `json:"task_ids,omitempty"`
FilePaths []string `json:"file_paths,omitempty"`
SourceRun string `json:"source_run,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
}