Add weighted random workflow steps and a random choice agent tool

This commit is contained in:
Ian Gulliver
2026-10-23 08:22:57 -07:00
parent f18dd88465
commit 6bbae3fffe
12 changed files with 673 additions and 388 deletions
+10 -5
View File
@@ -11,8 +11,9 @@ Each step is exactly one of:
| `stage` | A stage, by its name: one line, unique in the workflow. It runs as its own task, with `model` or `agents`, and `prompt`. |
| `order` | A list of steps run one after another. |
| `parallel` | A list of branches run at the same time, each a step (an `order` for a branch of several). |
| `random` | A list of steps of which one runs, picked when a run or review starts. Each step in it has a `weight` from 0 to 1, its chance of being picked, and the weights add up to 1. `weight` is refused anywhere else. |
A stage gets the outputs of the stages that end the step before it: the last stage of an `order`, or the last stage of every branch of a `parallel`. A stage with nothing before it starts from the brief.
A stage gets the outputs of the stages that end the step before it: the last stage of an `order`, or the last stage of every branch of a `parallel`. A stage with nothing before it starts from the brief. A run or review freezes the definition with each `random` replaced by the step it picked, so `read_workflow` with its `task_id` shows only what runs, and the stages of steps it didn't pick never appear in it.
| Stage field | Meaning |
|---|---|
@@ -20,18 +21,22 @@ A stage gets the outputs of the stages that end the step before it: the last sta
| `prompt` | The stage agent's instructions. |
| `agents` | In place of `model`: a list of at least two, each with `name`, `model` and `prompt`, that runs the stage as a team. They work in the stage's one task and worktree and talk through a shared board; each gets the stage's `prompt` and then its own, and the first agent finishes the stage. |
Where a stage's task branches from follows from the shape. A stage after exactly one stage starts from the commit that stage finished at. A stage after a `parallel` starts from the default branch and is told the branches it can merge. A stage with nothing before it starts where the workflow was started. A run on a brief has a task of its own, with no agent, worktree or branch, and its stage tasks are that task's subtasks; it closes once they all have. A workflow is finished once every stage has finished; when it ends with a `parallel`, each branch's last stage keeps its task to merge, and the run's other stage tasks are discarded. A review is delivered to the task's agent, every stage's findings in one message, once every stage has reviewed. An `order` or `parallel` can't be empty.
Where a stage's task branches from follows from the shape. A stage after exactly one stage starts from the commit that stage finished at. A stage after a `parallel` starts from the default branch and is told the branches it can merge. A stage with nothing before it starts where the workflow was started. A run on a brief has a task of its own, with no agent, worktree or branch, and its stage tasks are that task's subtasks; it closes once they all have. A workflow is finished once every stage has finished; when it ends with a `parallel`, each branch's last stage keeps its task to merge, and the run's other stage tasks are discarded. A review is delivered to the task's agent, every stage's findings in one message, once every stage has reviewed. An `order`, `parallel` or `random` can't be empty.
```json
{
"description": "Build a change, review and test it in parallel, then fix what they found.",
"description": "Build a change, review and test it in parallel, now and then auditing security in depth, then fix what they found.",
"steps": [
{"stage": "Build", "model": "claude-opus-5-5", "prompt": "Implement the brief."},
{"parallel": [
{"stage": "Review", "model": "gpt-6.1-sol", "prompt": "Review the Build stage's work and list the problems."},
{"stage": "Test", "model": "gemini-4-argon-high", "prompt": "Exercise the change and report what fails."}
{"stage": "Test", "model": "gemini-4-argon-high", "prompt": "Exercise the change and report what fails."},
{"random": [
{"weight": 0.8, "stage": "Security check", "model": "claude-sonnet-5-5", "prompt": "Check the change for security problems."},
{"weight": 0.2, "stage": "Security audit", "model": "claude-fable-5-1", "prompt": "Audit the change thoroughly for security problems, tracing every input it touches."}
]}
]},
{"stage": "Fix", "prompt": "Merge the branches you are given and address both reports.", "agents": [
{"stage": "Fix", "prompt": "Merge the branches you are given and address every report.", "agents": [
{"name": "fixer", "model": "claude-opus-5-5", "prompt": "Fix every problem the reports list."},
{"name": "checker", "model": "gpt-6.1-sol", "prompt": "Check each fix as the fixer lands it."}
]}
+1 -1
View File
@@ -33,7 +33,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
- You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it.
- Setup: when the human asks you to set byre up, or about the tools or macOS permissions its agents need, read the setup knowledge-base article and walk them through it, running its checks and fixes yourself with your own Bash on this Mac and with list_clients and client_bash on their Macs. Never restart byre with launchctl or tell the human to; the article says how.
- Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine. When they only hint at where it is, find it yourself. When they ask for a new one, make a local-only repository (git init, no remote, an empty first commit) where they say, or in a sensible place if they don't, and add that.
- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Each last stage's agent is asked whether its task is ready to merge as soon as its own stage has finished and been reviewed, without waiting for the other last stages. Whenever you show or propose a workflow, draw it as a fenced diagram block (its format is the diagrams knowledge-base article) of its stage names only, with no models: one row per step, a parallel's stages side by side in one row, and an arrow into each stage from the stages before it. For example, rows holding Build; Review and Test; Fix, with the arrows "build -> review", "build -> test", "review -> fix" and "test -> fix".
- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order, a parallel or a random (one of its weighted steps, picked when the run starts); every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Each last stage's agent is asked whether its task is ready to merge as soon as its own stage has finished and been reviewed, without waiting for the other last stages. Whenever you show or propose a workflow, draw it as a fenced diagram block (its format is the diagrams knowledge-base article) of its stage names only, with no models: one row per step, a parallel's stages side by side in one row, and an arrow into each stage from the stages before it. For example, rows holding Build; Review and Test; Fix, with the arrows "build -> review", "build -> test", "review -> fix" and "test -> fix".
- Schedules: when the human asks for work to run on a timer (every Monday, nightly), you manage it with list_schedules, save_schedule and delete_schedule; the human sees and edits the same schedules on the project's dashboard. Every schedule runs a saved workflow: for a one-agent chore, save a one-stage workflow with save_workflow first. On each run, byre starts the schedule's workflow as create_task with workflow would, created by byre, and you get its created signal; a run that fails to start reaches you as a signal.
- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/tskDa7e4K7305e). byre draws it in that task's color, and clicking it shows the task in the right pane. To also open one of its other views over the human's left pane, append it: #/task/<task_id>/<view>, where view is changes, files, terminal or board. To show a project's dashboard in the right pane, link to #/project/<project_id>.
- Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line <details><summary>Title</summary>, the Markdown content, then a line </details>.
+2 -2
View File
@@ -867,7 +867,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
if err != nil {
return nil, err
}
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTitlesTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool(), m.renderDiagramTool(), m.exportDiagramTool(l)}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTitlesTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l), m.bashTool(l), m.readKnowledgeTool(), m.renderDiagramTool(), m.exportDiagramTool(l), m.randomChoiceTool()}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
lead := true
if sess.Role == api.Implementer {
g += longTitleGuidance(task)
@@ -937,7 +937,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
return nil, err
}
l.notes = "\n\n" + notes
tools = append(append(m.leadTools(l), m.readKnowledgeTool(), m.renderDiagramTool(), m.exportDiagramTool(l)), m.clientShellTools()...)
tools = append(append(m.leadTools(l), m.readKnowledgeTool(), m.renderDiagramTool(), m.exportDiagramTool(l), m.randomChoiceTool()), m.clientShellTools()...)
}
env := git.IdentityEnv(l.gitName, l.gitEmail)
servers := []*mcp.Server{{Name: "byre", Version: "0.0.0", Tools: tools}}
+65
View File
@@ -0,0 +1,65 @@
package agents
import (
"context"
"encoding/json"
"fmt"
"g.fc.run/ai/byre/internal/mcp"
"g.fc.run/ai/byre/internal/workflow"
)
func (m *Manager) randomChoiceTool() mcp.Tool {
return mcp.Tool{
Name: "random_choice",
Description: "Pick one of several choices at random, each with its weight as the chance of being picked, and get back the one picked. " +
"When the weights add up to less than 1, the rest is the chance that none is picked, which comes back as \"No match\". " +
"Use it whenever you are told to choose by chance, rather than choosing yourself.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"choices": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"choice": {"type": "string", "description": "What this choice is, returned as it is when it is picked."},
"weight": {"type": "number", "minimum": 0, "maximum": 1, "description": "Its chance of being picked, from 0 to 1. The weights of all the choices add up to at most 1."}
},
"required": ["choice", "weight"]
}
}
},
"required": ["choices"]
}`),
Rich: func(ctx context.Context, raw json.RawMessage) mcp.Result {
args := struct {
Choices []struct {
Choice string `json:"choice"`
Weight float64 `json:"weight"`
} `json:"choices"`
}{}
err := json.Unmarshal(raw, &args)
if err != nil {
return mcp.TextResult(fmt.Sprintf("malformed random_choice call: %v", err), true)
}
if len(args.Choices) == 0 {
return mcp.TextResult("choices is empty", true)
}
weights := []float64{}
for _, c := range args.Choices {
weights = append(weights, c.Weight)
}
p := workflow.ChoiceWeightsProblem(weights)
if p != "" {
return mcp.TextResult(p, true)
}
i := workflow.Pick(weights, 1)
if i < 0 {
return mcp.TextResult("No match: none of the choices was picked.", false)
}
return mcp.TextResult(args.Choices[i].Choice, false)
},
}
}
+4 -2
View File
@@ -773,14 +773,16 @@ const workflowDefinitionSchema = `{
const workflowSchemaDefs = `"$defs": {
"step": {
"type": "object",
"description": "Exactly one of stage (with model or agents, and prompt), order or parallel.",
"description": "Exactly one of stage (with model or agents, and prompt), order, parallel or random. A step directly in a random also has its weight.",
"properties": {
"stage": {"type": "string"},
"model": {"type": "string"},
"prompt": {"type": "string"},
"agents": {"type": "array", "items": {"$ref": "#/$defs/agent"}},
"order": {"type": "array", "items": {"$ref": "#/$defs/step"}},
"parallel": {"type": "array", "items": {"$ref": "#/$defs/step"}}
"parallel": {"type": "array", "items": {"$ref": "#/$defs/step"}},
"random": {"type": "array", "items": {"$ref": "#/$defs/step"}},
"weight": {"type": "number", "minimum": 0, "maximum": 1}
},
"additionalProperties": false
},