Skip to content

Commit 7dd5917

Browse files
committed
instruments tool call to emit structured message
1 parent 471feae commit 7dd5917

File tree

7 files changed

+358
-94
lines changed

7 files changed

+358
-94
lines changed

crates/chat-cli-ui/src/conduit.rs

Lines changed: 87 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
use std::io::Write as _;
22
use std::marker::PhantomData;
33

4-
use crossterm::execute;
5-
use crossterm::style::Print;
4+
use crossterm::style::{
5+
self,
6+
Print,
7+
Stylize,
8+
};
9+
use crossterm::{
10+
execute,
11+
queue,
12+
};
613

14+
use crate::legacy_ui_util::ThemeSource;
715
use crate::protocol::{
816
Event,
917
LegacyPassThroughOutput,
18+
ToolCallStart,
1019
};
1120

21+
const TOOL_BULLET: &str = " ● ";
22+
const CONTINUATION_LINE: &str = " ⋮ ";
23+
const PURPOSE_ARROW: &str = " ↳ ";
24+
const SUCCESS_TICK: &str = " ✓ ";
25+
const ERROR_EXCLAMATION: &str = " ❗ ";
26+
const DELEGATE_NOTIFIER: &str = "[BACKGROUND TASK READY]";
27+
1228
#[derive(thiserror::Error, Debug)]
1329
pub enum ConduitError {
1430
#[error(transparent)]
@@ -39,6 +55,7 @@ impl ViewEnd {
3955
/// This blocks the current thread and consumes the [ViewEnd]
4056
pub fn into_legacy_mode(
4157
self,
58+
theme_source: impl ThemeSource,
4259
mut stderr: std::io::Stderr,
4360
mut stdout: std::io::Stdout,
4461
) -> Result<(), ConduitError> {
@@ -140,50 +157,59 @@ impl ViewEnd {
140157
);
141158
},
142159
Event::ToolCallStart(tool_call_start) => {
143-
let parent_info = tool_call_start
144-
.parent_message_id
145-
.as_ref()
146-
.map(|p| format!(" (Parent: {})", p))
147-
.unwrap_or_default();
148-
let _ = execute!(
149-
stderr,
160+
let ToolCallStart {
161+
tool_call_name,
162+
is_trusted,
163+
mcp_server_name,
164+
..
165+
} = tool_call_start;
166+
167+
queue!(
168+
stdout,
169+
theme_source.emphasis_fg(),
150170
Print(format!(
151-
"Tool call started - ID: {}, Tool: {}{}\n",
152-
tool_call_start.tool_call_id, tool_call_start.tool_call_name, parent_info
153-
))
154-
);
171+
"🛠️ Using tool: {}{}",
172+
tool_call_name,
173+
if is_trusted {
174+
" (trusted)".dark_green()
175+
} else {
176+
"".reset()
177+
}
178+
)),
179+
theme_source.reset(),
180+
)?;
181+
182+
if let Some(server_name) = mcp_server_name {
183+
queue!(
184+
stdout,
185+
theme_source.reset(),
186+
Print(" from mcp server "),
187+
theme_source.emphasis_fg(),
188+
Print(&server_name),
189+
theme_source.reset(),
190+
)?;
191+
}
192+
193+
execute!(
194+
stdout,
195+
Print("\n"),
196+
Print(CONTINUATION_LINE),
197+
Print("\n"),
198+
Print(TOOL_BULLET)
199+
)?;
155200
},
156201
Event::ToolCallArgs(tool_call_args) => {
157-
let _ = execute!(
158-
stderr,
159-
Print(format!(
160-
"Tool call args ({}): {}\n",
161-
tool_call_args.tool_call_id, tool_call_args.delta
162-
))
163-
);
202+
if let serde_json::Value::String(content) = tool_call_args.delta {
203+
execute!(stdout, style::Print(content))?;
204+
} else {
205+
execute!(stdout, style::Print(tool_call_args.delta))?;
206+
}
164207
},
165-
Event::ToolCallEnd(tool_call_end) => {
166-
let _ = execute!(
167-
stderr,
168-
Print(format!("Tool call ended - ID: {}\n", tool_call_end.tool_call_id))
169-
);
208+
Event::ToolCallEnd(_tool_call_end) => {
209+
// noop for now
170210
},
171-
Event::ToolCallResult(tool_call_result) => {
172-
let role_info = tool_call_result
173-
.role
174-
.as_ref()
175-
.map(|r| format!(" Role: {:?}", r))
176-
.unwrap_or_default();
177-
let _ = execute!(
178-
stderr,
179-
Print(format!(
180-
"Tool call result - Message: {}, Tool: {}{}\nContent: {}\n",
181-
tool_call_result.message_id,
182-
tool_call_result.tool_call_id,
183-
role_info,
184-
tool_call_result.content
185-
))
186-
);
211+
Event::ToolCallResult(_tool_call_result) => {
212+
// noop for now (currently we don't show the tool call results to users)
187213
},
188214
Event::StateSnapshot(state_snapshot) => {
189215
let _ = execute!(
@@ -307,6 +333,7 @@ impl ViewEnd {
307333
))
308334
);
309335
},
336+
Event::ToolCallRejection(tool_call_rejection) => todo!(),
310337
}
311338
}
312339

