2494
tc.arguments.chars().take(120).collect::<String>()
2495
);
2496
2497
+ let signature = format!("{}\n{}", tc.name, tc.arguments);
2498
+ let repeat_count = repeated_tool_calls
2499
+ .entry(signature)
2500
+ .and_modify(|count| *count += 1)
2501
+ .or_insert(1);
2502
+ // Repeated status checks are expected for a live background
2503
+ // command. command_output long-polls and the overall round cap
2504
+ // still bounds a confused model without cutting off a real
2505
+ // build or release while it is in progress.
2506
+ let repeated = *repeat_count >= 3 && tc.name != "command_output";
2507
+ if repeated {
2508
+ force_text = true;
2509
+ stopped_repeating = Some(tc.name.clone());
2510
+ log::warn!(
2511
+ "prompt({}) stopping repeated tool call `{}` after {} attempts",
2512
+ session_id,
2513
+ tc.name,
2514
+ repeat_count
2515
+ );
2516
+ }
2517
+
2518
+ // Decided before the call is announced, because the answer sets
2519
+ // the status it is announced with: a call that has to wait for
2520
+ // the user starts out `pending`, everything else is already
2521
+ // running. A repeated call is never run, so never asked about.
2522
+ let decision = (!repeated).then(|| {
2523
+ permissions::decision_for(&session_id.to_string(), &tc.name, &tc.arguments)
2524
+ });
2525
+ let announced_status = match decision {
2526
+ Some(permissions::Decision::Ask) => ToolCallStatus::Pending,
2527
+ _ => ToolCallStatus::InProgress,
2528
+ };
2529
+
2530
// Only treat the call as a plan once its arguments have actually
2531
// produced one. Keying off the tool name alone means a
2532
// `write_todos` the converter rejects sends no plan *and* skips
2558
SessionUpdate::ToolCall(
2559
ToolCall::new(tc.id.clone(), chat::tool_title(&tc.name, &tc.arguments))
2560
.kind(tool_kind_for(&tc.name))
2528
- .status(ToolCallStatus::InProgress)
2561
+ .status(announced_status)
2562
.content(vec![invocation_content])
2563
.locations(tool_call_locations(&tc.name, &tc.arguments))
2564
.raw_input(raw_input),
2567
.ok();
2568
}
2569
2537
- let signature = format!("{}\n{}", tc.name, tc.arguments);
2538
- let repeat_count = repeated_tool_calls
2539
- .entry(signature)
2540
- .and_modify(|count| *count += 1)
2541
- .or_insert(1);
2542
- // Repeated status checks are expected for a live background
2543
- // command. command_output long-polls and the overall round cap
2544
- // still bounds a confused model without cutting off a real
2545
- // build or release while it is in progress.
2546
- let repeated = *repeat_count >= 3 && tc.name != "command_output";
2547
- if repeated {
2548
- force_text = true;
2549
- stopped_repeating = Some(tc.name.clone());
2550
- log::warn!(
2551
- "prompt({}) stopping repeated tool call `{}` after {} attempts",
2552
- session_id,
2553
- tc.name,
2554
- repeat_count
2555
- );
2556
- }
2557
-
2570
// Permission gate: read-only tools pass straight through; a
2571
// mutating tool consults policy and may ask the client.
2560
- let output = if repeated {
2561
- format!(
2572
+ let output = match decision {
2573
+ None => format!(
2574
"The tool `{}` was not executed again because the model repeated \
2575
the same call three times. Continue without this tool.",
2576
tc.name
2565
- )
2566
- } else {
2567
- match permissions::decision_for(
2568
- &session_id.to_string(),
2569
- &tc.name,
2570
- &tc.arguments,
2571
- ) {
2577
+ ),
2578
+ Some(decision) => match decision {
2579
permissions::Decision::Allow => {
2580
tools::execute_tool(&tc.name, &tc.arguments).await
2581
}
2585
}
2586
permissions::Decision::Ask => {
2587
match self
2581
- .request_tool_permission(cx, &session_id, &tc.name, &tc.arguments)
2588
+ .request_tool_permission(
2589
+ cx,
2590
+ &session_id,
2591
+ &tc.id,
2592
+ &tc.name,
2593
+ &tc.arguments,
2594
+ )
2595
.await
2596
{
2597
PermissionVerdict::Approved => {
2598
+ // The call the user just approved leaves
2599
+ // `pending` here. The title goes back to
2600
+ // the card's own: the permission request
2601
+ // replaced it with the full arguments.
2602
+ if !render_as_plan {
2603
+ self.send_tool_call_update(
2604
+ cx,
2605
+ session_id.clone(),
2606
+ SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
2607
+ tc.id.clone(),
2608
+ ToolCallUpdateFields::new()
2609
+ .status(ToolCallStatus::InProgress)
2610
+ .title(chat::tool_title(
2611
+ &tc.name,
2612
+ &tc.arguments,
2613
+ )),
2614
+ )),
2615
+ )
2616
+ .ok();
2617
+ }
2618
tools::execute_tool(&tc.name, &tc.arguments).await
2619
}
2620
PermissionVerdict::Denied(reason) => {
2665
}
2666
}
2667
}
2635
- }
2668
+ },
2669
};
2670
2671
log::info!(" ← {} chars", output.len());
2831
Ok(PromptResponse::new(stop_reason))
2832
}
2833
2801
- /// Ask the ACP client for permission to run one tool call. Presents
2834
+ /// Ask the ACP client for permission to run one tool call. The request
2835
+ /// names the call by `tool_call_id`, the id it was announced under, so the
2836
+ /// client updates that one card instead of drawing a second. Presents
2837
/// allow-once / allow-for-session / deny; an "always allow" choice is
2838
/// recorded via [`permissions::grant_for_session`]. Only safe to call from
2839
/// a spawned task (see the handler registration in `run_acp_server`): the
2842
&self,
2843
cx: &ConnectionTo<Client>,
2844
session_id: &SessionId,
2845
+ tool_call_id: &str,
2846
tool_name: &str,
2847
arguments: &str,
2848
) -> PermissionVerdict {
2864
let request = RequestPermissionRequest::new(
2865
session_id.clone(),
2866
ToolCallUpdate::new(
2831
- format!("perm-{}", uuid::Uuid::new_v4()),
2867
+ tool_call_id.to_string(),
2868
ToolCallUpdateFields::new()
2869
.title(title)
2870
.kind(tool_kind_for(tool_name))