Drop workflow kinds, deliver reviews when done, rename/delete workflows
This commit is contained in:
@@ -15,7 +15,7 @@ import (
|
||||
"g.fc.run/ai/byre/internal/workflow"
|
||||
)
|
||||
|
||||
func (m *Manager) SaveWorkflow(ctx context.Context, name string, file []byte, actor string) (api.WorkflowSummary, error) {
|
||||
func (m *Manager) SaveWorkflow(ctx context.Context, name, from string, file []byte, actor string) (api.WorkflowSummary, error) {
|
||||
name, err := workflow.CleanName(name)
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
@@ -24,15 +24,14 @@ func (m *Manager) SaveWorkflow(ctx context.Context, name string, file []byte, ac
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
err = m.store.PutWorkflow(ctx, name, def, actor)
|
||||
err = m.store.PutWorkflow(ctx, name, from, def, actor)
|
||||
if errors.Is(err, store.ErrWorkflowExists) {
|
||||
return api.WorkflowSummary{}, fmt.Errorf("%w: %w", ErrConflict, err)
|
||||
}
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
kind := api.WorkflowSummaryKindTask
|
||||
if def.Kind == api.WorkflowKindReview {
|
||||
kind = api.WorkflowSummaryKindReview
|
||||
}
|
||||
return api.WorkflowSummary{Name: name, Description: def.Description, Kind: kind}, nil
|
||||
return api.WorkflowSummary{Name: name, Description: def.Description}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetReviewWorkflow(ctx context.Context, projectID, name, actor string) (api.Project, error) {
|
||||
@@ -42,13 +41,23 @@ func (m *Manager) SetReviewWorkflow(ctx context.Context, projectID, name, actor
|
||||
if err != nil {
|
||||
return api.Project{}, err
|
||||
}
|
||||
if def.Kind != api.WorkflowKindReview {
|
||||
return api.Project{}, fmt.Errorf("%w: workflow %q is a task workflow; a project reviews with a workflow of kind review", ErrInvalid, name)
|
||||
err = reviewProblem(def)
|
||||
if err != nil {
|
||||
return api.Project{}, fmt.Errorf("%w: workflow %q can't review: %w", ErrInvalid, name, err)
|
||||
}
|
||||
}
|
||||
return m.store.SetReviewWorkflow(ctx, projectID, name, actor)
|
||||
}
|
||||
|
||||
func reviewProblem(def api.WorkflowDefinition) error {
|
||||
for _, s := range workflow.Stages(def) {
|
||||
if s.Agents != nil {
|
||||
return fmt.Errorf("stage %q is a team, and each review stage is one reviewer", s.Stage)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) WorkflowFile(ctx context.Context, name string) ([]byte, error) {
|
||||
def, err := m.store.GetWorkflow(ctx, name)
|
||||
if err != nil {
|
||||
@@ -230,9 +239,6 @@ func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creato
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if frozen.Definition.Kind == api.WorkflowKindReview {
|
||||
return nil, fmt.Errorf("%w: workflow %q is a review workflow, which a project names to review its snapshots; it doesn't run on a brief", ErrInvalid, name)
|
||||
}
|
||||
frozen.Brief = brief
|
||||
frozen.Title = title
|
||||
|
||||
@@ -405,11 +411,40 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er
|
||||
|
||||
next := workflow.Next(frozen.Definition, *task.WorkflowStage)
|
||||
if len(next) == 0 {
|
||||
err = m.discardStages(ctx, task)
|
||||
finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, m.store.FinishWorkflow(ctx, task, sess.Id, frozen.Name, output)
|
||||
if finished {
|
||||
return nil, m.store.ClearStage(ctx, sess.Id)
|
||||
}
|
||||
tasks, err := m.store.RunTasks(ctx, *task.WorkflowRun)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keep := map[string]bool{}
|
||||
kept := []string{}
|
||||
outputs := []string{}
|
||||
for _, end := range workflow.Ends(frozen.Definition) {
|
||||
p, found, err := m.finishedStage(ctx, tasks, end)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, m.store.ClearStage(ctx, sess.Id)
|
||||
}
|
||||
keep[p.task.Id] = true
|
||||
kept = append(kept, p.task.Id)
|
||||
outputs = append(outputs, fmt.Sprintf("## %s\n\n%s", end, strings.TrimSpace(p.output)))
|
||||
}
|
||||
if len(outputs) > 1 {
|
||||
output = strings.Join(outputs, "\n\n")
|
||||
}
|
||||
err = m.discardStages(ctx, tasks, keep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, m.store.FinishWorkflow(ctx, task, sess.Id, frozen.Name, output, kept)
|
||||
}
|
||||
|
||||
started := []string{}
|
||||
@@ -555,16 +590,12 @@ func teamMember(members []api.TeamMember, name string) (api.TeamMember, error) {
|
||||
return api.TeamMember{}, fmt.Errorf("the stage's frozen workflow has no agent %q", name)
|
||||
}
|
||||
|
||||
func (m *Manager) discardStages(ctx context.Context, final api.Task) error {
|
||||
tasks, err := m.store.RunTasks(ctx, *final.WorkflowRun)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (m *Manager) discardStages(ctx context.Context, tasks []api.Task, keep map[string]bool) error {
|
||||
for _, t := range tasks {
|
||||
if t.Id == final.Id || t.Status != api.TaskStatusActive {
|
||||
if keep[t.Id] || t.Status != api.TaskStatusActive {
|
||||
continue
|
||||
}
|
||||
_, err = m.Discard(ctx, t, "byre")
|
||||
_, err := m.Discard(ctx, t, "byre")
|
||||
if errors.Is(err, ErrConflict) {
|
||||
slog.Info("stage task not discarded", "task", t.Id, "err", err)
|
||||
continue
|
||||
@@ -599,12 +630,12 @@ func (m *Manager) finishStageTool(l *live) mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "finish_stage",
|
||||
Description: "Finish this task's stage of its workflow. Call it once the stage's work is done; when this turn ends, byre hands the output to the stages that come after this one, " +
|
||||
"each of which starts in its own task once every stage it comes after has finished (after the final stage, the workflow is finished). Calling it again in the same turn replaces the output. " +
|
||||
"each of which starts in its own task once every stage it comes after has finished (once every stage has finished, the workflow is finished). Calling it again in the same turn replaces the output. " +
|
||||
"The call counts only for the turn that makes it: if that turn ends in an error or is interrupted, it is discarded, and you call it again once the stage's work is done.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"output": {"type": "string", "description": "What the stages after this one start from: everything they need from this stage, since they see nothing else of your context. For the final stage, what the workflow produced."}
|
||||
"output": {"type": "string", "description": "What the stages after this one start from: everything they need from this stage, since they see nothing else of your context. For a last stage, what this branch of the workflow produced."}
|
||||
},
|
||||
"required": ["output"]
|
||||
}`),
|
||||
@@ -636,7 +667,7 @@ func (m *Manager) finishStageTool(l *live) mcp.Tool {
|
||||
}
|
||||
next := workflow.Next(frozen.Definition, l.stage)
|
||||
if len(next) == 0 {
|
||||
return "This is the workflow's final stage: it finishes when this turn ends, and byre discards the workflow's other stage tasks. End your turn now with your summary.", nil
|
||||
return lastStageText(frozen.Definition) + " End your turn now with your summary.", nil
|
||||
}
|
||||
return fmt.Sprintf("When this turn ends, byre hands the output to %s. End your turn now with your summary.", quoteList(next)), nil
|
||||
},
|
||||
@@ -659,9 +690,17 @@ func quoteList(names []string) string {
|
||||
return strings.Join(quoted, ", ")
|
||||
}
|
||||
|
||||
func lastStageText(def api.WorkflowDefinition) string {
|
||||
ends := workflow.Ends(def)
|
||||
if len(ends) == 1 {
|
||||
return "This is the workflow's last stage: the workflow finishes when this turn ends, and byre discards its other stage tasks."
|
||||
}
|
||||
return fmt.Sprintf("This is one of the workflow's last stages (%s): the workflow finishes once all of them have, and byre then discards its other stage tasks, keeping each last stage's task.", quoteList(ends))
|
||||
}
|
||||
|
||||
func stageGuidance(frozen store.FrozenWorkflow, stage string) string {
|
||||
next := workflow.Next(frozen.Definition, stage)
|
||||
then := "it is the workflow's final stage, so the workflow then finishes and byre discards its other stage tasks"
|
||||
then := strings.TrimSuffix(lastStageText(frozen.Definition), ".")
|
||||
if len(next) > 0 {
|
||||
then = fmt.Sprintf("byre then hands the output to %s, each in its own task, which gets the output and nothing else of your context", quoteList(next))
|
||||
}
|
||||
@@ -740,7 +779,7 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
w, err := m.SaveWorkflow(ctx, args.Name, []byte(args.Definition), leadActor(user.Login))
|
||||
w, err := m.SaveWorkflow(ctx, args.Name, "", []byte(args.Definition), leadActor(user.Login))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -748,7 +787,7 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
|
||||
}),
|
||||
|
||||
tool("set_review_workflow",
|
||||
"Set the review workflow (a saved workflow of kind review) whose stages review every snapshot of a project's tasks, from the next snapshot on; an empty workflow turns reviews off for the project. Only when the human asks.",
|
||||
"Set the review workflow (any saved workflow whose stages are single agents, not teams) whose stages review every snapshot of a project's tasks, from the next snapshot on; an empty workflow turns reviews off for the project. Only when the human asks.",
|
||||
`{"type": "object", "properties": {
|
||||
"project_id": {"type": "string"},
|
||||
"workflow": {"type": "string"}
|
||||
|
||||
Reference in New Issue
Block a user