projectstate

package
v0.0.80 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: GPL-3.0 Imports: 24 Imported by: 0

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:

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

View Source
const (
	TaskStatusOpen       = "open"
	TaskStatusInProgress = "in_progress"
	TaskStatusBlocked    = "blocked"
	TaskStatusClosed     = "closed"
	TaskStatusDeferred   = "deferred"
)
View Source
const (
	TaskTypeTask    = "task"
	TaskTypeBug     = "bug"
	TaskTypeFeature = "feature"
	TaskTypeChore   = "chore"
	TaskTypeEpic    = "epic"
)
View Source
const (
	MemoryKindPinned     = "pinned"
	MemoryKindSemantic   = "semantic"
	MemoryKindEpisodic   = "episodic"
	MemoryKindProcedural = "procedural"
)
View Source
const (
	MemoryScopeProject = "project"
	MemoryScopeUser    = "user"
	MemoryScopeTask    = "task"
	MemoryScopeFile    = "file"
)
View Source
const SchemaVersion = 1

Variables

This section is empty.

Functions

func DefaultStateDir

func DefaultStateDir(projectID string) (string, error)

func DeriveProjectID

func DeriveProjectID(workDir string) string

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 Event

type Event struct {
	Seq       int64           `json:"seq"`
	EventID   string          `json:"event_id"`
	ProjectID string          `json:"project_id"`
	RunID     string          `json:"run_id,omitempty"`
	Actor     string          `json:"actor,omitempty"`
	Time      time.Time       `json:"time"`
	Type      string          `json:"type"`
	Payload   json.RawMessage `json:"payload"`
}

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 (s FilesystemStore) AddDependency(ctx context.Context, taskID, dependsOnID string) error

func (FilesystemStore) ClaimTask

func (s FilesystemStore) ClaimTask(ctx context.Context, id, actor string) (*Task, error)

func (FilesystemStore) Close

func (s FilesystemStore) Close() error

func (FilesystemStore) CloseTask

func (s FilesystemStore) CloseTask(ctx context.Context, id, reason string) (*Task, error)

func (FilesystemStore) CreateTask

func (s FilesystemStore) CreateTask(ctx context.Context, in CreateTaskInput) (*Task, error)

func (FilesystemStore) DeleteMemory

func (s FilesystemStore) DeleteMemory(ctx context.Context, id string) error

func (FilesystemStore) GetTask

func (s FilesystemStore) GetTask(ctx context.Context, id string) (*Task, error)

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

func (s FilesystemStore) ListTasks(ctx context.Context) ([]Task, 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) ProjectID

func (s FilesystemStore) ProjectID() string

func (FilesystemStore) ReadyTasks

func (s FilesystemStore) ReadyTasks(ctx context.Context, filter TaskFilter) ([]Task, error)

func (FilesystemStore) RemoveDependency

func (s FilesystemStore) RemoveDependency(ctx context.Context, taskID, dependsOnID string) error

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 (s FilesystemStore) UpdateTask(ctx context.Context, id string, patch TaskPatch) (*Task, error)

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 MemoryFilter struct {
	Query string   `json:"query,omitempty"`
	Kinds []string `json:"kinds,omitempty"`
	Tags  []string `json:"tags,omitempty"`
	Limit int      `json:"limit,omitempty"`
}

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

func (e *OpenAIEmbedder) Embed(ctx context.Context, texts []string) ([][]float32, error)

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 PrimeOptions struct {
	Actor        string `json:"actor,omitempty"`
	ActiveTaskID string `json:"active_task_id,omitempty"`
	ReadyLimit   int    `json:"ready_limit,omitempty"`
	MemoryLimit  int    `json:"memory_limit,omitempty"`
}

type PrimeStore

type PrimeStore interface {
	PrimeContext(ctx context.Context, opts PrimeOptions) (string, error)
}

type Project

type Project struct {
	SchemaVersion int       `json:"schema_version"`
	ProjectID     string    `json:"project_id"`
	WorkDir       string    `json:"workdir,omitempty"`
	StateDir      string    `json:"state_dir,omitempty"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
}

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 (s SQLiteStore) AddDependency(ctx context.Context, taskID, dependsOnID string) error

func (SQLiteStore) ClaimTask added in v0.0.4

func (s SQLiteStore) ClaimTask(ctx context.Context, id, actor string) (*Task, error)

func (SQLiteStore) Close added in v0.0.4

func (s SQLiteStore) Close() error

func (SQLiteStore) CloseTask added in v0.0.4

func (s SQLiteStore) CloseTask(ctx context.Context, id, reason string) (*Task, error)

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 (s SQLiteStore) DeleteMemory(ctx context.Context, id string) error

func (SQLiteStore) GetTask added in v0.0.4

func (s SQLiteStore) GetTask(ctx context.Context, id string) (*Task, error)

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) ListTasks added in v0.0.4

func (s SQLiteStore) ListTasks(ctx context.Context) ([]Task, 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) ProjectID added in v0.0.4

func (s SQLiteStore) ProjectID() string

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 (s SQLiteStore) RemoveDependency(ctx context.Context, taskID, dependsOnID string) error

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 (s SQLiteStore) UpdateTask(ctx context.Context, id string, patch TaskPatch) (*Task, error)

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 SessionSummary struct {
	ID        string    `json:"id"`
	RunID     string    `json:"run_id,omitempty"`
	Summary   string    `json:"summary"`
	TaskIDs   []string  `json:"task_ids,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

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 TaskComment struct {
	ID        string    `json:"id"`
	Actor     string    `json:"actor,omitempty"`
	Body      string    `json:"body"`
	CreatedAt time.Time `json:"created_at"`
}

type TaskFilter

type TaskFilter struct {
	Actor           string   `json:"actor,omitempty"`
	Assignee        string   `json:"assignee,omitempty"`
	Labels          []string `json:"labels,omitempty"`
	Limit           int      `json:"limit,omitempty"`
	IncludeAssigned bool     `json:"include_assigned,omitempty"`
}

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"`
}

Jump to

Keyboard shortcuts

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