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