mirror of
https://github.com/Hmbown/DeepSeek-TUI.git
synced 2026-09-03 06:50:13 +08:00
whaleflow: bound fleet workflow shape
Add Fleet launch-shape validation for WhaleFlow IR with a 100-agent default population cap, five recursive rings, and bounded loop/expand requirements. Update the Fleet setup surface and docs to frame Fleet as the durable sub-agent config layer while WhaleFlow owns agent-authored orchestration.
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added WhaleFlow-to-Fleet launch-shape validation: the default Fleet workflow
|
||||
contract allows up to 100 total agents and 5 recursive rings, requires
|
||||
bounded loops/expands before launch, and preserves per-slot model selection.
|
||||
|
||||
### Changed
|
||||
|
||||
- Clarified the Fleet setup surface and docs so Fleet is treated as the durable
|
||||
sub-agent configuration layer while WhaleFlow is the agent-authored
|
||||
orchestration plan that selects and monitors Fleet slots.
|
||||
|
||||
## [0.8.65] - 2026-06-24
|
||||
|
||||
### Added
|
||||
|
||||
@@ -391,6 +391,12 @@ fn build_lanes(snapshot: &FleetSetupSnapshot) -> Vec<FleetSetupLane> {
|
||||
FleetSetupRow::new("fast", "scout", "opt-in low-latency fanout"),
|
||||
FleetSetupRow::new("balanced", "auto class", "normal build/review when chosen")
|
||||
.tone(RowTone::Ready),
|
||||
FleetSetupRow::new(
|
||||
"preset",
|
||||
"editable",
|
||||
"recommended tiers never force the orchestrator",
|
||||
)
|
||||
.tone(RowTone::Current),
|
||||
FleetSetupRow::new("strong", "hard", "security, release, architecture"),
|
||||
FleetSetupRow::new(
|
||||
"fixed model",
|
||||
@@ -459,6 +465,18 @@ fn build_lanes(snapshot: &FleetSetupSnapshot) -> Vec<FleetSetupLane> {
|
||||
title: "5 Org",
|
||||
subtitle: "team and recursion",
|
||||
rows: vec![
|
||||
FleetSetupRow::new(
|
||||
"Fleet config",
|
||||
"sub-agents",
|
||||
"durable slots, profiles, models, tools, ledger",
|
||||
)
|
||||
.tone(RowTone::Current),
|
||||
FleetSetupRow::new(
|
||||
"WhaleFlow",
|
||||
"agent plan",
|
||||
"agent-authored workflow selects and monitors slots",
|
||||
)
|
||||
.tone(RowTone::Ready),
|
||||
FleetSetupRow::new(
|
||||
"role workers",
|
||||
if snapshot.subagents_enabled {
|
||||
@@ -486,25 +504,20 @@ fn build_lanes(snapshot: &FleetSetupSnapshot) -> Vec<FleetSetupLane> {
|
||||
)
|
||||
.tone(RowTone::Ready),
|
||||
FleetSetupRow::new(
|
||||
"starter team",
|
||||
"3 scout + 1 each",
|
||||
"builder, reviewer, verifier, synthesizer, operator",
|
||||
"slot grid",
|
||||
"1-5 slots",
|
||||
"add or drill right into a recursive ring",
|
||||
)
|
||||
.tone(RowTone::Ready),
|
||||
FleetSetupRow::new(
|
||||
"scout tree",
|
||||
"3 scouts",
|
||||
"recursive exploration splits breadth-first",
|
||||
"limits",
|
||||
"100 total / depth 5",
|
||||
"workflow population cap, separate from launch concurrency",
|
||||
),
|
||||
FleetSetupRow::new(
|
||||
"builder tree",
|
||||
"builder+reviewer",
|
||||
"implementation gets paired review by default",
|
||||
),
|
||||
FleetSetupRow::new(
|
||||
"verifier tree",
|
||||
"verifier+reviewer",
|
||||
"test evidence gets interpreted before handoff",
|
||||
"DeepSeek preset",
|
||||
"Pro -> Flash",
|
||||
"editable per slot; cheaper as rings expand",
|
||||
),
|
||||
FleetSetupRow::new(
|
||||
"budget",
|
||||
@@ -697,9 +710,12 @@ fn profile_authoring_prompt(
|
||||
Do not include provider, base_url, api_key, auth, secrets, trust, allow_shell, or approval_required=false.\n\
|
||||
If model is present, keep it to a visible model id such as deepseek-v4-pro or glm-5.2.\n\
|
||||
Fleet product shape:\n\
|
||||
- Fleet is the durable sub-agent config surface: slots, profiles, models, tools, and ledger\n\
|
||||
- one main orchestrator profile coordinates the Fleet run and verifies returned claims\n\
|
||||
- workers are summoned as focused Fleet members with only their assigned slice\n\
|
||||
- default model behavior is same-route inheritance; choose fast/strong/code/review only when the role needs it\n\
|
||||
- DeepSeek-style model tiers are recommendations, not hierarchy rules; every slot may override model\n\
|
||||
- WhaleFlow plans may select and monitor Fleet slots, but Fleet owns the worker config\n\
|
||||
- do not encode a recursive worker tree in [instructions].text; topology belongs to the orchestrator, not each worker\n\n\
|
||||
Keep the profile permission-narrowing and compatible with recursive Fleet role workers.",
|
||||
provider = snapshot.provider,
|
||||
@@ -767,8 +783,11 @@ mod tests {
|
||||
assert!(text.contains("provider = DeepSeek"));
|
||||
assert!(text.contains("model (optional explicit model id"));
|
||||
assert!(text.contains("Do not include provider, base_url"));
|
||||
assert!(text.contains("Fleet is the durable sub-agent config surface"));
|
||||
assert!(text.contains("workers are summoned as focused Fleet members"));
|
||||
assert!(text.contains("default model behavior is same-route inheritance"));
|
||||
assert!(text.contains("model tiers are recommendations"));
|
||||
assert!(text.contains("Fleet owns the worker config"));
|
||||
assert!(text.contains("topology belongs to the orchestrator"));
|
||||
}
|
||||
other => panic!("expected profile prompt insertion, got {other:?}"),
|
||||
@@ -811,6 +830,10 @@ mod tests {
|
||||
assert!(view.profile_prompt().contains("Current route context only"));
|
||||
assert!(view.profile_prompt().contains("permission-narrowing"));
|
||||
assert!(view.profile_prompt().contains("same-route inheritance"));
|
||||
assert!(
|
||||
view.profile_prompt()
|
||||
.contains("durable sub-agent config surface")
|
||||
);
|
||||
assert!(
|
||||
view.profile_prompt()
|
||||
.contains("topology belongs to the orchestrator")
|
||||
@@ -833,7 +856,13 @@ mod tests {
|
||||
view.lanes
|
||||
.iter()
|
||||
.flat_map(|lane| lane.rows.iter())
|
||||
.any(|row| row.label == "starter team" && row.value == "3 scout + 1 each")
|
||||
.any(|row| row.label == "slot grid" && row.value == "1-5 slots")
|
||||
);
|
||||
assert!(
|
||||
view.lanes
|
||||
.iter()
|
||||
.flat_map(|lane| lane.rows.iter())
|
||||
.any(|row| row.label == "limits" && row.value == "100 total / depth 5")
|
||||
);
|
||||
view.render(Rect::new(0, 0, 120, 32), &mut buf);
|
||||
let rendered = buf
|
||||
@@ -844,5 +873,6 @@ mod tests {
|
||||
|
||||
assert!(rendered.contains("Fleet"));
|
||||
assert!(rendered.contains("recursion"));
|
||||
assert!(rendered.contains("WhaleFlow"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ pub use starlark_authoring::{
|
||||
compile_starlark_workflow, compile_starlark_workflow_with_repair, repair_starlark_workflow_once,
|
||||
};
|
||||
|
||||
pub const DEFAULT_FLEET_WORKFLOW_MAX_AGENTS: usize = 100;
|
||||
pub const DEFAULT_FLEET_WORKFLOW_MAX_DEPTH: usize = 5;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WorkflowConfig {
|
||||
pub goal: String,
|
||||
@@ -67,6 +70,34 @@ pub struct WorkflowSpec {
|
||||
pub nodes: Vec<WorkflowNode>,
|
||||
}
|
||||
|
||||
impl WorkflowSpec {
|
||||
pub fn validate_for_fleet(&self) -> Result<WorkflowFleetShape, WorkflowFleetLimitError> {
|
||||
self.validate_for_fleet_with_limits(WorkflowFleetLimits::default())
|
||||
}
|
||||
|
||||
pub fn validate_for_fleet_with_limits(
|
||||
&self,
|
||||
limits: WorkflowFleetLimits,
|
||||
) -> Result<WorkflowFleetShape, WorkflowFleetLimitError> {
|
||||
validate_workflow_nodes(&self.nodes)
|
||||
.map_err(|source| WorkflowFleetLimitError::InvalidWorkflow { source })?;
|
||||
let shape = estimate_fleet_shape(&self.nodes)?;
|
||||
if shape.total_agents > limits.max_total_agents {
|
||||
return Err(WorkflowFleetLimitError::TooManyAgents {
|
||||
total_agents: shape.total_agents,
|
||||
max_total_agents: limits.max_total_agents,
|
||||
});
|
||||
}
|
||||
if shape.max_depth > limits.max_depth {
|
||||
return Err(WorkflowFleetLimitError::RecursionTooDeep {
|
||||
depth: shape.max_depth,
|
||||
max_depth: limits.max_depth,
|
||||
});
|
||||
}
|
||||
Ok(shape)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", content = "spec", rename_all = "snake_case")]
|
||||
pub enum WorkflowNode {
|
||||
@@ -1481,6 +1512,127 @@ pub enum WorkflowExecutionError {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct WorkflowFleetLimits {
|
||||
pub max_total_agents: usize,
|
||||
pub max_depth: usize,
|
||||
}
|
||||
|
||||
impl Default for WorkflowFleetLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_total_agents: DEFAULT_FLEET_WORKFLOW_MAX_AGENTS,
|
||||
max_depth: DEFAULT_FLEET_WORKFLOW_MAX_DEPTH,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct WorkflowFleetShape {
|
||||
pub total_agents: usize,
|
||||
pub max_depth: usize,
|
||||
}
|
||||
|
||||
impl WorkflowFleetShape {
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
total_agents: self.total_agents.saturating_add(other.total_agents),
|
||||
max_depth: self.max_depth.max(other.max_depth),
|
||||
}
|
||||
}
|
||||
|
||||
fn repeat(self, times: usize) -> Self {
|
||||
Self {
|
||||
total_agents: self.total_agents.saturating_mul(times),
|
||||
max_depth: self.max_depth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum WorkflowFleetLimitError {
|
||||
#[error("workflow IR is invalid for Fleet: {source}")]
|
||||
InvalidWorkflow {
|
||||
#[from]
|
||||
source: WorkflowExecutionError,
|
||||
},
|
||||
#[error(
|
||||
"workflow would launch {total_agents} agents; Fleet WhaleFlow limit is {max_total_agents}"
|
||||
)]
|
||||
TooManyAgents {
|
||||
total_agents: usize,
|
||||
max_total_agents: usize,
|
||||
},
|
||||
#[error("workflow reaches recursion depth {depth}; Fleet WhaleFlow limit is {max_depth}")]
|
||||
RecursionTooDeep { depth: usize, max_depth: usize },
|
||||
#[error("expand node `{node}` must declare max_children before Fleet launch")]
|
||||
UnboundedExpand { node: String },
|
||||
#[error("expand node `{node}` must include a template before Fleet launch")]
|
||||
MissingExpandTemplate { node: String },
|
||||
#[error("loop_until node `{node}` must declare max_iterations before Fleet launch")]
|
||||
UnboundedLoop { node: String },
|
||||
}
|
||||
|
||||
fn estimate_fleet_shape(
|
||||
nodes: &[WorkflowNode],
|
||||
) -> Result<WorkflowFleetShape, WorkflowFleetLimitError> {
|
||||
estimate_fleet_shape_at_depth(nodes, 1)
|
||||
}
|
||||
|
||||
fn estimate_fleet_shape_at_depth(
|
||||
nodes: &[WorkflowNode],
|
||||
depth: usize,
|
||||
) -> Result<WorkflowFleetShape, WorkflowFleetLimitError> {
|
||||
nodes
|
||||
.iter()
|
||||
.try_fold(WorkflowFleetShape::default(), |shape, node| {
|
||||
Ok(shape.add(estimate_node_fleet_shape(node, depth)?))
|
||||
})
|
||||
}
|
||||
|
||||
fn estimate_node_fleet_shape(
|
||||
node: &WorkflowNode,
|
||||
depth: usize,
|
||||
) -> Result<WorkflowFleetShape, WorkflowFleetLimitError> {
|
||||
match node {
|
||||
WorkflowNode::Leaf(_) => Ok(WorkflowFleetShape {
|
||||
total_agents: 1,
|
||||
max_depth: depth,
|
||||
}),
|
||||
WorkflowNode::BranchSet(spec) => estimate_fleet_shape_at_depth(&spec.children, depth + 1),
|
||||
WorkflowNode::Sequence(spec) => estimate_fleet_shape_at_depth(&spec.children, depth),
|
||||
WorkflowNode::Reduce(_) | WorkflowNode::TeacherReview(_) => Ok(WorkflowFleetShape {
|
||||
total_agents: 0,
|
||||
max_depth: 0,
|
||||
}),
|
||||
WorkflowNode::LoopUntil(spec) => {
|
||||
let iterations =
|
||||
spec.max_iterations
|
||||
.ok_or_else(|| WorkflowFleetLimitError::UnboundedLoop {
|
||||
node: spec.id.clone(),
|
||||
})? as usize;
|
||||
Ok(estimate_fleet_shape_at_depth(&spec.children, depth)?.repeat(iterations.max(1)))
|
||||
}
|
||||
WorkflowNode::Cond(spec) => Ok(estimate_fleet_shape_at_depth(&spec.then_nodes, depth)?
|
||||
.add(estimate_fleet_shape_at_depth(&spec.else_nodes, depth)?)),
|
||||
WorkflowNode::Expand(spec) => {
|
||||
let max_children =
|
||||
spec.max_children
|
||||
.ok_or_else(|| WorkflowFleetLimitError::UnboundedExpand {
|
||||
node: spec.id.clone(),
|
||||
})?;
|
||||
let template = spec.template.as_deref().ok_or_else(|| {
|
||||
WorkflowFleetLimitError::MissingExpandTemplate {
|
||||
node: spec.id.clone(),
|
||||
}
|
||||
})?;
|
||||
validate_workflow_node_shapes(std::slice::from_ref(template))
|
||||
.map_err(|source| WorkflowFleetLimitError::InvalidWorkflow { source })?;
|
||||
Ok(estimate_node_fleet_shape(template, depth)?.repeat(max_children))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_frontier_limit() -> usize {
|
||||
8
|
||||
}
|
||||
@@ -2308,6 +2460,149 @@ mod tests {
|
||||
assert_eq!(minimal.model_policy, ModelPolicy::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fleet_validation_accepts_one_hundred_agents_and_variable_models() {
|
||||
let nodes = (0..DEFAULT_FLEET_WORKFLOW_MAX_AGENTS)
|
||||
.map(|index| {
|
||||
let mut leaf = match leaf_node(&format!("agent-{index}")) {
|
||||
WorkflowNode::Leaf(leaf) => leaf,
|
||||
_ => unreachable!("leaf helper returns a leaf"),
|
||||
};
|
||||
leaf.model_policy = if index == 0 {
|
||||
ModelPolicy {
|
||||
provider: Some("deepseek".to_string()),
|
||||
model: Some("deepseek-v4-pro".to_string()),
|
||||
fallback_models: Vec::new(),
|
||||
}
|
||||
} else {
|
||||
ModelPolicy {
|
||||
provider: Some("deepseek".to_string()),
|
||||
model: Some("deepseek-v4-flash".to_string()),
|
||||
fallback_models: Vec::new(),
|
||||
}
|
||||
};
|
||||
WorkflowNode::Leaf(leaf)
|
||||
})
|
||||
.collect();
|
||||
let workflow = workflow_spec(nodes);
|
||||
|
||||
let shape = workflow
|
||||
.validate_for_fleet()
|
||||
.expect("one hundred agents should fit the Fleet WhaleFlow limit");
|
||||
|
||||
assert_eq!(shape.total_agents, DEFAULT_FLEET_WORKFLOW_MAX_AGENTS);
|
||||
assert_eq!(shape.max_depth, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fleet_validation_rejects_more_than_one_hundred_agents() {
|
||||
let nodes = (0..=DEFAULT_FLEET_WORKFLOW_MAX_AGENTS)
|
||||
.map(|index| leaf_node(&format!("agent-{index}")))
|
||||
.collect();
|
||||
let workflow = workflow_spec(nodes);
|
||||
|
||||
let err = workflow
|
||||
.validate_for_fleet()
|
||||
.expect_err("agent population should be bounded before Fleet launch");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
WorkflowFleetLimitError::TooManyAgents {
|
||||
total_agents: DEFAULT_FLEET_WORKFLOW_MAX_AGENTS + 1,
|
||||
max_total_agents: DEFAULT_FLEET_WORKFLOW_MAX_AGENTS,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fleet_validation_rejects_depth_beyond_five() {
|
||||
let mut node = leaf_node("deep-leaf");
|
||||
for depth in (0..DEFAULT_FLEET_WORKFLOW_MAX_DEPTH).rev() {
|
||||
node = WorkflowNode::BranchSet(BranchSpec {
|
||||
id: format!("ring-{depth}"),
|
||||
description: None,
|
||||
parallel: true,
|
||||
budget: BudgetSpec::default(),
|
||||
permissions: PermissionSpec::default(),
|
||||
model_policy: ModelPolicy::default(),
|
||||
children: vec![node],
|
||||
});
|
||||
}
|
||||
let workflow = workflow_spec(vec![node]);
|
||||
|
||||
let err = workflow
|
||||
.validate_for_fleet()
|
||||
.expect_err("sixth agent ring should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
err,
|
||||
WorkflowFleetLimitError::RecursionTooDeep {
|
||||
depth: DEFAULT_FLEET_WORKFLOW_MAX_DEPTH + 1,
|
||||
max_depth: DEFAULT_FLEET_WORKFLOW_MAX_DEPTH,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fleet_validation_counts_loop_and_expand_fanout_conservatively() {
|
||||
let workflow = workflow_spec(vec![
|
||||
WorkflowNode::LoopUntil(LoopUntilSpec {
|
||||
id: "retry-ring".to_string(),
|
||||
condition: "verifier passes".to_string(),
|
||||
max_iterations: Some(3),
|
||||
children: vec![leaf_node("retry-worker")],
|
||||
}),
|
||||
WorkflowNode::Expand(ExpandSpec {
|
||||
id: "split".to_string(),
|
||||
source: "retry-ring".to_string(),
|
||||
max_children: Some(4),
|
||||
template: Some(Box::new(leaf_node("split-template"))),
|
||||
}),
|
||||
]);
|
||||
|
||||
let shape = workflow
|
||||
.validate_for_fleet()
|
||||
.expect("bounded loop and expand should validate");
|
||||
|
||||
assert_eq!(shape.total_agents, 7);
|
||||
assert_eq!(shape.max_depth, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fleet_validation_rejects_unbounded_loop_or_expand_before_launch() {
|
||||
let workflow = workflow_spec(vec![
|
||||
WorkflowNode::LoopUntil(LoopUntilSpec {
|
||||
id: "retry-ring".to_string(),
|
||||
condition: "verifier passes".to_string(),
|
||||
max_iterations: None,
|
||||
children: vec![leaf_node("retry-worker")],
|
||||
}),
|
||||
WorkflowNode::Expand(ExpandSpec {
|
||||
id: "split".to_string(),
|
||||
source: "retry-ring".to_string(),
|
||||
max_children: Some(4),
|
||||
template: Some(Box::new(leaf_node("split-template"))),
|
||||
}),
|
||||
]);
|
||||
|
||||
assert!(matches!(
|
||||
workflow.validate_for_fleet(),
|
||||
Err(WorkflowFleetLimitError::UnboundedLoop { node }) if node == "retry-ring"
|
||||
));
|
||||
|
||||
let workflow = workflow_spec(vec![WorkflowNode::Expand(ExpandSpec {
|
||||
id: "split".to_string(),
|
||||
source: "retry-ring".to_string(),
|
||||
max_children: None,
|
||||
template: Some(Box::new(leaf_node("split-template"))),
|
||||
})]);
|
||||
|
||||
assert!(matches!(
|
||||
workflow.validate_for_fleet(),
|
||||
Err(WorkflowFleetLimitError::UnboundedExpand { node }) if node == "split"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branch_result_serialization() {
|
||||
let result = BranchResult {
|
||||
|
||||
@@ -47,8 +47,9 @@ can run on top of those modes when the task needs a continuous workflow.
|
||||
intermediate results out of the main conversation, and can be inspected or
|
||||
rerun. A WhaleFlow run should have a visible progress view and a clear active
|
||||
header state instead of feeling like a hidden background task.
|
||||
- **Fleet** is the execution substrate: headless workers, local/SSH hosts,
|
||||
trust policy, leases, heartbeats, logs, receipts, and status APIs.
|
||||
- **Fleet** is the durable sub-agent configuration and execution substrate:
|
||||
slots, profiles, per-slot models, tool posture, local/SSH hosts, trust
|
||||
policy, leases, heartbeats, logs, receipts, and status APIs.
|
||||
- **Swarm** is the high-fanout behavior inside WhaleFlow. It is gated in
|
||||
v0.8.61: `/swarm` must not revive prompt-only sub-agent fanout. It should
|
||||
compile into a WhaleFlow-backed fleet run once the durable worker and goal
|
||||
@@ -60,6 +61,37 @@ counts, receipts, and nested indentation for child workers. Use the whale mark
|
||||
sparingly as an active header/status signal; avoid repeating emoji-heavy rows
|
||||
for every worker.
|
||||
|
||||
## WhaleFlow on Fleet
|
||||
|
||||
The intended high-capability path is agent-authored. When the main agent
|
||||
decides a task needs more durable coordination than turn-by-turn sub-agent
|
||||
calls, it drafts a WhaleFlow script/IR, presents the run plan according to the
|
||||
active permission mode, and the runtime compiles it into typed Fleet work.
|
||||
|
||||
Fleet remains the sub-agent config surface. It owns slot count, role profiles,
|
||||
model/loadout selection, tool posture, launch concurrency, and the ledger.
|
||||
WhaleFlow owns only the orchestration plan: branch, sequence, loop, expand,
|
||||
review, and reduce decisions. The workflow script must not get direct shell,
|
||||
filesystem, network, provider-secret, cancellation, or TUI authority; workers
|
||||
perform real work as `codewhale exec` processes.
|
||||
|
||||
Default WhaleFlow-to-Fleet validation is intentionally bounded:
|
||||
|
||||
- 100 total worker agents per workflow run;
|
||||
- 5 recursive Fleet rings;
|
||||
- bounded loops only (`max_iterations` required);
|
||||
- bounded dynamic expansion only (`max_children` plus a template required).
|
||||
|
||||
These are population limits, not a demand to launch everything at once. A
|
||||
100-agent workflow should still drain through the configured Fleet worker pool.
|
||||
Recommended model layouts, such as a DeepSeek Pro orchestrator with Flash
|
||||
workers in the first ring and cheaper workers farther out, are presets only.
|
||||
Every slot can inherit the active model or carry an explicit model override.
|
||||
|
||||
The setup UI should render this as an expanding grid: an orchestrator plus a
|
||||
small number of visible sub-agent slots, with Right/Enter drilling into a slot's
|
||||
next recursive ring rather than trying to show the whole tree at once.
|
||||
|
||||
## Task Spec
|
||||
|
||||
`codewhale fleet run` accepts JSON or TOML. A minimal JSON spec:
|
||||
@@ -511,7 +543,7 @@ max_trust_level = "operator"
|
||||
[fleet.exec]
|
||||
# Recursion depth shares ONE axis with standalone sub-agents — a fleet worker
|
||||
# IS a headless sub-agent. 0 blocks child agents (the root worker still runs);
|
||||
# 3 is the default and the ceiling, affording at least three nested levels.
|
||||
# 3 is the default; explicit config clamps to the shared safety ceiling.
|
||||
max_spawn_depth = 3
|
||||
```
|
||||
|
||||
|
||||
@@ -57,3 +57,31 @@ a familiar declaration format, not a second execution runtime.
|
||||
- `cargo test -p codewhale-whaleflow --locked starlark`
|
||||
|
||||
Current example: `workflows/issue_audit.workflow.js`.
|
||||
|
||||
## Agent-Written Fleet Workflows
|
||||
|
||||
The primary product flow is not "ask the user to write a script." The main
|
||||
agent should decide when a task deserves workflow orchestration, draft the
|
||||
WhaleFlow source, show the plan for the current permission mode, and then let
|
||||
the runtime compile and monitor it.
|
||||
|
||||
WhaleFlow owns the plan: phases, branches, loops, reducers, and intermediate
|
||||
results. Fleet owns the durable sub-agent configuration: slots, profiles,
|
||||
models, tool posture, launch concurrency, leases, heartbeats, logs, receipts,
|
||||
and resume/stop/restart controls. In other words, a workflow can choose and
|
||||
monitor Fleet slots, but it must not become a second executor with its own shell
|
||||
or filesystem authority.
|
||||
|
||||
Fleet launch validation applies a conservative default shape before any
|
||||
WhaleFlow IR is lowered to workers:
|
||||
|
||||
- up to 100 total worker agents per workflow run;
|
||||
- up to 5 recursive Fleet rings;
|
||||
- loops require `max_iterations`;
|
||||
- dynamic `expand` nodes require `max_children` and a template.
|
||||
|
||||
Those limits bound the workflow population, not instantaneous launch
|
||||
concurrency. A valid 100-agent workflow can still drain through a smaller Fleet
|
||||
worker pool. Model selection stays per slot: a DeepSeek preset can suggest
|
||||
`deepseek-v4-pro` for the orchestrator and `deepseek-v4-flash` for nearby
|
||||
workers, but users and agents may override any slot when the task calls for it.
|
||||
|
||||
@@ -18,10 +18,9 @@
|
||||
# codewhale fleet inspect <worker-id-from-status>
|
||||
# codewhale fleet logs <worker-id-from-status>
|
||||
#
|
||||
# NOTE: wiring the manager run loop to drive FleetExecutor for real workers is
|
||||
# the in-progress cutover; until then the manual run path uses the local
|
||||
# simulation harness. The automated smoke above already proves the real
|
||||
# exec-subprocess -> ledger-event path.
|
||||
# NOTE: this manual run path now drives real `codewhale exec` workers through
|
||||
# the FleetExecutor. Use `--once` when you only want to enqueue/lease once and
|
||||
# inspect state manually instead of keeping the manager loop attached.
|
||||
|
||||
name = "dogfood smoke"
|
||||
labels = { milestone = "v0.8.60", class = "smoke" }
|
||||
|
||||
Reference in New Issue
Block a user