Store workflow definitions as table rows and drop YAML
This commit is contained in:
+6
-13
@@ -1233,28 +1233,21 @@ paths:
|
||||
operationId: getWorkflow
|
||||
responses:
|
||||
"200":
|
||||
description: >
|
||||
The workflow's WorkflowDefinition: as JSON when the request accepts only application/json, else its file as
|
||||
YAML, as an attachment named after it.
|
||||
description: The workflow's definition.
|
||||
content:
|
||||
application/yaml:
|
||||
schema: { type: string }
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/WorkflowDefinition" }
|
||||
"404": { $ref: "#/components/responses/Error" }
|
||||
put:
|
||||
operationId: putWorkflow
|
||||
description: >
|
||||
Creates or replaces the workflow from its file, a WorkflowDefinition as YAML or JSON. The name is one line of
|
||||
1 to 100 characters. Tasks that already froze the workflow keep their copy. With from, it renames workflow
|
||||
Creates or replaces the workflow from its definition. The name is one line of 1 to 100 characters. Tasks that already froze the workflow keep their copy. With from, it renames workflow
|
||||
from to name, which must not be taken, and projects that review with from review with name instead.
|
||||
parameters:
|
||||
- { name: from, in: query, required: false, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/yaml:
|
||||
schema: { type: string }
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/WorkflowDefinition" }
|
||||
responses:
|
||||
@@ -1479,9 +1472,9 @@ components:
|
||||
WorkflowDefinition:
|
||||
type: object
|
||||
description: >
|
||||
A workflow's file, which holds everything but its name: its steps, run in order. Run on a brief, each stage
|
||||
runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a
|
||||
reviewer of a snapshot and gets the earlier stages' reviews. Sent and returned as YAML.
|
||||
A workflow's definition, which holds everything but its name: its steps, run in order. Run on a brief, each
|
||||
stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage
|
||||
is a reviewer of a snapshot and gets the earlier stages' reviews.
|
||||
required: [steps]
|
||||
properties:
|
||||
description: { type: string, x-go-type-skip-optional-pointer: true }
|
||||
@@ -1528,7 +1521,7 @@ components:
|
||||
required: [name, definition]
|
||||
properties:
|
||||
name: { type: string }
|
||||
definition: { type: string, description: The WorkflowDefinition as YAML. }
|
||||
definition: { $ref: "#/components/schemas/WorkflowDefinition" }
|
||||
|
||||
WorkflowRun:
|
||||
type: object
|
||||
|
||||
+10
-9
File diff suppressed because one or more lines are too long
@@ -6,8 +6,8 @@ require (
|
||||
github.com/coder/websocket v1.8.15
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/getkin/kin-openapi v0.142.0
|
||||
github.com/go-git/go-git/v5 v5.19.3
|
||||
github.com/oapi-codegen/runtime v1.7.0
|
||||
go.yaml.in/yaml/v3 v3.0.5
|
||||
modernc.org/sqlite v1.60.1
|
||||
tailscale.com v1.104.2
|
||||
)
|
||||
@@ -25,7 +25,6 @@ require (
|
||||
github.com/gaissmai/bart v0.29.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.9.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.19.3 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
@@ -60,6 +59,7 @@ require (
|
||||
github.com/tailscale/wireguard-go v0.0.0-20260928213032-417aef361226 // indirect
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
go4.org/netipx v0.0.0-20260823151212-3075585bcbeb // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
|
||||
@@ -227,12 +227,14 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
|
||||
github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
@@ -403,7 +405,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+167
-169
@@ -1404,9 +1404,9 @@ type TaskTab string
|
||||
|
||||
// TaskWorkflow The workflow a task froze when it was created, as it was then.
|
||||
type TaskWorkflow struct {
|
||||
// Definition The WorkflowDefinition as YAML.
|
||||
Definition string `json:"definition"`
|
||||
Name string `json:"name"`
|
||||
// Definition A workflow's definition, which holds everything but its name: its steps, run in order. Run on a brief, each stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a reviewer of a snapshot and gets the earlier stages' reviews.
|
||||
Definition WorkflowDefinition `json:"definition"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TeamMember defines model for TeamMember.
|
||||
@@ -1539,7 +1539,7 @@ type UserUpdate struct {
|
||||
GitName string `json:"git_name"`
|
||||
}
|
||||
|
||||
// WorkflowDefinition A workflow's file, which holds everything but its name: its steps, run in order. Run on a brief, each stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a reviewer of a snapshot and gets the earlier stages' reviews. Sent and returned as YAML.
|
||||
// WorkflowDefinition A workflow's definition, which holds everything but its name: its steps, run in order. Run on a brief, each stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a reviewer of a snapshot and gets the earlier stages' reviews.
|
||||
type WorkflowDefinition struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Steps []WorkflowStep `json:"steps"`
|
||||
@@ -1549,7 +1549,7 @@ type WorkflowDefinition struct {
|
||||
type WorkflowRun struct {
|
||||
Brief string `json:"brief"`
|
||||
|
||||
// Definition A workflow's file, which holds everything but its name: its steps, run in order. Run on a brief, each stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a reviewer of a snapshot and gets the earlier stages' reviews. Sent and returned as YAML.
|
||||
// Definition A workflow's definition, which holds everything but its name: its steps, run in order. Run on a brief, each stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a reviewer of a snapshot and gets the earlier stages' reviews.
|
||||
Definition WorkflowDefinition `json:"definition"`
|
||||
|
||||
// Finished Whether the final stage finished, which ends the run.
|
||||
@@ -4230,8 +4230,8 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
// const string: with thousands of chunks the chained `+` fold is several
|
||||
// times slower for the Go compiler than parsing a slice literal.
|
||||
var swaggerSpec = []string{
|
||||
"7L1rcxs5lij4VxDcjbA9k6Jc09V3d6TYD267Hp4tV3kt11bc7a7gQEyQRCsJZAFIySqH97ffOA8gkUkk",
|
||||
"ScmWyz09X2wqE4nHwcHBeZ/3s6XdttYoE/zs7P3MKd9a4xX+8Y1z1sGPpTVBmQA/Zds2eimDtub0794a",
|
||||
"7L1rcxs5lij4VxDcjbA9k6Jc09V3d6TYD267Hr5brvJarq2I7a7gQEyQRCsJZAFIySqH97ffOA8gkUkk",
|
||||
"ScmWy909X2wqE4nHwcHBeZ/3s6XdttYoE/zs7P3MKd9a4xX+8Y1z1sGPpTVBmQA/Zds2eimDtub0794a",
|
||||
"eOaXG7WV8Ot/d2o1O5v9b6d9r6f01p9Sbx8+fKhmtfJLp1voZHZGw8xn8ILbQlfP1sqE184ulce/W2db",
|
||||
"5YKmqW2UbMIGfg378qFbXomtksYLKULnjNBeuM4YbdZCmloYK3xwSm6FulYmiI30Qjqnr1UtVtYJ7GEh",
|
||||
"V0G5hVdLa2p/LtQ7HVTN/YaNEi1NDDpfW6Pms2qmTLednf11Zq9mFc0DHuKHs1+rWbhtFU7QabOeVbN3",
|
||||
@@ -4246,7 +4246,7 @@ var swaggerSpec = []string{
|
||||
"cbOxXuEiaeG0v9rHiQIIZIScj3SiMJKzjdodKMLsTEiGX9+NeOyVEm/w6ZPznVG8aLQPMLVrRDvllbBd",
|
||||
"yEm/3raN2ioTlANyoGQ9q9KQu5dATv1SF3Wj4CO6sWYV3s+NOv4SySnLBfT+knrcef4mDbHz6nk/Jsyx",
|
||||
"u8St8Lvg/N7eiK00t5FCRCxPnwjpVLx+K3Epl1drZztTA2Z7oc2y6WpVC7xusQekFWEj040DT7d4SytT",
|
||||
"wwVUFe6YjNaPeIsMaRCp9qFMfysMO/nWNo298QKoE04SERx/RdS5kV4wBcaTeybWbTj5F2qJDAgcbaR/",
|
||||
"wwVUFe6YjNaPeIsMaRCp9qFMfysMO/nWNo298QKoE04SERx/RdS5kV4wBcaTeybWbTj5N2qJDAgcbaR/",
|
||||
"lVirrTa6fwvXngl67eS1DrcVMCbulukQDWVNTlNzlFviY8STWr2bVTPZ9zT7dXKRi+GtPFxvNlR2OnRd",
|
||||
"iSWcTiMub8XlrVPnwtKm4bpE2DiAMNwXLl+QWFpzrZyXIXZDR7kzQQ+hmNH3EvkYXc6jS5lIS3Y5EwUs",
|
||||
"rbi/spFKJMo9uEWr/BrOT0HxPg9BLjdbZpJHt7lkwBauDGXqBfBjZdZJb+W6QMTw8ZmoZZDi5zc/FHH5",
|
||||
@@ -4261,7 +4261,7 @@ var swaggerSpec = []string{
|
||||
"89ba5g32OJ/tYPXuEdyH+bgTBcEUBEwWh1gKQkZuq9wayLy/8kIC1hu1DKoWr+QSyHxtK4HSnL1WTpzK",
|
||||
"Vp8ucSB/ym1hwiOK6NbdNvK4HwOd1FEBKMjMb6Wpp5AK2ESU+373GyFhcdnexutuY7dK1NqpZbDuthK6",
|
||||
"l8Ciugd47LaIAmVygoi2S09AVo405Vy00gFQ48haEXPPxDNH7c7Ab1ULWK4OgtYPiK2XG2wBOwVtPC2X",
|
||||
"VVVwDnAtcO0urVnpNcrLHhl4fNGu50S7xGPbKtOuW0CJd39++u9PKrGUSBUkdyCbxvOdf5De1UUE7NrG",
|
||||
"VVVwDnAtcO0urVnpNcrLHhl4fNGu50S7xGPbKtOuW0CJd39++p9PKrGUSBUkdyCbxvOdf5De1UUE7NrG",
|
||||
"yvqMTyQKOG650dcAHGuUUCa4W6FM7UXXDvkOvPLhfOMC///Tc3GzkQE4e6FRenQKfjjVNnKp6ruoGXSt",
|
||||
"TGGyPfwJ7AFJkRd/m/0ot0r8rXv69E9LtZW6wZ/qb7MKTgt8Bs0SWX8ll48GWwCyl7Gi88rNmWpdqdsy",
|
||||
"zWxznhOQelYxFGfVrLY3hn+mC2xWZdg3q+hEzSpcTZH9LHOOsetKpJ4rUUJr8di6CvGp/wSfW5dQ+/YJ",
|
||||
@@ -4289,7 +4289,7 @@ var swaggerSpec = []string{
|
||||
"9lvtvarJ7NKoVQBLS8S4V8qtlbiRDpSjOYOKAxUZweNggh/nIGGfnHx5P7c1L3KIB8m3aN8tM4DTzlWI",
|
||||
"T4vgBV6ggPyD2zpzqhsoI3aJEH9HdFqvN0G00ijhN/bGn+fbExuSxoOudGIwQtkH9PIQAN5Kf/VWXu71",
|
||||
"InrLE5iY3846D9F7mFYJqi/NpX33MqiiDyTqEDSwaoaxVPorVWdaBVRIwxrwz4Iu8qF4zomuMr5uuBg0",
|
||||
"Kr0L4vEr6a5ALdybgZYbq5foGoZuytKL/zwRj8UTvJD+U5zin/8S/2z18urEGhQh//NE/FX8mrf767tf",
|
||||
"Kr0L4vEr6a5ALdybgZYbq5foGoZuytKL/zoRj8UTvJD+S5zin/8W/2z18urEGhQh/+tE/FX8mrf767tf",
|
||||
"83bgp4YufMh+PakSRCthcWZePFZyueE5oGfmu4AGNutq5Z5UYts1QbdgRQiuI1/X2PWTSjgFahB0UxOP",
|
||||
"YTFb6WCHeE2PsL8nlZDJWcjP/2ZKdsIDXK9T3jbXd9yk9NGEK8XA9f8oDhXRNd1DBU71KAqEvTD9uQOb",
|
||||
"jlqoDE79pwXhKmfgo+vXwLurX/zkuaSF1qX79qMAxwpxYOy1eUmffzWG5cQdeHC6Jf4gQ7+jZ5z5txX2",
|
||||
@@ -4358,7 +4358,7 @@ var swaggerSpec = []string{
|
||||
"BFBjuRqcpXNfhB5NhKLOpokIgwdd+2KFMo4VMf3+RGtYSgPDFEebddpRa5YxsA66xNPiRSxlaAAZtBcb",
|
||||
"1dyBMNEakhKmgGzwKmHyAKC0sq28UrFSACImftCzG9HlK/i0YHYRCYts9EkIH6AfPXhz6sF3Xa8v1MGP",
|
||||
"i2ZW0QyHz4lvhW+j2oQWiw3bDn+wFt36aIvC2KZJJAUGHmj7gszG+1bBl+yKberdJeMEu5BSByMkijSM",
|
||||
"X/KS+ZrSYVqnTh8sDicFIv6dNpCJfU93ou6a5nonsgPC+AIu7SkdtfMDXiIGFMCzFjOSP05dwBFNf/wr",
|
||||
"X/KS+ZrSYVqnTh8sDicFIv6dNpCJfU93ou6a5nonsgPC+AIu7SkdtfMDXiIGFMCzFjOSP05dwBFNf/w7",
|
||||
"FEtoJfp4Meiit1stgnJb1MlKL17/9ObtXn3KwaKeu9O25AoZ1OBmAc73SrUBtBkBFe0oMrNtXm45Xzyl",
|
||||
"X4apkTs6uQ7RIsRjYirn1DvJVuRx4QXn9YHWT+aiB1+vYTsrsS5ETOhWPRf9VX0O/CHK3lEMQknBoJdb",
|
||||
"4g/PI21KUW+vIpcYcXmMu9pzpcobeUufqwWQqjSQT0eZTc+JXMQOcwIZuxjcOMPJ7N5IfdfwPz0877UN",
|
||||
@@ -4377,164 +4377,162 @@ var swaggerSpec = []string{
|
||||
"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",
|
||||
"8KkO4Oerw7FD3CcU5AEDq0aM1xejovvw4X8NAA==",
|
||||
"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",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
@@ -4,14 +4,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
"g.fc.run/ai/byre/internal/workflow"
|
||||
)
|
||||
|
||||
const maxWorkflowFileBytes = 1 << 20
|
||||
const maxWorkflowBytes = 1 << 20
|
||||
|
||||
func (s *Server) ListWorkflows(w http.ResponseWriter, r *http.Request) {
|
||||
workflows, err := s.store.ListWorkflows(r.Context())
|
||||
@@ -23,38 +20,25 @@ func (s *Server) ListWorkflows(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) GetWorkflow(w http.ResponseWriter, r *http.Request, name string) {
|
||||
accept := r.Header.Get("Accept")
|
||||
if strings.Contains(accept, "application/json") && !strings.Contains(accept, "yaml") {
|
||||
def, err := s.store.GetWorkflow(r.Context(), name)
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, def)
|
||||
return
|
||||
}
|
||||
file, err := s.agents.WorkflowFile(r.Context(), name)
|
||||
def, err := s.store.GetWorkflow(r.Context(), name)
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/yaml")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(workflow.Filename(name)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(file)
|
||||
writeJSON(w, http.StatusOK, def)
|
||||
}
|
||||
|
||||
func (s *Server) PutWorkflow(w http.ResponseWriter, r *http.Request, name string, params api.PutWorkflowParams) {
|
||||
file, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxWorkflowFileBytes))
|
||||
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxWorkflowBytes))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, fmt.Errorf("read workflow file: %w", err))
|
||||
writeError(w, http.StatusBadRequest, fmt.Errorf("read workflow: %w", err))
|
||||
return
|
||||
}
|
||||
from := ""
|
||||
if params.From != nil {
|
||||
from = *params.From
|
||||
}
|
||||
summary, err := s.agents.SaveWorkflow(r.Context(), name, from, file, actor(r))
|
||||
summary, err := s.agents.SaveWorkflow(r.Context(), name, from, body, actor(r))
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
)
|
||||
|
||||
func insertDefinition(ctx context.Context, tx *sql.Tx, def api.WorkflowDefinition) (string, error) {
|
||||
id := NewID()
|
||||
_, err := tx.ExecContext(ctx, `INSERT INTO workflow_definitions (id, description) VALUES (?, ?)`, id, def.Description)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
next := 0
|
||||
var insert func(steps []api.WorkflowStep, parent *int) error
|
||||
insert = func(steps []api.WorkflowStep, parent *int) error {
|
||||
for pos, s := range steps {
|
||||
stepID := next
|
||||
next++
|
||||
kind, children := "stage", (*[]api.WorkflowStep)(nil)
|
||||
switch {
|
||||
case s.Order != nil:
|
||||
kind, children = "order", s.Order
|
||||
case s.Parallel != nil:
|
||||
kind, children = "parallel", s.Parallel
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, `INSERT INTO workflow_steps (definition_id, id, parent, position, kind, stage, model, prompt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, stepID, parent, pos, kind, s.Stage, s.Model, s.Prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Agents != nil {
|
||||
for i, a := range *s.Agents {
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO workflow_agents (definition_id, step_id, position, name, model, prompt) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
id, stepID, i, a.Name, a.Model, a.Prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if children != nil {
|
||||
err = insert(*children, &stepID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return id, insert(def.Steps, nil)
|
||||
}
|
||||
|
||||
type stepRow struct {
|
||||
id int
|
||||
kind string
|
||||
step api.WorkflowStep
|
||||
}
|
||||
|
||||
func loadDefinition(ctx context.Context, q querier, id string) (api.WorkflowDefinition, error) {
|
||||
def := api.WorkflowDefinition{}
|
||||
err := q.QueryRowContext(ctx, `SELECT description FROM workflow_definitions WHERE id = ?`, id).Scan(&def.Description)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return def, fmt.Errorf("%w: no workflow definition %s", ErrNotFound, id)
|
||||
}
|
||||
if err != nil {
|
||||
return def, err
|
||||
}
|
||||
|
||||
agents := map[int][]api.TeamMember{}
|
||||
rows, err := q.QueryContext(ctx, `SELECT step_id, name, model, prompt FROM workflow_agents WHERE definition_id = ? ORDER BY step_id, position`, id)
|
||||
if err != nil {
|
||||
return def, err
|
||||
}
|
||||
for rows.Next() {
|
||||
step := 0
|
||||
a := api.TeamMember{}
|
||||
err = rows.Scan(&step, &a.Name, &a.Model, &a.Prompt)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return def, err
|
||||
}
|
||||
agents[step] = append(agents[step], a)
|
||||
}
|
||||
err = errors.Join(rows.Err(), rows.Close())
|
||||
if err != nil {
|
||||
return def, err
|
||||
}
|
||||
|
||||
children := map[int][]stepRow{}
|
||||
rows, err = q.QueryContext(ctx, `SELECT id, coalesce(parent, -1), kind, stage, model, prompt FROM workflow_steps WHERE definition_id = ? ORDER BY coalesce(parent, -1), position`, id)
|
||||
if err != nil {
|
||||
return def, err
|
||||
}
|
||||
for rows.Next() {
|
||||
r := stepRow{}
|
||||
parent := 0
|
||||
err = rows.Scan(&r.id, &parent, &r.kind, &r.step.Stage, &r.step.Model, &r.step.Prompt)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return def, err
|
||||
}
|
||||
if a, found := agents[r.id]; found {
|
||||
r.step.Agents = &a
|
||||
}
|
||||
children[parent] = append(children[parent], r)
|
||||
}
|
||||
err = errors.Join(rows.Err(), rows.Close())
|
||||
if err != nil {
|
||||
return def, err
|
||||
}
|
||||
|
||||
var build func(parent int) []api.WorkflowStep
|
||||
build = func(parent int) []api.WorkflowStep {
|
||||
steps := []api.WorkflowStep{}
|
||||
for _, r := range children[parent] {
|
||||
switch r.kind {
|
||||
case "order":
|
||||
sub := build(r.id)
|
||||
r.step.Order = &sub
|
||||
case "parallel":
|
||||
sub := build(r.id)
|
||||
r.step.Parallel = &sub
|
||||
}
|
||||
steps = append(steps, r.step)
|
||||
}
|
||||
return steps
|
||||
}
|
||||
def.Steps = build(-1)
|
||||
return def, nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
)
|
||||
|
||||
func migrateWorkflowDefinitions(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE workflow_definitions (
|
||||
id TEXT PRIMARY KEY,
|
||||
description TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE workflow_steps (
|
||||
definition_id TEXT NOT NULL REFERENCES workflow_definitions(id),
|
||||
id INTEGER NOT NULL,
|
||||
parent INTEGER,
|
||||
position INTEGER NOT NULL,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('stage', 'order', 'parallel')),
|
||||
stage TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
prompt TEXT NOT NULL,
|
||||
PRIMARY KEY (definition_id, id),
|
||||
UNIQUE (definition_id, parent, position)
|
||||
);
|
||||
CREATE TABLE workflow_agents (
|
||||
definition_id TEXT NOT NULL REFERENCES workflow_definitions(id),
|
||||
step_id INTEGER NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
prompt TEXT NOT NULL,
|
||||
PRIMARY KEY (definition_id, step_id, position),
|
||||
FOREIGN KEY (definition_id, step_id) REFERENCES workflow_steps(definition_id, id)
|
||||
);
|
||||
ALTER TABLE tasks ADD COLUMN workflow_definition_id TEXT REFERENCES workflow_definitions(id);
|
||||
ALTER TABLE reviews ADD COLUMN definition_id TEXT REFERENCES workflow_definitions(id);
|
||||
ALTER TABLE workflows ADD COLUMN definition_id TEXT REFERENCES workflow_definitions(id);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ids := map[string]string{}
|
||||
for _, c := range []struct{ table, key, from, to string }{
|
||||
{"workflows", "name", "definition", "definition_id"},
|
||||
{"tasks", "id", "workflow_definition", "workflow_definition_id"},
|
||||
{"reviews", "id", "definition", "definition_id"},
|
||||
} {
|
||||
err = moveDefinitions(tx, ids, c.table, c.key, c.from, c.to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE workflows_next (
|
||||
name TEXT PRIMARY KEY,
|
||||
definition_id TEXT NOT NULL REFERENCES workflow_definitions(id)
|
||||
);
|
||||
INSERT INTO workflows_next (name, definition_id) SELECT name, definition_id FROM workflows;
|
||||
DROP TABLE workflows;
|
||||
ALTER TABLE workflows_next RENAME TO workflows;
|
||||
ALTER TABLE tasks DROP COLUMN workflow_definition;
|
||||
ALTER TABLE reviews DROP COLUMN definition;
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
func moveDefinitions(tx *sql.Tx, ids map[string]string, table, key, from, to string) error {
|
||||
rows, err := tx.Query(fmt.Sprintf(`SELECT %s, %s FROM %s WHERE %s != ''`, key, from, table, from))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type row struct{ key, def string }
|
||||
all := []row{}
|
||||
for rows.Next() {
|
||||
r := row{}
|
||||
err = rows.Scan(&r.key, &r.def)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
all = append(all, r)
|
||||
}
|
||||
rows.Close()
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, r := range all {
|
||||
id, found := ids[r.def]
|
||||
if !found {
|
||||
def := api.WorkflowDefinition{}
|
||||
dec := json.NewDecoder(bytes.NewReader([]byte(r.def)))
|
||||
dec.DisallowUnknownFields()
|
||||
err = dec.Decode(&def)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %w", table, r.key, err)
|
||||
}
|
||||
id, err = insertDefinition(ctx, tx, def)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %w", table, r.key, err)
|
||||
}
|
||||
back, err := loadDefinition(ctx, tx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s: %w", table, r.key, err)
|
||||
}
|
||||
want, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
got, err := json.Marshal(back)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(want, got) {
|
||||
return fmt.Errorf("%s %s: definition reads back as %s, not %s", table, r.key, got, want)
|
||||
}
|
||||
ids[r.def] = id
|
||||
}
|
||||
_, err = tx.Exec(fmt.Sprintf(`UPDATE %s SET %s = ? WHERE %s = ?`, table, to, key), id, r.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+13
-25
@@ -12,24 +12,16 @@ import (
|
||||
|
||||
const reviewColumns = `id, task_id, snapshot_n, stage, coalesce(agent_session_id, ''), vendor, model, state, result, error, created_at, finished_at, finding_states, finding_responses`
|
||||
|
||||
func (s *Store) CreateReview(ctx context.Context, r api.Review, def *api.WorkflowDefinition) error {
|
||||
func (s *Store) CreateReview(ctx context.Context, r api.Review, definitionID string) error {
|
||||
finishedAt := ""
|
||||
if r.FinishedAt != nil {
|
||||
finishedAt = formatTime(*r.FinishedAt)
|
||||
}
|
||||
defText := ""
|
||||
if def != nil {
|
||||
b, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defText = string(b)
|
||||
}
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO reviews (id, task_id, snapshot_n, stage, definition, agent_session_id, vendor, model, state, error, created_at, finished_at)
|
||||
VALUES (?, ?, ?, ?, ?, nullif(?, ''), ?, ?, ?, ?, ?, ?)
|
||||
`, r.Id, r.TaskId, r.SnapshotN, r.Stage, defText, r.AgentSessionId, r.Vendor, r.Model, string(r.State), r.Error, formatTime(r.CreatedAt), finishedAt)
|
||||
INSERT INTO reviews (id, task_id, snapshot_n, stage, definition_id, agent_session_id, vendor, model, state, error, created_at, finished_at)
|
||||
VALUES (?, ?, ?, ?, nullif(?, ''), nullif(?, ''), ?, ?, ?, ?, ?, ?)
|
||||
`, r.Id, r.TaskId, r.SnapshotN, r.Stage, definitionID, r.AgentSessionId, r.Vendor, r.Model, string(r.State), r.Error, formatTime(r.CreatedAt), finishedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -40,21 +32,17 @@ func (s *Store) CreateReview(ctx context.Context, r api.Review, def *api.Workflo
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) ReviewDefinition(ctx context.Context, id string) (api.WorkflowDefinition, error) {
|
||||
text := ""
|
||||
err := s.db.QueryRowContext(ctx, `SELECT definition FROM reviews WHERE id = ?`, id).Scan(&text)
|
||||
func (s *Store) ReviewDefinition(ctx context.Context, id string) (string, api.WorkflowDefinition, error) {
|
||||
defID := sql.NullString{}
|
||||
err := s.db.QueryRowContext(ctx, `SELECT definition_id FROM reviews WHERE id = ?`, id).Scan(&defID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return api.WorkflowDefinition{}, ErrNotFound
|
||||
return "", api.WorkflowDefinition{}, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, err
|
||||
if err != nil || !defID.Valid {
|
||||
return "", api.WorkflowDefinition{}, err
|
||||
}
|
||||
def := api.WorkflowDefinition{}
|
||||
if text == "" {
|
||||
return def, nil
|
||||
}
|
||||
err = json.Unmarshal([]byte(text), &def)
|
||||
return def, err
|
||||
def, err := loadDefinition(ctx, s.db, defID.String)
|
||||
return defID.String, def, err
|
||||
}
|
||||
|
||||
func (s *Store) GetReview(ctx context.Context, id string) (api.Review, error) {
|
||||
@@ -92,7 +80,7 @@ func (s *Store) RunningReviews(ctx context.Context) ([]api.Review, error) {
|
||||
func (s *Store) LatestDoneStages(ctx context.Context) ([]string, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT r.id FROM reviews r JOIN tasks t ON t.id = r.task_id
|
||||
WHERE t.status = ? AND r.state = ? AND r.definition != ''
|
||||
WHERE t.status = ? AND r.state = ? AND r.definition_id IS NOT NULL
|
||||
AND r.snapshot_n = (SELECT max(snapshot_n) FROM reviews WHERE task_id = r.task_id)
|
||||
ORDER BY r.created_at`, string(api.TaskStatusActive), string(api.ReviewStateDone))
|
||||
if err != nil {
|
||||
|
||||
@@ -33,7 +33,8 @@ type migration struct {
|
||||
}
|
||||
|
||||
var goMigrations = map[string]func(*sql.Tx) error{
|
||||
"071-workflow-steps": migrateWorkflowSteps,
|
||||
"071-workflow-steps": migrateWorkflowSteps,
|
||||
"078-normalize-workflow-definitions": migrateWorkflowDefinitions,
|
||||
}
|
||||
|
||||
func loadMigrations() ([]migration, error) {
|
||||
@@ -337,18 +338,14 @@ func (s *Store) GetProject(ctx context.Context, id string) (api.Project, error)
|
||||
|
||||
func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkflow, actor string) error {
|
||||
workflowName := ""
|
||||
workflowDefinition := ""
|
||||
var workflowDefinitionID *string
|
||||
workflowBrief := ""
|
||||
workflowTitle := ""
|
||||
workflowRun := ""
|
||||
workflowStage := ""
|
||||
if frozen != nil {
|
||||
b, err := json.Marshal(frozen.Definition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workflowName = frozen.Name
|
||||
workflowDefinition = string(b)
|
||||
workflowDefinitionID = &frozen.DefinitionID
|
||||
workflowBrief = frozen.Brief
|
||||
workflowTitle = frozen.Title
|
||||
workflowRun = *t.WorkflowRun
|
||||
@@ -411,9 +408,9 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkfl
|
||||
return err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at, workflow_name, workflow_definition, workflow_brief, workflow_title, workflow_run, workflow_stage, parent_task_id, audit)
|
||||
INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at, workflow_name, workflow_definition_id, workflow_brief, workflow_title, workflow_run, workflow_stage, parent_task_id, audit)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt, workflowName, workflowDefinition, workflowBrief, workflowTitle, workflowRun, workflowStage, parentTaskID, audit)
|
||||
`, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt, workflowName, workflowDefinitionID, workflowBrief, workflowTitle, workflowRun, workflowStage, parentTaskID, audit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+27
-30
@@ -3,7 +3,6 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -11,15 +10,16 @@ import (
|
||||
)
|
||||
|
||||
type FrozenWorkflow struct {
|
||||
Name string
|
||||
Definition api.WorkflowDefinition
|
||||
Brief string
|
||||
Title string
|
||||
Audit string
|
||||
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 name, coalesce(json_extract(definition, '$.description'), '') FROM workflows ORDER BY name`)
|
||||
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
|
||||
}
|
||||
@@ -38,26 +38,13 @@ func (s *Store) ListWorkflows(ctx context.Context) ([]api.WorkflowSummary, error
|
||||
}
|
||||
|
||||
func (s *Store) GetWorkflow(ctx context.Context, name string) (api.WorkflowDefinition, error) {
|
||||
def := ""
|
||||
err := s.db.QueryRowContext(ctx, `SELECT definition FROM workflows WHERE name = ?`, name).Scan(&def)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("%w: no workflow %q", ErrNotFound, name)
|
||||
}
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, err
|
||||
}
|
||||
w := api.WorkflowDefinition{}
|
||||
err = json.Unmarshal([]byte(def), &w)
|
||||
return w, err
|
||||
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 {
|
||||
b, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
if from != "" {
|
||||
found := false
|
||||
@@ -91,8 +78,12 @@ func (s *Store) PutWorkflow(ctx context.Context, name, from string, def api.Work
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := tx.ExecContext(ctx, `INSERT INTO workflows (name, definition) VALUES (?, ?)
|
||||
ON CONFLICT (name) DO UPDATE SET definition = excluded.definition`, name, string(b))
|
||||
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
|
||||
}
|
||||
@@ -154,27 +145,33 @@ func (s *Store) DeleteWorkflow(ctx context.Context, name, actor string) error {
|
||||
}
|
||||
|
||||
func (s *Store) FreezeWorkflow(ctx context.Context, name string) (FrozenWorkflow, error) {
|
||||
def, err := s.GetWorkflow(ctx, name)
|
||||
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
|
||||
}
|
||||
return FrozenWorkflow{Name: name, Definition: def}, nil
|
||||
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{}
|
||||
def := ""
|
||||
err := s.db.QueryRowContext(ctx, `SELECT workflow_name, workflow_definition, workflow_brief, workflow_title, audit FROM tasks WHERE id = ?`, taskID).Scan(&f.Name, &def, &f.Brief, &f.Title, &f.Audit)
|
||||
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 f.Name == "" {
|
||||
if !defID.Valid {
|
||||
return FrozenWorkflow{}, fmt.Errorf("%w: task %s has no workflow", ErrNotFound, taskID)
|
||||
}
|
||||
err = json.Unmarshal([]byte(def), &f.Definition)
|
||||
f.DefinitionID = defID.String
|
||||
f.Definition, err = loadDefinition(ctx, s.db, f.DefinitionID)
|
||||
return f, err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
)
|
||||
|
||||
var keyOrder = []string{"description", "kind", "steps", "stage", "name", "model", "prompt", "agents", "order", "parallel"}
|
||||
|
||||
func Parse(data []byte) (api.WorkflowDefinition, error) {
|
||||
raw := any(nil)
|
||||
err := yaml.Unmarshal(data, &raw)
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||
}
|
||||
j, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||
}
|
||||
|
||||
def := api.WorkflowDefinition{}
|
||||
dec := json.NewDecoder(bytes.NewReader(j))
|
||||
dec.DisallowUnknownFields()
|
||||
err = dec.Decode(&def)
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||
}
|
||||
return def, Validate(def)
|
||||
}
|
||||
|
||||
func Format(def api.WorkflowDefinition) ([]byte, error) {
|
||||
j, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc := yaml.Node{}
|
||||
err = yaml.Unmarshal(j, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
restyle(&doc)
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
enc := yaml.NewEncoder(out)
|
||||
enc.SetIndent(2)
|
||||
err = enc.Encode(&doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = enc.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
func Filename(name string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if strings.ContainsRune(`/\:*?"<>|`, r) {
|
||||
return '_'
|
||||
}
|
||||
return r
|
||||
}, name) + ".yaml"
|
||||
}
|
||||
|
||||
func restyle(n *yaml.Node) {
|
||||
switch n.Kind {
|
||||
case yaml.ScalarNode:
|
||||
n.Style = 0
|
||||
if n.Tag == "!!str" && strings.Contains(n.Value, "\n") {
|
||||
n.Style = yaml.LiteralStyle
|
||||
}
|
||||
case yaml.MappingNode:
|
||||
n.Style = 0
|
||||
pairs := [][2]*yaml.Node{}
|
||||
for i := 0; i+1 < len(n.Content); i += 2 {
|
||||
pairs = append(pairs, [2]*yaml.Node{n.Content[i], n.Content[i+1]})
|
||||
}
|
||||
slices.SortStableFunc(pairs, func(a, b [2]*yaml.Node) int {
|
||||
return keyRank(a[0].Value) - keyRank(b[0].Value)
|
||||
})
|
||||
n.Content = n.Content[:0]
|
||||
for _, p := range pairs {
|
||||
n.Content = append(n.Content, p[0], p[1])
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
n.Style = 0
|
||||
}
|
||||
for _, c := range n.Content {
|
||||
restyle(c)
|
||||
}
|
||||
}
|
||||
|
||||
func keyRank(key string) int {
|
||||
i := slices.Index(keyOrder, key)
|
||||
if i < 0 {
|
||||
return len(keyOrder)
|
||||
}
|
||||
return i
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
)
|
||||
|
||||
func Parse(data []byte) (api.WorkflowDefinition, error) {
|
||||
def := api.WorkflowDefinition{}
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.DisallowUnknownFields()
|
||||
err := dec.Decode(&def)
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||
}
|
||||
return def, Validate(def)
|
||||
}
|
||||
@@ -228,7 +228,7 @@ struct WorkflowEditor: View {
|
||||
return
|
||||
}
|
||||
do {
|
||||
let def = try await store.api.client.getWorkflow(path: .init(name: original), headers: .init(accept: [.init(contentType: .json)])).ok.body.json
|
||||
let def = try await store.api.client.getWorkflow(path: .init(name: original)).ok.body.json
|
||||
name = original
|
||||
description = def.description ?? ""
|
||||
root = StepNode(kind: .order, children: def.steps.map { StepNode($0) })
|
||||
|
||||
Reference in New Issue
Block a user