You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
package projectmemory
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// kindHeadings maps each memory kind to its AGENTS.md section heading.
|
||||
var kindHeadings = map[string]string{
|
||||
KindDecision: "Decisions",
|
||||
KindConvention: "Conventions",
|
||||
KindFileMap: "File Map",
|
||||
KindGotcha: "Gotchas",
|
||||
}
|
||||
|
||||
// agentsMDHeader is the generated-file banner. AGENTS.md is written into the
|
||||
// worktree before spawn; the banner marks it generated so it is not committed or
|
||||
// picked up by auto-doc diffs.
|
||||
const agentsMDHeader = "<!-- GENERATED by AgentCodingWorkflow project memory. Do not edit; changes are overwritten on spawn. -->"
|
||||
|
||||
// renderAgentsMD produces deterministic markdown grouped by kind. Within a kind,
|
||||
// entries are sorted by title then id for stable output. When wsID is set,
|
||||
// workspace-scoped entries plus project-wide (workspace_id NULL) entries are
|
||||
// included; otherwise only project-wide entries.
|
||||
func renderAgentsMD(entries []Entry, wsID *uuid.UUID) string {
|
||||
byKind := map[string][]Entry{}
|
||||
for _, e := range entries {
|
||||
if !includeForWorkspace(e, wsID) {
|
||||
continue
|
||||
}
|
||||
byKind[e.Kind] = append(byKind[e.Kind], e)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(agentsMDHeader)
|
||||
b.WriteString("\n\n# AGENTS.md\n\n")
|
||||
b.WriteString("Project knowledge curated by agents and maintainers. Treat as authoritative context.\n")
|
||||
|
||||
wrote := false
|
||||
for _, kind := range validKinds {
|
||||
items := byKind[kind]
|
||||
if len(items) == 0 {
|
||||
continue
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].Title != items[j].Title {
|
||||
return items[i].Title < items[j].Title
|
||||
}
|
||||
return items[i].ID.String() < items[j].ID.String()
|
||||
})
|
||||
b.WriteString("\n## ")
|
||||
b.WriteString(kindHeadings[kind])
|
||||
b.WriteString("\n\n")
|
||||
for _, e := range items {
|
||||
b.WriteString("### ")
|
||||
b.WriteString(e.Title)
|
||||
b.WriteString("\n")
|
||||
if len(e.Tags) > 0 {
|
||||
tags := append([]string(nil), e.Tags...)
|
||||
sort.Strings(tags)
|
||||
b.WriteString("Tags: ")
|
||||
b.WriteString(strings.Join(tags, ", "))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
b.WriteString(strings.TrimRight(e.Body, "\n"))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
wrote = true
|
||||
}
|
||||
if !wrote {
|
||||
b.WriteString("\n(No project memory recorded yet.)\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// includeForWorkspace decides whether an entry belongs in a worktree's AGENTS.md.
|
||||
func includeForWorkspace(e Entry, wsID *uuid.UUID) bool {
|
||||
if e.WorkspaceID == nil {
|
||||
return true // project-wide
|
||||
}
|
||||
if wsID == nil {
|
||||
return false
|
||||
}
|
||||
return *e.WorkspaceID == *wsID
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package projectmemory
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRenderAgentsMD_GroupsByKindDeterministic(t *testing.T) {
|
||||
entries := []Entry{
|
||||
{ID: uuid.New(), Kind: KindGotcha, Title: "Zebra gotcha", Body: "watch out"},
|
||||
{ID: uuid.New(), Kind: KindDecision, Title: "Use Postgres", Body: "ACID matters", Tags: []string{"db", "arch"}},
|
||||
{ID: uuid.New(), Kind: KindDecision, Title: "Adopt Chi router", Body: "lightweight"},
|
||||
{ID: uuid.New(), Kind: KindConvention, Title: "Tabs not spaces", Body: "gofmt"},
|
||||
}
|
||||
out := renderAgentsMD(entries, nil)
|
||||
|
||||
// Sections appear in canonical kind order: Decisions, Conventions, Gotchas.
|
||||
di := strings.Index(out, "## Decisions")
|
||||
ci := strings.Index(out, "## Conventions")
|
||||
gi := strings.Index(out, "## Gotchas")
|
||||
require.Greater(t, di, 0)
|
||||
require.Greater(t, ci, di)
|
||||
require.Greater(t, gi, ci)
|
||||
|
||||
// Within Decisions, titles are sorted: "Adopt Chi router" before "Use Postgres".
|
||||
require.Less(t, strings.Index(out, "Adopt Chi router"), strings.Index(out, "Use Postgres"))
|
||||
|
||||
// Tags are rendered sorted.
|
||||
require.Contains(t, out, "Tags: arch, db")
|
||||
|
||||
// Generated banner present.
|
||||
require.True(t, strings.HasPrefix(out, agentsMDHeader))
|
||||
|
||||
// Determinism: same input → identical output.
|
||||
require.Equal(t, out, renderAgentsMD(entries, nil))
|
||||
}
|
||||
|
||||
func TestRenderAgentsMD_WorkspaceScoping(t *testing.T) {
|
||||
ws := uuid.New()
|
||||
other := uuid.New()
|
||||
entries := []Entry{
|
||||
{ID: uuid.New(), Kind: KindDecision, Title: "Project-wide", Body: "x"}, // ws nil → always
|
||||
{ID: uuid.New(), Kind: KindDecision, Title: "WS-specific", Body: "y", WorkspaceID: &ws}, // matches ws
|
||||
{ID: uuid.New(), Kind: KindDecision, Title: "Other-WS", Body: "z", WorkspaceID: &other}, // excluded
|
||||
}
|
||||
out := renderAgentsMD(entries, &ws)
|
||||
require.Contains(t, out, "Project-wide")
|
||||
require.Contains(t, out, "WS-specific")
|
||||
require.NotContains(t, out, "Other-WS")
|
||||
|
||||
// With no workspace, only project-wide entries appear.
|
||||
outNil := renderAgentsMD(entries, nil)
|
||||
require.Contains(t, outNil, "Project-wide")
|
||||
require.NotContains(t, outNil, "WS-specific")
|
||||
}
|
||||
|
||||
func TestRenderAgentsMD_Empty(t *testing.T) {
|
||||
out := renderAgentsMD(nil, nil)
|
||||
require.Contains(t, out, "No project memory recorded yet")
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Package projectmemory stores durable, typed project knowledge (decisions,
|
||||
// conventions, file maps, gotchas) with optional embeddings. Write optionally
|
||||
// embeds title+body (best-effort; null embedding when no embedder is configured).
|
||||
// Search is hybrid: vector KNN when embeddings exist, ILIKE/tags fallback else.
|
||||
// RenderAgentsMD groups entries into a deterministic AGENTS.md seeded into the
|
||||
// worktree before an agent spawns.
|
||||
package projectmemory
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Kind values mirror the project_memory.kind CHECK constraint.
|
||||
const (
|
||||
KindDecision = "decision"
|
||||
KindConvention = "convention"
|
||||
KindFileMap = "file_map"
|
||||
KindGotcha = "gotcha"
|
||||
)
|
||||
|
||||
// validKinds is the ordered set used by RenderAgentsMD for deterministic output.
|
||||
var validKinds = []string{KindDecision, KindConvention, KindFileMap, KindGotcha}
|
||||
|
||||
// IsValidKind reports whether k is an allowed memory kind.
|
||||
func IsValidKind(k string) bool {
|
||||
for _, v := range validKinds {
|
||||
if v == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Source values mirror the project_memory.source CHECK constraint.
|
||||
const (
|
||||
SourceAgent = "agent"
|
||||
SourceUser = "user"
|
||||
SourceAuto = "auto"
|
||||
)
|
||||
|
||||
// Entry mirrors a project_memory row (embedding excluded from the domain type).
|
||||
type Entry struct {
|
||||
ID uuid.UUID
|
||||
ProjectID uuid.UUID
|
||||
WorkspaceID *uuid.UUID
|
||||
Kind string
|
||||
Title string
|
||||
Body string
|
||||
Tags []string
|
||||
Source string
|
||||
EmbeddingModel *string
|
||||
CreatedBy *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// WriteInput is the projectmemory.Write payload.
|
||||
type WriteInput struct {
|
||||
ProjectID uuid.UUID
|
||||
WorkspaceID *uuid.UUID
|
||||
Kind string
|
||||
Title string
|
||||
Body string
|
||||
Tags []string
|
||||
Source string
|
||||
CreatedBy *uuid.UUID
|
||||
}
|
||||
|
||||
// Query parameterizes a memory search.
|
||||
type Query struct {
|
||||
ProjectID uuid.UUID
|
||||
Text string
|
||||
Kind string
|
||||
TopK int
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
-- The embedding (vector) column is excluded from sqlc queries (no native pgvector
|
||||
-- type). Write sets embedding via a hand-written pgx UPDATE; vector KNN search is
|
||||
-- hand-written in repository.go. These queries cover the non-vector path.
|
||||
|
||||
-- name: InsertEntry :one
|
||||
INSERT INTO project_memory (
|
||||
id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by, created_at, updated_at;
|
||||
|
||||
-- name: GetEntry :one
|
||||
SELECT id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by, created_at, updated_at
|
||||
FROM project_memory
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListByProject :many
|
||||
SELECT id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by, created_at, updated_at
|
||||
FROM project_memory
|
||||
WHERE project_id = $1
|
||||
AND ($2::text = '' OR kind = $2)
|
||||
ORDER BY kind ASC, created_at DESC;
|
||||
|
||||
-- name: SearchByKeyword :many
|
||||
SELECT id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by, created_at, updated_at
|
||||
FROM project_memory
|
||||
WHERE project_id = $1
|
||||
AND ($2::text = '' OR kind = $2)
|
||||
AND ($3::text = '' OR title ILIKE '%' || $3 || '%' OR body ILIKE '%' || $3 || '%' OR $3 = ANY(tags))
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $4;
|
||||
|
||||
-- name: DeleteEntry :exec
|
||||
DELETE FROM project_memory WHERE id = $1;
|
||||
@@ -0,0 +1,230 @@
|
||||
package projectmemory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/pgvector/pgvector-go"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
projectmemorysqlc "github.com/yan1h/agent-coding-workflow/internal/projectmemory/sqlc"
|
||||
)
|
||||
|
||||
// Repository is the projectmemory persistence contract.
|
||||
type Repository interface {
|
||||
Insert(ctx context.Context, in WriteInput, embeddingModel *string) (*Entry, error)
|
||||
SetEmbedding(ctx context.Context, id uuid.UUID, vec []float32, model string) error
|
||||
Get(ctx context.Context, id uuid.UUID) (*Entry, error)
|
||||
ListByProject(ctx context.Context, projectID uuid.UUID, kind string) ([]Entry, error)
|
||||
SearchKeyword(ctx context.Context, projectID uuid.UUID, kind, text string, topK int) ([]Entry, error)
|
||||
SearchKNN(ctx context.Context, projectID uuid.UUID, vec []float32, kind string, topK int) ([]Entry, error)
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// PgRepository is the production Repository on a pgxpool.Pool.
|
||||
type PgRepository struct {
|
||||
pool *pgxpool.Pool
|
||||
q *projectmemorysqlc.Queries
|
||||
}
|
||||
|
||||
// NewPostgresRepository builds a PgRepository.
|
||||
func NewPostgresRepository(pool *pgxpool.Pool) *PgRepository {
|
||||
return &PgRepository{pool: pool, q: projectmemorysqlc.New(pool)}
|
||||
}
|
||||
|
||||
var _ Repository = (*PgRepository)(nil)
|
||||
|
||||
func pgUUID(u uuid.UUID) pgtype.UUID { return pgtype.UUID{Bytes: u, Valid: true} }
|
||||
|
||||
func pgUUIDPtr(u *uuid.UUID) pgtype.UUID {
|
||||
if u == nil {
|
||||
return pgtype.UUID{}
|
||||
}
|
||||
return pgtype.UUID{Bytes: *u, Valid: true}
|
||||
}
|
||||
|
||||
func uuidPtr(p pgtype.UUID) *uuid.UUID {
|
||||
if !p.Valid {
|
||||
return nil
|
||||
}
|
||||
id := uuid.UUID(p.Bytes)
|
||||
return &id
|
||||
}
|
||||
|
||||
// entryFields is the common projection shared by every sqlc row type in this
|
||||
// package; a tiny adapter keeps a single conversion path.
|
||||
type entryFields struct {
|
||||
ID pgtype.UUID
|
||||
ProjectID pgtype.UUID
|
||||
WorkspaceID pgtype.UUID
|
||||
Kind string
|
||||
Title string
|
||||
Body string
|
||||
Tags []string
|
||||
Source string
|
||||
EmbeddingModel *string
|
||||
CreatedBy pgtype.UUID
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
func toEntry(f entryFields) Entry {
|
||||
e := Entry{
|
||||
ID: uuid.UUID(f.ID.Bytes),
|
||||
ProjectID: uuid.UUID(f.ProjectID.Bytes),
|
||||
WorkspaceID: uuidPtr(f.WorkspaceID),
|
||||
Kind: f.Kind,
|
||||
Title: f.Title,
|
||||
Body: f.Body,
|
||||
Tags: f.Tags,
|
||||
Source: f.Source,
|
||||
EmbeddingModel: f.EmbeddingModel,
|
||||
CreatedBy: uuidPtr(f.CreatedBy),
|
||||
}
|
||||
if e.Tags == nil {
|
||||
e.Tags = []string{}
|
||||
}
|
||||
if f.CreatedAt.Valid {
|
||||
e.CreatedAt = f.CreatedAt.Time
|
||||
}
|
||||
if f.UpdatedAt.Valid {
|
||||
e.UpdatedAt = f.UpdatedAt.Time
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (r *PgRepository) Insert(ctx context.Context, in WriteInput, embeddingModel *string) (*Entry, error) {
|
||||
tags := in.Tags
|
||||
if tags == nil {
|
||||
// TEXT[] NOT NULL: never write a nil slice (would violate the constraint).
|
||||
tags = []string{}
|
||||
}
|
||||
source := in.Source
|
||||
if source == "" {
|
||||
source = SourceAgent
|
||||
}
|
||||
row, err := r.q.InsertEntry(ctx, projectmemorysqlc.InsertEntryParams{
|
||||
ID: pgUUID(uuid.New()),
|
||||
ProjectID: pgUUID(in.ProjectID),
|
||||
WorkspaceID: pgUUIDPtr(in.WorkspaceID),
|
||||
Kind: in.Kind,
|
||||
Title: in.Title,
|
||||
Body: in.Body,
|
||||
Tags: tags,
|
||||
Source: source,
|
||||
EmbeddingModel: embeddingModel,
|
||||
CreatedBy: pgUUIDPtr(in.CreatedBy),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "insert project memory")
|
||||
}
|
||||
e := toEntry(entryFields(row))
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
// SetEmbedding writes the vector + model with a hand-written pgx UPDATE (the
|
||||
// embedding column is excluded from sqlc). updated_at is bumped.
|
||||
func (r *PgRepository) SetEmbedding(ctx context.Context, id uuid.UUID, vec []float32, model string) error {
|
||||
_, err := r.pool.Exec(ctx,
|
||||
`UPDATE project_memory SET embedding = $2::vector, embedding_model = $3, updated_at = now() WHERE id = $1`,
|
||||
pgUUID(id), pgvector.NewVector(vec), model)
|
||||
if err != nil {
|
||||
return errs.Wrap(err, errs.CodeInternal, "set memory embedding")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PgRepository) Get(ctx context.Context, id uuid.UUID) (*Entry, error) {
|
||||
row, err := r.q.GetEntry(ctx, pgUUID(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errs.New(errs.CodeNotFound, "memory entry not found")
|
||||
}
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "get memory entry")
|
||||
}
|
||||
e := toEntry(entryFields(row))
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
func (r *PgRepository) ListByProject(ctx context.Context, projectID uuid.UUID, kind string) ([]Entry, error) {
|
||||
rows, err := r.q.ListByProject(ctx, projectmemorysqlc.ListByProjectParams{
|
||||
ProjectID: pgUUID(projectID),
|
||||
Column2: kind,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "list project memory")
|
||||
}
|
||||
out := make([]Entry, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, toEntry(entryFields(row)))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *PgRepository) SearchKeyword(ctx context.Context, projectID uuid.UUID, kind, text string, topK int) ([]Entry, error) {
|
||||
if topK <= 0 {
|
||||
topK = 10
|
||||
}
|
||||
rows, err := r.q.SearchByKeyword(ctx, projectmemorysqlc.SearchByKeywordParams{
|
||||
ProjectID: pgUUID(projectID),
|
||||
Column2: kind,
|
||||
Column3: text,
|
||||
Limit: int32(topK),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "search memory keyword")
|
||||
}
|
||||
out := make([]Entry, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, toEntry(entryFields(row)))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SearchKNN runs cosine-KNN over entries that have an embedding, scoped to the
|
||||
// project (and optional kind). Hand-written because the vector column is excluded
|
||||
// from sqlc.
|
||||
func (r *PgRepository) SearchKNN(ctx context.Context, projectID uuid.UUID, vec []float32, kind string, topK int) ([]Entry, error) {
|
||||
if topK <= 0 {
|
||||
topK = 10
|
||||
}
|
||||
q := `
|
||||
SELECT id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by, created_at, updated_at
|
||||
FROM project_memory
|
||||
WHERE project_id = $1
|
||||
AND embedding IS NOT NULL
|
||||
AND ($3::text = '' OR kind = $3)
|
||||
ORDER BY embedding <=> $2::vector
|
||||
LIMIT $4`
|
||||
rows, err := r.pool.Query(ctx, q, pgUUID(projectID), pgvector.NewVector(vec), kind, int32(topK))
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "search memory knn")
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []Entry
|
||||
for rows.Next() {
|
||||
var f entryFields
|
||||
if err := rows.Scan(&f.ID, &f.ProjectID, &f.WorkspaceID, &f.Kind, &f.Title,
|
||||
&f.Body, &f.Tags, &f.Source, &f.EmbeddingModel, &f.CreatedBy,
|
||||
&f.CreatedAt, &f.UpdatedAt); err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "scan memory knn row")
|
||||
}
|
||||
out = append(out, toEntry(f))
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "memory knn rows")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *PgRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
if err := r.q.DeleteEntry(ctx, pgUUID(id)); err != nil {
|
||||
return errs.Wrap(err, errs.CodeInternal, "delete memory entry")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package projectmemory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
|
||||
)
|
||||
|
||||
// EmbedderSelector resolves the configured platform embedder, or a typed error
|
||||
// when embeddings are disabled. Implemented in app.go (shared with codeindex).
|
||||
type EmbedderSelector interface {
|
||||
Embedder(ctx context.Context) (emb llm.Embedder, endpointID uuid.UUID, model string, dims int, err error)
|
||||
}
|
||||
|
||||
// Service is the projectmemory application service. Scope enforcement (project
|
||||
// access) is performed by callers (MCP tools / supervisor) before invoking it.
|
||||
type Service interface {
|
||||
Write(ctx context.Context, in WriteInput) (*Entry, error)
|
||||
Search(ctx context.Context, q Query) ([]Entry, error)
|
||||
List(ctx context.Context, projectID uuid.UUID, kind string) ([]Entry, error)
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
RenderAgentsMD(ctx context.Context, projectID uuid.UUID, wsID *uuid.UUID) (string, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
embedSel EmbedderSelector
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewService constructs the projectmemory Service. embedSel may be nil (no
|
||||
// embeddings → keyword fallback only); log may be nil.
|
||||
func NewService(repo Repository, embedSel EmbedderSelector, log *slog.Logger) Service {
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
return &service{repo: repo, embedSel: embedSel, log: log}
|
||||
}
|
||||
|
||||
func (s *service) Write(ctx context.Context, in WriteInput) (*Entry, error) {
|
||||
if !IsValidKind(in.Kind) {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "invalid memory kind")
|
||||
}
|
||||
if in.Title == "" || in.Body == "" {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "title and body required")
|
||||
}
|
||||
// Best-effort embed of title+body; null embedding when disabled/unavailable.
|
||||
var model *string
|
||||
var vec []float32
|
||||
if emb, _, m, _, err := s.embedder(ctx); err == nil {
|
||||
vecs, eerr := emb.Embed(ctx, m, []string{in.Title + "\n\n" + in.Body})
|
||||
if eerr != nil {
|
||||
s.log.Warn("projectmemory.embed_failed", "err", eerr.Error())
|
||||
} else if len(vecs) == 1 {
|
||||
vec = vecs[0]
|
||||
model = &m
|
||||
}
|
||||
}
|
||||
entry, err := s.repo.Insert(ctx, in, model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if vec != nil && model != nil {
|
||||
if err := s.repo.SetEmbedding(ctx, entry.ID, vec, *model); err != nil {
|
||||
// Non-fatal: the entry exists and is keyword-searchable.
|
||||
s.log.Warn("projectmemory.set_embedding_failed", "id", entry.ID, "err", err.Error())
|
||||
} else {
|
||||
entry.EmbeddingModel = model
|
||||
}
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (s *service) Search(ctx context.Context, q Query) ([]Entry, error) {
|
||||
if q.Text == "" {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "query text required")
|
||||
}
|
||||
if q.Kind != "" && !IsValidKind(q.Kind) {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "invalid memory kind")
|
||||
}
|
||||
// Vector path when an embedder is configured; ILIKE/tags fallback otherwise.
|
||||
if emb, _, model, _, err := s.embedder(ctx); err == nil {
|
||||
vecs, eerr := emb.Embed(ctx, model, []string{q.Text})
|
||||
if eerr == nil && len(vecs) == 1 {
|
||||
res, kerr := s.repo.SearchKNN(ctx, q.ProjectID, vecs[0], q.Kind, q.TopK)
|
||||
if kerr != nil {
|
||||
return nil, kerr
|
||||
}
|
||||
// If the project has no embedded entries yet, fall back to keyword.
|
||||
if len(res) > 0 {
|
||||
return res, nil
|
||||
}
|
||||
} else if eerr != nil {
|
||||
s.log.Warn("projectmemory.search_embed_failed", "err", eerr.Error())
|
||||
}
|
||||
}
|
||||
return s.repo.SearchKeyword(ctx, q.ProjectID, q.Kind, q.Text, q.TopK)
|
||||
}
|
||||
|
||||
func (s *service) List(ctx context.Context, projectID uuid.UUID, kind string) ([]Entry, error) {
|
||||
if kind != "" && !IsValidKind(kind) {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "invalid memory kind")
|
||||
}
|
||||
return s.repo.ListByProject(ctx, projectID, kind)
|
||||
}
|
||||
|
||||
func (s *service) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *service) RenderAgentsMD(ctx context.Context, projectID uuid.UUID, wsID *uuid.UUID) (string, error) {
|
||||
entries, err := s.repo.ListByProject(ctx, projectID, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return renderAgentsMD(entries, wsID), nil
|
||||
}
|
||||
|
||||
// embedder returns the selector result, or an error when no selector is set.
|
||||
func (s *service) embedder(ctx context.Context) (llm.Embedder, uuid.UUID, string, int, error) {
|
||||
if s.embedSel == nil {
|
||||
return nil, uuid.Nil, "", 0, llm.ErrEmbeddingsUnsupported
|
||||
}
|
||||
return s.embedSel.Embedder(ctx)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package projectmemory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
|
||||
)
|
||||
|
||||
// fakeRepo is an in-memory Repository for service tests.
|
||||
type fakeRepo struct {
|
||||
entries map[uuid.UUID]*Entry
|
||||
embedding map[uuid.UUID][]float32
|
||||
knnResult []Entry // returned by SearchKNN
|
||||
keywordHits []Entry // returned by SearchKeyword
|
||||
knnCalls int
|
||||
kwCalls int
|
||||
}
|
||||
|
||||
func newFakeRepo() *fakeRepo {
|
||||
return &fakeRepo{entries: map[uuid.UUID]*Entry{}, embedding: map[uuid.UUID][]float32{}}
|
||||
}
|
||||
|
||||
func (r *fakeRepo) Insert(_ context.Context, in WriteInput, model *string) (*Entry, error) {
|
||||
e := &Entry{
|
||||
ID: uuid.New(), ProjectID: in.ProjectID, WorkspaceID: in.WorkspaceID,
|
||||
Kind: in.Kind, Title: in.Title, Body: in.Body, Tags: in.Tags, Source: in.Source,
|
||||
EmbeddingModel: model, CreatedBy: in.CreatedBy, CreatedAt: time.Now(), UpdatedAt: time.Now(),
|
||||
}
|
||||
if e.Tags == nil {
|
||||
e.Tags = []string{}
|
||||
}
|
||||
r.entries[e.ID] = e
|
||||
return e, nil
|
||||
}
|
||||
func (r *fakeRepo) SetEmbedding(_ context.Context, id uuid.UUID, vec []float32, _ string) error {
|
||||
r.embedding[id] = vec
|
||||
return nil
|
||||
}
|
||||
func (r *fakeRepo) Get(_ context.Context, id uuid.UUID) (*Entry, error) { return r.entries[id], nil }
|
||||
func (r *fakeRepo) ListByProject(_ context.Context, _ uuid.UUID, _ string) ([]Entry, error) {
|
||||
out := make([]Entry, 0, len(r.entries))
|
||||
for _, e := range r.entries {
|
||||
out = append(out, *e)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func (r *fakeRepo) SearchKeyword(_ context.Context, _ uuid.UUID, _, _ string, _ int) ([]Entry, error) {
|
||||
r.kwCalls++
|
||||
return r.keywordHits, nil
|
||||
}
|
||||
func (r *fakeRepo) SearchKNN(_ context.Context, _ uuid.UUID, _ []float32, _ string, _ int) ([]Entry, error) {
|
||||
r.knnCalls++
|
||||
return r.knnResult, nil
|
||||
}
|
||||
func (r *fakeRepo) Delete(_ context.Context, id uuid.UUID) error { delete(r.entries, id); return nil }
|
||||
|
||||
type fakeSel struct{ emb llm.Embedder }
|
||||
|
||||
func (s fakeSel) Embedder(context.Context) (llm.Embedder, uuid.UUID, string, int, error) {
|
||||
if s.emb == nil {
|
||||
return nil, uuid.Nil, "", 0, llm.ErrEmbeddingsUnsupported
|
||||
}
|
||||
return s.emb, uuid.New(), "fake-model", llm.PlatformEmbeddingDims, nil
|
||||
}
|
||||
|
||||
func TestService_Write_WithEmbedder(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
svc := NewService(repo, fakeSel{emb: llm.NewFakeEmbedder()}, nil)
|
||||
e, err := svc.Write(context.Background(), WriteInput{
|
||||
ProjectID: uuid.New(), Kind: KindDecision, Title: "t", Body: "b",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, e.EmbeddingModel)
|
||||
require.NotEmpty(t, repo.embedding[e.ID], "embedding must be stored")
|
||||
}
|
||||
|
||||
func TestService_Write_WithoutEmbedder(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
svc := NewService(repo, fakeSel{emb: nil}, nil) // disabled
|
||||
e, err := svc.Write(context.Background(), WriteInput{
|
||||
ProjectID: uuid.New(), Kind: KindGotcha, Title: "t", Body: "b",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, e.EmbeddingModel)
|
||||
require.Empty(t, repo.embedding[e.ID])
|
||||
}
|
||||
|
||||
func TestService_Write_InvalidKind(t *testing.T) {
|
||||
svc := NewService(newFakeRepo(), fakeSel{}, nil)
|
||||
_, err := svc.Write(context.Background(), WriteInput{ProjectID: uuid.New(), Kind: "bogus", Title: "t", Body: "b"})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestService_Search_VectorPath(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
repo.knnResult = []Entry{{ID: uuid.New(), Kind: KindDecision, Title: "hit"}}
|
||||
svc := NewService(repo, fakeSel{emb: llm.NewFakeEmbedder()}, nil)
|
||||
res, err := svc.Search(context.Background(), Query{ProjectID: uuid.New(), Text: "q"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 1, repo.knnCalls)
|
||||
require.Equal(t, 0, repo.kwCalls)
|
||||
}
|
||||
|
||||
func TestService_Search_KeywordFallback_NoEmbedder(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
repo.keywordHits = []Entry{{ID: uuid.New(), Kind: KindDecision, Title: "kw"}}
|
||||
svc := NewService(repo, fakeSel{emb: nil}, nil) // disabled → keyword path
|
||||
res, err := svc.Search(context.Background(), Query{ProjectID: uuid.New(), Text: "q"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 0, repo.knnCalls)
|
||||
require.Equal(t, 1, repo.kwCalls)
|
||||
}
|
||||
|
||||
func TestService_Search_VectorEmptyFallsBackToKeyword(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
repo.knnResult = nil // no embedded entries yet
|
||||
repo.keywordHits = []Entry{{ID: uuid.New(), Title: "kw"}}
|
||||
svc := NewService(repo, fakeSel{emb: llm.NewFakeEmbedder()}, nil)
|
||||
res, err := svc.Search(context.Background(), Query{ProjectID: uuid.New(), Text: "q"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
require.Equal(t, 1, repo.knnCalls)
|
||||
require.Equal(t, 1, repo.kwCalls)
|
||||
}
|
||||
|
||||
var _ Repository = (*fakeRepo)(nil)
|
||||
@@ -0,0 +1,32 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
|
||||
package projectmemorysqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: memory.sql
|
||||
|
||||
package projectmemorysqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const deleteEntry = `-- name: DeleteEntry :exec
|
||||
DELETE FROM project_memory WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteEntry(ctx context.Context, id pgtype.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteEntry, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getEntry = `-- name: GetEntry :one
|
||||
SELECT id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by, created_at, updated_at
|
||||
FROM project_memory
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
type GetEntryRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
EmbeddingModel *string `json:"embedding_model"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetEntry(ctx context.Context, id pgtype.UUID) (GetEntryRow, error) {
|
||||
row := q.db.QueryRow(ctx, getEntry, id)
|
||||
var i GetEntryRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.WorkspaceID,
|
||||
&i.Kind,
|
||||
&i.Title,
|
||||
&i.Body,
|
||||
&i.Tags,
|
||||
&i.Source,
|
||||
&i.EmbeddingModel,
|
||||
&i.CreatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertEntry = `-- name: InsertEntry :one
|
||||
|
||||
INSERT INTO project_memory (
|
||||
id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by, created_at, updated_at
|
||||
`
|
||||
|
||||
type InsertEntryParams struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
EmbeddingModel *string `json:"embedding_model"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
}
|
||||
|
||||
type InsertEntryRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
EmbeddingModel *string `json:"embedding_model"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
// The embedding (vector) column is excluded from sqlc queries (no native pgvector
|
||||
// type). Write sets embedding via a hand-written pgx UPDATE; vector KNN search is
|
||||
// hand-written in repository.go. These queries cover the non-vector path.
|
||||
func (q *Queries) InsertEntry(ctx context.Context, arg InsertEntryParams) (InsertEntryRow, error) {
|
||||
row := q.db.QueryRow(ctx, insertEntry,
|
||||
arg.ID,
|
||||
arg.ProjectID,
|
||||
arg.WorkspaceID,
|
||||
arg.Kind,
|
||||
arg.Title,
|
||||
arg.Body,
|
||||
arg.Tags,
|
||||
arg.Source,
|
||||
arg.EmbeddingModel,
|
||||
arg.CreatedBy,
|
||||
)
|
||||
var i InsertEntryRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.WorkspaceID,
|
||||
&i.Kind,
|
||||
&i.Title,
|
||||
&i.Body,
|
||||
&i.Tags,
|
||||
&i.Source,
|
||||
&i.EmbeddingModel,
|
||||
&i.CreatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listByProject = `-- name: ListByProject :many
|
||||
SELECT id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by, created_at, updated_at
|
||||
FROM project_memory
|
||||
WHERE project_id = $1
|
||||
AND ($2::text = '' OR kind = $2)
|
||||
ORDER BY kind ASC, created_at DESC
|
||||
`
|
||||
|
||||
type ListByProjectParams struct {
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
Column2 string `json:"column_2"`
|
||||
}
|
||||
|
||||
type ListByProjectRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
EmbeddingModel *string `json:"embedding_model"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListByProject(ctx context.Context, arg ListByProjectParams) ([]ListByProjectRow, error) {
|
||||
rows, err := q.db.Query(ctx, listByProject, arg.ProjectID, arg.Column2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListByProjectRow
|
||||
for rows.Next() {
|
||||
var i ListByProjectRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.WorkspaceID,
|
||||
&i.Kind,
|
||||
&i.Title,
|
||||
&i.Body,
|
||||
&i.Tags,
|
||||
&i.Source,
|
||||
&i.EmbeddingModel,
|
||||
&i.CreatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const searchByKeyword = `-- name: SearchByKeyword :many
|
||||
SELECT id, project_id, workspace_id, kind, title, body, tags, source,
|
||||
embedding_model, created_by, created_at, updated_at
|
||||
FROM project_memory
|
||||
WHERE project_id = $1
|
||||
AND ($2::text = '' OR kind = $2)
|
||||
AND ($3::text = '' OR title ILIKE '%' || $3 || '%' OR body ILIKE '%' || $3 || '%' OR $3 = ANY(tags))
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $4
|
||||
`
|
||||
|
||||
type SearchByKeywordParams struct {
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
Column2 string `json:"column_2"`
|
||||
Column3 string `json:"column_3"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
type SearchByKeywordRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
EmbeddingModel *string `json:"embedding_model"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) SearchByKeyword(ctx context.Context, arg SearchByKeywordParams) ([]SearchByKeywordRow, error) {
|
||||
rows, err := q.db.Query(ctx, searchByKeyword,
|
||||
arg.ProjectID,
|
||||
arg.Column2,
|
||||
arg.Column3,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []SearchByKeywordRow
|
||||
for rows.Next() {
|
||||
var i SearchByKeywordRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.WorkspaceID,
|
||||
&i.Kind,
|
||||
&i.Title,
|
||||
&i.Body,
|
||||
&i.Tags,
|
||||
&i.Source,
|
||||
&i.EmbeddingModel,
|
||||
&i.CreatedBy,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
|
||||
package projectmemorysqlc
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/pgvector/pgvector-go"
|
||||
)
|
||||
|
||||
type AcpAgentKind struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Description string `json:"description"`
|
||||
BinaryPath string `json:"binary_path"`
|
||||
Args []string `json:"args"`
|
||||
EncryptedEnv []byte `json:"encrypted_env"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
ToolAllowlist []string `json:"tool_allowlist"`
|
||||
ClientType string `json:"client_type"`
|
||||
EncryptedMcpServers []byte `json:"encrypted_mcp_servers"`
|
||||
ModelID pgtype.UUID `json:"model_id"`
|
||||
MaxCostUsd pgtype.Numeric `json:"max_cost_usd"`
|
||||
MaxTokens *int64 `json:"max_tokens"`
|
||||
MaxWallClockSeconds *int32 `json:"max_wall_clock_seconds"`
|
||||
KeyVersion int16 `json:"key_version"`
|
||||
}
|
||||
|
||||
type AcpAgentKindConfigFile struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
AgentKindID pgtype.UUID `json:"agent_kind_id"`
|
||||
RelPath string `json:"rel_path"`
|
||||
EncryptedContent []byte `json:"encrypted_content"`
|
||||
UpdatedBy pgtype.UUID `json:"updated_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
KeyVersion int16 `json:"key_version"`
|
||||
}
|
||||
|
||||
type AcpEvent struct {
|
||||
ID int64 `json:"id"`
|
||||
SessionID pgtype.UUID `json:"session_id"`
|
||||
Direction string `json:"direction"`
|
||||
RpcKind string `json:"rpc_kind"`
|
||||
Method *string `json:"method"`
|
||||
Payload []byte `json:"payload"`
|
||||
PayloadSize int32 `json:"payload_size"`
|
||||
Truncated bool `json:"truncated"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type AcpPermissionRequest struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
SessionID pgtype.UUID `json:"session_id"`
|
||||
AgentRequestID string `json:"agent_request_id"`
|
||||
ToolName string `json:"tool_name"`
|
||||
ToolCall []byte `json:"tool_call"`
|
||||
Options []byte `json:"options"`
|
||||
Status string `json:"status"`
|
||||
ChosenOptionID *string `json:"chosen_option_id"`
|
||||
DecidedBy pgtype.UUID `json:"decided_by"`
|
||||
DecidedAt pgtype.Timestamptz `json:"decided_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type AcpSession struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
AgentKindID pgtype.UUID `json:"agent_kind_id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
IssueID pgtype.UUID `json:"issue_id"`
|
||||
RequirementID pgtype.UUID `json:"requirement_id"`
|
||||
AgentSessionID *string `json:"agent_session_id"`
|
||||
Branch string `json:"branch"`
|
||||
CwdPath string `json:"cwd_path"`
|
||||
IsMainWorktree bool `json:"is_main_worktree"`
|
||||
Status string `json:"status"`
|
||||
Pid *int32 `json:"pid"`
|
||||
ExitCode *int32 `json:"exit_code"`
|
||||
LastError *string `json:"last_error"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
EndedAt pgtype.Timestamptz `json:"ended_at"`
|
||||
LastStopReason *string `json:"last_stop_reason"`
|
||||
OrchestratorStepID pgtype.UUID `json:"orchestrator_step_id"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
ThinkingTokens int64 `json:"thinking_tokens"`
|
||||
TotalCostUsd pgtype.Numeric `json:"total_cost_usd"`
|
||||
LastActivityAt pgtype.Timestamptz `json:"last_activity_at"`
|
||||
BudgetMaxCostUsd pgtype.Numeric `json:"budget_max_cost_usd"`
|
||||
BudgetMaxTokens *int64 `json:"budget_max_tokens"`
|
||||
BudgetMaxWallClockSeconds *int32 `json:"budget_max_wall_clock_seconds"`
|
||||
TerminatedReason *string `json:"terminated_reason"`
|
||||
SandboxMode *string `json:"sandbox_mode"`
|
||||
CostUsd pgtype.Numeric `json:"cost_usd"`
|
||||
TokensTotal int64 `json:"tokens_total"`
|
||||
}
|
||||
|
||||
type AcpSessionUsage struct {
|
||||
ID int64 `json:"id"`
|
||||
SessionID pgtype.UUID `json:"session_id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
AgentKindID pgtype.UUID `json:"agent_kind_id"`
|
||||
ModelID pgtype.UUID `json:"model_id"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
ThinkingTokens int64 `json:"thinking_tokens"`
|
||||
CostUsd pgtype.Numeric `json:"cost_usd"`
|
||||
SourceEventID *int64 `json:"source_event_id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type AcpTurn struct {
|
||||
ID int64 `json:"id"`
|
||||
SessionID pgtype.UUID `json:"session_id"`
|
||||
TurnIndex int32 `json:"turn_index"`
|
||||
PromptRequestID *string `json:"prompt_request_id"`
|
||||
Status string `json:"status"`
|
||||
StopReason *string `json:"stop_reason"`
|
||||
UpdateCount int32 `json:"update_count"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
}
|
||||
|
||||
type AuditLog struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Action string `json:"action"`
|
||||
TargetType *string `json:"target_type"`
|
||||
TargetID *string `json:"target_id"`
|
||||
Ip *netip.Addr `json:"ip"`
|
||||
Metadata []byte `json:"metadata"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type ChangeRequest struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
RequirementID pgtype.UUID `json:"requirement_id"`
|
||||
IssueID pgtype.UUID `json:"issue_id"`
|
||||
Number int32 `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
SourceBranch string `json:"source_branch"`
|
||||
TargetBranch string `json:"target_branch"`
|
||||
State string `json:"state"`
|
||||
ReviewVerdict string `json:"review_verdict"`
|
||||
CiState string `json:"ci_state"`
|
||||
Provider string `json:"provider"`
|
||||
ExternalID *int64 `json:"external_id"`
|
||||
ExternalUrl string `json:"external_url"`
|
||||
MergeCommitSha string `json:"merge_commit_sha"`
|
||||
ReviewedBy pgtype.UUID `json:"reviewed_by"`
|
||||
ReviewedAt pgtype.Timestamptz `json:"reviewed_at"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
MergedAt pgtype.Timestamptz `json:"merged_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CodeChunk struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
RunID pgtype.UUID `json:"run_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
CommitSha string `json:"commit_sha"`
|
||||
FilePath string `json:"file_path"`
|
||||
Lang *string `json:"lang"`
|
||||
StartLine int32 `json:"start_line"`
|
||||
EndLine int32 `json:"end_line"`
|
||||
Content string `json:"content"`
|
||||
ContentSha []byte `json:"content_sha"`
|
||||
TokenCount *int32 `json:"token_count"`
|
||||
Embedding *pgvector.Vector `json:"embedding"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type CodeIndexRun struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
WorktreeID pgtype.UUID `json:"worktree_id"`
|
||||
Branch string `json:"branch"`
|
||||
CommitSha string `json:"commit_sha"`
|
||||
Status string `json:"status"`
|
||||
EmbeddingEndpointID pgtype.UUID `json:"embedding_endpoint_id"`
|
||||
EmbeddingModel string `json:"embedding_model"`
|
||||
Dims int32 `json:"dims"`
|
||||
FilesIndexed int32 `json:"files_indexed"`
|
||||
ChunksIndexed int32 `json:"chunks_indexed"`
|
||||
Error *string `json:"error"`
|
||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
||||
FinishedAt pgtype.Timestamptz `json:"finished_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type CommitSummary struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
WorktreeID pgtype.UUID `json:"worktree_id"`
|
||||
Branch string `json:"branch"`
|
||||
CommitSha string `json:"commit_sha"`
|
||||
Kind string `json:"kind"`
|
||||
Title *string `json:"title"`
|
||||
BodyMd string `json:"body_md"`
|
||||
Model *string `json:"model"`
|
||||
Status string `json:"status"`
|
||||
Error *string `json:"error"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Conversation struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
IssueID pgtype.UUID `json:"issue_id"`
|
||||
ModelID pgtype.UUID `json:"model_id"`
|
||||
SystemPrompt string `json:"system_prompt"`
|
||||
Title *string `json:"title"`
|
||||
TitleGeneratedAt pgtype.Timestamptz `json:"title_generated_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
|
||||
RequirementID pgtype.UUID `json:"requirement_id"`
|
||||
}
|
||||
|
||||
type CryptoKey struct {
|
||||
Version int32 `json:"version"`
|
||||
Provider string `json:"provider"`
|
||||
KeyRef *string `json:"key_ref"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type GitCredential struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
Kind string `json:"kind"`
|
||||
Username *string `json:"username"`
|
||||
EncryptedSecret []byte `json:"encrypted_secret"`
|
||||
Fingerprint *string `json:"fingerprint"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
KeyVersion int16 `json:"key_version"`
|
||||
}
|
||||
|
||||
type Issue struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
RequirementID pgtype.UUID `json:"requirement_id"`
|
||||
Number int32 `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
AssigneeID pgtype.UUID `json:"assignee_id"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
ClosedAt pgtype.Timestamptz `json:"closed_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
ParentID pgtype.UUID `json:"parent_id"`
|
||||
Priority int32 `json:"priority"`
|
||||
}
|
||||
|
||||
type IssueDependency struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
BlockedID pgtype.UUID `json:"blocked_id"`
|
||||
BlockerID pgtype.UUID `json:"blocker_id"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Job struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Payload []byte `json:"payload"`
|
||||
Status string `json:"status"`
|
||||
ScheduledAt pgtype.Timestamptz `json:"scheduled_at"`
|
||||
Attempts int32 `json:"attempts"`
|
||||
MaxAttempts int32 `json:"max_attempts"`
|
||||
LeasedAt pgtype.Timestamptz `json:"leased_at"`
|
||||
LeasedBy *string `json:"leased_by"`
|
||||
LastError *string `json:"last_error"`
|
||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type LlmEndpoint struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
DisplayName string `json:"display_name"`
|
||||
BaseUrl string `json:"base_url"`
|
||||
ApiKeyEncrypted []byte `json:"api_key_encrypted"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
KeyVersion int16 `json:"key_version"`
|
||||
}
|
||||
|
||||
type LlmModel struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
EndpointID pgtype.UUID `json:"endpoint_id"`
|
||||
ModelID string `json:"model_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Capabilities []byte `json:"capabilities"`
|
||||
ContextWindow int32 `json:"context_window"`
|
||||
MaxOutputTokens int32 `json:"max_output_tokens"`
|
||||
PromptPricePerMillionUsd pgtype.Numeric `json:"prompt_price_per_million_usd"`
|
||||
CompletionPricePerMillionUsd pgtype.Numeric `json:"completion_price_per_million_usd"`
|
||||
ThinkingPricePerMillionUsd pgtype.Numeric `json:"thinking_price_per_million_usd"`
|
||||
IsTitleGenerator bool `json:"is_title_generator"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type LlmUsage struct {
|
||||
ID int64 `json:"id"`
|
||||
ConversationID pgtype.UUID `json:"conversation_id"`
|
||||
MessageID int64 `json:"message_id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
EndpointID pgtype.UUID `json:"endpoint_id"`
|
||||
ModelID pgtype.UUID `json:"model_id"`
|
||||
PromptTokens int32 `json:"prompt_tokens"`
|
||||
CompletionTokens int32 `json:"completion_tokens"`
|
||||
ThinkingTokens int32 `json:"thinking_tokens"`
|
||||
CostUsd pgtype.Numeric `json:"cost_usd"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type McpToken struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
TokenHash []byte `json:"token_hash"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Issuer string `json:"issuer"`
|
||||
Scope []byte `json:"scope"`
|
||||
AcpSessionID pgtype.UUID `json:"acp_session_id"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
LastUsedAt pgtype.Timestamptz `json:"last_used_at"`
|
||||
RevokedAt pgtype.Timestamptz `json:"revoked_at"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID int64 `json:"id"`
|
||||
ConversationID pgtype.UUID `json:"conversation_id"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Thinking *string `json:"thinking"`
|
||||
ToolCalls []byte `json:"tool_calls"`
|
||||
Status string `json:"status"`
|
||||
ErrorMessage *string `json:"error_message"`
|
||||
PromptTokens *int32 `json:"prompt_tokens"`
|
||||
CompletionTokens *int32 `json:"completion_tokens"`
|
||||
ThinkingTokens *int32 `json:"thinking_tokens"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type MessageAttachment struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
MessageID *int64 `json:"message_id"`
|
||||
Filename string `json:"filename"`
|
||||
MimeType string `json:"mime_type"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
Sha256 string `json:"sha256"`
|
||||
StoragePath string `json:"storage_path"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Topic string `json:"topic"`
|
||||
Severity string `json:"severity"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Link *string `json:"link"`
|
||||
Metadata []byte `json:"metadata"`
|
||||
ReadAt pgtype.Timestamptz `json:"read_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type NotificationPref struct {
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
EmailEnabled bool `json:"email_enabled"`
|
||||
ImEnabled bool `json:"im_enabled"`
|
||||
ImWebhookUrlEnc []byte `json:"im_webhook_url_enc"`
|
||||
MinSeverity string `json:"min_severity"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type OrchestratorRun struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
RequirementID pgtype.UUID `json:"requirement_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
OwnerID pgtype.UUID `json:"owner_id"`
|
||||
Status string `json:"status"`
|
||||
CurrentPhase string `json:"current_phase"`
|
||||
Config []byte `json:"config"`
|
||||
LastError *string `json:"last_error"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
EndedAt pgtype.Timestamptz `json:"ended_at"`
|
||||
}
|
||||
|
||||
type OrchestratorStep struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
RunID pgtype.UUID `json:"run_id"`
|
||||
Phase string `json:"phase"`
|
||||
Attempt int32 `json:"attempt"`
|
||||
ParentStepID pgtype.UUID `json:"parent_step_id"`
|
||||
Depth int32 `json:"depth"`
|
||||
Status string `json:"status"`
|
||||
AgentKindID pgtype.UUID `json:"agent_kind_id"`
|
||||
AcpSessionID pgtype.UUID `json:"acp_session_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
StopReason *string `json:"stop_reason"`
|
||||
GateResult []byte `json:"gate_result"`
|
||||
LastError *string `json:"last_error"`
|
||||
JobID pgtype.UUID `json:"job_id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
EndedAt pgtype.Timestamptz `json:"ended_at"`
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Visibility string `json:"visibility"`
|
||||
OwnerID pgtype.UUID `json:"owner_id"`
|
||||
ArchivedAt pgtype.Timestamptz `json:"archived_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ProjectMember struct {
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
AddedBy pgtype.UUID `json:"added_by"`
|
||||
}
|
||||
|
||||
type ProjectMemory struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
Embedding *pgvector.Vector `json:"embedding"`
|
||||
EmbeddingModel *string `json:"embedding_model"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type PromptTemplate struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Scope string `json:"scope"`
|
||||
OwnerID pgtype.UUID `json:"owner_id"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Requirement struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
Number int32 `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Phase string `json:"phase"`
|
||||
Status string `json:"status"`
|
||||
OwnerID pgtype.UUID `json:"owner_id"`
|
||||
ClosedAt pgtype.Timestamptz `json:"closed_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
}
|
||||
|
||||
type RequirementArtifact struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
RequirementID pgtype.UUID `json:"requirement_id"`
|
||||
Phase string `json:"phase"`
|
||||
Version int32 `json:"version"`
|
||||
Content string `json:"content"`
|
||||
Note string `json:"note"`
|
||||
SourceMessageID *int64 `json:"source_message_id"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
Verdict string `json:"verdict"`
|
||||
}
|
||||
|
||||
type RequirementPhasePointer struct {
|
||||
RequirementID pgtype.UUID `json:"requirement_id"`
|
||||
Phase string `json:"phase"`
|
||||
ApprovedArtifactID pgtype.UUID `json:"approved_artifact_id"`
|
||||
ApprovedBy pgtype.UUID `json:"approved_by"`
|
||||
ApprovedAt pgtype.Timestamptz `json:"approved_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
DisplayName string `json:"display_name"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type UserSession struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
TokenHash []byte `json:"token_hash"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
LastSeenAt pgtype.Timestamptz `json:"last_seen_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Workspace struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
GitRemoteUrl string `json:"git_remote_url"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
MainPath string `json:"main_path"`
|
||||
SyncStatus string `json:"sync_status"`
|
||||
LastSyncedAt pgtype.Timestamptz `json:"last_synced_at"`
|
||||
LastSyncError *string `json:"last_sync_error"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
ActiveMainSessionID pgtype.UUID `json:"active_main_session_id"`
|
||||
}
|
||||
|
||||
type WorkspaceRunProfile struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
EncryptedEnv []byte `json:"encrypted_env"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LastStartedAt pgtype.Timestamptz `json:"last_started_at"`
|
||||
LastExitCode *int32 `json:"last_exit_code"`
|
||||
LastError string `json:"last_error"`
|
||||
CreatedBy pgtype.UUID `json:"created_by"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
KeyVersion int16 `json:"key_version"`
|
||||
}
|
||||
|
||||
type WorkspaceWorktree struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
||||
Branch string `json:"branch"`
|
||||
Path string `json:"path"`
|
||||
Status string `json:"status"`
|
||||
ActiveHolder *string `json:"active_holder"`
|
||||
AcquiredAt pgtype.Timestamptz `json:"acquired_at"`
|
||||
LastUsedAt pgtype.Timestamptz `json:"last_used_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
PruneWarningAt pgtype.Timestamptz `json:"prune_warning_at"`
|
||||
}
|
||||
Reference in New Issue
Block a user