Store workflow definitions as table rows and drop YAML
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Workflow files
|
||||
# Workflow definitions
|
||||
|
||||
A workflow's file is YAML: an optional `description`, then `steps`, which run in order. The workflow's name is not in the file. Unknown fields are refused.
|
||||
A workflow's definition is an object: an optional `description`, then `steps`, which run in order. The workflow's name is not in it. `save_workflow` takes it as its `definition` argument and `read_workflow` returns it, both as JSON. Unknown fields are refused.
|
||||
|
||||
Any workflow can run on a brief, each stage as its own task, and any workflow whose stages are single agents can be a project's review workflow (set with `set_review_workflow`), which reviews its tasks' snapshots: each stage is then one reviewer agent on its `model`, given the task's change and its stage's `prompt` (what to look for), and a stage after others also gets their reviews. A project can't review with a workflow that has a team stage (`agents`).
|
||||
|
||||
@@ -22,26 +22,19 @@ A stage gets the outputs of the stages that end the step before it: the last sta
|
||||
|
||||
Where a stage's task branches from follows from the shape. A stage after exactly one stage starts from the commit that stage finished at. A stage after a `parallel` starts from the default branch and is told the branches it can merge. A stage with nothing before it starts where the workflow was started. A workflow is finished once every stage has finished; when it ends with a `parallel`, each branch's last stage keeps its task to merge, and the run's other stage tasks are discarded. A review is delivered to the task's agent, every stage's findings in one message, once every stage has reviewed. An `order` or `parallel` can't be empty.
|
||||
|
||||
```yaml
|
||||
description: Build a change, review and test it in parallel, then fix what they found.
|
||||
steps:
|
||||
- stage: Build
|
||||
model: claude-opus-5-5
|
||||
prompt: Implement the brief.
|
||||
- parallel:
|
||||
- stage: Review
|
||||
model: gpt-6.1-sol
|
||||
prompt: Review the Build stage's work and list the problems.
|
||||
- stage: Test
|
||||
model: gemini-4-argon-high
|
||||
prompt: Exercise the change and report what fails.
|
||||
- stage: Fix
|
||||
agents:
|
||||
- name: fixer
|
||||
model: claude-opus-5-5
|
||||
prompt: Fix every problem the reports list.
|
||||
- name: checker
|
||||
model: gpt-6.1-sol
|
||||
prompt: Check each fix as the fixer lands it.
|
||||
prompt: Merge the branches you are given and address both reports.
|
||||
```json
|
||||
{
|
||||
"description": "Build a change, review and test it in parallel, then fix what they found.",
|
||||
"steps": [
|
||||
{"stage": "Build", "model": "claude-opus-5-5", "prompt": "Implement the brief."},
|
||||
{"parallel": [
|
||||
{"stage": "Review", "model": "gpt-6.1-sol", "prompt": "Review the Build stage's work and list the problems."},
|
||||
{"stage": "Test", "model": "gemini-4-argon-high", "prompt": "Exercise the change and report what fails."}
|
||||
]},
|
||||
{"stage": "Fix", "prompt": "Merge the branches you are given and address both reports.", "agents": [
|
||||
{"name": "fixer", "model": "claude-opus-5-5", "prompt": "Fix every problem the reports list."},
|
||||
{"name": "checker", "model": "gpt-6.1-sol", "prompt": "Check each fix as the fixer lands it."}
|
||||
]}
|
||||
]
|
||||
}
|
||||
```
|
||||
+12
-12
@@ -122,7 +122,7 @@ func (m *Manager) reviewsSettled(ctx context.Context, reviews []api.Review) (boo
|
||||
if r.State != api.ReviewStateDone {
|
||||
continue
|
||||
}
|
||||
def, err := m.store.ReviewDefinition(ctx, r.Id)
|
||||
_, def, err := m.store.ReviewDefinition(ctx, r.Id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -192,12 +192,12 @@ func (m *Manager) startReviews(ctx context.Context, task api.Task, snap api.Snap
|
||||
now := time.Now().UTC()
|
||||
if len(changes.Files) == 0 {
|
||||
slog.Info("review skipped, snapshot has no diff", "task", task.Id, "snapshot", snap.N)
|
||||
return m.store.CreateReview(ctx, api.Review{Id: store.NewID(), TaskId: task.Id, SnapshotN: snap.N, State: api.ReviewStateSkipped, CreatedAt: now, FinishedAt: &now}, nil)
|
||||
return m.store.CreateReview(ctx, api.Review{Id: store.NewID(), TaskId: task.Id, SnapshotN: snap.N, State: api.ReviewStateSkipped, CreatedAt: now, FinishedAt: &now}, "")
|
||||
}
|
||||
|
||||
def, err := m.store.GetWorkflow(ctx, project.ReviewWorkflow)
|
||||
frozen, err := m.store.FreezeWorkflow(ctx, project.ReviewWorkflow)
|
||||
if err == nil {
|
||||
err = reviewProblem(def)
|
||||
err = reviewProblem(frozen.Definition)
|
||||
}
|
||||
if err != nil {
|
||||
return m.store.CreateReview(ctx, api.Review{
|
||||
@@ -209,11 +209,11 @@ func (m *Manager) startReviews(ctx context.Context, task api.Task, snap api.Snap
|
||||
Error: fmt.Sprintf("project %s reviews with workflow %q, which can't run: %v", project.Name, project.ReviewWorkflow, err),
|
||||
CreatedAt: now,
|
||||
FinishedAt: &now,
|
||||
}, nil)
|
||||
}, "")
|
||||
}
|
||||
|
||||
for _, stage := range workflow.Starts(def) {
|
||||
err = m.startReviewer(ctx, task, project, snap, def, stage, nil)
|
||||
for _, stage := range workflow.Starts(frozen.Definition) {
|
||||
err = m.startReviewer(ctx, task, project, snap, frozen.DefinitionID, frozen.Definition, stage, nil)
|
||||
if err != nil {
|
||||
slog.Error("start review stage", "task", task.Id, "snapshot", snap.N, "stage", stage, "err", err)
|
||||
}
|
||||
@@ -228,7 +228,7 @@ func (m *Manager) advanceReviews(ctx context.Context, r api.Review) bool {
|
||||
m.reviewMu.Lock()
|
||||
defer m.reviewMu.Unlock()
|
||||
|
||||
def, err := m.store.ReviewDefinition(ctx, r.Id)
|
||||
defID, def, err := m.store.ReviewDefinition(ctx, r.Id)
|
||||
if err != nil {
|
||||
slog.Error("load review definition", "review", r.Id, "err", err)
|
||||
return false
|
||||
@@ -274,7 +274,7 @@ func (m *Manager) advanceReviews(ctx context.Context, r api.Review) bool {
|
||||
if len(inputs) != len(workflow.After(def, next)) {
|
||||
continue
|
||||
}
|
||||
err = m.startReviewer(ctx, task, project, snap, def, next, inputs)
|
||||
err = m.startReviewer(ctx, task, project, snap, defID, def, next, inputs)
|
||||
if err != nil {
|
||||
slog.Error("start review stage", "task", task.Id, "snapshot", r.SnapshotN, "stage", next, "err", err)
|
||||
continue
|
||||
@@ -284,7 +284,7 @@ func (m *Manager) advanceReviews(ctx context.Context, r api.Review) bool {
|
||||
return started
|
||||
}
|
||||
|
||||
func (m *Manager) startReviewer(ctx context.Context, task api.Task, project api.Project, snap api.Snapshot, def api.WorkflowDefinition, stage string, inputs []api.Review) (err error) {
|
||||
func (m *Manager) startReviewer(ctx context.Context, task api.Task, project api.Project, snap api.Snapshot, defID string, def api.WorkflowDefinition, stage string, inputs []api.Review) (err error) {
|
||||
model := workflow.Get(def, stage).Model
|
||||
vendor := workflow.VendorOf(model)
|
||||
reviewID := store.NewID()
|
||||
@@ -305,7 +305,7 @@ func (m *Manager) startReviewer(ctx context.Context, task api.Task, project api.
|
||||
Error: "the reviewer didn't start: " + err.Error(),
|
||||
CreatedAt: now,
|
||||
FinishedAt: &now,
|
||||
}, &def)
|
||||
}, defID)
|
||||
if rerr != nil {
|
||||
slog.Error("record review that didn't start", "task", task.Id, "stage", stage, "err", rerr)
|
||||
return
|
||||
@@ -356,7 +356,7 @@ func (m *Manager) startReviewer(ctx context.Context, task api.Task, project api.
|
||||
Model: model,
|
||||
State: api.ReviewStateRunning,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}, &def)
|
||||
}, defID)
|
||||
if err != nil {
|
||||
m.endReviewer(ctx, sess.Id, project, reviewID)
|
||||
return err
|
||||
|
||||
@@ -15,12 +15,12 @@ import (
|
||||
"g.fc.run/ai/byre/internal/workflow"
|
||||
)
|
||||
|
||||
func (m *Manager) SaveWorkflow(ctx context.Context, name, from string, file []byte, actor string) (api.WorkflowSummary, error) {
|
||||
func (m *Manager) SaveWorkflow(ctx context.Context, name, from string, definition []byte, actor string) (api.WorkflowSummary, error) {
|
||||
name, err := workflow.CleanName(name)
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
def, err := workflow.Parse(file)
|
||||
def, err := workflow.Parse(definition)
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
@@ -58,24 +58,12 @@ func reviewProblem(def api.WorkflowDefinition) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) WorkflowFile(ctx context.Context, name string) ([]byte, error) {
|
||||
def, err := m.store.GetWorkflow(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workflow.Format(def)
|
||||
}
|
||||
|
||||
func (m *Manager) TaskWorkflow(ctx context.Context, taskID string) (api.TaskWorkflow, error) {
|
||||
frozen, err := m.store.TaskWorkflow(ctx, taskID)
|
||||
if err != nil {
|
||||
return api.TaskWorkflow{}, err
|
||||
}
|
||||
file, err := workflow.Format(frozen.Definition)
|
||||
if err != nil {
|
||||
return api.TaskWorkflow{}, err
|
||||
}
|
||||
return api.TaskWorkflow{Name: frozen.Name, Definition: string(file)}, nil
|
||||
return api.TaskWorkflow{Name: frozen.Name, Definition: frozen.Definition}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) WorkflowRun(ctx context.Context, taskID string) (api.WorkflowRun, error) {
|
||||
@@ -719,7 +707,48 @@ func stageGuidance(frozen store.FrozenWorkflow, stage string) string {
|
||||
"%s. Until you call it, the workflow waits on this stage.", stage, frozen.Name, then)
|
||||
}
|
||||
|
||||
const workflowFileHelp = "The file's YAML format is the read_knowledge article workflow-files."
|
||||
const workflowHelp = "The definition's fields are the read_knowledge article workflow-definitions."
|
||||
|
||||
const workflowDefinitionSchema = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {"type": "string"},
|
||||
"steps": {"type": "array", "items": {"$ref": "#/$defs/step"}}
|
||||
},
|
||||
"required": ["steps"],
|
||||
"additionalProperties": false
|
||||
}`
|
||||
|
||||
const workflowSchemaDefs = `"$defs": {
|
||||
"step": {
|
||||
"type": "object",
|
||||
"description": "Exactly one of stage (with model or agents, and prompt), order or parallel.",
|
||||
"properties": {
|
||||
"stage": {"type": "string"},
|
||||
"model": {"type": "string"},
|
||||
"prompt": {"type": "string"},
|
||||
"agents": {"type": "array", "items": {"$ref": "#/$defs/agent"}},
|
||||
"order": {"type": "array", "items": {"$ref": "#/$defs/step"}},
|
||||
"parallel": {"type": "array", "items": {"$ref": "#/$defs/step"}}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"agent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"model": {"type": "string"},
|
||||
"prompt": {"type": "string"}
|
||||
},
|
||||
"required": ["name", "model", "prompt"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}`
|
||||
|
||||
func formatDefinition(def api.WorkflowDefinition) (string, error) {
|
||||
b, err := json.MarshalIndent(def, "", " ")
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
func (m *Manager) workflowTools(tool func(name, description, schema string, run func(ctx context.Context, user api.User, raw json.RawMessage) (string, error)) mcp.Tool) []mcp.Tool {
|
||||
return []mcp.Tool{
|
||||
@@ -746,7 +775,7 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
|
||||
}),
|
||||
|
||||
tool("read_workflow",
|
||||
"Read a workflow's file: with name, the workflow on the server; with task_id, the copy that task froze when it was created. "+workflowFileHelp,
|
||||
"Read a workflow's definition: with name, the workflow on the server; with task_id, the copy that task froze when it was created. "+workflowHelp,
|
||||
`{"type": "object", "properties": {
|
||||
"name": {"type": "string"},
|
||||
"task_id": {"type": "string"}
|
||||
@@ -764,33 +793,40 @@ func (m *Manager) workflowTools(tool func(name, description, schema string, run
|
||||
return "", fmt.Errorf("%w: give exactly one of name and task_id", ErrInvalid)
|
||||
}
|
||||
if args.TaskID != "" {
|
||||
tw, err := m.TaskWorkflow(ctx, args.TaskID)
|
||||
frozen, err := m.store.TaskWorkflow(ctx, args.TaskID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Task %s froze workflow %q:\n\n%s", args.TaskID, tw.Name, tw.Definition), nil
|
||||
text, err := formatDefinition(frozen.Definition)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Task %s froze workflow %q:\n\n%s", args.TaskID, frozen.Name, text), nil
|
||||
}
|
||||
file, err := m.WorkflowFile(ctx, args.Name)
|
||||
return string(file), err
|
||||
def, err := m.store.GetWorkflow(ctx, args.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return formatDefinition(def)
|
||||
}),
|
||||
|
||||
tool("save_workflow",
|
||||
"Create or replace a workflow on the server from its file. Tasks that already froze it keep their copy. "+
|
||||
"The name is one line of 1 to 100 characters, shown to the human on buttons and pickers. "+workflowFileHelp,
|
||||
"Create or replace a workflow on the server from its definition. Tasks that already froze it keep their copy. "+
|
||||
"The name is one line of 1 to 100 characters, shown to the human on buttons and pickers. "+workflowHelp,
|
||||
`{"type": "object", "properties": {
|
||||
"name": {"type": "string"},
|
||||
"definition": {"type": "string", "description": "The workflow's file, as YAML."}
|
||||
}, "required": ["name", "definition"]}`,
|
||||
"definition": `+workflowDefinitionSchema+`
|
||||
}, "required": ["name", "definition"], `+workflowSchemaDefs+`}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
Name string `json:"name"`
|
||||
Definition string `json:"definition"`
|
||||
Name string `json:"name"`
|
||||
Definition json.RawMessage `json:"definition"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
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, "", args.Definition, leadActor(user.Login))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user