Skip to content

Commit 1ac48da

Browse files
committed
feat(orchestrator): add human action reporting
- define the `report_human_action` tool/schema/parser/response so the orchestrator can request manual intervention and describe it in the tool log - track reported human actions inside `TaskManager`, log them, and format a highlighted reminder when the run finishes - update orchestrator logging/mocks and prompts to surface the “human action recorded” message so users know when manual steps remain
1 parent 2781ec9 commit 1ac48da

13 files changed

Lines changed: 286 additions & 12 deletions

File tree

prompts/orchestrator.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ Skip tasks when they are:
3737
- No longer relevant to the goal
3838
- Blocked by external factors that cannot be resolved
3939

40+
## HUMAN ACTION REPORTING:
41+
42+
When you encounter work that cannot be automated, use `report_human_action()` to record what the user needs to do manually. Examples:
43+
- Creating API tokens or secrets (requires web interface)
44+
- Setting up external service accounts
45+
- Making decisions that require human judgment
46+
- Pushing git tags or creating releases (if not appropriate to automate)
47+
48+
Provide clear, actionable descriptions. These will be displayed prominently at the end of the run so the user knows exactly what manual steps remain.
49+
4050
## REFLECTION & ADAPTATION:
4151

4252
After each batch of agents completes, reflect on their results before proceeding:

prompts/planner.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ You are part of a multi-agent orchestration pipeline. Your job is to decompose l
2323
- Your plan will be followed by AI agents, not humans. Keep that in mind. This means human time frames and effort limitations don't matter. Large refactors are possible if justified. And instructions should be clearest for the things that AI agents easily get wrong.
2424
- If you are already provided with a full plan, or if you find one in the codebase, adjust it to fit into the criteria above, then use the MCP tools to record it.
2525
- Try to stay within ~50% of your context. If you're asked to plan for something that would require more than that, either (1) create very broad tasks that encompass the whole request, or (2) create tasks for the immediate next steps, and then create a single broad task for everything else. The broad tasks will be further decomposed by other agents following the same process.
26+
- For requests that encompass many different files, consider ways to automate the changes. E.g.: a first task could be to create a script that applies changes to the whole codebase; a second task could be to run the script, verify the results and fix any undesirable side-effects; a third task could be to look only at remaining files to check if there was something that the script didn't catch.
2627
- When you are done, **always** use the provided MCP tool to signal completion.

src/agents/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ pub const ORCHESTRATOR_CONFIG: AgentToolConfig = AgentToolConfig {
102102
"create_task",
103103
"skip_tasks",
104104
"list_tasks",
105+
"report_human_action",
105106
],
106107
removed_auggie_tools: &[
107108
// No file editing - orchestrator delegates to implementers

src/app/orchestrator.rs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,12 @@ impl App {
554554
.await;
555555
let _ = response_tx.send(response);
556556
}
557+
ToolCall::ReportHumanAction { ref description, ref task_id } => {
558+
let response = self
559+
.handle_report_human_action(description, task_id.as_ref(), &request.request_id, writer)
560+
.await;
561+
let _ = response_tx.send(response);
562+
}
557563
}
558564
}
559565

@@ -772,6 +778,41 @@ impl App {
772778
response
773779
}
774780

