You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
package codeindex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/git"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/jobs"
|
||||
)
|
||||
|
||||
// ErrEmbeddingDisabled is returned by Search/EnqueueBuild when no embedding
|
||||
// endpoint is configured. Callers degrade to keyword discovery (fs/grep) and
|
||||
// expose this as a typed, non-fatal "feature off" condition.
|
||||
var ErrEmbeddingDisabled = errors.New("codeindex: no embedding endpoint configured")
|
||||
|
||||
// WorkspaceInfo is the minimal workspace data the indexer needs. Provided by a
|
||||
// locator implemented in app.go over the workspace repository (no import cycle).
|
||||
type WorkspaceInfo struct {
|
||||
ID uuid.UUID
|
||||
MainPath string
|
||||
DefaultBranch string
|
||||
}
|
||||
|
||||
// WorkspaceLocator resolves a workspace's on-disk path and default branch.
|
||||
type WorkspaceLocator interface {
|
||||
LocateWorkspace(ctx context.Context, wsID uuid.UUID) (WorkspaceInfo, error)
|
||||
// LocateWorktree resolves a worktree's on-disk path + branch (for branch builds).
|
||||
LocateWorktree(ctx context.Context, worktreeID uuid.UUID) (path, branch string, wsID uuid.UUID, err error)
|
||||
}
|
||||
|
||||
// EmbedderSelector resolves the configured platform embedder, or nil + a typed
|
||||
// error when embeddings are disabled (no endpoint configured / unsupported).
|
||||
type EmbedderSelector interface {
|
||||
// Embedder returns the embedder, the embedding endpoint id, the model id and
|
||||
// the platform dims. Returns ErrEmbeddingDisabled when not configured.
|
||||
Embedder(ctx context.Context) (emb llm.Embedder, endpointID uuid.UUID, model string, dims int, err error)
|
||||
}
|
||||
|
||||
// Enqueuer is the subset of jobs.Repository the service needs.
|
||||
type Enqueuer interface {
|
||||
Enqueue(ctx context.Context, typ jobs.JobType, payload json.RawMessage, scheduledAt time.Time, maxAttempts int32) (*jobs.Job, error)
|
||||
}
|
||||
|
||||
// Service is the codeindex application service.
|
||||
type Service interface {
|
||||
// EnqueueBuild resolves the build commit (HEAD of the worktree/main) and
|
||||
// enqueues a codeindex.build job. Idempotent on (workspace, commit, model):
|
||||
// a re-enqueue at the same commit returns the existing run without rebuild.
|
||||
EnqueueBuild(ctx context.Context, wsID uuid.UUID, worktreeID *uuid.UUID, commitSHA string) (*Run, error)
|
||||
Search(ctx context.Context, wsID uuid.UUID, q SearchQuery) ([]Snippet, error)
|
||||
MarkStale(ctx context.Context, wsID uuid.UUID) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
repo Repository
|
||||
locator WorkspaceLocator
|
||||
gitr git.Runner
|
||||
embedSel EmbedderSelector
|
||||
jobs Enqueuer
|
||||
maxTopK int
|
||||
}
|
||||
|
||||
// NewService constructs the codeindex Service. maxTopK caps Search top_k.
|
||||
func NewService(repo Repository, locator WorkspaceLocator, gitr git.Runner, embedSel EmbedderSelector, jobsRepo Enqueuer, maxTopK int) Service {
|
||||
if maxTopK <= 0 {
|
||||
maxTopK = 50
|
||||
}
|
||||
return &service{repo: repo, locator: locator, gitr: gitr, embedSel: embedSel, jobs: jobsRepo, maxTopK: maxTopK}
|
||||
}
|
||||
|
||||
func (s *service) EnqueueBuild(ctx context.Context, wsID uuid.UUID, worktreeID *uuid.UUID, commitSHA string) (*Run, error) {
|
||||
emb, endpointID, model, dims, err := s.embedSel.Embedder(ctx)
|
||||
if err != nil {
|
||||
return nil, err // ErrEmbeddingDisabled or config error
|
||||
}
|
||||
_ = emb
|
||||
|
||||
branch := ""
|
||||
dir := ""
|
||||
if worktreeID != nil {
|
||||
path, b, locWS, lerr := s.locator.LocateWorktree(ctx, *worktreeID)
|
||||
if lerr != nil {
|
||||
return nil, lerr
|
||||
}
|
||||
dir, branch, wsID = path, b, locWS
|
||||
} else {
|
||||
info, lerr := s.locator.LocateWorkspace(ctx, wsID)
|
||||
if lerr != nil {
|
||||
return nil, lerr
|
||||
}
|
||||
dir, branch = info.MainPath, info.DefaultBranch
|
||||
}
|
||||
|
||||
// Resolve HEAD commit when not supplied.
|
||||
if commitSHA == "" {
|
||||
commits, lerr := s.gitr.Log(ctx, dir, 1)
|
||||
if lerr != nil {
|
||||
return nil, errs.Wrap(lerr, errs.CodeGitCmdFailed, "resolve HEAD")
|
||||
}
|
||||
if len(commits) == 0 {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "empty repository: no commit to index")
|
||||
}
|
||||
commitSHA = commits[0].Hash
|
||||
}
|
||||
|
||||
// Idempotency: an existing run at this (ws, commit, model) is reused unless it
|
||||
// previously failed/stale (in which case we re-enqueue against the same row).
|
||||
existing, lerr := s.repo.GetRunByCommit(ctx, wsID, commitSHA, model)
|
||||
if lerr != nil {
|
||||
return nil, lerr
|
||||
}
|
||||
if existing != nil {
|
||||
switch existing.Status {
|
||||
case StatusPending, StatusRunning, StatusCompleted:
|
||||
return existing, nil
|
||||
}
|
||||
// failed/stale: fall through and create a fresh run is blocked by UNIQUE;
|
||||
// re-enqueue the existing run id instead.
|
||||
if err := s.enqueueJob(ctx, existing.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
run := &Run{
|
||||
ID: uuid.New(),
|
||||
WorkspaceID: wsID,
|
||||
WorktreeID: worktreeID,
|
||||
Branch: branch,
|
||||
CommitSHA: commitSHA,
|
||||
Status: StatusPending,
|
||||
EmbeddingEndpointID: &endpointID,
|
||||
EmbeddingModel: model,
|
||||
Dims: dims,
|
||||
}
|
||||
saved, err := s.repo.InsertRun(ctx, run)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.enqueueJob(ctx, saved.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return saved, nil
|
||||
}
|
||||
|
||||
func (s *service) enqueueJob(ctx context.Context, runID uuid.UUID) error {
|
||||
payload, _ := json.Marshal(buildPayload{RunID: runID})
|
||||
if _, err := s.jobs.Enqueue(ctx, JobTypeBuild, payload, time.Now(), 3); err != nil {
|
||||
return errs.Wrap(err, errs.CodeInternal, "enqueue codeindex.build")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) Search(ctx context.Context, wsID uuid.UUID, q SearchQuery) ([]Snippet, error) {
|
||||
emb, _, model, _, err := s.embedSel.Embedder(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if q.Text == "" {
|
||||
return nil, errs.New(errs.CodeInvalidInput, "query text required")
|
||||
}
|
||||
topK := q.TopK
|
||||
if topK <= 0 {
|
||||
topK = 10
|
||||
}
|
||||
if topK > s.maxTopK {
|
||||
topK = s.maxTopK
|
||||
}
|
||||
// Ensure there is a completed run before paying for an embedding call.
|
||||
latest, err := s.repo.GetLatestCompletedRun(ctx, wsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if latest == nil {
|
||||
return nil, errs.New(errs.CodeNotFound, "no completed code index for workspace; reindex first")
|
||||
}
|
||||
vecs, err := emb.Embed(ctx, model, []string{q.Text})
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err, errs.CodeInternal, "embed query")
|
||||
}
|
||||
if len(vecs) != 1 {
|
||||
return nil, errs.New(errs.CodeInternal, "embed query: unexpected vector count")
|
||||
}
|
||||
return s.repo.SearchKNN(ctx, wsID, vecs[0], topK, q.PathPrefix, q.Lang)
|
||||
}
|
||||
|
||||
func (s *service) MarkStale(ctx context.Context, wsID uuid.UUID) error {
|
||||
return s.repo.MarkStale(ctx, wsID)
|
||||
}
|
||||
Reference in New Issue
Block a user