2026-10-15 21:17:58 -07:00
package agents
import (
"context"
"encoding/json"
"fmt"
2026-10-15 22:11:25 -07:00
"log/slog"
2026-10-15 21:17:58 -07:00
"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
}
2026-10-15 22:11:25 -07:00
var errModelWithWorkflow = fmt . Errorf ( "%w: model can't be combined with workflow: each stage names its own model" , ErrInvalid )
func ( m * Manager ) freeze ( ctx context . Context , name , brief string ) ( * store . FrozenWorkflow , error ) {
2026-10-15 21:17:58 -07:00
if name == "" {
return nil , nil
}
frozen , err := m . store . FreezeWorkflow ( ctx , name )
if err != nil {
return nil , err
}
2026-10-15 22:11:25 -07:00
frozen . Brief = brief
2026-10-15 21:17:58 -07:00
return & frozen , nil
}
2026-10-15 22:11:25 -07:00
func stageMessage ( frozen store . FrozenWorkflow , n int , previous string ) string {
stages := frozen . Definition . Stages
stage := stages [ n - 1 ]
b := & strings . Builder {}
fmt . Fprintf ( b , "Workflow %q, stage %d of %d: %s\n\n%s" , frozen . Name , n , len ( stages ), stage . Name , strings . TrimSpace ( stage . Prompt ))
if strings . TrimSpace ( frozen . Brief ) != "" {
fmt . Fprintf ( b , "\n\n## Brief\n\n%s" , strings . TrimSpace ( frozen . Brief ))
}
if n > 1 {
fmt . Fprintf ( b , "\n\n## From the previous stage (%s)\n\n%s" , stages [ n - 2 ]. Name , strings . TrimSpace ( previous ))
}
return b . String ()
}
func ( m * Manager ) startStage ( ctx context . Context , task api . Task , creator api . User , frozen store . FrozenWorkflow , n int , previous , actor string ) ( api . AgentSession , error ) {
stage := frozen . Definition . Stages [ n - 1 ]
err := m . store . AppendEvent ( ctx , api . Event {
Kind : "task.workflow_stage" ,
ProjectId : & task . ProjectId ,
TaskId : & task . Id ,
Actor : actor ,
Payload : map [ string ] any { "id" : task . Id , "workflow" : frozen . Name , "stage" : n , "stages" : len ( frozen . Definition . Stages ), "name" : stage . Name },
})
if err != nil {
return api . AgentSession {}, err
}
return m . createAt ( ctx , task , creator , stageMessage ( frozen , n , previous ), stage . Model , "byre" , nil , n )
}
func ( m * Manager ) startWorkflow ( ctx context . Context , task api . Task , creator api . User , actor string ) error {
frozen , err := m . store . TaskWorkflow ( ctx , task . Id )
if err == nil {
_ , err = m . startStage ( ctx , task , creator , frozen , 1 , "" , actor )
}
if err != nil {
return fmt . Errorf ( "task %s created, but its workflow did not start: %w" , task . Id , err )
}
return nil
}
func ( m * Manager ) advanceStage ( l * live ) {
_ , err := m . nextStage ( context . Background (), l . id )
if err != nil {
slog . Error ( "advance workflow stage" , "session" , l . id , "err" , err )
m . fileError ( l , fmt . Sprintf ( "The workflow didn't move on from this stage: %v. byre tries again when it next starts." , err ))
}
}
func ( m * Manager ) recoverStages ( ctx context . Context ) ( map [ string ] bool , error ) {
ids , err := m . store . FinishedStages ( ctx )
if err != nil {
return nil , err
}
started := map [ string ] bool {}
for _ , id := range ids {
next , err := m . nextStage ( ctx , id )
if err != nil {
slog . Error ( "advance workflow stage" , "session" , id , "err" , err )
continue
}
if next != "" {
started [ next ] = true
}
slog . Info ( "advanced workflow stage" , "session" , id , "next" , next )
}
return started , nil
}
func ( m * Manager ) nextStage ( ctx context . Context , sessionID string ) ( string , error ) {
sess , err := m . store . GetSession ( ctx , sessionID )
if err != nil {
return "" , err
}
task , err := m . store . GetTask ( ctx , sess . TaskId )
if err != nil {
return "" , err
}
if task . Status != api . TaskStatusActive {
return "" , m . store . ClearStage ( ctx , sess . Id )
}
frozen , err := m . store . TaskWorkflow ( ctx , task . Id )
if err != nil {
return "" , err
}
output , err := m . store . StageOutput ( ctx , sess . Id )
if err != nil {
return "" , err
}
next := * sess . WorkflowStage + 1
if next > len ( frozen . Definition . Stages ) {
return "" , m . store . FinishWorkflow ( ctx , task , sess . Id , frozen . Name , output )
}
creator , err := m . store . GetUser ( ctx , sess . CreatedBy )
if err != nil {
return "" , err
}
if sess . State != api . AgentSessionStateCompleted {
m . logUndelivered ( ctx , sess . Id , "stage finished" )
err = m . retire ( ctx , sess , "byre" )
if err != nil {
return "" , err
}
}
successor , found , err := m . successor ( ctx , sess , next )
if err != nil {
return "" , err
}
sent := true
switch {
case ! found :
successor , err = m . startStage ( ctx , task , creator , frozen , next , output , "byre" )
default :
sent , err = m . resendStage ( ctx , successor , task , frozen , next , output )
}
if err != nil {
return "" , err
}
err = m . store . ClearStage ( ctx , sess . Id )
if err != nil || ! sent {
return "" , err
}
return successor . Id , nil
}
func ( m * Manager ) successor ( ctx context . Context , sess api . AgentSession , n int ) ( api . AgentSession , bool , error ) {
sessions , err := m . store . ListSessions ( ctx , sess . TaskId )
if err != nil {
return api . AgentSession {}, false , err
}
for _ , s := range sessions {
if s . CreatedAt . Before ( sess . CreatedAt ) || s . Id == sess . Id || s . WorkflowStage == nil || * s . WorkflowStage != n {
continue
}
return s , true , nil
}
return api . AgentSession {}, false , nil
}
func ( m * Manager ) resendStage ( ctx context . Context , successor api . AgentSession , task api . Task , frozen store . FrozenWorkflow , n int , previous string ) ( bool , error ) {
started , err := m . store . SessionStarted ( ctx , successor . Id )
if err != nil || started || successor . State == api . AgentSessionStateCompleted {
return false , err
}
if successor . State == api . AgentSessionStateRunning {
err = m . store . SetSessionState ( ctx , successor . Id , api . AgentSessionStateIdle , "byre" , nil )
if err != nil {
return false , err
}
}
_ , err = m . turn ( ctx , successor , task , "byre" , stageMessage ( frozen , n , previous ), nil )
return err == nil , err
}
func ( m * Manager ) workflowStatus ( ctx context . Context , taskID string ) ( string , error ) {
finished , err := m . store . WorkflowFinished ( ctx , taskID )
if err != nil || finished {
return "finished" , err
}
frozen , err := m . store . TaskWorkflow ( ctx , taskID )
if err != nil {
return "" , err
}
sessions , err := m . store . ListSessions ( ctx , taskID )
if err != nil {
return "" , err
}
if len ( sessions ) == 0 || sessions [ len ( sessions ) - 1 ]. WorkflowStage == nil {
return "not started" , nil
}
n := * sessions [ len ( sessions ) - 1 ]. WorkflowStage
stages := frozen . Definition . Stages
return fmt . Sprintf ( "stage %d/%d %s" , n , len ( stages ), stages [ n - 1 ]. Name ), nil
}
func ( m * Manager ) finishStageTool ( l * live ) mcp . Tool {
return mcp . Tool {
Name : "finish_stage" ,
Description : "Finish your stage of this task's workflow. Call it once the stage's work is done; byre starts the next stage, in a fresh session, when this turn ends " +
"(after the last stage, the workflow is finished). Calling it again in the same turn replaces the output. " +
"The call counts only for the turn that makes it: if that turn ends in an error or is interrupted, it is discarded, and you call it again once the stage's work is done." ,
InputSchema : json . RawMessage ( `{
"type": "object",
"properties": {
"output": {"type": "string", "description": "What the next stage starts from: everything it needs from this stage, since it sees nothing else of your context. For the last stage, what the workflow produced."}
},
"required": ["output"]
}` ),
Handler : func ( ctx context . Context , raw json . RawMessage ) ( string , error ) {
args := struct {
Output string `json:"output"`
}{}
err := json . Unmarshal ( raw , & args )
if err != nil {
return "" , fmt . Errorf ( "malformed finish_stage call: %w" , err )
}
if strings . TrimSpace ( args . Output ) == "" {
return "" , fmt . Errorf ( "%w: output is required" , ErrInvalid )
}
err = m . store . SetStageOutput ( ctx , l . id , args . Output )
if err != nil {
return "" , err
}
if l . stage == l . stages {
return "This is the workflow's last stage: it finishes when this turn ends. End your turn now with your summary." , nil
}
return fmt . Sprintf ( "byre starts stage %d when this turn ends. End your turn now with your summary." , l . stage + 1 ), nil
},
}
}
func stageGuidance ( frozen store . FrozenWorkflow , n int ) string {
return fmt . Sprintf ( "\n- This task runs byre workflow %q, and you are its stage %d of %d, %q. When this stage's work is done, call byre's finish_stage tool with its output, then end your turn: " +
"byre then starts the next stage in a fresh session, which gets the output and nothing else of your context. Until you call it, the task stays in your stage." ,
frozen . Name , n , len ( frozen . Definition . Stages ), frozen . Definition . Stages [ n - 1 ]. Name )
}
2026-10-15 21:17:58 -07:00
const workflowFileHelp = "A workflow's file is YAML: an optional description, then stages, each with name (one line, unique in the workflow), " +
2026-10-15 22:11:25 -07:00
"model (which also picks the vendor: gpt-* models run on Codex, every other model on Claude Code) and prompt (the stage agent's instructions). " +
2026-10-15 21:17:58 -07:00
"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
}),
}
}