781+
/// Handle the `report_human_action` tool call.
782+
///
783+
/// Records an action that requires manual user intervention.
784+
async fn handle_report_human_action(
785+
&self,
786+
description: &str,
787+
task_id: Option<&String>,
788+
request_id: &str,
789+
writer: &mut AgentWriter,
790+
) -> ToolResponse {
791+
let depth = self.current_scope.depth();
792+
let preview = truncate_for_log(description, 50);
793+
let _ = writer
794+
.write_mcp_tool_call("report_human_action", &preview)
795+
.await;
796+
tracing::info!("[L{}] 📋 report_human_action: {}", depth, preview);
797+
798+
// Add to task manager
799+
{
800+
let mut tm = self.task_manager.write().await;
801+
tm.add_human_action(description.to_string(), task_id.cloned());
802+
}
803+
804+
let response = ToolResponse::success(
805+
request_id.to_string(),
806+
"Human action recorded. It will be displayed prominently at the end of the run."
807+
.to_string(),
808+
);
809+
let _ = writer
810+
.write_mcp_tool_result("report_human_action", true, "Action recorded")
811+
.await;
812+
813+
response
814+
}
815+
775816
/// Handle the `skip_tasks` tool call.
776817
///
777818
/// Marks tasks as skipped if they are in `NotStarted` status.
@@ -839,11 +880,14 @@ impl App {
839880
errors.push(msg);
840881
}
841882
TaskStatus::Failed { .. } => {
842-
let msg = format!(
843-
"Task '{task_id}' has already failed and cannot be skipped"
883+
// Failed tasks don't need to be skipped - they already have a
884+
// definitive status. This is not an error, just informational.
885+
tracing::info!(
886+
"ℹ️ Task '{}' has already failed (no skip needed)",
887+
task_id
844888
);
845-
tracing::warn!("⚠️ {}", msg);
846-
errors.push(msg);
889+
// Don't add to errors - failed tasks are already accounted for
890+
// in task reconciliation, so the orchestrator can proceed.
847891
}
848892
TaskStatus::Skipped { .. } => {
849893
tracing::info!("⏭️ Task '{}' is already skipped", task_id);

src/main.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,14 @@ async fn main() -> Result<()> {
759759
}
760760
}
761761

762+
// Print human action items if any exist
763+
{
764+
let task_manager = app.task_manager().read().await;
765+
if let Some(human_actions) = task_manager.format_human_actions_required() {
766+
println!("{human_actions}");
767+
}
768+
}
769+
762770
if result.success {
763771
println!("\n✅ Task completed successfully!");
764772
} else {

src/mcp_server/handlers/mod.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ fn handle_tools_list(id: Option<&Value>) -> Value {
110110
tool_schemas::complete_schema_orchestrator(),
111111
tool_schemas::create_task_schema_orchestrator(),
112112
tool_schemas::skip_tasks_schema(),
113-
tool_schemas::list_tasks_schema()
113+
tool_schemas::list_tasks_schema(),
114+
tool_schemas::report_human_action_schema()
114115
]
115116
})
116117
}
@@ -197,6 +198,16 @@ async fn handle_tool_call(
197198
Ok(tc) => tc,
198199
Err(msg) => return Ok(Some(invalid_params_error(id.as_ref(), "list_tasks", msg))),
199200
},
201+
"report_human_action" => match tool_parsing::parse_report_human_action(arguments) {
202+
Ok(tc) => tc,
203+
Err(msg) => {
204+
return Ok(Some(invalid_params_error(
205+
id.as_ref(),
206+
"report_human_action",
207+
msg,
208+
)))
209+
}
210+
},
200211
_ => {
201212
tracing::warn!("⚠️ Unknown tool requested: {}", name);
202213
return Ok(Some(method_not_found_error(
@@ -210,6 +221,7 @@ async fn handle_tool_call(
210221
"set_goal",
211222
"skip_tasks",
212223
"list_tasks",
224+
"report_human_action",
213225
],
214226
)));
215227
}
@@ -366,7 +378,7 @@ mod tests {
366378

367379
let tool_names = extract_tool_names(&response);
368380

369-
// Orchestrator should have: decompose, spawn_agents, complete, create_task, skip_tasks
381+
// Orchestrator should have: decompose, spawn_agents, complete, create_task, skip_tasks, list_tasks, report_human_action
370382
assert!(
371383
tool_names.contains("decompose"),
372384
"Orchestrator should have decompose tool"
@@ -391,12 +403,16 @@ mod tests {
391403
tool_names.contains("list_tasks"),
392404
"Orchestrator should have list_tasks tool"
393405
);
406+
assert!(
407+
tool_names.contains("report_human_action"),
408+
"Orchestrator should have report_human_action tool"
409+
);
394410

395411
// Verify exact count
396412
assert_eq!(
397413
tool_names.len(),
398-
6,
399-
"Orchestrator should have exactly 6 tools, got: {tool_names:?}",
414+
7,
415+
"Orchestrator should have exactly 7 tools, got: {tool_names:?}",
400416
);
401417
}
402418

src/mcp_server/handlers/response.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ pub fn build_response_text_with_state(
138138
ToolCall::ListTasks { status_filter } => {
139139
build_list_tasks_response(status_filter.as_deref(), response)
140140
}
141+
ToolCall::ReportHumanAction { description, .. } => {
142+
build_report_human_action_response(description, response)
143+
}
141144
}
142145
}
143146

@@ -162,6 +165,28 @@ fn build_list_tasks_response(status_filter: Option<&str>, response: &ToolRespons
162165
}
163166
}
164167

