shared active tools: Agent writes, TUI reads directly
Move active tool tracking from TUI message-passing to shared Arc<RwLock> state. Agent pushes on dispatch, removes on apply_tool_result. TUI reads during render. Background tasks show as active until drained at next turn start. Co-Developed-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
d25033b9f4
commit
474b66c834
6 changed files with 62 additions and 49 deletions
|
|
@ -80,6 +80,8 @@ pub struct Agent {
|
||||||
pub memory_scores: Option<crate::agent::training::MemoryScore>,
|
pub memory_scores: Option<crate::agent::training::MemoryScore>,
|
||||||
/// Whether a /score task is currently running.
|
/// Whether a /score task is currently running.
|
||||||
pub scoring_in_flight: bool,
|
pub scoring_in_flight: bool,
|
||||||
|
/// Shared active tools — Agent writes, TUI reads.
|
||||||
|
pub active_tools: crate::user::ui_channel::SharedActiveTools,
|
||||||
/// Background tool calls that outlive the current turn.
|
/// Background tool calls that outlive the current turn.
|
||||||
background_tasks: futures::stream::FuturesUnordered<
|
background_tasks: futures::stream::FuturesUnordered<
|
||||||
std::pin::Pin<Box<dyn std::future::Future<Output = (ToolCall, tools::ToolOutput)> + Send>>
|
std::pin::Pin<Box<dyn std::future::Future<Output = (ToolCall, tools::ToolOutput)> + Send>>
|
||||||
|
|
@ -105,6 +107,7 @@ impl Agent {
|
||||||
prompt_file: String,
|
prompt_file: String,
|
||||||
conversation_log: Option<ConversationLog>,
|
conversation_log: Option<ConversationLog>,
|
||||||
shared_context: SharedContextState,
|
shared_context: SharedContextState,
|
||||||
|
active_tools: crate::user::ui_channel::SharedActiveTools,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let tool_defs = tools::definitions();
|
let tool_defs = tools::definitions();
|
||||||
let tokenizer = tiktoken_rs::cl100k_base()
|
let tokenizer = tiktoken_rs::cl100k_base()
|
||||||
|
|
@ -135,6 +138,7 @@ impl Agent {
|
||||||
agent_cycles,
|
agent_cycles,
|
||||||
memory_scores: None,
|
memory_scores: None,
|
||||||
scoring_in_flight: false,
|
scoring_in_flight: false,
|
||||||
|
active_tools,
|
||||||
background_tasks: futures::stream::FuturesUnordered::new(),
|
background_tasks: futures::stream::FuturesUnordered::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -240,20 +244,15 @@ impl Agent {
|
||||||
// Inject completed background task results
|
// Inject completed background task results
|
||||||
{
|
{
|
||||||
use futures::{StreamExt, FutureExt};
|
use futures::{StreamExt, FutureExt};
|
||||||
|
let mut bg_ds = DispatchState {
|
||||||
|
yield_requested: false, had_tool_calls: false,
|
||||||
|
tool_errors: 0, model_switch: None, dmn_pause: false,
|
||||||
|
};
|
||||||
while let Some(Some((call, output))) =
|
while let Some(Some((call, output))) =
|
||||||
std::pin::Pin::new(&mut self.background_tasks).next().now_or_never()
|
std::pin::Pin::new(&mut self.background_tasks).next().now_or_never()
|
||||||
{
|
{
|
||||||
let preview = &output.text[..output.text.len().min(500)];
|
// Show result in TUI and inject into conversation
|
||||||
let _ = ui_tx.send(UiMessage::Info(format!(
|
self.apply_tool_result(&call, output, ui_tx, &mut bg_ds);
|
||||||
"[background] {} completed: {}",
|
|
||||||
call.function.name,
|
|
||||||
&preview[..preview.len().min(80)],
|
|
||||||
)));
|
|
||||||
let notification = format!(
|
|
||||||
"<task-notification>\nTool: {}\nResult: {}\n</task-notification>",
|
|
||||||
call.function.name, preview,
|
|
||||||
);
|
|
||||||
self.push_message(Message::user(notification));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,11 +323,14 @@ impl Agent {
|
||||||
name: call.function.name.clone(),
|
name: call.function.name.clone(),
|
||||||
args_summary: args_summary.clone(),
|
args_summary: args_summary.clone(),
|
||||||
});
|
});
|
||||||
let _ = ui_tx.send(UiMessage::ToolStarted {
|
self.active_tools.write().unwrap().push(
|
||||||
id: call.id.clone(),
|
crate::user::ui_channel::ActiveTool {
|
||||||
name: call.function.name.clone(),
|
id: call.id.clone(),
|
||||||
detail: args_summary,
|
name: call.function.name.clone(),
|
||||||
});
|
detail: args_summary,
|
||||||
|
started: std::time::Instant::now(),
|
||||||
|
}
|
||||||
|
);
|
||||||
let tracker = self.process_tracker.clone();
|
let tracker = self.process_tracker.clone();
|
||||||
let is_background = args.get("run_in_background")
|
let is_background = args.get("run_in_background")
|
||||||
.and_then(|v| v.as_bool())
|
.and_then(|v| v.as_bool())
|
||||||
|
|
@ -548,11 +550,14 @@ impl Agent {
|
||||||
name: call.function.name.clone(),
|
name: call.function.name.clone(),
|
||||||
args_summary: args_summary.clone(),
|
args_summary: args_summary.clone(),
|
||||||
});
|
});
|
||||||
let _ = ui_tx.send(UiMessage::ToolStarted {
|
self.active_tools.write().unwrap().push(
|
||||||
id: call.id.clone(),
|
crate::user::ui_channel::ActiveTool {
|
||||||
name: call.function.name.clone(),
|
id: call.id.clone(),
|
||||||
detail: args_summary,
|
name: call.function.name.clone(),
|
||||||
});
|
detail: args_summary,
|
||||||
|
started: std::time::Instant::now(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Handle working_stack tool — needs &mut self for context state
|
// Handle working_stack tool — needs &mut self for context state
|
||||||
if call.function.name == "working_stack" {
|
if call.function.name == "working_stack" {
|
||||||
|
|
@ -568,7 +573,7 @@ impl Agent {
|
||||||
name: call.function.name.clone(),
|
name: call.function.name.clone(),
|
||||||
result: output.text.clone(),
|
result: output.text.clone(),
|
||||||
});
|
});
|
||||||
let _ = ui_tx.send(UiMessage::ToolFinished { id: call.id.clone() });
|
self.active_tools.write().unwrap().retain(|t| t.id != call.id);
|
||||||
self.push_message(Message::tool_result(&call.id, &output.text));
|
self.push_message(Message::tool_result(&call.id, &output.text));
|
||||||
ds.had_tool_calls = true;
|
ds.had_tool_calls = true;
|
||||||
|
|
||||||
|
|
@ -616,7 +621,7 @@ impl Agent {
|
||||||
name: call.function.name.clone(),
|
name: call.function.name.clone(),
|
||||||
result: output.text.clone(),
|
result: output.text.clone(),
|
||||||
});
|
});
|
||||||
let _ = ui_tx.send(UiMessage::ToolFinished { id: call.id.clone() });
|
self.active_tools.write().unwrap().retain(|t| t.id != call.id);
|
||||||
|
|
||||||
// Tag memory_render results for context deduplication
|
// Tag memory_render results for context deduplication
|
||||||
if call.function.name == "memory_render" && !output.text.starts_with("Error:") {
|
if call.function.name == "memory_render" && !output.text.starts_with("Error:") {
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,7 @@ impl Session {
|
||||||
.ok();
|
.ok();
|
||||||
let mut agent_guard = self.agent.lock().await;
|
let mut agent_guard = self.agent.lock().await;
|
||||||
let shared_ctx = agent_guard.shared_context.clone();
|
let shared_ctx = agent_guard.shared_context.clone();
|
||||||
|
let shared_tools = agent_guard.active_tools.clone();
|
||||||
*agent_guard = Agent::new(
|
*agent_guard = Agent::new(
|
||||||
ApiClient::new(
|
ApiClient::new(
|
||||||
&self.config.api_base,
|
&self.config.api_base,
|
||||||
|
|
@ -390,6 +391,7 @@ impl Session {
|
||||||
self.config.prompt_file.clone(),
|
self.config.prompt_file.clone(),
|
||||||
new_log,
|
new_log,
|
||||||
shared_ctx,
|
shared_ctx,
|
||||||
|
shared_tools,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.dmn = dmn::State::Resting {
|
self.dmn = dmn::State::Resting {
|
||||||
|
|
@ -826,12 +828,13 @@ async fn run(cli: cli::CliArgs) -> Result<()> {
|
||||||
// Create UI channel
|
// Create UI channel
|
||||||
let (ui_tx, mut ui_rx) = ui_channel::channel();
|
let (ui_tx, mut ui_rx) = ui_channel::channel();
|
||||||
|
|
||||||
// Shared context state — agent writes, TUI reads for debug screen
|
// Shared state — agent writes, TUI reads
|
||||||
let shared_context = ui_channel::shared_context_state();
|
let shared_context = ui_channel::shared_context_state();
|
||||||
|
let shared_active_tools = ui_channel::shared_active_tools();
|
||||||
|
|
||||||
// Initialize TUI
|
// Initialize TUI
|
||||||
let mut terminal = tui::init_terminal()?;
|
let mut terminal = tui::init_terminal()?;
|
||||||
let mut app = tui::App::new(config.model.clone(), shared_context.clone());
|
let mut app = tui::App::new(config.model.clone(), shared_context.clone(), shared_active_tools.clone());
|
||||||
|
|
||||||
// Show startup info
|
// Show startup info
|
||||||
let _ = ui_tx.send(UiMessage::Info("consciousness v0.3 (tui)".into()));
|
let _ = ui_tx.send(UiMessage::Info("consciousness v0.3 (tui)".into()));
|
||||||
|
|
@ -868,6 +871,7 @@ async fn run(cli: cli::CliArgs) -> Result<()> {
|
||||||
config.prompt_file.clone(),
|
config.prompt_file.clone(),
|
||||||
Some(conversation_log),
|
Some(conversation_log),
|
||||||
shared_context,
|
shared_context,
|
||||||
|
shared_active_tools,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
// Keep a reference to the process tracker outside the agent lock
|
// Keep a reference to the process tracker outside the agent lock
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ impl App {
|
||||||
)));
|
)));
|
||||||
lines.push(Line::raw(format!(" Reasoning: {}", self.reasoning_effort)));
|
lines.push(Line::raw(format!(" Reasoning: {}", self.reasoning_effort)));
|
||||||
lines.push(Line::raw(format!(" Running processes: {}", self.running_processes)));
|
lines.push(Line::raw(format!(" Running processes: {}", self.running_processes)));
|
||||||
lines.push(Line::raw(format!(" Active tools: {}", self.active_tools.len())));
|
lines.push(Line::raw(format!(" Active tools: {}", self.active_tools.read().unwrap().len())));
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ impl App {
|
||||||
/// Draw the main (F1) screen — four-pane layout with status bar.
|
/// Draw the main (F1) screen — four-pane layout with status bar.
|
||||||
pub(crate) fn draw_main(&mut self, frame: &mut Frame, size: Rect) {
|
pub(crate) fn draw_main(&mut self, frame: &mut Frame, size: Rect) {
|
||||||
// Main layout: content area + active tools overlay + status bar
|
// Main layout: content area + active tools overlay + status bar
|
||||||
let tool_lines = self.active_tools.len() as u16;
|
let active_tools = self.active_tools.read().unwrap();
|
||||||
|
let tool_lines = active_tools.len() as u16;
|
||||||
let main_chunks = Layout::default()
|
let main_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
|
|
@ -107,9 +108,9 @@ impl App {
|
||||||
frame.render_widget(&self.textarea, input_chunks[1]);
|
frame.render_widget(&self.textarea, input_chunks[1]);
|
||||||
|
|
||||||
// Draw active tools overlay
|
// Draw active tools overlay
|
||||||
if !self.active_tools.is_empty() {
|
if !active_tools.is_empty() {
|
||||||
let tool_style = Style::default().fg(Color::Yellow).add_modifier(Modifier::DIM);
|
let tool_style = Style::default().fg(Color::Yellow).add_modifier(Modifier::DIM);
|
||||||
let tool_text: Vec<Line> = self.active_tools.iter().map(|t| {
|
let tool_text: Vec<Line> = active_tools.iter().map(|t| {
|
||||||
let elapsed = t.started.elapsed().as_secs();
|
let elapsed = t.started.elapsed().as_secs();
|
||||||
let line = if t.detail.is_empty() {
|
let line = if t.detail.is_empty() {
|
||||||
format!(" [{}] ({}s)", t.name, elapsed)
|
format!(" [{}] ({}s)", t.name, elapsed)
|
||||||
|
|
|
||||||
|
|
@ -308,12 +308,8 @@ pub(crate) fn parse_markdown(md: &str) -> Vec<Line<'static>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A tool call currently in flight — shown above the status bar.
|
/// A tool call currently in flight — shown above the status bar.
|
||||||
pub(crate) struct ActiveTool {
|
// ActiveTool moved to ui_channel — shared between Agent and TUI
|
||||||
pub(crate) id: String,
|
pub(crate) use crate::user::ui_channel::ActiveTool;
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) detail: String,
|
|
||||||
pub(crate) started: std::time::Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main TUI application state.
|
/// Main TUI application state.
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
|
@ -335,7 +331,7 @@ pub struct App {
|
||||||
pub running_processes: u32,
|
pub running_processes: u32,
|
||||||
/// Current reasoning effort level (for status display).
|
/// Current reasoning effort level (for status display).
|
||||||
pub reasoning_effort: String,
|
pub reasoning_effort: String,
|
||||||
pub(crate) active_tools: Vec<ActiveTool>,
|
pub(crate) active_tools: crate::user::ui_channel::SharedActiveTools,
|
||||||
pub(crate) active_pane: ActivePane,
|
pub(crate) active_pane: ActivePane,
|
||||||
/// User input editor (handles wrapping, cursor positioning).
|
/// User input editor (handles wrapping, cursor positioning).
|
||||||
pub textarea: tui_textarea::TextArea<'static>,
|
pub textarea: tui_textarea::TextArea<'static>,
|
||||||
|
|
@ -422,7 +418,7 @@ pub enum HotkeyAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(model: String, shared_context: SharedContextState) -> Self {
|
pub fn new(model: String, shared_context: SharedContextState, active_tools: crate::user::ui_channel::SharedActiveTools) -> Self {
|
||||||
Self {
|
Self {
|
||||||
autonomous: PaneState::new(true), // markdown
|
autonomous: PaneState::new(true), // markdown
|
||||||
conversation: PaneState::new(true), // markdown
|
conversation: PaneState::new(true), // markdown
|
||||||
|
|
@ -444,7 +440,7 @@ impl App {
|
||||||
needs_assistant_marker: false,
|
needs_assistant_marker: false,
|
||||||
running_processes: 0,
|
running_processes: 0,
|
||||||
reasoning_effort: "none".to_string(),
|
reasoning_effort: "none".to_string(),
|
||||||
active_tools: Vec::new(),
|
active_tools,
|
||||||
active_pane: ActivePane::Conversation,
|
active_pane: ActivePane::Conversation,
|
||||||
textarea: new_textarea(vec![String::new()]),
|
textarea: new_textarea(vec![String::new()]),
|
||||||
input_history: Vec::new(),
|
input_history: Vec::new(),
|
||||||
|
|
@ -548,17 +544,8 @@ impl App {
|
||||||
self.autonomous.current_color = Color::DarkGray;
|
self.autonomous.current_color = Color::DarkGray;
|
||||||
self.autonomous.append_text(&text);
|
self.autonomous.append_text(&text);
|
||||||
}
|
}
|
||||||
UiMessage::ToolStarted { id, name, detail } => {
|
UiMessage::ToolStarted { .. } => {} // handled by shared active_tools
|
||||||
self.active_tools.push(ActiveTool {
|
UiMessage::ToolFinished { .. } => {}
|
||||||
id,
|
|
||||||
name,
|
|
||||||
detail,
|
|
||||||
started: std::time::Instant::now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
UiMessage::ToolFinished { id } => {
|
|
||||||
self.active_tools.retain(|t| t.id != id);
|
|
||||||
}
|
|
||||||
UiMessage::Debug(text) => {
|
UiMessage::Debug(text) => {
|
||||||
self.tools.push_line(format!("[debug] {}", text), Color::DarkGray);
|
self.tools.push_line(format!("[debug] {}", text), Color::DarkGray);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,22 @@ pub fn shared_context_state() -> SharedContextState {
|
||||||
Arc::new(RwLock::new(Vec::new()))
|
Arc::new(RwLock::new(Vec::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Active tool info for TUI display.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ActiveTool {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub detail: String,
|
||||||
|
pub started: std::time::Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared active tools — agent writes, TUI reads.
|
||||||
|
pub type SharedActiveTools = Arc<RwLock<Vec<ActiveTool>>>;
|
||||||
|
|
||||||
|
pub fn shared_active_tools() -> SharedActiveTools {
|
||||||
|
Arc::new(RwLock::new(Vec::new()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Which pane streaming text should go to.
|
/// Which pane streaming text should go to.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum StreamTarget {
|
pub enum StreamTarget {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue