Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [
"crates/tools",
"crates/tui",
"crates/tui-core",
"crates/whaleflow",
]
default-members = ["crates/cli", "crates/app-server", "crates/tui"]
resolver = "2"
Expand Down
1 change: 1 addition & 0 deletions crates/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ codewhale-protocol = { path = "../protocol", version = "0.8.48" }
codewhale-release = { path = "../release", version = "0.8.48" }
codewhale-secrets = { path = "../secrets", version = "0.8.48" }
codewhale-tools = { path = "../tools", version = "0.8.48" }
codewhale-whaleflow = { path = "../whaleflow", version = "0.8.48" }
schemaui = { version = "0.12.0", default-features = false, optional = true }
async-stream = "0.3.6"
async-trait = "0.1"
Expand Down
30 changes: 23 additions & 7 deletions crates/tui/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ use crate::tools::subagent::{
};
use crate::tools::todo::{SharedTodoList, new_shared_todo_list};
use crate::tools::user_input::{UserInputRequest, UserInputResponse};
use crate::tools::workflow::WhaleFlowSpawner;
use crate::tools::{ToolContext, ToolRegistryBuilder};
use crate::tui::app::AppMode;
use crate::utils::spawn_supervised;
Expand Down Expand Up @@ -1233,14 +1234,29 @@ impl Engine {
} else {
None
};
Some(
builder
.with_subagent_tools(

// Build the WhaleFlow spawner for the workflow_run tool.
// It uses the same manager and runtime that drive
// sub-agent management tools.
let workflow_spawner = runtime
.as_ref()
.map(|rt| {
Arc::new(WhaleFlowSpawner::new(
self.subagent_manager.clone(),
runtime.expect("sub-agent runtime should exist with active client"),
)
.build(tool_context),
)
rt.clone(),
tool_context.workspace.clone(),
))
});

let mut builder = builder
.with_subagent_tools(
self.subagent_manager.clone(),
runtime.expect("sub-agent runtime should exist with active client"),
);
if let Some(spawner) = workflow_spawner {
builder = builder.with_workflow_tool(spawner);
}
Some(builder.build(tool_context))
} else {
Some(builder.build(tool_context))
}
Expand Down
1 change: 1 addition & 0 deletions crates/tui/src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub mod user_input;
pub mod validate_data;
pub mod web_run;
pub mod web_search;
pub mod workflow;

pub use registry::{ToolRegistry, ToolRegistryBuilder};
pub use review::ReviewOutput;
Expand Down
10 changes: 10 additions & 0 deletions crates/tui/src/tools/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,16 @@ impl ToolRegistryBuilder {
.with_tool(Arc::new(AgentCloseTool::new(manager)))
}

/// Include the WhaleFlow workflow_run tool.
#[must_use]
pub fn with_workflow_tool(
self,
spawner: std::sync::Arc<super::workflow::WhaleFlowSpawner>,
) -> Self {
use super::workflow::WorkflowRunTool;
self.with_tool(Arc::new(WorkflowRunTool::new(spawner)))
}

/// Build the registry with the given context.
#[must_use]
pub fn build(self, context: ToolContext) -> ToolRegistry {
Expand Down
22 changes: 21 additions & 1 deletion crates/tui/src/tools/subagent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,12 @@ pub struct SubAgentResult {
pub result: Option<String>,
pub steps_taken: u32,
pub duration_ms: u64,
/// Total tokens consumed (prompt + completion) by this agent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tokens_used: Option<u64>,
/// Estimated cost in USD for this agent's API usage.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_usd: Option<f64>,
/// `true` when this agent was loaded from a prior-session persisted
/// state file rather than spawned in the current session (#405).
/// Lets `agent_list` filter out historical noise by default while
Expand All @@ -589,6 +595,11 @@ pub(crate) struct SubAgentSpawnOptions {
pub model: Option<String>,
pub nickname: Option<String>,
pub fork_context: bool,
/// Per-spawn override for the max tool-call steps budget.
/// When `None` (default), the manager's global `max_steps` is used.
/// WhaleFlow passes the task-level `max_steps` here to bound
/// per-agent recursion without affecting other concurrent agents.
pub max_steps: Option<u32>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -1006,6 +1017,10 @@ pub struct SubAgent {
/// against the manager's `current_session_boot_id` to classify the
/// agent as in-session vs prior-session at list time.
pub session_boot_id: String,
/// Cumulative token counters updated each step.
pub tokens_used: u64,
/// Cumulative cost estimate updated each step.
pub cost_usd: f64,
input_tx: Option<mpsc::UnboundedSender<SubAgentInput>>,
task_handle: Option<JoinHandle<()>>,
}
Expand Down Expand Up @@ -1042,6 +1057,8 @@ impl SubAgent {
started_at: Instant::now(),
allowed_tools,
session_boot_id,
tokens_used: 0,
cost_usd: 0.0,
input_tx: Some(input_tx),
task_handle: None,
}
Expand All @@ -1063,6 +1080,8 @@ impl SubAgent {
result: self.result.clone(),
steps_taken: self.steps_taken,
duration_ms: u64::try_from(self.started_at.elapsed().as_millis()).unwrap_or(u64::MAX),
tokens_used: if self.tokens_used > 0 { Some(self.tokens_used) } else { None },
cost_usd: if self.cost_usd > 0.0 { Some(self.cost_usd) } else { None },
// Snapshots from the agent itself don't know the manager's
// current boot id, so default to false. The manager fills
// this in when it produces a snapshot via its own
Expand Down Expand Up @@ -1365,7 +1384,7 @@ impl SubAgentManager {
agent.fork_context = options.fork_context;
let agent_id = agent.id.clone();
let started_at = agent.started_at;
let max_steps = self.max_steps;
let max_steps = options.max_steps.unwrap_or(self.max_steps);

if let Some(event_tx) = runtime.event_tx.clone() {
let _ = event_tx.try_send(Event::AgentSpawned {
Expand Down Expand Up @@ -2414,6 +2433,7 @@ impl ToolSpec for AgentSpawnTool {
model: Some(effective_model),
nickname: None,
fork_context: spawn_request.fork_context,
max_steps: None,
},
)
.map_err(|e| ToolError::execution_failed(format!("Failed to spawn sub-agent: {e}")))?;
Expand Down
Loading
Loading