Skip to content
Merged
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
1 change: 1 addition & 0 deletions crates/punch-kernel/src/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ pub async fn run_gorilla_tick(
memory: Arc::clone(memory),
driver: Arc::clone(driver),
available_tools,
mcp_tools: Vec::new(),
max_iterations: Some(10),
context_window: None,
tool_timeout_secs: None,
Expand Down
1 change: 1 addition & 0 deletions crates/punch-kernel/src/heartbeat_scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ async fn heartbeat_loop(mut cfg: HeartbeatLoopConfig) {
memory: Arc::clone(memory),
driver: Arc::clone(driver),
available_tools: tools_for_capabilities(&manifest.capabilities),
mcp_tools: Vec::new(),
max_iterations: Some(10),
context_window: None,
tool_timeout_secs: Some(60),
Expand Down
18 changes: 9 additions & 9 deletions crates/punch-kernel/src/ring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ use tokio::task::JoinHandle;
use tracing::{info, instrument, warn};

use punch_memory::{BoutId, MemorySubstrate};
use punch_runtime::{
FighterLoopParams, FighterLoopResult, LlmDriver, McpClient, run_fighter_loop,
tools_for_capabilities,
};
use punch_runtime::{FighterLoopParams, FighterLoopResult, LlmDriver, McpClient, run_fighter_loop};
use punch_types::{
AgentCoordinator, AgentInfo, AgentMessageResult, CoordinationStrategy, FighterId,
FighterManifest, FighterStatus, GorillaId, GorillaManifest, GorillaMetrics, GorillaStatus,
Expand Down Expand Up @@ -763,9 +760,9 @@ impl Ring {
let fighter_name = manifest.name.clone();
drop(entry); // Release the DashMap guard before any async calls.

let mut available_tools = tools_for_capabilities(&manifest.capabilities);

// Merge MCP tools if the fighter has McpAccess capability.
// Collect MCP tools separately (the fighter loop handles dynamic
// built-in tool selection via ToolSelector when available_tools is empty).
let mut mcp_tools = Vec::new();
let has_mcp_access = manifest
.capabilities
.iter()
Expand All @@ -782,7 +779,7 @@ impl Ring {
});
if can_access {
match mcp_entry.value().list_tools().await {
Ok(tools) => available_tools.extend(tools),
Ok(tools) => mcp_tools.extend(tools),
Err(e) => {
warn!(
server = %server_name,
Expand All @@ -804,14 +801,17 @@ impl Ring {
});

// Run the fighter loop.
// available_tools is empty — the fighter loop uses ToolSelector for
// context-aware dynamic tool loading per turn.
let params = FighterLoopParams {
manifest: manifest.clone(),
user_message: message,
bout_id,
fighter_id: *fighter_id,
memory: Arc::clone(&self.memory),
driver: Arc::clone(&self.driver),
available_tools,
available_tools: Vec::new(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep capability tools when enabling dynamic selection

Ring::send_message now always passes available_tools: Vec::new(), which forces run_fighter_loop into keyword-triggered dynamic selection for every fighter. Because ToolSelector::core_tools only includes a subset of capabilities, restricted fighters without ShellExec can start a bout without their granted tools unless the user wording matches a hard-coded trigger, so requests that previously worked can fail on the first turn.

Useful? React with 👍 / 👎.

mcp_tools,
max_iterations: None,
context_window: None,
tool_timeout_secs: None,
Expand Down
1 change: 1 addition & 0 deletions crates/punch-kernel/src/workflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,7 @@ impl WorkflowEngine {
memory: Arc::clone(memory),
driver: Arc::clone(driver),
available_tools,
mcp_tools: Vec::new(),
max_iterations: Some(20),
context_window: None,
tool_timeout_secs: Some(timeout_secs),
Expand Down
46 changes: 43 additions & 3 deletions crates/punch-runtime/src/fighter_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ pub struct FighterLoopParams {
/// The LLM driver to use for completions.
pub driver: Arc<dyn LlmDriver>,
/// Tools available for this fighter to use.
/// When provided, bypasses dynamic tool selection (used by workflows, gorillas, tests).
/// When empty, the fighter loop uses `ToolSelector` for context-aware tool loading.
pub available_tools: Vec<ToolDefinition>,
/// Pre-fetched MCP tools for this fighter (merged into tool list each turn).
pub mcp_tools: Vec<ToolDefinition>,
/// Maximum loop iterations before forced termination (default: 50).
pub max_iterations: Option<usize>,
/// Context window size in tokens (default: 200K).
Expand Down Expand Up @@ -243,17 +247,53 @@ pub async fn run_fighter_loop(params: FighterLoopParams) -> PunchResult<FighterL
}
}

// --- Dynamic tool selection ---
// If available_tools is pre-populated (workflows, gorillas, tests), use it as-is.
// Otherwise, use ToolSelector for context-aware per-turn tool loading.
let use_dynamic_tools = params.available_tools.is_empty();
let mut tool_selector = if use_dynamic_tools {
Some(crate::tools::ToolSelector::new(
&params.manifest.capabilities,
))
} else {
None
};

// Pre-build static tool list (avoids cloning per loop iteration for static path).
let static_tools: Option<Vec<ToolDefinition>> = if !use_dynamic_tools {
Some(params.available_tools)
} else {
None
};

let static_tool_count = static_tools.as_ref().map_or(0, |t| t.len());
info!(
tool_count = params.available_tools.len(),
dynamic_tools = use_dynamic_tools,
static_tool_count,
mcp_tool_count = params.mcp_tools.len(),
fighter = %params.manifest.name,
model = %params.manifest.model.model,
"fighter loop starting"
);

// 4. Main loop.
loop {
// --- Dynamic tool selection: pick tools for this turn ---
let turn_tools = if let Some(ref mut selector) = tool_selector {
let (mut selected, _changed) = selector.select_tools(&messages);
// Merge MCP tools (already capability-filtered in ring.rs).
selected.extend(params.mcp_tools.iter().cloned());
selected
} else {
// Static path: clone from pre-built list (CompletionRequest takes ownership).
static_tools
.as_ref()
.expect("static_tools set when not using dynamic selection")
.clone()
};

// --- Context Budget: check and trim before LLM call ---
if let Some(trim_action) = budget.check_trim_needed(&messages, &params.available_tools) {
if let Some(trim_action) = budget.check_trim_needed(&messages, &turn_tools) {
budget.apply_trim(&mut messages, trim_action);

// Re-run session repair after trimming (may create orphans).
Expand All @@ -270,7 +310,7 @@ pub async fn run_fighter_loop(params: FighterLoopParams) -> PunchResult<FighterL
let request = CompletionRequest {
model: params.manifest.model.model.clone(),
messages: messages.clone(),
tools: params.available_tools.clone(),
tools: turn_tools,
max_tokens: params.manifest.model.max_tokens.unwrap_or(
// Reasoning models (Qwen, DeepSeek) use thinking tokens internally,
// so they need a much higher default to leave room for visible output.
Expand Down
2 changes: 1 addition & 1 deletion crates/punch-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ pub use mcp::McpClient;
pub use model_router::{ModelRouter, ModelTier};
pub use session_repair::{RepairStats, repair_session};
pub use tool_executor::{ToolExecutionContext, execute_tool};
pub use tools::{all_tools, tools_for_capabilities};
pub use tools::{ToolGroup, ToolSelector, all_tools, tools_for_capabilities};
Loading
Loading