Add server-side workflows with YAML files, task freeze and lead tools
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/mcp"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
"g.fc.run/misc/byre/internal/workflow"
|
||||
)
|
||||
|
||||
func (m *Manager) SaveWorkflow(ctx context.Context, name string, file []byte, actor string) (api.WorkflowSummary, error) {
|
||||
name, err := workflow.CleanName(name)
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
def, err := workflow.Parse(file)
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
err = m.store.PutWorkflow(ctx, name, def, actor)
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
return api.WorkflowSummary{Name: name, Description: def.Description}, 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
|
||||
}
|
||||
|
||||
func (m *Manager) freeze(ctx context.Context, name string) (*store.FrozenWorkflow, error) {
|
||||
if name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
frozen, err := m.store.FreezeWorkflow(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &frozen, nil
|
||||
}
|
||||
|
||||
const workflowFileHelp = "A workflow's file is YAML: an optional description, then stages, each with name (one line, unique in the workflow), " +
|
||||
"vendor (claude or codex: gpt-* models run on codex, every other model on claude), model and prompt (the stage agent's instructions). " +
|
||||
"The workflow's name is not in the file."
|
||||
|
||||
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{
|
||||
tool("list_workflows",
|
||||
"List the workflows on the server: each one's name and description.",
|
||||
`{"type": "object", "properties": {}}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
workflows, err := m.store.ListWorkflows(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(workflows) == 0 {
|
||||
return "No workflows.", nil
|
||||
}
|
||||
b := &strings.Builder{}
|
||||
for _, w := range workflows {
|
||||
fmt.Fprintf(b, "- %q", w.Name)
|
||||
if w.Description != "" {
|
||||
fmt.Fprintf(b, ": %s", w.Description)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return b.String(), nil
|
||||
}),
|
||||
|
||||
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,
|
||||
`{"type": "object", "properties": {
|
||||
"name": {"type": "string"},
|
||||
"task_id": {"type": "string"}
|
||||
}}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
Name string `json:"name"`
|
||||
TaskID string `json:"task_id"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if (args.Name == "") == (args.TaskID == "") {
|
||||
return "", fmt.Errorf("%w: give exactly one of name and task_id", ErrInvalid)
|
||||
}
|
||||
if args.TaskID != "" {
|
||||
tw, err := m.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
|
||||
}
|
||||
file, err := m.WorkflowFile(ctx, args.Name)
|
||||
return string(file), err
|
||||
}),
|
||||
|
||||
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,
|
||||
`{"type": "object", "properties": {
|
||||
"name": {"type": "string"},
|
||||
"definition": {"type": "string", "description": "The workflow's file, as YAML."}
|
||||
}, "required": ["name", "definition"]}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
Name string `json:"name"`
|
||||
Definition string `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))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Saved workflow %q.", w.Name), nil
|
||||
}),
|
||||
|
||||
tool("delete_workflow",
|
||||
"Delete a workflow from the server. Tasks that froze it keep their copy. Only when the human asks.",
|
||||
`{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = m.store.DeleteWorkflow(ctx, args.Name, leadActor(user.Login))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Deleted workflow %q.", args.Name), nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user