161 lines
5.1 KiB
Go
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
|
|
}),
|
|
}
|
|
}
|