Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b490722
feat(task): add task tool for agent delegation with session resumption
amitksingh1490 Feb 3, 2026
c888b52
feat(task): capitalize task tool name and prevent self-delegation in …
amitksingh1490 Feb 3, 2026
6aeac17
style(tests): reformat function call arguments to comply with line wi…
amitksingh1490 Feb 3, 2026
37df3ed
Merge remote-tracking branch 'origin/main' into task_tool_final
amitksingh1490 Mar 9, 2026
7f9de1c
fix(tool_resolver): add missing alias for deprecated tool name "Task"
amitksingh1490 Mar 9, 2026
347c295
test(tool_resolver): add test for capitalized "Task" alias resolution
amitksingh1490 Mar 9, 2026
fd3567c
Merge branch 'main' into task_tool_final
amitksingh1490 Mar 16, 2026
fcc4b75
Merge branch 'main' into task_tool_final
amitksingh1490 Mar 25, 2026
99980e9
Merge branch 'main' into task_tool_final
amitksingh1490 Mar 25, 2026
433b75e
docs: enhance guidelines for tool usage and parallel execution in For…
amitksingh1490 Mar 25, 2026
6be706f
refactor(orch): execute task tool calls in parallel and keep others s…
amitksingh1490 Mar 25, 2026
8dea159
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 25, 2026
350c9ec
refactor(orch): improve tool call execution logic and enhance case-in…
amitksingh1490 Mar 25, 2026
6de74b6
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 25, 2026
ee008da
refactor(orch): enhance case-insensitive tool call comparison and imp…
amitksingh1490 Mar 25, 2026
8d58003
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 25, 2026
ca0b9ba
Merge branch 'main' into task_tool_final
amitksingh1490 Mar 25, 2026
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
30 changes: 22 additions & 8 deletions crates/forge_app/src/agent_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@ impl<S: Services> AgentExecutor<S> {
}

