Store workflow definitions as table rows and drop YAML

This commit is contained in:
Ian Gulliver
2026-10-20 21:30:39 -07:00
parent 5823af6ff9
commit 32ebf08b66
17 changed files with 624 additions and 454 deletions
+6 -13
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -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
+3 -2
View File
@@ -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
View File
@@ -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
+64 -28
View File
@@ -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
View File
@@ -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,
+7 -23
View File
@@ -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
+134
View File
@@ -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
}
+135
View File
@@ -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
View File
@@ -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 {
+6 -9
View File
@@ -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
View File
@@ -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
}
-107
View File
@@ -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
}
+20
View File
@@ -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)
}
+1 -1
View File
@@ -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) })