This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+15
View File
@@ -112,6 +112,13 @@ type Conversation struct {
DeletedAt *time.Time
}
// HasFKMount 报告会话是否挂载了任一 PM/工作区 FK(project/workspace/issue/requirement)。
// composeHistory 据此决定是否调用 ContextReader 组装 FK 上下文简报。
func (c *Conversation) HasFKMount() bool {
return c.ProjectID != nil || c.WorkspaceID != nil ||
c.IssueID != nil || c.RequirementID != nil
}
// Attachment 是 message 的附件(user_id 必填,message_id 上传后未关联时为 nil)。
type Attachment struct {
ID uuid.UUID
@@ -169,6 +176,14 @@ type ConversationReader interface {
ListConversationsByUser(ctx context.Context, userID uuid.UUID, limit int) ([]Conversation, error)
}
// ContextReader 是 chat 模块对 FK 上下文组装器的窄接口(adapter 在 app.go 实现,
// 内部委托 internal/brief.Composer)。给定一个挂载了 project/workspace/issue/requirement
// 的会话,返回组装好的 FK 上下文简报;无任何挂载时返回空串。messageService 在
// composeHistory 注入它的输出到有效 system prompt。chat 不直接依赖 project/brief。
type ContextReader interface {
BriefForConversation(ctx context.Context, conv *Conversation) (string, error)
}
// ===== Service 接口(在 service_*.go 实现) =====
// ConversationService — 会话 CRUD + 标题生成 hook。
+11 -3
View File
@@ -2,14 +2,22 @@ package chat
import "strconv"
// computeCost returns the cost in USD for a single LLM call.
// Prices on LLMModel are expressed in USD per million tokens.
func computeCost(m LLMModel, ptok, ctok, ttok int) float64 {
// ComputeCost returns the cost in USD for a single LLM call.
// Prices on LLMModel are expressed in USD per million tokens. Exported so other
// modules (e.g. internal/acp session cost accounting) reuse the single pricing
// formula instead of replicating it.
func ComputeCost(m LLMModel, ptok, ctok, ttok int) float64 {
return (float64(ptok)*m.PromptPricePerMillionUSD +
float64(ctok)*m.CompletionPricePerMillionUSD +
float64(ttok)*m.ThinkingPricePerMillionUSD) / 1_000_000
}
// computeCost is a thin alias kept for the existing internal caller (streamer.go)
// so that change stays untouched while ComputeCost is the public entry point.
func computeCost(m LLMModel, ptok, ctok, ttok int) float64 {
return ComputeCost(m, ptok, ctok, ttok)
}
// int64ToString converts an int64 to its decimal string representation.
// Uses strconv.FormatInt rather than fmt.Sprintf for efficiency.
func int64ToString(v int64) string { return strconv.FormatInt(v, 10) }
+13
View File
@@ -30,6 +30,19 @@ func TestComputeCost_BasicMath(t *testing.T) {
require.InDelta(t, 0.021, got, 1e-9)
}
// TestExportedComputeCost_EqualsUnexported guards that the exported ComputeCost
// (reused by internal/acp) returns identical results to the unexported alias.
func TestExportedComputeCost_EqualsUnexported(t *testing.T) {
m := LLMModel{
PromptPricePerMillionUSD: 3.0,
CompletionPricePerMillionUSD: 15.0,
ThinkingPricePerMillionUSD: 3.0,
}
for _, c := range [][3]int{{1000, 0, 0}, {0, 1000, 0}, {0, 0, 1000}, {1234, 5678, 90}} {
require.Equal(t, computeCost(m, c[0], c[1], c[2]), ComputeCost(m, c[0], c[1], c[2]))
}
}
func TestComputeCost_ZeroTokens(t *testing.T) {
m := LLMModel{
PromptPricePerMillionUSD: 3.0,
+57 -21
View File
@@ -26,18 +26,20 @@ type MessageServiceConfig struct {
}
type messageService struct {
repo Repository
storage storage.Storage
hub StreamerHub
auditor audit.Recorder
log *slog.Logger
mimeWhite map[string]bool
maxFile int64
maxMsg int64
safetyPct float64
repo Repository
storage storage.Storage
hub StreamerHub
auditor audit.Recorder
log *slog.Logger
mimeWhite map[string]bool
maxFile int64
maxMsg int64
safetyPct float64
briefReader ContextReader // 可为 nil;非 nil 时 composeHistory 注入 FK 上下文
}
// NewMessageService constructs a MessageService.
// NewMessageService constructs a MessageService. briefReader 可为 nil:nil 时
// composeHistory 完全保留旧行为(仅用 conv.SystemPrompt),不做 FK 上下文注入。
func NewMessageService(
repo Repository,
st storage.Storage,
@@ -45,21 +47,23 @@ func NewMessageService(
auditor audit.Recorder,
log *slog.Logger,
cfg MessageServiceConfig,
briefReader ContextReader,
) MessageService {
mimeWhite := make(map[string]bool, len(cfg.MimeWhitelist))
for _, m := range cfg.MimeWhitelist {
mimeWhite[m] = true
}
return &messageService{
repo: repo,
storage: st,
hub: hub,
auditor: auditor,
log: log,
mimeWhite: mimeWhite,
maxFile: cfg.MaxFileBytes,
maxMsg: cfg.MaxMsgBytes,
safetyPct: cfg.SafetyPct,
repo: repo,
storage: st,
hub: hub,
auditor: auditor,
log: log,
mimeWhite: mimeWhite,
maxFile: cfg.MaxFileBytes,
maxMsg: cfg.MaxMsgBytes,
safetyPct: cfg.SafetyPct,
briefReader: briefReader,
}
}
@@ -483,20 +487,52 @@ func (s *messageService) composeHistory(ctx context.Context, conv *Conversation,
}
}
// Resolve the effective system prompt by merging conv.SystemPrompt with the
// FK-derived context brief (requirement/issue/project mounts). The brief is
// pre-bounded by the composer, so it cannot starve message history to zero
// (truncateByTokens still reserves the remaining budget for messages).
effectiveSystemPrompt := s.effectiveSystemPrompt(ctx, conv)
// Compute token budget: context window minus max output tokens, then apply safety margin.
budget := model.ContextWindow - model.MaxOutputTokens
budget = int(float64(budget) * (1 - s.safetyPct))
msgs = truncateByTokens(msgs, conv.SystemPrompt, budget)
msgs = truncateByTokens(msgs, effectiveSystemPrompt, budget)
return llm.Request{
SystemPrompt: conv.SystemPrompt,
SystemPrompt: effectiveSystemPrompt,
Messages: msgs,
MaxOutputTokens: model.MaxOutputTokens,
Reasoning: model.Capabilities.Reasoning,
}, nil
}
// effectiveSystemPrompt 把会话静态 system prompt 与 FK 派生上下文简报合并。
// briefReader 为 nil、会话无任何 FK 挂载、或简报为空时,直接返回 conv.SystemPrompt
// (与旧行为完全一致)。合并是追加式的(FK 简报置于静态 prompt 之后),便于
// 让旧会话(曾把角色 prompt 烘焙进 SystemPrompt)与新会话都能正常工作。
func (s *messageService) effectiveSystemPrompt(ctx context.Context, conv *Conversation) string {
if s.briefReader == nil || !conv.HasFKMount() {
return conv.SystemPrompt
}
fkBrief, err := s.briefReader.BriefForConversation(ctx, conv)
if err != nil {
if s.log != nil {
s.log.Warn("chat.compose_history.brief_failed",
"conversation_id", conv.ID, "err", err.Error())
}
return conv.SystemPrompt
}
fkBrief = strings.TrimSpace(fkBrief)
if fkBrief == "" {
return conv.SystemPrompt
}
if strings.TrimSpace(conv.SystemPrompt) == "" {
return fkBrief
}
return conv.SystemPrompt + "\n\n" + fkBrief
}
// truncateByTokens walks messages backwards and drops oldest messages until the
// estimated token count (system prompt + messages) fits within budget.
func truncateByTokens(msgs []llm.Message, systemPrompt string, budget int) []llm.Message {
+127 -2
View File
@@ -9,6 +9,7 @@ import (
"context"
"errors"
"io"
"strings"
"sync"
"testing"
@@ -282,7 +283,7 @@ func (f *fakeStorage) Delete(_ context.Context, _ string) error {
// ===== test builder =====
func buildMessageService(repo *msgFakeRepo, hub *fakeHub, cfg MessageServiceConfig) MessageService {
return NewMessageService(repo, newFakeStorage(), hub, noopAuditor{}, discardLogger(), cfg)
return NewMessageService(repo, newFakeStorage(), hub, noopAuditor{}, discardLogger(), cfg, nil)
}
func defaultMsgConfig() MessageServiceConfig {
@@ -457,7 +458,7 @@ func TestMessageService_Send_CapabilityMismatch(t *testing.T) {
a := &Attachment{ID: uuid.New(), UserID: userID, MimeType: "image/png", SizeBytes: 100}
specialRepo.addAttachment(a)
svc := NewMessageService(specialRepo, newFakeStorage(), hub, noopAuditor{}, discardLogger(), defaultMsgConfig())
svc := NewMessageService(specialRepo, newFakeStorage(), hub, noopAuditor{}, discardLogger(), defaultMsgConfig(), nil)
_, _, err := svc.Send(context.Background(), userID, conv.ID, "hi", []uuid.UUID{a.ID})
require.Error(t, err)
@@ -990,6 +991,130 @@ func TestTruncateByTokens_PreservesNewest(t *testing.T) {
require.Equal(t, "hi", result[0].Blocks[0].Text)
}
// ===== composeHistory FK context injection tests =====
// fakeContextReader 是 ContextReader 的测试替身。
type fakeContextReader struct {
brief string
err error
called bool
}
func (f *fakeContextReader) BriefForConversation(_ context.Context, _ *Conversation) (string, error) {
f.called = true
return f.brief, f.err
}
func newMsgSvcWithReader(repo *msgFakeRepo, reader ContextReader) *messageService {
return &messageService{
repo: repo,
storage: newFakeStorage(),
auditor: noopAuditor{},
log: discardLogger(),
mimeWhite: map[string]bool{},
safetyPct: 0.0,
briefReader: reader,
}
}
func TestComposeHistory_FKBriefMergedIntoSystemPrompt(t *testing.T) {
repo := newMsgFakeRepo()
reqID := uuid.New()
conv := newConv(uuid.New(), uuid.New())
conv.SystemPrompt = "STATIC PROMPT"
conv.RequirementID = &reqID
repo.addConversation(conv)
repo.addMessage(&Message{ID: 1, ConversationID: conv.ID, Role: RoleUser, Content: "hi", Status: StatusOK})
reader := &fakeContextReader{brief: "## 当前需求\n需求 #7:导出报表"}
svc := newMsgSvcWithReader(repo, reader)
model := &LLMModel{ContextWindow: 4096, MaxOutputTokens: 1024}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.True(t, reader.called)
require.Contains(t, req.SystemPrompt, "STATIC PROMPT")
require.Contains(t, req.SystemPrompt, "需求 #7:导出报表")
}
func TestComposeHistory_FKBriefCountsAgainstTokenBudget(t *testing.T) {
repo := newMsgFakeRepo()
reqID := uuid.New()
conv := newConv(uuid.New(), uuid.New())
conv.RequirementID = &reqID
repo.addConversation(conv)
// 多条历史消息,预算很小,FK 简报应挤占消息预算导致历史被截断。
for i := int64(1); i <= 5; i++ {
repo.addMessage(&Message{
ID: i, ConversationID: conv.ID, Role: RoleUser,
Content: "message content that consumes tokens in the budget", Status: StatusOK,
})
}
hugeBrief := strings.Repeat("需求上下文 ", 300)
reader := &fakeContextReader{brief: hugeBrief}
svc := newMsgSvcWithReader(repo, reader)
// ContextWindow 紧到只够装下简报 + 少量消息:
// budget = (500-100)*(1-0) = 400 tokens;简报约 450 tokens(已超 budget),
// 剩余预算 < 5 条消息所需 → 消息被截断。
model := &LLMModel{ContextWindow: 500, MaxOutputTokens: 100}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.Contains(t, req.SystemPrompt, "需求上下文")
// 简报占了预算 → 消息被截断(少于 5 条)。
require.Less(t, len(req.Messages), 5)
}
func TestComposeHistory_NilReaderPreservesBehavior(t *testing.T) {
repo := newMsgFakeRepo()
reqID := uuid.New()
conv := newConv(uuid.New(), uuid.New())
conv.SystemPrompt = "ONLY STATIC"
conv.RequirementID = &reqID
repo.addConversation(conv)
repo.addMessage(&Message{ID: 1, ConversationID: conv.ID, Role: RoleUser, Content: "hi", Status: StatusOK})
svc := newMsgSvcWithReader(repo, nil) // 无 reader
model := &LLMModel{ContextWindow: 4096, MaxOutputTokens: 1024}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.Equal(t, "ONLY STATIC", req.SystemPrompt)
}
func TestComposeHistory_NoFKMountSkipsReader(t *testing.T) {
repo := newMsgFakeRepo()
conv := newConv(uuid.New(), uuid.New()) // 无任何 FK 挂载
conv.SystemPrompt = "STATIC"
repo.addConversation(conv)
repo.addMessage(&Message{ID: 1, ConversationID: conv.ID, Role: RoleUser, Content: "hi", Status: StatusOK})
reader := &fakeContextReader{brief: "SHOULD NOT APPEAR"}
svc := newMsgSvcWithReader(repo, reader)
model := &LLMModel{ContextWindow: 4096, MaxOutputTokens: 1024}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.False(t, reader.called, "reader must not be called without FK mount")
require.Equal(t, "STATIC", req.SystemPrompt)
}
func TestComposeHistory_ReaderErrorFallsBackToStatic(t *testing.T) {
repo := newMsgFakeRepo()
reqID := uuid.New()
conv := newConv(uuid.New(), uuid.New())
conv.SystemPrompt = "STATIC FALLBACK"
conv.RequirementID = &reqID
repo.addConversation(conv)
repo.addMessage(&Message{ID: 1, ConversationID: conv.ID, Role: RoleUser, Content: "hi", Status: StatusOK})
reader := &fakeContextReader{err: errs.New(errs.CodeInternal, "boom")}
svc := newMsgSvcWithReader(repo, reader)
model := &LLMModel{ContextWindow: 4096, MaxOutputTokens: 1024}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.Equal(t, "STATIC FALLBACK", req.SystemPrompt)
}
// ===== mimeAllowed / capabilityAllows helpers =====
func TestMimeAllowed(t *testing.T) {
+6 -3
View File
@@ -21,7 +21,7 @@ func (q *Queries) DeleteLLMEndpoint(ctx context.Context, id pgtype.UUID) error {
}
const getLLMEndpoint = `-- name: GetLLMEndpoint :one
SELECT id, provider, display_name, base_url, api_key_encrypted, created_by, created_at, updated_at FROM llm_endpoints WHERE id = $1
SELECT id, provider, display_name, base_url, api_key_encrypted, created_by, created_at, updated_at, key_version FROM llm_endpoints WHERE id = $1
`
func (q *Queries) GetLLMEndpoint(ctx context.Context, id pgtype.UUID) (LlmEndpoint, error) {
@@ -36,6 +36,7 @@ func (q *Queries) GetLLMEndpoint(ctx context.Context, id pgtype.UUID) (LlmEndpoi
&i.CreatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.KeyVersion,
)
return i, err
}
@@ -43,7 +44,7 @@ func (q *Queries) GetLLMEndpoint(ctx context.Context, id pgtype.UUID) (LlmEndpoi
const insertLLMEndpoint = `-- name: InsertLLMEndpoint :one
INSERT INTO llm_endpoints (id, provider, display_name, base_url, api_key_encrypted, created_by)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, provider, display_name, base_url, api_key_encrypted, created_by, created_at, updated_at
RETURNING id, provider, display_name, base_url, api_key_encrypted, created_by, created_at, updated_at, key_version
`
type InsertLLMEndpointParams struct {
@@ -74,12 +75,13 @@ func (q *Queries) InsertLLMEndpoint(ctx context.Context, arg InsertLLMEndpointPa
&i.CreatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.KeyVersion,
)
return i, err
}
const listLLMEndpoints = `-- name: ListLLMEndpoints :many
SELECT id, provider, display_name, base_url, api_key_encrypted, created_by, created_at, updated_at FROM llm_endpoints ORDER BY created_at DESC
SELECT id, provider, display_name, base_url, api_key_encrypted, created_by, created_at, updated_at, key_version FROM llm_endpoints ORDER BY created_at DESC
`
func (q *Queries) ListLLMEndpoints(ctx context.Context) ([]LlmEndpoint, error) {
@@ -100,6 +102,7 @@ func (q *Queries) ListLLMEndpoints(ctx context.Context) ([]LlmEndpoint, error) {
&i.CreatedBy,
&i.CreatedAt,
&i.UpdatedAt,
&i.KeyVersion,
); err != nil {
return nil, err
}
+240 -17
View File
@@ -8,6 +8,7 @@ import (
"net/netip"
"github.com/jackc/pgx/v5/pgtype"
"github.com/pgvector/pgvector-go"
)
type AcpAgentKind struct {
@@ -25,6 +26,11 @@ type AcpAgentKind struct {
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 {
@@ -35,6 +41,7 @@ type AcpAgentKindConfigFile struct {
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 {
@@ -64,23 +71,64 @@ type AcpPermissionRequest struct {
}
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"`
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 {
@@ -94,6 +142,81 @@ type AuditLog struct {
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"`
@@ -110,6 +233,14 @@ type Conversation struct {
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"`
@@ -119,6 +250,7 @@ type GitCredential struct {
Fingerprint *string `json:"fingerprint"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
KeyVersion int16 `json:"key_version"`
}
type Issue struct {
@@ -135,6 +267,17 @@ type Issue struct {
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 {
@@ -162,6 +305,7 @@ type LlmEndpoint struct {
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 {
@@ -252,6 +396,50 @@ type Notification struct {
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"`
@@ -264,6 +452,30 @@ type Project struct {
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"`
@@ -299,6 +511,16 @@ type RequirementArtifact struct {
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 {
@@ -354,6 +576,7 @@ type WorkspaceRunProfile struct {
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 {