Skip to content

Commit db8b348

Browse files
committed
fix the issue of losing context when continuing a conversation
1 parent 7ee5381 commit db8b348

5 files changed

Lines changed: 399 additions & 8 deletions

File tree

cli/src/interactive/app.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ use coro_core::ResolvedLlmConfig;
1414
use iocraft::prelude::*;
1515
use regex::Regex;
1616
use std::path::{Path, PathBuf};
17-
use tokio::sync::broadcast;
17+
use std::sync::Arc;
18+
use tokio::sync::{broadcast, Mutex};
1819

1920
/// Represents a file reference found in user input
2021
#[derive(Debug, Clone)]
@@ -269,14 +270,15 @@ pub fn submit_task_with_file_processing(
269270
llm_config: ResolvedLlmConfig,
270271
project_path: PathBuf,
271272
ui_sender: broadcast::Sender<AppMessage>,
273+
agent: Arc<Mutex<Option<coro_core::agent::AgentCore>>>,
272274
) {
273-
use crate::interactive::components::input_section::spawn_ui_agent_task;
274275
use crate::interactive::message_handler::get_random_status_word;
275276

276277
// Process file references asynchronously and send combined message
277278
let ui_sender_clone = ui_sender.clone();
278279
let llm_config_clone = llm_config.clone();
279280
let project_path_clone = project_path.clone();
281+
let agent_clone = agent.clone();
280282

281283
tokio::spawn(async move {
282284
let input_clone = input.clone();
@@ -297,12 +299,13 @@ pub fn submit_task_with_file_processing(
297299
operation: get_random_status_word(),
298300
});
299301

300-
// Use the existing spawn_ui_agent_task with enhanced input
301-
spawn_ui_agent_task(
302+
// Use the enhanced spawn_ui_agent_task_with_context with enhanced input
303+
crate::interactive::components::input_section::spawn_ui_agent_task_with_context(
302304
enhanced_input,
303305
llm_config_clone,
304306
project_path_clone,
305307
ui_sender_clone,
308+
agent_clone.clone(),
306309
);
307310
}
308311
Err(e) => {
@@ -317,25 +320,28 @@ pub fn submit_task_with_file_processing(
317320
});
318321

319322
// Fall back to original input
320-
spawn_ui_agent_task(
323+
crate::interactive::components::input_section::spawn_ui_agent_task_with_context(
321324
input_clone,
322325
llm_config_clone,
323326
project_path_clone,
324327
ui_sender_clone,
328+
agent_clone,
325329
);
326330
}
327331
}
328332
});
329333
}
330334

331335
/// Context for interactive mode - immutable application configuration
332-
#[derive(Debug, Clone)]
336+
#[derive(Clone)]
333337
struct AppContext {
334338
llm_config: ResolvedLlmConfig,
335339
project_path: PathBuf,
336340
ui_sender: broadcast::Sender<AppMessage>,
337341
ui_anim: UiAnimationConfig,
338342
debug_model: bool,
343+
// Persistent agent instance for conversation continuity
344+
agent: Arc<Mutex<Option<coro_core::agent::AgentCore>>>,
339345
}
340346

341347
impl AppContext {
@@ -353,6 +359,7 @@ impl AppContext {
353359
ui_sender,
354360
ui_anim,
355361
debug_model,
362+
agent: Arc::new(Mutex::new(None)),
356363
}
357364
}
358365
}
@@ -542,6 +549,7 @@ fn CoroApp(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
542549
llm_config: app_context.llm_config.clone(),
543550
project_path: app_context.project_path.clone(),
544551
ui_sender: app_context.ui_sender.clone(),
552+
agent: app_context.agent.clone(),
545553
};
546554

547555
// Create router configuration with main page using new API

cli/src/interactive/components/input_section.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ use coro_core::ResolvedLlmConfig;
1616
use iocraft::prelude::*;
1717
use std::cmp::min;
1818
use std::path::PathBuf;
19+
use std::sync::Arc;
1920
use std::time::{Duration, Instant};
20-
use tokio::sync::broadcast;
21+
use tokio::sync::{broadcast, Mutex};
2122
use unicode_width::UnicodeWidthStr;
2223

2324
/// Find the nearest character boundary at or before the given byte position
@@ -117,17 +118,19 @@ impl Default for InputSectionProps {
117118
),
118119
project_path: PathBuf::new(),
119120
ui_sender: tokio::sync::broadcast::channel(1).0,
121+
agent: Arc::new(Mutex::new(None)),
120122
},
121123
}
122124
}
123125
}
124126

