2026-10-15 21:17:58 -07:00
package agents
import (
"context"
"encoding/json"
2026-10-16 10:08:08 -07:00
"errors"
2026-10-15 21:17:58 -07:00
"fmt"
2026-10-15 22:11:25 -07:00
"log/slog"
2026-10-15 21:17:58 -07:00
"strings"
2026-10-18 15:32:07 -07:00
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/git"
"g.fc.run/ai/byre/internal/mcp"
"g.fc.run/ai/byre/internal/store"
"g.fc.run/ai/byre/internal/workflow"
2026-10-15 21:17:58 -07:00
)
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-16 11:01:53 -07:00
func ( m * Manager ) WorkflowRun ( ctx context . Context , taskID string ) ( api . WorkflowRun , error ) {
frozen , err := m . store . TaskWorkflow ( ctx , taskID )
if err != nil {
return api . WorkflowRun {}, err
}
task , err := m . store . GetTask ( ctx , taskID )
if err != nil {
return api . WorkflowRun {}, err
}
tasks , err := m . store . RunTasks ( ctx , * task . WorkflowRun )
if err != nil {
return api . WorkflowRun {}, err
}
events , err := m . store . RunEvents ( ctx , * task . WorkflowRun )
if err != nil {
return api . WorkflowRun {}, err
}
run := api . WorkflowRun { Name : frozen . Name , Run : * task . WorkflowRun , Title : frozen . Title , Brief : frozen . Brief , Definition : frozen . Definition , Stages : [] api . WorkflowRunStage {}}
after := map [ string ][] string {}
discardedBy := map [ string ] string {}
for _ , ev := range events {
switch ev . Kind {
case "task.workflow_stage" :
ids := [] string {}
list , _ := ev . Payload [ "after" ].([] any )
for _ , v := range list {
id , _ := v .( string )
ids = append ( ids , id )
}
after [ * ev . TaskId ] = ids
case "task.discarded" :
discardedBy [ * ev . TaskId ] = ev . Actor
case "task.workflow_finished" :
run . Finished = true
}
}
byID := map [ string ] api . Task {}
for _ , t := range tasks {
byID [ t . Id ] = t
}
var inputs func ( id string ) [] api . StageInput
inputs = func ( id string ) [] api . StageInput {
ids , found := after [ id ]
if ! found {
t := byID [ id ]
if t . ForkedFrom == nil {
return [] api . StageInput {}
}
return inputs ( * t . ForkedFrom )
}
in := [] api . StageInput {}
for _ , a := range ids {
stage := ""
if t , found := byID [ a ]; found {
stage = * t . WorkflowStage
}
in = append ( in , api . StageInput { Stage : stage , TaskId : a })
}
return in
}
attempts := map [ string ][] api . StageAttempt {}
done := map [ string ] bool {}
for _ , t := range tasks {
output , commit , err := m . store . StageResult ( ctx , t . Id )
if err != nil {
return api . WorkflowRun {}, err
}
running := false
if t . Status == api . TaskStatusActive {
sessions , err := m . store . ListSessions ( ctx , t . Id )
if err != nil {
return api . WorkflowRun {}, err
}
for _ , s := range sessions {
running = running || s . State == api . AgentSessionStateRunning
}
}
a := api . StageAttempt {
TaskId : t . Id ,
Title : t . Title ,
State : attemptState ( t , output != "" , running ),
Discarding : t . Discarding ,
ForkedFrom : t . ForkedFrom ,
Inputs : inputs ( t . Id ),
}
if output != "" {
a . Output = & output
a . Commit = & commit
done [ * t . WorkflowStage ] = true
}
if by , found := discardedBy [ t . Id ]; found && t . Status == api . TaskStatusDiscarded {
a . DiscardedBy = & by
}
attempts [ * t . WorkflowStage ] = append ( attempts [ * t . WorkflowStage ], a )
}
for _ , s := range frozen . Definition . Stages {
stage := api . WorkflowRunStage {
Name : s . Name ,
After : workflow . After ( frozen . Definition , s . Name ),
State : api . StageStateWaiting ,
WaitingOn : [] string {},
Attempts : attempts [ s . Name ],
}
if stage . Attempts == nil {
stage . Attempts = [] api . StageAttempt {}
}
for _ , a := range stage . After {
if ! done [ a ] {
stage . WaitingOn = append ( stage . WaitingOn , a )
}
}
if len ( stage . Attempts ) > 0 {
deciding := stage . Attempts [ len ( stage . Attempts ) - 1 ]
for _ , a := range stage . Attempts {
if a . Output != nil {
deciding = a
}
}
stage . State = api . WorkflowRunStageState ( deciding . State )
}
run . Stages = append ( run . Stages , stage )
}
return run , nil
}
func attemptState ( t api . Task , finished , running bool ) api . StageAttemptState {
switch {
case t . Status == api . TaskStatusMerged :
return api . AttemptStateMerged
case t . Status == api . TaskStatusDiscarded && finished :
return api . AttemptStateCleanedUp
case t . Status == api . TaskStatusDiscarded :
return api . AttemptStateDiscarded
case finished :
return api . AttemptStateFinished
case running :
return api . AttemptStateRunning
}
return api . AttemptStateIdle
}
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 )
2026-10-15 22:53:54 -07:00
type parentStage struct {
task api . Task
output string
commit string
}
func ( m * Manager ) StartWorkflow ( ctx context . Context , project api . Project , creator api . User , title , base , actor , name , brief string , todoItem * string ) ([] api . Task , error ) {
if creator . GitName == "" || creator . GitEmail == "" {
return nil , ErrNoGitIdentity
2026-10-15 21:17:58 -07:00
}
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 22:53:54 -07:00
frozen . Title = title
2026-10-15 21:17:58 -07:00
2026-10-15 22:53:54 -07:00
run := store . NewID ()
tasks := [] api . Task {}
for _ , stage := range workflow . Starts ( frozen . Definition ) {
from := base
if workflow . FromMain ( frozen . Definition , stage ) {
from = project . DefaultBranch
}
task , err := m . startStage ( ctx , project , creator , frozen , run , stage , from , todoItem , nil , actor )
if err != nil {
return tasks , fmt . Errorf ( "workflow %q: stage %q did not start: %w" , name , stage , err )
}
tasks = append ( tasks , task )
}
return tasks , nil
}
2026-10-15 22:11:25 -07:00
2026-10-15 22:53:54 -07:00
func ( m * Manager ) startStage ( ctx context . Context , project api . Project , creator api . User , frozen store . FrozenWorkflow , run , stage , base string , todoItem * string , parents [] parentStage , actor string ) ( api . Task , error ) {
task , err := m . createTask ( ctx , project , api . Task {
Title : fmt . Sprintf ( "%s: %s" , frozen . Title , stage ),
MergeTitle : frozen . Title ,
CreatedBy : creator . Login ,
TodoItem : todoItem ,
WorkflowRun : & run ,
WorkflowStage : & stage ,
}, & frozen , base , actor )
if err != nil {
return api . Task {}, err
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
_ , err = m . beginStage ( ctx , task , project , creator , frozen , parents , actor )
if err != nil {
return task , fmt . Errorf ( "task %s created, but its agent did not start: %w" , task . Id , err )
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
return task , nil
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
func ( m * Manager ) beginStage ( ctx context . Context , task api . Task , project api . Project , creator api . User , frozen store . FrozenWorkflow , parents [] parentStage , actor string ) ( api . AgentSession , error ) {
stage := * task . WorkflowStage
after := [] string {}
for _ , p := range parents {
after = append ( after , p . task . Id )
}
2026-10-15 22:11:25 -07:00
err := m . store . AppendEvent ( ctx , api . Event {
Kind : "task.workflow_stage" ,
ProjectId : & task . ProjectId ,
TaskId : & task . Id ,
Actor : actor ,
2026-10-15 22:53:54 -07:00
Payload : map [ string ] any { "id" : task . Id , "workflow" : frozen . Name , "run" : * task . WorkflowRun , "stage" : stage , "after" : after },
2026-10-15 22:11:25 -07:00
})
if err != nil {
return api . AgentSession {}, err
}
2026-10-18 08:48:57 -07:00
s := workflow . Get ( frozen . Definition , stage )
message := stageMessage ( frozen , stage , parents , project . DefaultBranch )
if s . Agents == nil {
return m . create ( ctx , task , creator , "" , message , s . Model , "byre" , nil )
}
err = m . startTeam ( ctx , task , creator , message , * s . Agents , "byre" )
if err != nil {
return api . AgentSession {}, err
}
sess , _ , err := m . liveSession ( ctx , task . Id )
return sess , err
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
func stageMessage ( frozen store . FrozenWorkflow , stage string , parents [] parentStage , defaultBranch string ) string {
b := & strings . Builder {}
fmt . Fprintf ( b , "Workflow %q, stage %q\n\n%s" , frozen . Name , stage , strings . TrimSpace ( workflow . Get ( frozen . Definition , stage ). Prompt ))
if strings . TrimSpace ( frozen . Brief ) != "" {
fmt . Fprintf ( b , "\n\n## Brief\n\n%s" , strings . TrimSpace ( frozen . Brief ))
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
for _ , p := range parents {
fmt . Fprintf ( b , "\n\n## From stage %q (task %s, branch %s)\n\n%s" , * p . task . WorkflowStage , p . task . Id , p . task . Branch , strings . TrimSpace ( p . output ))
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
switch {
case len ( parents ) == 0 :
case workflow . FromMain ( frozen . Definition , stage ):
fmt . Fprintf ( b , "\n\nThis task starts from %s. The branch of each stage above holds that stage's work: `git merge` the ones this stage needs, or read them with `git show` and `git diff`." , defaultBranch )
default :
fmt . Fprintf ( b , "\n\nThis task starts from the commit stage %q finished at, so its work is already in this worktree." , * parents [ 0 ]. task . WorkflowStage )
}
return b . String ()
2026-10-15 22:11:25 -07:00
}
2026-10-18 08:48:57 -07:00
func ( m * Manager ) recordStage ( ctx context . Context , l * live ) error {
output , err := m . store . StageOutput ( ctx , l . id )
if err != nil {
return err
}
head , err := git . Head ( l . dir )
if err != nil {
return err
}
return m . store . RecordStage ( ctx , l . taskID , output , head )
}
2026-10-15 22:11:25 -07:00
func ( m * Manager ) advanceStage ( l * live ) {
2026-10-15 22:53:54 -07:00
ctx := context . Background ()
_ , err := m . nextStage ( ctx , l . id )
2026-10-15 22:11:25 -07:00
if err != nil {
slog . Error ( "advance workflow stage" , "session" , l . id , "err" , err )
2026-10-18 15:06:06 -07:00
m . recordError ( l , fmt . Sprintf ( "The workflow didn't move on from this stage: %v. byre tries again when it next starts." , err ))
2026-10-15 22:11:25 -07:00
}
2026-10-16 10:08:08 -07:00
m . afterTurn ( ctx , l )
2026-10-15 22:11:25 -07:00
}
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
}
2026-10-15 22:53:54 -07:00
for _ , n := range next {
started [ n ] = true
2026-10-15 22:11:25 -07:00
}
slog . Info ( "advanced workflow stage" , "session" , id , "next" , next )
}
return started , nil
}
2026-10-15 22:53:54 -07:00
func ( m * Manager ) nextStage ( ctx context . Context , sessionID string ) ([] string , error ) {
2026-10-15 22:11:25 -07:00
sess , err := m . store . GetSession ( ctx , sessionID )
if err != nil {
2026-10-15 22:53:54 -07:00
return nil , err
2026-10-15 22:11:25 -07:00
}
task , err := m . store . GetTask ( ctx , sess . TaskId )
if err != nil {
2026-10-15 22:53:54 -07:00
return nil , err
2026-10-15 22:11:25 -07:00
}
2026-10-16 10:08:08 -07:00
discard , err := m . store . GetDiscard ( ctx , task . Id )
if err != nil {
return nil , err
}
if task . Status != api . TaskStatusActive || discard . State == store . DiscardClosing {
2026-10-15 22:53:54 -07:00
return nil , m . store . ClearStage ( ctx , sess . Id )
2026-10-15 22:11:25 -07:00
}
frozen , err := m . store . TaskWorkflow ( ctx , task . Id )
if err != nil {
2026-10-15 22:53:54 -07:00
return nil , err
2026-10-15 22:11:25 -07:00
}
output , err := m . store . StageOutput ( ctx , sess . Id )
if err != nil {
2026-10-15 22:53:54 -07:00
return nil , err
}
recorded , _ , err := m . store . StageResult ( ctx , task . Id )
if err != nil {
return nil , err
}
if recorded == "" {
head , err := git . Head ( task . WorktreePath )
if err != nil {
return nil , err
}
err = m . store . RecordStage ( ctx , task . Id , output , head )
if err != nil {
return nil , err
}
}
m . workflowMu . Lock ()
defer m . workflowMu . Unlock ()
next := workflow . Next ( frozen . Definition , * task . WorkflowStage )
if len ( next ) == 0 {
err = m . discardStages ( ctx , task )
if err != nil {
return nil , err
}
return nil , m . store . FinishWorkflow ( ctx , task , sess . Id , frozen . Name , output )
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
started := [] string {}
for _ , n := range next {
2026-10-18 08:48:57 -07:00
ids , err := m . advanceTo ( ctx , task , frozen , n )
2026-10-15 22:53:54 -07:00
if err != nil {
return started , err
}
2026-10-18 08:48:57 -07:00
started = append ( started , ids ... )
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
return started , m . store . ClearStage ( ctx , sess . Id )
}
2026-10-15 22:11:25 -07:00
2026-10-18 08:48:57 -07:00
func ( m * Manager ) advanceTo ( ctx context . Context , from api . Task , frozen store . FrozenWorkflow , stage string ) ([] string , error ) {
2026-10-15 22:53:54 -07:00
tasks , err := m . store . RunTasks ( ctx , * from . WorkflowRun )
2026-10-15 22:11:25 -07:00
if err != nil {
2026-10-18 08:48:57 -07:00
return nil , err
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
parents := [] parentStage {}
for _ , a := range workflow . After ( frozen . Definition , stage ) {
p , found , err := m . finishedStage ( ctx , tasks , a )
if err != nil || ! found {
2026-10-18 08:48:57 -07:00
return nil , err
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
parents = append ( parents , p )
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
project , err := m . store . GetProject ( ctx , from . ProjectId )
2026-10-15 22:11:25 -07:00
if err != nil {
2026-10-18 08:48:57 -07:00
return nil , err
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
creator , err := m . store . GetUser ( ctx , from . CreatedBy )
if err != nil {
2026-10-18 08:48:57 -07:00
return nil , err
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
existing , found := latestStage ( tasks , stage )
if found {
2026-10-16 10:08:08 -07:00
if existing . Status != api . TaskStatusActive || existing . Discarding {
2026-10-18 08:48:57 -07:00
return nil , nil
2026-10-15 22:53:54 -07:00
}
return m . resumeStage ( ctx , existing , project , creator , frozen , parents )
}
base := parents [ 0 ]. commit
if workflow . FromMain ( frozen . Definition , stage ) {
base = project . DefaultBranch
}
task , err := m . startStage ( ctx , project , creator , frozen , * from . WorkflowRun , stage , base , from . TodoItem , parents , "byre" )
2026-10-15 22:11:25 -07:00
if err != nil {
2026-10-18 08:48:57 -07:00
return nil , err
2026-10-15 22:11:25 -07:00
}
2026-10-18 08:48:57 -07:00
return m . sessionIDs ( ctx , task . Id )
}
func ( m * Manager ) sessionIDs ( ctx context . Context , taskID string ) ([] string , error ) {
sessions , err := m . store . ListSessions ( ctx , taskID )
if err != nil {
return nil , err
}
ids := [] string {}
for _ , s := range sessions {
ids = append ( ids , s . Id )
2026-10-15 22:11:25 -07:00
}
2026-10-18 08:48:57 -07:00
return ids , nil
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
func latestStage ( tasks [] api . Task , stage string ) ( api . Task , bool ) {
for i := len ( tasks ) - 1 ; i >= 0 ; i -- {
if * tasks [ i ]. WorkflowStage == stage {
return tasks [ i ], true
}
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
return api . Task {}, false
}
func ( m * Manager ) finishedStage ( ctx context . Context , tasks [] api . Task , stage string ) ( parentStage , bool , error ) {
for i := len ( tasks ) - 1 ; i >= 0 ; i -- {
if * tasks [ i ]. WorkflowStage != stage {
2026-10-15 22:11:25 -07:00
continue
}
2026-10-15 22:53:54 -07:00
output , commit , err := m . store . StageResult ( ctx , tasks [ i ]. Id )
if err != nil {
return parentStage {}, false , err
}
if output != "" {
return parentStage { task : tasks [ i ], output : output , commit : commit }, true , nil
}
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
return parentStage {}, false , nil
2026-10-15 22:11:25 -07:00
}
2026-10-18 08:48:57 -07:00
func ( m * Manager ) resumeStage ( ctx context . Context , task api . Task , project api . Project , creator api . User , frozen store . FrozenWorkflow , parents [] parentStage ) ([] string , error ) {
2026-10-15 22:53:54 -07:00
sessions , err := m . store . ListSessions ( ctx , task . Id )
if err != nil {
2026-10-18 08:48:57 -07:00
return nil , err
2026-10-15 22:53:54 -07:00
}
if len ( sessions ) == 0 {
2026-10-18 08:48:57 -07:00
_ , err := m . beginStage ( ctx , task , project , creator , frozen , parents , "byre" )
if err != nil {
return nil , err
}
return m . sessionIDs ( ctx , task . Id )
2026-10-15 22:11:25 -07:00
}
2026-10-18 08:48:57 -07:00
s := workflow . Get ( frozen . Definition , * task . WorkflowStage )
ids := [] string {}
for _ , sess := range sessions {
started , err := m . store . SessionStarted ( ctx , sess . Id )
2026-10-15 22:11:25 -07:00
if err != nil {
2026-10-18 08:48:57 -07:00
return ids , err
}
if started || sess . State == api . AgentSessionStateCompleted {
continue
}
if sess . State == api . AgentSessionStateRunning {
err = m . store . SetSessionState ( ctx , sess . Id , api . AgentSessionStateIdle , "byre" , nil )
if err != nil {
return ids , err
}
}
message := stageMessage ( frozen , * task . WorkflowStage , parents , project . DefaultBranch )
if s . Agents != nil {
mem , err := teamMember ( * s . Agents , sess . Member )
if err != nil {
return ids , err
}
message = teamPrompt ( message , mem )
}
_ , err = m . turn ( ctx , sess , task , "byre" , message , nil )
if err != nil {
return ids , err
}
ids = append ( ids , sess . Id )
}
return ids , nil
}
func teamMember ( members [] api . TeamMember , name string ) ( api . TeamMember , error ) {
for _ , mem := range members {
if mem . Name == name {
return mem , nil
2026-10-15 22:11:25 -07:00
}
}
2026-10-18 08:48:57 -07:00
return api . TeamMember {}, fmt . Errorf ( "the stage's frozen workflow has no agent %q" , name )
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
func ( m * Manager ) discardStages ( ctx context . Context , final api . Task ) error {
tasks , err := m . store . RunTasks ( ctx , * final . WorkflowRun )
if err != nil {
return err
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
for _ , t := range tasks {
if t . Id == final . Id || t . Status != api . TaskStatusActive {
continue
}
2026-10-16 10:08:08 -07:00
_ , err = m . Discard ( ctx , t , "byre" )
if errors . Is ( err , ErrConflict ) {
slog . Info ( "stage task not discarded" , "task" , t . Id , "err" , err )
continue
}
2026-10-15 22:53:54 -07:00
if err != nil {
return fmt . Errorf ( "discard stage task %s: %w" , t . Id , err )
}
}
return nil
}
func ( m * Manager ) workflowStatus ( ctx context . Context , task api . Task ) ( string , error ) {
finished , err := m . store . WorkflowFinished ( ctx , * task . WorkflowRun )
2026-10-15 22:11:25 -07:00
if err != nil {
return "" , err
}
2026-10-15 22:53:54 -07:00
output , _ , err := m . store . StageResult ( ctx , task . Id )
2026-10-15 22:11:25 -07:00
if err != nil {
return "" , err
}
2026-10-15 22:53:54 -07:00
status := fmt . Sprintf ( "stage %q" , * task . WorkflowStage )
if output != "" {
status += " (finished)"
}
if finished {
status += ", workflow finished"
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
return status , nil
2026-10-15 22:11:25 -07:00
}
func ( m * Manager ) finishStageTool ( l * live ) mcp . Tool {
return mcp . Tool {
Name : "finish_stage" ,
2026-10-15 22:53:54 -07:00
Description : "Finish this task's stage of its workflow. Call it once the stage's work is done; when this turn ends, byre hands the output to the stages that come after this one, " +
"each of which starts in its own task once every stage it comes after has finished (after the final stage, the workflow is finished). Calling it again in the same turn replaces the output. " +
2026-10-15 22:11:25 -07:00
"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": {
2026-10-15 22:53:54 -07:00
"output": {"type": "string", "description": "What the stages after this one start from: everything they need from this stage, since they see nothing else of your context. For the final stage, what the workflow produced."}
2026-10-15 22:11:25 -07:00
},
"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 )
}
2026-10-15 22:53:54 -07:00
recorded , _ , err := m . store . StageResult ( ctx , l . taskID )
if err != nil {
return "" , err
}
if recorded != "" {
return "" , fmt . Errorf ( "%w: stage %q already finished; its output went to the stages after it" , ErrConflict , l . stage )
}
frozen , err := m . store . TaskWorkflow ( ctx , l . taskID )
if err != nil {
return "" , err
}
2026-10-15 22:11:25 -07:00
err = m . store . SetStageOutput ( ctx , l . id , args . Output )
if err != nil {
return "" , err
}
2026-10-15 22:53:54 -07:00
next := workflow . Next ( frozen . Definition , l . stage )
if len ( next ) == 0 {
return "This is the workflow's final stage: it finishes when this turn ends, and byre discards the workflow's other stage tasks. End your turn now with your summary." , nil
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
return fmt . Sprintf ( "When this turn ends, byre hands the output to %s. End your turn now with your summary." , quoteList ( next )), nil
2026-10-15 22:11:25 -07:00
},
}
}
2026-10-15 22:53:54 -07:00
func describeTasks ( tasks [] api . Task ) string {
parts := [] string {}
for _ , t := range tasks {
parts = append ( parts , fmt . Sprintf ( "task %s %q from %s" , t . Id , t . Title , t . BaseRef ))
}
return strings . Join ( parts , ", " )
}
func quoteList ( names [] string ) string {
quoted := [] string {}
for _ , n := range names {
quoted = append ( quoted , fmt . Sprintf ( "%q" , n ))
}
return strings . Join ( quoted , ", " )
}
func stageGuidance ( frozen store . FrozenWorkflow , stage string ) string {
next := workflow . Next ( frozen . Definition , stage )
then := "it is the workflow's final stage, so the workflow then finishes and byre discards its other stage tasks"
if len ( next ) > 0 {
then = fmt . Sprintf ( "byre then hands the output to %s, each in its own task, which gets the output and nothing else of your context" , quoteList ( next ))
}
return fmt . Sprintf ( "\n- This task runs stage %q of byre workflow %q. When this stage's work is done, call byre's finish_stage tool with its output, then end your turn: " +
"%s. Until you call it, the workflow waits on this stage." , stage , frozen . Name , then )
2026-10-15 22:11:25 -07:00
}
2026-10-19 15:45:08 -07:00
const workflowFileHelp = "The file's YAML format is the read_knowledge article workflow-files."
2026-10-15 21:17:58 -07:00
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
}),
}
}