168+
fn build_report_human_action_response(description: &str, response: &ToolResponse) -> String {
169+
if response.success {
170+
let preview = if description.len() > 60 {
171+
format!("{}...", &description[..57])
172+
} else {
173+
description.to_string()
174+
};
175+
format!(
176+
"✅ Human action recorded: \"{preview}\"\n\n\
177+
This will be displayed prominently at the end of the run."
178+
)
179+
} else {
180+
let error_msg = response.error.as_deref().unwrap_or(&response.summary);
181+
format!(
182+
"❌ Failed to record human action: {error_msg}\n\n\
183+
## How to Fix\n\
184+
- Ensure the description is a non-empty string\n\
185+
- Try calling the tool again with a valid description"
186+
)
187+
}
188+
}
189+
165190
fn build_decompose_response(
166191
task_id: Option<&String>,
167192
task: Option<&String>,

src/mcp_server/handlers/tool_parsing.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,23 @@ pub fn parse_list_tasks(arguments: &serde_json::Map<String, Value>) -> ParseResu
188188

189189
Ok(ToolCall::ListTasks { status_filter })
190190
}
191+
192+
/// Parse the `report_human_action` tool call arguments.
193+
pub fn parse_report_human_action(arguments: &serde_json::Map<String, Value>) -> ParseResult {
194+
let description = if let Some(d) = arguments.get("description").and_then(|v| v.as_str()) {
195+
d.to_string()
196+
} else {
197+
tracing::warn!("⚠️ report_human_action tool missing 'description' argument");
198+
return Err("requires 'description' string argument");
199+
};
200+
201+
let task_id = arguments
202+
.get("task_id")
203+
.and_then(|v| v.as_str())
204+
.map(String::from);
205+
206+
Ok(ToolCall::ReportHumanAction {
207+
description,
208+
task_id,
209+
})
210+
}

src/mcp_server/handlers/tool_schemas.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ pub fn create_task_schema_orchestrator() -> Value {
229229
pub fn skip_tasks_schema() -> Value {
230230
json!({
231231
"name": "skip_tasks",
232-
"description": "<usecase>Skip one or more tasks that are no longer needed.</usecase>\n<instructions>Use this when tasks become unnecessary (e.g., already done by another task, no longer relevant, or blocked permanently). Skipped tasks will not be executed and will be marked as skipped in the plan.</instructions>\n<on_error>If a task_id is not found, use list_tasks() to see available task IDs. If a task cannot be skipped because it's already completed or in progress, no action is needed - the task will proceed as planned. Provide a reason to help future agents understand why the task was skipped.</on_error>",
232+
"description": "<usecase>Skip one or more tasks that are no longer needed.</usecase>\n<instructions>Use this when tasks become unnecessary (e.g., already done by another task, no longer relevant, or blocked permanently). Skipped tasks will not be executed and will be marked as skipped in the plan.</instructions>\n<on_error>If a task_id is not found, use list_tasks() to see available task IDs. If a task cannot be skipped because it's already completed, in progress, or has already failed, no action is needed - the task already has a definitive status. Failed tasks do not need to be skipped; they are already accounted for in task reconciliation. Provide a reason to help future agents understand why the task was skipped.</on_error>",
233233
"inputSchema": {
234234
"type": "object",
235235
"properties": {
@@ -266,3 +266,25 @@ pub fn list_tasks_schema() -> Value {
266266
}
267267
})
268268
}
269+
270+
/// Generate the `report_human_action` tool definition.
271+
pub fn report_human_action_schema() -> Value {
272+
json!({
273+
"name": "report_human_action",
274+
"description": "<usecase>Report that something requires manual user intervention.</usecase>\n<instructions>Use this when you encounter work that cannot be automated and needs the user to take action manually. Examples: creating API tokens, setting up external services, making decisions that require human judgment, pushing git tags. The description should be clear and actionable - tell the user exactly what they need to do. These will be prominently displayed at the end of the run.</instructions>\n<on_error>If the description is too vague, be more specific about what action is needed, where to do it, and why it's necessary.</on_error>",
275+
"inputSchema": {
276+
"type": "object",
277+
"properties": {
278+
"description": {
279+
"type": "string",
280+
"description": "Clear, actionable description of what the user needs to do manually. Include specific steps if possible."
281+
},
282+
"task_id": {
283+
"type": "string",
284+
"description": "Optional task ID this action relates to (e.g., 'task003'). Helps provide context."
285+
}
286+
},
287+
"required": ["description"]
288+
}
289+
})
290+
}

src/mcp_server/server.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,11 +369,11 @@ mod tests {
369369
let resp = response.unwrap();
370370
let tools = resp["result"]["tools"].as_array().unwrap();
371371

372-
// Orchestrator has 6 tools: decompose, spawn_agents, complete, create_task, skip_tasks, list_tasks
372+
// Orchestrator has 7 tools: decompose, spawn_agents, complete, create_task, skip_tasks, list_tasks, report_human_action
373373
assert_eq!(
374374
tools.len(),
375-
6,
376-
"Expected 6 orchestrator tools, got: {:?}",
375+
7,
376+
"Expected 7 orchestrator tools, got: {:?}",
377377
tools
378378
.iter()
379379
.map(|t| t["name"].as_str().unwrap_or("?"))
@@ -388,6 +388,7 @@ mod tests {
388388
assert!(tool_names.contains(&"create_task"));
389389
assert!(tool_names.contains(&"skip_tasks"));
390390
assert!(tool_names.contains(&"list_tasks"));
391+
assert!(tool_names.contains(&"report_human_action"));
391392
}
392393

393394
#[tokio::test]

0 commit comments

Comments
 (0)