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
+64 -15
View File
@@ -9,7 +9,7 @@ typealias TeamMember = Components.Schemas.TeamMember
@Observable
final class StepNode: Identifiable {
enum Kind {
case stage, order, parallel
case stage, order, parallel, random
}
let id = UUID()
@@ -17,6 +17,7 @@ final class StepNode: Identifiable {
var name = ""
var model = ""
var prompt = ""
var weight = 0.0
var agents: [TeamMember]?
var children: [StepNode] = []
weak var parent: StepNode?
@@ -35,6 +36,8 @@ final class StepNode: Identifiable {
self.init(kind: .order, children: order.map { StepNode($0) })
} else if let parallel = step.parallel {
self.init(kind: .parallel, children: parallel.map { StepNode($0) })
} else if let random = step.random {
self.init(kind: .random, children: random.map { StepNode($0) })
} else {
self.init(kind: .stage)
name = step.stage ?? ""
@@ -42,19 +45,27 @@ final class StepNode: Identifiable {
prompt = step.prompt ?? ""
agents = step.agents
}
weight = step.weight ?? 0
}
var step: WorkflowStep {
switch kind {
let w = parent?.kind == .random ? weight : nil
return switch kind {
case .stage:
WorkflowStep(stage: name, model: agents == nil ? model : nil, prompt: prompt, agents: agents)
WorkflowStep(stage: name, model: agents == nil ? model : nil, prompt: prompt, weight: w, agents: agents)
case .order:
WorkflowStep(order: children.map(\.step))
WorkflowStep(order: children.map(\.step), weight: w)
case .parallel:
WorkflowStep(parallel: children.map(\.step))
WorkflowStep(parallel: children.map(\.step), weight: w)
case .random:
WorkflowStep(random: children.map(\.step), weight: w)
}
}
var odds: Double {
parent?.kind == .random ? weight : 1
}
var index: Int {
parent!.children.firstIndex { $0 === self }!
}
@@ -132,21 +143,27 @@ func stageDiagram(_ stages: [GraphStage]) -> String {
return lines.joined(separator: "\n")
}
private func percent(_ odds: Double) -> String {
(odds * 100).formatted(.number.precision(.fractionLength(0)).grouping(.never)) + "%"
}
@MainActor
private func stepDiagram(_ root: StepNode) -> String {
var stages: [GraphStage] = []
func walk(_ node: StepNode, _ before: [Int]) -> [Int] {
func walk(_ node: StepNode, _ before: [Int], _ odds: Double) -> [Int] {
let odds = odds * node.odds
switch node.kind {
case .order:
return node.children.reduce(before) { walk($1, $0) }
case .parallel:
return node.children.flatMap { walk($0, before) }
return node.children.reduce(before) { walk($1, $0, odds) }
case .parallel, .random:
return node.children.flatMap { walk($0, before, odds) }
case .stage:
stages.append(GraphStage(name: node.name, agents: node.agents, after: before))
let name = odds < 1 ? "\(node.name) · \(percent(odds))" : node.name
stages.append(GraphStage(name: name, agents: node.agents, after: before))
return [stages.count - 1]
}
}
_ = walk(root, [])
_ = walk(root, [], 1)
return stageDiagram(stages)
}
@@ -318,6 +335,9 @@ private struct StepRow: View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(guide).foregroundStyle(Palette.muted)
if node.parent?.kind == .random {
WeightField(node: node)
}
switch node.kind {
case .stage:
Button {
@@ -343,6 +363,9 @@ private struct StepRow: View {
case .parallel:
Text("in parallel").foregroundStyle(Palette.muted)
Spacer()
case .random:
Text("one at random").foregroundStyle(Palette.muted)
Spacer()
}
actions
}
@@ -386,11 +409,12 @@ private struct StepRow: View {
Button("Add stage after") { addAfter() }
Button("Run in parallel with a new stage") { wrap(.parallel) }
Button("Group with a new stage in order") { wrap(.order) }
case .order, .parallel:
Button("Pick at random between it and a new stage") { wrap(.random) }
case .order, .parallel, .random:
Button("Add stage inside") { node.insert(stage(), at: node.children.count) }
Button("Add stage after") { addAfter() }
Button(node.kind == .order ? "Change to in parallel" : "Change to in order") {
node.kind = node.kind == .order ? .parallel : .order
ForEach(groupKinds.filter { $0.0 != node.kind }, id: \.1) { kind, label in
Button("Change to \(label)") { node.kind = kind }
}
Button("Ungroup") { ungroup() }
}
@@ -416,6 +440,8 @@ private struct StepRow: View {
.fixedSize()
}
private let groupKinds: [(StepNode.Kind, String)] = [(.order, "in order"), (.parallel, "in parallel"), (.random, "one at random")]
private func stage() -> StepNode {
newStage(root, store.defaultModel)
}
@@ -429,8 +455,14 @@ private struct StepRow: View {
let i = node.index
node.detach()
let group = StepNode(kind: kind)
let added = stage()
group.weight = node.weight
if kind == .random {
node.weight = 0.5
added.weight = 0.5
}
group.insert(node, at: 0)
group.insert(stage(), at: 1)
group.insert(added, at: 1)
parent.insert(group, at: i)
}
@@ -477,6 +509,23 @@ private struct StepRow: View {
}
}
private struct WeightField: View {
@Bindable var node: StepNode
var body: some View {
HStack(spacing: 4) {
TextField("weight", value: $node.weight, format: .number)
.textFieldStyle(.plain)
.multilineTextAlignment(.trailing)
.frame(width: 36)
.padding(.horizontal, 4)
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 4))
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Palette.border))
}
.help("Weight: its chance of being picked, from 0 to 1. The weights in one random add up to 1.")
}
}
struct ModelPicker: View {
@Binding var model: String