125127
/// Context for the input section component
126-
#[derive(Debug, Clone)]
128+
#[derive(Clone)]
127129
pub struct InputSectionContext {
128130
pub llm_config: ResolvedLlmConfig,
129131
pub project_path: PathBuf,
130132
pub ui_sender: broadcast::Sender<AppMessage>,
133+
pub agent: Arc<Mutex<Option<coro_core::agent::AgentCore>>>,
131134
}
132135

133136
/// Enhanced text input component that wraps iocraft's TextInput with submit handling
@@ -912,6 +915,69 @@ pub fn spawn_ui_agent_task(
912915
});
913916
}
914917

918+
/// Spawn agent task execution with persistent agent for conversation continuity
919+
pub fn spawn_ui_agent_task_with_context(
920+
input: String,
921+
llm_config: ResolvedLlmConfig,
922+
project_path: PathBuf,
923+
ui_sender: broadcast::Sender<AppMessage>,
924+
agent: Arc<Mutex<Option<coro_core::agent::AgentCore>>>,
925+
) {
926+
use crate::interactive::message_handler::get_random_status_word;
927+
use crate::interactive::task_executor::execute_agent_task_with_context;
928+
929+
// Start with a random status word
930+
let _ = ui_sender.send(AppMessage::AgentTaskStarted {
931+
operation: get_random_status_word(),
932+
});
933+
934+
// Create a cancellation token for the timer
935+
let (cancel_sender, mut cancel_receiver) = tokio::sync::oneshot::channel::<()>();
936+
937+
// Change status word once after 1 second (unless cancelled)
938+
let ui_sender_timer = ui_sender.clone();
939+
tokio::spawn(async move {
940+
tokio::select! {
941+
_ = tokio::time::sleep(tokio::time::Duration::from_secs(1)) => {
942+
let _ = ui_sender_timer.send(AppMessage::AgentTaskStarted {
943+
operation: get_random_status_word(),
944+
});
945+
}
946+
_ = &mut cancel_receiver => {
947+
// Timer cancelled, do nothing
948+
}
949+
}
950+
});
951+
952+
// Execute agent task with persistent context
953+
tokio::spawn(async move {
954+
match execute_agent_task_with_context(
955+
input,
956+
llm_config,
957+
project_path,
958+
ui_sender.clone(),
959+
agent,
960+
)
961+
.await
962+
{
963+
Ok(_) => {
964+
let _ = cancel_sender.send(()); // Cancel the timer
965+
let _ = ui_sender.send(AppMessage::AgentExecutionCompleted);
966+
}
967+
Err(e) => {
968+
let _ = cancel_sender.send(()); // Cancel the timer
969+
// Check if it's an interruption error
970+
if e.to_string().contains("Task interrupted by user") {
971+
// Don't show error message for user interruptions
972+
} else {
973+
let _ = ui_sender.send(AppMessage::SystemMessage(format!("Error: {}", e)));
974+
}
975+
let _ = ui_sender.send(AppMessage::AgentExecutionCompleted);
976+
}
977+
}
978+
});
979+
}
980+
915981
/// Input Section Component - Fixed bottom area for input and status
916982
#[component]
917983
pub fn InputSection(mut hooks: Hooks, props: &InputSectionProps) -> impl Into<AnyElement<'static>> {
@@ -1105,6 +1171,7 @@ pub fn InputSection(mut hooks: Hooks, props: &InputSectionProps) -> impl Into<An
11051171
let ui_sender = ui_sender.clone();
11061172
let llm_config = llm_config.clone();
11071173
let project_path = project_path.clone();
1174+
let agent = context.agent.clone();
11081175
move |input: String| {
11091176
if input.trim().is_empty() {
11101177
return;
@@ -1137,6 +1204,7 @@ pub fn InputSection(mut hooks: Hooks, props: &InputSectionProps) -> impl Into<An
11371204
llm_config.clone(),
11381205
project_path.clone(),
11391206
ui_sender.clone(),
1207+
agent.clone(),
11401208
);
11411209
}
11421210
},

cli/src/interactive/pages/main_page.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ impl Default for MainPageProps {
3131
),
3232
project_path: std::path::PathBuf::from("."),
3333
ui_sender: tokio::sync::broadcast::channel(1).0,
34+
agent: std::sync::Arc::new(tokio::sync::Mutex::new(None)),
3435
},
3536
}
3637
}