/// Executes an agent tool call by creating a new chat request for the
/// specified agent.
/// Executes an agent tool call by creating a new chat request for the
/// specified agent. If conversation_id is provided, the agent will reuse
/// that conversation, maintaining context across invocations. Otherwise,
/// a new conversation is created.
pub async fn execute(
&self,
agent_id: AgentId,
task: String,
ctx: &ToolCallContext,
conversation_id: Option<String>,
) -> anyhow::Result<ToolOutput> {
ctx.send_tool_input(
TitleFormat::debug(format!(
Expand All @@ -52,13 +56,23 @@ impl<S: Services> AgentExecutor<S> {
)
.await?;

// Create a new conversation for agent execution
let conversation = Conversation::generate().title(task.clone());
self.services
.conversation_service()
.upsert_conversation(conversation.clone())
.await?;
// Execute the request through the ForgeApp
// Reuse existing conversation if provided, otherwise create a new one
let conversation = if let Some(cid) = conversation_id {
let conversation_id = forge_domain::ConversationId::parse(&cid)
.map_err(|_| Error::ConversationNotFound { id: cid.clone() })?;
self.services
.conversation_service()
.find_conversation(&conversation_id)
.await?
.ok_or(Error::ConversationNotFound { id: cid })?
} else {
let conversation = Conversation::generate().title(task.clone());
self.services
.conversation_service()
.upsert_conversation(conversation.clone())
.await?;
conversation
};
let app = crate::ForgeApp::new(self.services.clone());
let mut response_stream = app
.chat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ impl Transformer for CapitalizeToolNames {
tool.name = match tool.name.as_str() {
"read" => "Read".to_string(),
"write" => "Write".to_string(),
"task" => "Task".to_string(),
_ => tool.name.clone(),
};
}
Expand Down
3 changes: 3 additions & 0 deletions crates/forge_app/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ pub enum Error {
#[error("Agent '{0}' not found")]
AgentNotFound(forge_domain::AgentId),

#[error("Conversation '{id}' not found")]
ConversationNotFound { id: String },

#[error("No active provider configured")]
NoActiveProvider,

Expand Down
3 changes: 3 additions & 0 deletions crates/forge_app/src/fmt/fmt_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ impl FormatContent for ToolCatalog {
.into(),
),
ToolCatalog::TodoRead(_) => Some(TitleFormat::debug("Read Todos").into()),
ToolCatalog::Task(input) => {
Some(TitleFormat::debug("Task").sub_title(&input.agent_id).into())
}
}
}
}
65 changes: 54 additions & 11 deletions crates/forge_app/src/orch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use async_recursion::async_recursion;
use derive_setters::Setters;
use forge_domain::{Agent, *};
use forge_template::Element;
use futures::future::join_all;
use tokio::sync::Notify;
use tracing::warn;

Expand Down Expand Up @@ -53,27 +54,57 @@ impl<S: AgentService> Orchestrator<S> {

// Helper function to get all tool results from a vector of tool calls
#[async_recursion]
async fn execute_tool_calls<'a>(
async fn execute_tool_calls(
&mut self,
tool_calls: &[ToolCallFull],
tool_context: &ToolCallContext,
) -> anyhow::Result<Vec<(ToolCallFull, ToolResult)>> {
// Always process tool calls sequentially
let mut tool_call_records = Vec::with_capacity(tool_calls.len());
let task_tool_name = ToolKind::Task.name();

// Use a case-insensitive comparison since the model may send "Task" or "task".
let is_task = |tc: &ToolCallFull| {
tc.name
.as_str()
.eq_ignore_ascii_case(task_tool_name.as_str())
};

// Partition into task tool calls (run in parallel) and all others (run
// sequentially). Use a case-insensitive comparison since the model may
// send "Task" or "task".
let is_task_call =
|tc: &&ToolCallFull| tc.name.as_str().to_lowercase() == task_tool_name.as_str();
let (task_calls, other_calls): (Vec<_>, Vec<_>) = tool_calls.iter().partition(is_task_call);

// Execute task tool calls in parallel — mirrors how direct agent-as-tool calls
// work.
let task_results: Vec<(ToolCallFull, ToolResult)> = join_all(
task_calls
.iter()
.map(|tc| self.services.call(&self.agent, tool_context, (*tc).clone())),
)
.await
.into_iter()
.zip(task_calls.iter())
.map(|(result, tc)| ((*tc).clone(), result))
.collect();

let system_tools = self
.tool_definitions
.iter()
.map(|tool| &tool.name)
.collect::<HashSet<_>>();

for tool_call in tool_calls {
// Process non-task tool calls sequentially (preserving UI notifier handshake
// and hooks).
let mut other_results: Vec<(ToolCallFull, ToolResult)> =
Vec::with_capacity(other_calls.len());
for tool_call in &other_calls {
// Send the start notification for system tools and not agent as a tool
let is_system_tool = system_tools.contains(&tool_call.name);
if is_system_tool {
let notifier = Arc::new(Notify::new());
self.send(ChatResponse::ToolCallStart {
tool_call: tool_call.clone(),
tool_call: (*tool_call).clone(),
notifier: notifier.clone(),
})
.await?;
Expand All @@ -87,7 +118,7 @@ impl<S: AgentService> Orchestrator<S> {
let toolcall_start_event = LifecycleEvent::ToolcallStart(EventData::new(
self.agent.clone(),
self.agent.model.clone(),
ToolcallStartPayload::new(tool_call.clone()),
ToolcallStartPayload::new((*tool_call).clone()),
));
self.hook
.handle(&toolcall_start_event, &mut self.conversation)
Expand All @@ -96,14 +127,14 @@ impl<S: AgentService> Orchestrator<S> {
// Execute the tool
let tool_result = self
.services
.call(&self.agent, tool_context, tool_call.clone())
.call(&self.agent, tool_context, (*tool_call).clone())
.await;

// Fire the ToolcallEnd lifecycle event (fires on both success and failure)
let toolcall_end_event = LifecycleEvent::ToolcallEnd(EventData::new(
self.agent.clone(),
self.agent.model.clone(),
ToolcallEndPayload::new(tool_call.clone(), tool_result.clone()),
ToolcallEndPayload::new((*tool_call).clone(), tool_result.clone()),
));
self.hook
.handle(&toolcall_end_event, &mut self.conversation)
Expand All @@ -114,11 +145,23 @@ impl<S: AgentService> Orchestrator<S> {
self.send(ChatResponse::ToolCallEnd(tool_result.clone()))
.await?;
}
// Ensure all tool calls and results are recorded
// Adding task completion records is critical for compaction to work correctly
tool_call_records.push((tool_call.clone(), tool_result));
other_results.push(((*tool_call).clone(), tool_result));
}

// Reconstruct results in the original order of tool_calls.
let mut task_iter = task_results.into_iter();
let mut other_iter = other_results.into_iter();
let tool_call_records = tool_calls
.iter()
.map(|tc| {
if is_task(tc) {
task_iter.next().expect("task result count mismatch")
} else {
other_iter.next().expect("other result count mismatch")
}
})
.collect();

Ok(tool_call_records)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,75 @@ Retrieves the current todo list for this coding session. Use this tool to check
## Output

Returns all current todos with their IDs, content, and status (`pending`, `in_progress`, `completed`). If no todos exist yet, returns an empty list.

---

### task

Launch a new agent to handle complex, multi-step tasks autonomously.

The task tool launches specialized agents (subprocesses) that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.

Available agent types and the tools they have access to:
- **sage**: Specialized in researching codebases
- Tools: read, fs_search, sem_search, fetch
- **debug**: Specialized in debugging issues
- Tools: read, shell, fs_search, sem_search, fetch

When using the task tool, you must specify a agent_id parameter to select which agent type to use.

When NOT to use the task tool:
- If you want to read a specific file path, use the read or fs_search tool instead of the task tool, to find the match more quickly
- If you are searching for a specific class definition like "class Foo", use the fs_search tool instead, to find the match more quickly
- If you are searching for code within a specific file or set of 2-3 files, use the read tool instead of the task tool, to find the match more quickly
- Other tasks that are not related to the agent descriptions above


Usage notes:
- Always include a short description (3-5 words) summarizing what the agent will do
- Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
- Agents can be resumed using the \`session_id\` parameter by passing the agent ID from a previous invocation. When resumed, the agent continues with its full previous context preserved. When NOT resuming, each invocation starts fresh and you should provide a detailed task description with all necessary context.
- When the agent is done, it will return a single message back to you along with its agent ID. You can use this ID to resume the agent later if needed for follow-up work.
- Provide clear, detailed prompts so the agent can work autonomously and return exactly the information you need.
- Agents with "access to current context" can see the full conversation history before the tool call. When using these agents, you can write concise prompts that reference earlier context (e.g., "investigate the error discussed above") instead of repeating information. The agent will receive all prior messages and understand the context.
- The agent's outputs should generally be trusted
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent
- If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
- If the user specifies that they want you to run agents "in parallel", you MUST send a single message with multiple task tool use content blocks. For example, if you need to launch both a build-validator agent and a test-runner agent in parallel, send a single message with both tool calls.

Example usage:

<example_agent_descriptions>
"test-runner": use this agent after you are done writing code to run tests
"greeting-responder": use this agent when to respond to user greetings with a friendly joke
</example_agent_description>

<example>
user: "Please write a function that checks if a number is prime"
assistant: Sure let me write a function that checks if a number is prime
assistant: First let me use the write tool to write a function that checks if a number is prime
assistant: I'm going to use the write tool to write the following code:
<code>
function isPrime(n) {
if (n <= 1) return false
for (let i = 2; i * i <= n; i++) {
if (n % i === 0) return false
}
return true
}
</code>
<commentary>
Since a significant piece of code was written and the task was completed, now use the test-runner agent to run the tests
</commentary>
assistant: Now let me use the test-runner agent to run the tests
assistant: Uses the task tool to launch the test-runner agent
</example>

<example>
user: "Hello"
<commentary>
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke
</commentary>
assistant: "I'm going to use the task tool to launch the greeting-responder agent"
</example>
2 changes: 2 additions & 0 deletions crates/forge_app/src/system_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ impl<S: SkillFetchService + ShellService> SystemPrompt<S> {
model: None,
tool_names,
extensions,
agents: Vec::new(), /* Empty for system prompt (agents list is for tool
* descriptions only) */
};

let static_block = TemplateEngine::default()
Expand Down
4 changes: 4 additions & 0 deletions crates/forge_app/src/tool_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ impl<
let todos = context.get_todos()?;
ToolOperation::TodoRead { output: todos }
}
ToolCatalog::Task(_) => {
// Task tools are handled in ToolRegistry before reaching here
unreachable!("Task tool should be handled in ToolRegistry")
}
})
}

Expand Down
Loading
Loading