Skip to content

Commit 3ff18db

Browse files
committed
fix(agent): propagate delegated confirmations
1 parent daa25a4 commit 3ff18db

3 files changed

Lines changed: 130 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Forwarded delegated child confirmation-required, confirmation-received, and
13+
confirmation-timeout events through the parent runtime stream so shared HITL
14+
providers cannot deadlock while the host UI waits for an event it never saw.
15+
- Built MCP tool-selection context from the last six text-bearing messages
16+
instead of counting tool-use and tool-result messages that are discarded,
17+
preserving the original request across long delegated tool sequences.
18+
1019
## [6.1.0] - 2026-07-20
1120

1221
### Added

core/src/agent_api/runtime_events.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,13 @@ fn should_bridge_agent_event(event: &AgentEvent) -> bool {
487487
AgentEvent::SubagentStart { .. }
488488
| AgentEvent::SubagentProgress { .. }
489489
| AgentEvent::SubagentEnd { .. }
490+
// A delegated child that inherits the parent confirmation provider
491+
// waits for the parent UI to answer these events. Filtering them
492+
// here leaves the child blocked even though its confirmation is
493+
// registered on the shared provider.
494+
| AgentEvent::ConfirmationRequired { .. }
495+
| AgentEvent::ConfirmationReceived { .. }
496+
| AgentEvent::ConfirmationTimeout { .. }
490497
)
491498
}
492499

@@ -761,6 +768,72 @@ mod tests {
761768
)));
762769
}
763770

771+
#[tokio::test]
772+
async fn forwarder_exposes_delegated_confirmation_lifecycle() {
773+
let run_store = Arc::new(crate::run::InMemoryRunStore::new());
774+
let run = run_store.create_run("session-1", "prompt").await;
775+
let sink = RuntimeEventSink::new(RuntimeEventSinkConfig {
776+
run_store: Arc::clone(&run_store),
777+
run_id: run.id.clone(),
778+
session_id: "session-1".to_string(),
779+
hook_executor: None,
780+
security_provider: None,
781+
persistence_state: persistence_state(),
782+
active_tools: active_tools(),
783+
subagent_tasks: Arc::new(
784+
crate::subagent_task_tracker::InMemorySubagentTaskTracker::new(),
785+
),
786+
});
787+
let (runtime_tx, runtime_rx) = mpsc::channel(4);
788+
let (stream_tx, mut stream_rx) = mpsc::channel(4);
789+
let (agent_tx, barrier, agent_rx) = run_agent_event_channel(8);
790+
let forwarder = sink.spawn_forwarder(runtime_rx, stream_tx, Some(agent_rx));
791+
792+
let expected = vec![
793+
AgentEvent::ConfirmationRequired {
794+
tool_id: "child-tool-1".to_string(),
795+
tool_name: "install".to_string(),
796+
args: serde_json::json!({"component": "browser"}),
797+
timeout_ms: 30_000,
798+
},
799+
AgentEvent::ConfirmationReceived {
800+
tool_id: "child-tool-1".to_string(),
801+
approved: true,
802+
reason: Some("approved by parent".to_string()),
803+
},
804+
AgentEvent::ConfirmationTimeout {
805+
tool_id: "child-tool-2".to_string(),
806+
action_taken: "rejected".to_string(),
807+
},
808+
];
809+
for event in &expected {
810+
agent_tx.send(event.clone()).unwrap();
811+
}
812+
barrier.flush().await;
813+
drop(agent_tx);
814+
drop(runtime_tx);
815+
forwarder.await.unwrap();
816+
817+
let mut streamed = Vec::new();
818+
while let Some(event) = stream_rx.recv().await {
819+
streamed.push(event);
820+
}
821+
assert_eq!(
822+
serde_json::to_value(&streamed).unwrap(),
823+
serde_json::to_value(&expected).unwrap()
824+
);
825+
let persisted = run_store
826+
.events(&run.id)
827+
.await
828+
.into_iter()
829+
.map(|record| record.event)
830+
.collect::<Vec<_>>();
831+
assert_eq!(
832+
serde_json::to_value(&persisted).unwrap(),
833+
serde_json::to_value(&expected).unwrap()
834+
);
835+
}
836+
764837
#[tokio::test]
765838
async fn forwarder_exposes_and_persists_only_sanitized_events() {
766839
let run_store = Arc::new(crate::run::InMemoryRunStore::new());

core/src/tools/selector.rs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,17 +191,27 @@ fn should_include_mcp_tool(
191191
}
192192

193193
fn selection_context(messages: &[Message]) -> String {
194-
let mut parts = Vec::new();
195-
for message in messages.iter().rev().take(6).rev() {
196-
if message.role == "tool" {
197-
continue;
198-
}
199-
for block in &message.content {
200-
if let ContentBlock::Text { text } = block {
201-
parts.push(text.as_str());
194+
let mut parts = messages
195+
.iter()
196+
.rev()
197+
.filter_map(|message| {
198+
if message.role == "tool" {
199+
return None;
202200
}
203-
}
204-
}
201+
let text = message
202+
.content
203+
.iter()
204+
.filter_map(|block| match block {
205+
ContentBlock::Text { text } => Some(text.as_str()),
206+
_ => None,
207+
})
208+
.collect::<Vec<_>>()
209+
.join("\n");
210+
(!text.is_empty()).then_some(text)
211+
})
212+
.take(6)
213+
.collect::<Vec<_>>();
214+
parts.reverse();
205215
parts.join("\n")
206216
}
207217

@@ -411,4 +421,32 @@ mod tests {
411421

412422
assert_eq!(names, vec!["mcp__use_report__fixture_tool"]);
413423
}
424+
425+
#[test]
426+
fn tool_heavy_history_keeps_the_original_textual_intent() {
427+
let tool = "mcp__use_browser__agent_browser_open";
428+
let mut messages = vec![Message::user(&format!("Call {tool} after diagnostics"))];
429+
for index in 0..3 {
430+
let id = format!("call-{index}");
431+
messages.push(Message {
432+
role: "assistant".to_string(),
433+
content: vec![ContentBlock::ToolUse {
434+
id: id.clone(),
435+
name: format!("diagnostic-{index}"),
436+
input: json!({}),
437+
}],
438+
reasoning_content: None,
439+
});
440+
messages.push(Message::tool_result(&id, "ok", false));
441+
}
442+
443+
let selected = select_tools_for_messages(&defs(&[tool]), &messages);
444+
assert_eq!(
445+
selected
446+
.iter()
447+
.map(|definition| definition.name.as_str())
448+
.collect::<Vec<_>>(),
449+
vec![tool]
450+
);
451+
}
414452
}

0 commit comments

Comments
 (0)