cli/src/interactive/task_executor.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,113 @@ impl coro_core::output::AgentOutput for TokenTrackingOutputHandler {
7575
}
7676
}
7777

78+
/// Execute agent task with persistent agent to maintain conversation context
79+
pub async fn execute_agent_task_with_context(
80+
task: String,
81+
llm_config: ResolvedLlmConfig,
82+
project_path: PathBuf,
83+
ui_sender: broadcast::Sender<AppMessage>,
84+
agent: std::sync::Arc<tokio::sync::Mutex<Option<coro_core::agent::AgentCore>>>,
85+
) -> Result<()> {
86+
// Create a receiver to listen for interruption signals
87+
let mut interrupt_receiver = ui_sender.subscribe();
88+
use crate::tools::StatusReportToolFactory;
89+
90+
// Create channel for InteractiveMessage and forward to AppMessage
91+
let (interactive_sender, mut interactive_receiver) = mpsc::unbounded_channel();
92+
let ui_sender_clone = ui_sender.clone();
93+
94+
// Forward InteractiveMessage to AppMessage
95+
tokio::spawn(async move {
96+
while let Some(interactive_msg) = interactive_receiver.recv().await {
97+
let _ = ui_sender_clone.send(AppMessage::InteractiveUpdate(interactive_msg));
98+
}
99+
});
100+
101+
// Lock the agent for the duration of this task
102+
let mut agent_guard = agent.lock().await;
103+
104+
// If no agent exists, create one
105+
if agent_guard.is_none() {
106+
// Create agent configuration with CLI tools and status_report tool for interactive mode
107+
let mut agent_config = coro_core::AgentConfig {
108+
tools: crate::tools::get_default_cli_tools(),
109+
..Default::default()
110+
};
111+
if !agent_config.tools.contains(&"status_report".to_string()) {
112+
agent_config.tools.push("status_report".to_string());
113+
}
114+
115+
// Create TokenTrackingOutputHandler with UI integration
116+
let interactive_config = InteractiveOutputConfig {
117+
realtime_updates: true,
118+
show_tool_details: true,
119+
};
120+
let token_tracking_output = Box::new(TokenTrackingOutputHandler::new(
121+
interactive_config,
122+
interactive_sender,
123+
ui_sender.clone(),
124+
));
125+
126+
// Create CLI tool registry with status_report tool for interactive mode
127+
let mut tool_registry = crate::tools::create_cli_tool_registry();
128+
tool_registry.register_factory(Box::new(StatusReportToolFactory::with_ui_sender(
129+
ui_sender.clone(),
130+
)));
131+
132+
// Create new agent
133+
let new_agent = coro_core::agent::AgentCore::new_with_output_and_registry(
134+
agent_config,
135+
llm_config,
136+
token_tracking_output,
137+
tool_registry,
138+
)
139+
.await?;
140+
141+
*agent_guard = Some(new_agent);
142+
}
143+
144+
// Get mutable reference to the agent
145+
let agent_ref = agent_guard.as_mut().unwrap();
146+
147+
// Execute task with conversation continuation
148+
let task_future = agent_ref.continue_conversation(&task, &project_path);
149+
150+
// Listen for interruption signals
151+
let interrupt_future = async {
152+
loop {
153+
match interrupt_receiver.recv().await {
154+
Ok(AppMessage::AgentExecutionInterrupted { .. }) => {
155+
tracing::warn!("Task interrupted by user");
156+
return Err(anyhow::anyhow!("Task interrupted by user"));
157+
}
158+
Ok(_) => continue, // Ignore other messages
159+
Err(_) => break, // Channel closed
160+
}
161+
}
162+
Ok(())
163+
};
164+
165+
// Add timeout to prevent hanging
166+
let timeout_future = tokio::time::sleep(tokio::time::Duration::from_secs(300)); // 5 minutes timeout
167+
168+
// Race between task execution, interruption, and timeout
169+
tokio::select! {
170+
result = task_future => {
171+
result?;
172+
}
173+
interrupt_result = interrupt_future => {
174+
interrupt_result?;
175+
}
176+
_ = timeout_future => {
177+
tracing::error!("Task execution timed out after 5 minutes");
178+
return Err(anyhow::anyhow!("Task execution timed out"));
179+
}
180+
}
181+
182+
Ok(())
183+
}
184+
78185
/// Execute agent task asynchronously and send updates to UI
79186
pub async fn execute_agent_task(
80187
task: String,

0 commit comments

Comments
 (0)