Files
byre/internal/agents/workflows.go
T

161 lines
5.1 KiB
Go

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
}),
}
}