@@ -330,6 +357,10 @@ pub struct ControlEnd<T> {
330357
pub current_event: Option<Event>,
331358
/// Used by the control to send state changes to the view
332359
pub sender: std::sync::mpsc::Sender<Event>,
360+
/// Flag indicating whether structured events should be sent through the conduit.
361+
/// When true, the control end will send structured event data in addition to
362+
/// raw pass-through content, enabling richer communication between layers.
363+
pub should_send_structured_event: bool,
333364
/// Phantom data to specify the destination type for pass-through operations.
334365
/// This allows the type system to track whether this ControlEnd is configured
335366
/// for stdout or stderr output without runtime overhead.
@@ -341,6 +372,7 @@ impl<T> Clone for ControlEnd<T> {
341372
Self {
342373
current_event: self.current_event.clone(),
343374
sender: self.sender.clone(),
375+
should_send_structured_event: self.should_send_structured_event,
344376
pass_through_destination: PhantomData,
345377
}
346378
}
@@ -365,6 +397,7 @@ impl ControlEnd<DestinationStderr> {
365397
pub fn as_stdout(&self) -> ControlEnd<DestinationStdout> {
366398
ControlEnd {
367399
current_event: self.current_event.clone(),
400+
should_send_structured_event: self.should_send_structured_event,
368401
sender: self.sender.clone(),
369402
pass_through_destination: PhantomData,
370403
}
@@ -375,6 +408,7 @@ impl ControlEnd<DestinationStdout> {
375408
pub fn as_stderr(&self) -> ControlEnd<DestinationStderr> {
376409
ControlEnd {
377410
current_event: self.current_event.clone(),
411+
should_send_structured_event: self.should_send_structured_event,
378412
sender: self.sender.clone(),
379413
pass_through_destination: PhantomData,
380414
}
@@ -447,11 +481,16 @@ impl std::io::Write for ControlEnd<DestinationStdout> {
447481
}
448482
}
449483

450-
/// Creates a set of legacy conduits for communication between view and control layers.
484+
/// Creates a set of legacy conduits forcommunication between view and control layers.
451485
///
452486
/// This function establishes the communication channels needed for the legacy mode operation,
453487
/// where the view layer and control layer can exchange events and byte data.
454488
///
489+
/// # Parameters
490+
///
491+
/// - `should_send_structured_event`: Flag indicating whether structured events should be sent
492+
/// through the conduit
493+
///
455494
/// # Returns
456495
///
457496
/// A tuple containing:
@@ -463,9 +502,11 @@ impl std::io::Write for ControlEnd<DestinationStdout> {
463502
/// # Example
464503
///
465504
/// ```rust
466-
/// let (view_end, input_receiver, stderr_control, stdout_control) = get_legacy_conduits();
505+
/// let (view_end, input_receiver, stderr_control, stdout_control) = get_legacy_conduits(true);
467506
/// ```
468-
pub fn get_legacy_conduits() -> (
507+
pub fn get_legacy_conduits(
508+
should_send_structured_event: bool,
509+
) -> (
469510
ViewEnd,
470511
InputReceiver,
471512
ControlEnd<DestinationStderr>,
@@ -482,11 +523,13 @@ pub fn get_legacy_conduits() -> (
482523
byte_rx,
483524
ControlEnd {
484525
current_event: None,
526+
should_send_structured_event,
485527
sender: state_tx.clone(),
486528
pass_through_destination: PhantomData,
487529
},
488530
ControlEnd {
489531
current_event: None,
532+
should_send_structured_event,
490533
sender: state_tx,
491534
pass_through_destination: PhantomData,
492535
},
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use crossterm::style::{
2+
ResetColor,
3+
SetAttribute,
4+
SetForegroundColor,
5+
};
6+
7+
/// This trait is purely here to facilitate a smooth transition from the old event loop to a new
8+
/// event loop. It is a way to achieve inversion of control to delegate the implementation of
9+
/// themes to the consumer of this crate. Without this, we would be running into a circular
10+
/// dependency.
11+
pub trait ThemeSource {
12+
fn error(&self, text: &str) -> String;
13+
fn info(&self, text: &str) -> String;
14+
fn emphasis(&self, text: &str) -> String;
15+
fn command(&self, text: &str) -> String;
16+
fn prompt(&self, text: &str) -> String;
17+
fn profile(&self, text: &str) -> String;
18+
fn tangent(&self, text: &str) -> String;
19+
fn usage_low(&self, text: &str) -> String;
20+
fn usage_medium(&self, text: &str) -> String;
21+
fn usage_high(&self, text: &str) -> String;
22+
fn brand(&self, text: &str) -> String;
23+
fn primary(&self, text: &str) -> String;
24+
fn secondary(&self, text: &str) -> String;
25+
fn success(&self, text: &str) -> String;
26+
fn error_fg(&self) -> SetForegroundColor;
27+
fn warning_fg(&self) -> SetForegroundColor;
28+
fn success_fg(&self) -> SetForegroundColor;
29+
fn info_fg(&self) -> SetForegroundColor;
30+
fn brand_fg(&self) -> SetForegroundColor;
31+
fn secondary_fg(&self) -> SetForegroundColor;
32+
fn emphasis_fg(&self) -> SetForegroundColor;
33+
fn reset(&self) -> ResetColor;
34+
fn reset_attributes(&self) -> SetAttribute;
35+
}

crates/chat-cli-ui/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod conduit;
22
pub mod input_bar;
3+
pub mod legacy_ui_util;
34
pub mod protocol;
45
pub mod ui;

crates/chat-cli-ui/src/protocol.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ pub struct ToolCallStart {
135135
pub tool_call_name: String,
136136
#[serde(skip_serializing_if = "Option::is_none")]
137137
pub parent_message_id: Option<String>,
138+
// bespoke fields
139+
pub mcp_server_name: Option<String>,
140+
pub is_trusted: bool,
138141
}
139142

140143
/// Represents a chunk of argument data for a tool call
@@ -163,6 +166,14 @@ pub struct ToolCallResult {
163166
pub role: Option<MessageRole>,
164167
}
165168

169+
/// Signifies a rejection to a tool call
170+
#[derive(Debug, Clone, Serialize, Deserialize)]
171+
#[serde(rename_all = "camelCase")]
172+
pub struct ToolCallRejection {
173+
pub tool_call_id: String,
174+
pub reason: String,
175+
}
176+
166177
// ============================================================================
167178
// State Management Events
168179
// ============================================================================
@@ -345,6 +356,8 @@ pub enum Event {
345356
ToolCallArgs(ToolCallArgs),
346357
ToolCallEnd(ToolCallEnd),
347358
ToolCallResult(ToolCallResult),
359+
// bespoke variant
360+
ToolCallRejection(ToolCallRejection),
348361

349362
// State Management Events
350363
StateSnapshot(StateSnapshot),
@@ -354,6 +367,7 @@ pub enum Event {
354367
// Special Events
355368
Raw(Raw),
356369
Custom(Custom),
370+
// bespoke variant
357371
LegacyPassThrough(LegacyPassThroughOutput),
358372

359373
// Draft Events - Activity Events
@@ -394,6 +408,7 @@ impl Event {
394408
Event::ToolCallArgs(_) => "toolCallArgs",
395409
Event::ToolCallEnd(_) => "toolCallEnd",
396410
Event::ToolCallResult(_) => "toolCallResult",
411+
Event::ToolCallRejection(_) => "toolCallRejection",
397412

398413
// State Management Events
399414
Event::StateSnapshot(_) => "stateSnapshot",

0 commit comments

Comments
 (0)