package store import ( "context" "database/sql" "errors" "fmt" "g.fc.run/ai/byre/internal/api" ) type FrozenWorkflow struct { Name string DefinitionID string Definition api.WorkflowDefinition Brief string Title string Audit string } func (s *Store) ListWorkflows(ctx context.Context) ([]api.WorkflowSummary, error) { rows, err := s.db.QueryContext(ctx, `SELECT w.name, d.description FROM workflows w JOIN workflow_definitions d ON d.id = w.definition_id ORDER BY w.name`) if err != nil { return nil, err } defer rows.Close() workflows := []api.WorkflowSummary{} for rows.Next() { w := api.WorkflowSummary{} err = rows.Scan(&w.Name, &w.Description) if err != nil { return nil, err } workflows = append(workflows, w) } return workflows, rows.Err() } func (s *Store) GetWorkflow(ctx context.Context, name string) (api.WorkflowDefinition, error) { f, err := s.FreezeWorkflow(ctx, name) return f.Definition, err } var ErrWorkflowExists = errors.New("workflow exists") func (s *Store) PutWorkflow(ctx context.Context, name, from string, def api.WorkflowDefinition, actor string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { if from != "" { found := false err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM workflows WHERE name = ?)`, from).Scan(&found) if err != nil { return err } if !found { return fmt.Errorf("%w: no workflow %q", ErrNotFound, from) } } if from != "" && from != name { taken := false err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM workflows WHERE name = ?)`, name).Scan(&taken) if err != nil { return err } if taken { return fmt.Errorf("%w: a workflow named %q already exists", ErrWorkflowExists, name) } _, err = tx.ExecContext(ctx, `DELETE FROM workflows WHERE name = ?`, from) if err != nil { return err } err = repointProjects(ctx, tx, emit, from, name, actor) if err != nil { return err } err = emit(api.Event{Kind: "workflow.deleted", Actor: actor, Payload: map[string]any{"name": from}}) if err != nil { return err } } defID, err := insertDefinition(ctx, tx, def) if err != nil { return err } _, err = tx.ExecContext(ctx, `INSERT INTO workflows (name, definition_id) VALUES (?, ?) ON CONFLICT (name) DO UPDATE SET definition_id = excluded.definition_id`, name, defID) if err != nil { return err } return emit(api.Event{Kind: "workflow.updated", Actor: actor, Payload: map[string]any{"name": name}}) }) } func repointProjects(ctx context.Context, tx *sql.Tx, emit emitFunc, from, to, actor string) error { rows, err := tx.QueryContext(ctx, ` UPDATE projects SET review_workflow = CASE WHEN review_workflow = ?1 THEN ?2 ELSE review_workflow END, audit_workflow = CASE WHEN audit_workflow = ?1 THEN ?2 ELSE audit_workflow END WHERE review_workflow = ?1 OR audit_workflow = ?1 RETURNING `+projectColumns, from, to) if err != nil { return err } projects := []api.Project{} for rows.Next() { p, err := scanProject(rows) if err != nil { rows.Close() return err } projects = append(projects, p) } err = errors.Join(rows.Err(), rows.Close()) if err != nil { return err } for _, p := range projects { err = emit(api.Event{Kind: "project.updated", ProjectId: &p.Id, Actor: actor, Payload: ToPayload(p)}) if err != nil { return err } } return nil } func (s *Store) DeleteWorkflow(ctx context.Context, name, actor string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { res, err := tx.ExecContext(ctx, `DELETE FROM workflows WHERE name = ?`, name) if err != nil { return err } n, err := res.RowsAffected() if err != nil { return err } if n == 0 { return fmt.Errorf("%w: no workflow %q", ErrNotFound, name) } err = repointProjects(ctx, tx, emit, name, "", actor) if err != nil { return err } return emit(api.Event{Kind: "workflow.deleted", Actor: actor, Payload: map[string]any{"name": name}}) }) } func (s *Store) FreezeWorkflow(ctx context.Context, name string) (FrozenWorkflow, error) { f := FrozenWorkflow{Name: name} err := s.db.QueryRowContext(ctx, `SELECT definition_id FROM workflows WHERE name = ?`, name).Scan(&f.DefinitionID) if errors.Is(err, sql.ErrNoRows) { return FrozenWorkflow{}, fmt.Errorf("%w: no workflow %q", ErrNotFound, name) } if err != nil { return FrozenWorkflow{}, err } f.Definition, err = loadDefinition(ctx, s.db, f.DefinitionID) return f, err } func (s *Store) TaskWorkflow(ctx context.Context, taskID string) (FrozenWorkflow, error) { f := FrozenWorkflow{} defID := sql.NullString{} err := s.db.QueryRowContext(ctx, `SELECT workflow_name, workflow_definition_id, workflow_brief, workflow_title, audit FROM tasks WHERE id = ?`, taskID).Scan(&f.Name, &defID, &f.Brief, &f.Title, &f.Audit) if errors.Is(err, sql.ErrNoRows) { return FrozenWorkflow{}, fmt.Errorf("%w: no task %s", ErrNotFound, taskID) } if err != nil { return FrozenWorkflow{}, err } if !defID.Valid { return FrozenWorkflow{}, fmt.Errorf("%w: task %s has no workflow", ErrNotFound, taskID) } f.DefinitionID = defID.String f.Definition, err = loadDefinition(ctx, s.db, f.DefinitionID) return f, err } func (s *Store) LastAudit(ctx context.Context, projectID string) (string, bool, error) { commit := "" active := false err := s.db.QueryRowContext(ctx, ` SELECT coalesce((SELECT audit FROM tasks WHERE project_id = ?1 AND audit != '' ORDER BY rowid DESC LIMIT 1), ''), EXISTS (SELECT 1 FROM tasks WHERE project_id = ?1 AND audit != '' AND status = ?2) `, projectID, string(api.TaskStatusActive)).Scan(&commit, &active) return commit, active, err } func (s *Store) RunTasks(ctx context.Context, run string) ([]api.Task, error) { return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE workflow_run = ? ORDER BY created_at`, run) } func (s *Store) RunEvents(ctx context.Context, run string) ([]api.Event, error) { rows, err := s.db.QueryContext(ctx, ` SELECT `+eventColumns+` FROM events WHERE task_id IN (SELECT id FROM tasks WHERE workflow_run = ?) AND kind IN ('task.workflow_stage', 'task.discarded', 'task.workflow_finished') ORDER BY seq `, run) if err != nil { return nil, err } return scanEvents(rows) } func (s *Store) RecordStage(ctx context.Context, taskID, output, commit string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { _, err := tx.ExecContext(ctx, `UPDATE tasks SET stage_output = ?, stage_commit = ? WHERE id = ?`, output, commit, taskID) if err != nil { return err } return settleTaskState(ctx, tx, emit, taskID, "byre") }) } func (s *Store) StageResult(ctx context.Context, taskID string) (string, string, error) { output := "" commit := "" err := s.db.QueryRowContext(ctx, `SELECT stage_output, stage_commit FROM tasks WHERE id = ?`, taskID).Scan(&output, &commit) return output, commit, err } func (s *Store) SetStageOutput(ctx context.Context, sessionID, output string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { _, err := tx.ExecContext(ctx, `UPDATE agent_sessions SET stage_output = ? WHERE id = ?`, output, sessionID) return err }) } func (s *Store) FinishStage(ctx context.Context, sessionID string) (bool, error) { finished := false err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { res, err := tx.ExecContext(ctx, `UPDATE agent_sessions SET stage_finished = 1 WHERE id = ? AND stage_output != '' AND NOT EXISTS (SELECT 1 FROM tasks WHERE tasks.id = agent_sessions.task_id AND tasks.discard_state != '')`, sessionID) if err != nil { return err } n, err := res.RowsAffected() finished = n > 0 return err }) return finished, err } func (s *Store) DropStageOutput(ctx context.Context, sessionID string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { _, err := tx.ExecContext(ctx, `UPDATE agent_sessions SET stage_output = '' WHERE id = ? AND stage_finished = 0`, sessionID) return err }) } func (s *Store) StageOutput(ctx context.Context, sessionID string) (string, error) { output := "" err := s.db.QueryRowContext(ctx, `SELECT stage_output FROM agent_sessions WHERE id = ?`, sessionID).Scan(&output) return output, err } func (s *Store) FinishedStages(ctx context.Context) ([]string, error) { rows, err := s.db.QueryContext(ctx, `SELECT id FROM agent_sessions WHERE stage_finished = 1 ORDER BY created_at`) if err != nil { return nil, err } defer rows.Close() ids := []string{} for rows.Next() { id := "" err = rows.Scan(&id) if err != nil { return nil, err } ids = append(ids, id) } return ids, rows.Err() } func (s *Store) ClearStage(ctx context.Context, sessionID string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { return clearStage(ctx, tx, sessionID) }) } func clearStage(ctx context.Context, tx *sql.Tx, sessionID string) error { _, err := tx.ExecContext(ctx, `UPDATE agent_sessions SET stage_output = '', stage_finished = 0 WHERE id = ?`, sessionID) return err } func (s *Store) FinishWorkflow(ctx context.Context, task api.Task, sessionID, workflowName, output string, kept []string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { err := clearStage(ctx, tx, sessionID) if err != nil { return err } return emit(api.Event{ Kind: "task.workflow_finished", ProjectId: &task.ProjectId, TaskId: &task.Id, Actor: "byre", Payload: map[string]any{"id": task.Id, "workflow": workflowName, "output": output, "kept": kept}, }) }) } func (s *Store) WorkflowFinished(ctx context.Context, run string) (bool, error) { finished := false err := s.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM events e JOIN tasks t ON t.id = e.task_id WHERE t.workflow_run = ? AND e.kind = 'task.workflow_finished')`, run).Scan(&finished) return finished, err }