Compare commits
3 Commits
93945b626c
...
e37dea886b
| Author | SHA1 | Date | |
|---|---|---|---|
| e37dea886b | |||
| 640829ce52 | |||
| 229221aab1 |
@ -20,8 +20,6 @@ use std::time::Instant;
|
|||||||
/// Minimum characters to keep when truncating
|
/// Minimum characters to keep when truncating
|
||||||
const TRUNCATION_SUFFIX_LEN: usize = 200;
|
const TRUNCATION_SUFFIX_LEN: usize = 200;
|
||||||
const PENDING_USER_ACTION_MARKER: &str = "__PICOBOT_PENDING_USER_ACTION__";
|
const PENDING_USER_ACTION_MARKER: &str = "__PICOBOT_PENDING_USER_ACTION__";
|
||||||
const DEFAULT_PENDING_ASSISTANT_MESSAGE: &str =
|
|
||||||
"工具已经启动并进入等待用户操作的状态。请先完成外部操作,完成后直接告诉我继续。";
|
|
||||||
const RECOVERABLE_LLM_ERROR_MESSAGE: &str = "模型服务暂时不可用或响应超时。请稍后重试。";
|
const RECOVERABLE_LLM_ERROR_MESSAGE: &str = "模型服务暂时不可用或响应超时。请稍后重试。";
|
||||||
|
|
||||||
const SUPPORTED_IMAGE_MIME_TYPES: &[&str] = &["image/jpeg", "image/png", "image/gif", "image/webp"];
|
const SUPPORTED_IMAGE_MIME_TYPES: &[&str] = &["image/jpeg", "image/png", "image/gif", "image/webp"];
|
||||||
@ -1091,31 +1089,9 @@ impl AgentLoop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((tool_call, pending_result)) = response
|
|
||||||
.tool_calls
|
|
||||||
.iter()
|
|
||||||
.zip(tool_results.iter())
|
|
||||||
.find(|(_, result)| result.state == ToolExecutionState::PendingUserAction)
|
|
||||||
{
|
|
||||||
let assistant_message = ChatMessage::assistant(format!(
|
|
||||||
"{}\n\n当前等待中的工具: {}",
|
|
||||||
pending_result
|
|
||||||
.output
|
|
||||||
.lines()
|
|
||||||
.next()
|
|
||||||
.filter(|line| !line.trim().is_empty())
|
|
||||||
.unwrap_or(DEFAULT_PENDING_ASSISTANT_MESSAGE),
|
|
||||||
tool_call.name,
|
|
||||||
));
|
|
||||||
emitted_messages.push(assistant_message.clone());
|
|
||||||
self.emit_live_tool_call_message(assistant_message.clone()).await;
|
|
||||||
return Ok(AgentProcessResult {
|
|
||||||
final_response: assistant_message,
|
|
||||||
emitted_messages,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop continues to next iteration with updated messages
|
// Loop continues to next iteration with updated messages
|
||||||
|
// PendingUserAction 工具的结果已在上方加入 messages,
|
||||||
|
// 模型将在下一轮看到完整的终端输出并生成智能回复
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
iteration,
|
iteration,
|
||||||
|
|||||||
@ -76,9 +76,6 @@ impl CommandHandler for ListTodosCommandHandler {
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
content: r.content,
|
content: r.content,
|
||||||
status: r.status,
|
status: r.status,
|
||||||
priority: r.priority,
|
|
||||||
created_at: r.created_at,
|
|
||||||
updated_at: r.updated_at,
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
@ -141,9 +141,15 @@ impl BusToolCallEmitter {
|
|||||||
|
|
||||||
let topic_id = self.metadata.get("topic_id").filter(|t| !t.is_empty()).cloned();
|
let topic_id = self.metadata.get("topic_id").filter(|t| !t.is_empty()).cloned();
|
||||||
|
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs() as i64;
|
||||||
|
|
||||||
let records: Vec<crate::storage::TodoRecord> = todos_array
|
let records: Vec<crate::storage::TodoRecord> = todos_array
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| {
|
.enumerate()
|
||||||
|
.filter_map(|(idx, item)| {
|
||||||
Some(crate::storage::TodoRecord {
|
Some(crate::storage::TodoRecord {
|
||||||
id: item.get("id")?.as_str()?.to_string(),
|
id: item.get("id")?.as_str()?.to_string(),
|
||||||
scope_key: scope_key.clone(),
|
scope_key: scope_key.clone(),
|
||||||
@ -151,9 +157,9 @@ impl BusToolCallEmitter {
|
|||||||
topic_id: topic_id.clone(),
|
topic_id: topic_id.clone(),
|
||||||
content: item.get("content")?.as_str()?.to_string(),
|
content: item.get("content")?.as_str()?.to_string(),
|
||||||
status: item.get("status")?.as_str()?.to_string(),
|
status: item.get("status")?.as_str()?.to_string(),
|
||||||
priority: item.get("priority")?.as_str()?.to_string(),
|
priority: "medium".to_string(),
|
||||||
created_at: item.get("created_at")?.as_i64()?,
|
created_at: now + idx as i64,
|
||||||
updated_at: item.get("updated_at")?.as_i64()?,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const TODO_WRITE_INSTRUCTIONS: &str = r#"
|
|||||||
|
|
||||||
### merge 参数
|
### merge 参数
|
||||||
- `merge: false`(默认):全量替换 — 只传入需要追踪的 todo,不在列表中的项将被移除
|
- `merge: false`(默认):全量替换 — 只传入需要追踪的 todo,不在列表中的项将被移除
|
||||||
- `merge: true`(推荐):增量更新 — 只传入需要添加或更新的项,未提及的项保持不变。**绝大多数情况应该使用 merge=true,这样你不需要记住所有 id**
|
- `merge: true`(推荐):增量更新 — 只传入需要添加或更新的项,未提及的项保持不变。**绝大多数情况应该使用 merge=true**
|
||||||
|
|
||||||
### 状态语义
|
### 状态语义
|
||||||
- `pending` — 尚未开始
|
- `pending` — 尚未开始
|
||||||
@ -39,33 +39,34 @@ const TODO_WRITE_INSTRUCTIONS: &str = r#"
|
|||||||
### 核心规则
|
### 核心规则
|
||||||
1. 同一时间只能有一个任务处于 `in_progress` 状态
|
1. 同一时间只能有一个任务处于 `in_progress` 状态
|
||||||
2. 必须先完成当前 `in_progress` 的任务,再开始下一个
|
2. 必须先完成当前 `in_progress` 的任务,再开始下一个
|
||||||
3. `completed` 和 `cancelled` 是终端状态,已完成的项不能被重新激活
|
3. `completed` 和 `cancelled` 的项可以重新激活(改回 `in_progress` 或 `pending`),用于任务返工或恢复
|
||||||
4. 不要先标记 completed 再去实际执行 — 先完成工作,再标记
|
4. `in_progress` 不能退回 `pending`,应直接标记为 `completed` 或 `cancelled`
|
||||||
5. `content` 字段保持简洁、可执行
|
5. 不要先标记 completed 再去实际执行 — 先完成工作,再标记
|
||||||
6. **更新已有任务必须传 `id`**。新任务不传 id(工具会自动生成),更新状态时必须传 id。id 可以从之前 todo_write 返回的 `current_todos` 中获取
|
6. `content` 字段保持简洁、可执行
|
||||||
|
7. **每个任务都必须传 `id`**。新任务由你生成一个短随机字符串作为 id(如 `"r9Tg8Kq2"`),更新任务时使用相同的 id。id 可以从之前 todo_write 返回的 `current_todos` 中获取
|
||||||
|
|
||||||
### 使用范例
|
### 使用范例
|
||||||
|
|
||||||
创建新任务(新任务必须 pending 或 in_progress,不传 id):
|
创建新任务(生成随机 id):
|
||||||
```json
|
```json
|
||||||
{"merge": true, "todos": [{"content": "修复登录 bug", "status": "in_progress"}]}
|
{"merge": true, "todos": [{"id": "aB3kLm9x", "content": "修复登录 bug", "status": "in_progress"}]}
|
||||||
```
|
```
|
||||||
|
|
||||||
追加新任务:
|
追加新任务:
|
||||||
```json
|
```json
|
||||||
{"merge": true, "todos": [{"content": "补充测试", "status": "pending"}]}
|
{"merge": true, "todos": [{"id": "pQ7nWy2z", "content": "补充测试", "status": "pending"}]}
|
||||||
```
|
```
|
||||||
|
|
||||||
更新已有任务(**必须传 id**,从上次返回的 current_todos 中取得):
|
更新已有任务(使用创建时的 id):
|
||||||
```json
|
```json
|
||||||
{"merge": true, "todos": [{"id": "abc-123", "content": "修复登录 bug", "status": "completed"}]}
|
{"merge": true, "todos": [{"id": "aB3kLm9x", "content": "修复登录 bug", "status": "completed"}]}
|
||||||
```
|
```
|
||||||
|
|
||||||
同时更新多项:
|
同时更新多项:
|
||||||
```json
|
```json
|
||||||
{"merge": true, "todos": [
|
{"merge": true, "todos": [
|
||||||
{"id": "abc-123", "content": "修复登录 bug", "status": "completed"},
|
{"id": "aB3kLm9x", "content": "修复登录 bug", "status": "completed"},
|
||||||
{"content": "代码审查", "status": "in_progress"}
|
{"id": "pQ7nWy2z", "content": "补充测试", "status": "in_progress"}
|
||||||
]}
|
]}
|
||||||
```
|
```
|
||||||
"#;
|
"#;
|
||||||
|
|||||||
@ -88,9 +88,6 @@ pub struct TodoItemSummary {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub priority: String,
|
|
||||||
pub created_at: i64,
|
|
||||||
pub updated_at: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@ -182,12 +182,17 @@ impl BashTool {
|
|||||||
let session_line = session_id
|
let session_line = session_id
|
||||||
.map(|id| format!("[session_id: {}]\n", id))
|
.map(|id| format!("[session_id: {}]\n", id))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let output_section = if output.trim().is_empty() {
|
||||||
|
"(进程尚未输出内容。进程正在等待输入,请使用 session_id 和 stdin_input 参数发送输入内容。)"
|
||||||
|
} else {
|
||||||
|
&self.truncate_output(output.trim())
|
||||||
|
};
|
||||||
format!(
|
format!(
|
||||||
"{}\n{}{}\n\n{}",
|
"{}\n{}{}\n\n{}",
|
||||||
PENDING_USER_ACTION_MARKER,
|
PENDING_USER_ACTION_MARKER,
|
||||||
session_line,
|
session_line,
|
||||||
hint,
|
hint,
|
||||||
self.truncate_output(output.trim())
|
output_section
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,6 +265,38 @@ async fn drain_available_chunks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 自适应 drain:循环读取直到输出稳定,确保进程的所有提示内容都被捕获
|
||||||
|
///
|
||||||
|
/// - 每次 drain 后等待 200ms 再检查是否有新数据
|
||||||
|
/// - 最多循环 10 次(即最多等待 2 秒)
|
||||||
|
/// - 如果连续一次 drain 没有新数据,立即返回
|
||||||
|
async fn drain_until_stable(
|
||||||
|
rx: &mut mpsc::UnboundedReceiver<(bool, String)>,
|
||||||
|
stdout_buf: &Arc<Mutex<String>>,
|
||||||
|
stderr_buf: &Arc<Mutex<String>>,
|
||||||
|
) {
|
||||||
|
const DRAIN_INTERVAL_MS: u64 = 200;
|
||||||
|
const MAX_DRAIN_ROUNDS: u32 = 10;
|
||||||
|
|
||||||
|
for _ in 0..MAX_DRAIN_ROUNDS {
|
||||||
|
let prev_stdout_len = stdout_buf.lock().await.len();
|
||||||
|
let prev_stderr_len = stderr_buf.lock().await.len();
|
||||||
|
|
||||||
|
drain_available_chunks(rx, stdout_buf, stderr_buf).await;
|
||||||
|
|
||||||
|
let new_stdout_len = stdout_buf.lock().await.len();
|
||||||
|
let new_stderr_len = stderr_buf.lock().await.len();
|
||||||
|
|
||||||
|
// 如果没有新数据,说明输出已稳定
|
||||||
|
if new_stdout_len == prev_stdout_len && new_stderr_len == prev_stderr_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有新数据,等待后再次检查
|
||||||
|
tokio::time::sleep(Duration::from_millis(DRAIN_INTERVAL_MS)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for BashTool {
|
impl Default for BashTool {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(Arc::new(ShellSessionManager::new()))
|
Self::new(Arc::new(ShellSessionManager::new()))
|
||||||
@ -472,11 +509,12 @@ impl BashTool {
|
|||||||
// Periodic safety net: check OS-level process state
|
// Periodic safety net: check OS-level process state
|
||||||
if let Some(pid) = child.id() {
|
if let Some(pid) = child.id() {
|
||||||
if crate::platform::is_process_waiting_on_stdin(pid) == Some(true) {
|
if crate::platform::is_process_waiting_on_stdin(pid) == Some(true) {
|
||||||
|
// 自适应 drain,等待输出稳定
|
||||||
if let Some(rx_ref) = rx.as_mut() {
|
if let Some(rx_ref) = rx.as_mut() {
|
||||||
drain_available_chunks(rx_ref, &stdout_buf, &stderr_buf).await;
|
drain_until_stable(rx_ref, &stdout_buf, &stderr_buf).await;
|
||||||
}
|
}
|
||||||
let combined = format_command_output(&stdout_buf.lock().await, &stderr_buf.lock().await, None);
|
let combined = format_command_output(&stdout_buf.lock().await, &stderr_buf.lock().await, None);
|
||||||
if !combined.trim().is_empty() {
|
// 始终创建 session,即使输出为空(进程可能还没写出提示)
|
||||||
if let Some(stdin) = child_stdin {
|
if let Some(stdin) = child_stdin {
|
||||||
if let Some(rx_val) = rx.take() {
|
if let Some(rx_val) = rx.take() {
|
||||||
let session_id = self.session_manager.save_session(
|
let session_id = self.session_manager.save_session(
|
||||||
@ -492,7 +530,6 @@ impl BashTool {
|
|||||||
return Ok(self.pending_output(&combined, None));
|
return Ok(self.pending_output(&combined, None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let combined = format_command_output(&stdout_buf.lock().await, &stderr_buf.lock().await, None);
|
let combined = format_command_output(&stdout_buf.lock().await, &stderr_buf.lock().await, None);
|
||||||
if self.should_return_pending(interactive, &combined) {
|
if self.should_return_pending(interactive, &combined) {
|
||||||
@ -523,9 +560,12 @@ impl BashTool {
|
|||||||
|
|
||||||
// OS-level check: if blocked on stdin, save as session
|
// OS-level check: if blocked on stdin, save as session
|
||||||
if let Some(pid) = child.id() {
|
if let Some(pid) = child.id() {
|
||||||
if crate::platform::is_process_waiting_on_stdin(pid) == Some(true)
|
if crate::platform::is_process_waiting_on_stdin(pid) == Some(true) {
|
||||||
&& !combined.trim().is_empty()
|
// 自适应 drain,等待输出稳定
|
||||||
{
|
if let Some(rx_ref) = rx.as_mut() {
|
||||||
|
drain_until_stable(rx_ref, &stdout_buf, &stderr_buf).await;
|
||||||
|
}
|
||||||
|
let combined = format_command_output(&stdout_buf.lock().await, &stderr_buf.lock().await, None);
|
||||||
if let Some(stdin) = child_stdin {
|
if let Some(stdin) = child_stdin {
|
||||||
if let Some(rx_val) = rx.take() {
|
if let Some(rx_val) = rx.take() {
|
||||||
let session_id = self.session_manager.save_session(
|
let session_id = self.session_manager.save_session(
|
||||||
|
|||||||
@ -57,20 +57,20 @@ impl Default for InMemoryTaskRepository {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl TaskRepository for InMemoryTaskRepository {
|
impl TaskRepository for InMemoryTaskRepository {
|
||||||
async fn save_task_session(&self, session: &TaskSession) -> Result<(), StorageError> {
|
async fn save_task_session(&self, session: &TaskSession) -> Result<(), StorageError> {
|
||||||
tracing::warn!(
|
tracing::debug!(
|
||||||
task_id = %session.id,
|
task_id = %session.id,
|
||||||
session_id = %session.session_id,
|
session_id = %session.session_id,
|
||||||
state = ?session.state,
|
state = ?session.state,
|
||||||
"REPO_SAVE: Saving task session"
|
"Saving task session"
|
||||||
);
|
);
|
||||||
self.sessions
|
self.sessions
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(session.id.clone(), session.clone());
|
.insert(session.id.clone(), session.clone());
|
||||||
tracing::warn!(
|
tracing::debug!(
|
||||||
task_id = %session.id,
|
task_id = %session.id,
|
||||||
total_tasks = self.sessions.read().unwrap().len(),
|
total_tasks = self.sessions.read().unwrap().len(),
|
||||||
"REPO_SAVE: Task session saved, current repository size"
|
"Task session saved, current repository size"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -79,11 +79,11 @@ impl TaskRepository for InMemoryTaskRepository {
|
|||||||
let sessions = self.sessions.read().unwrap();
|
let sessions = self.sessions.read().unwrap();
|
||||||
let total = sessions.len();
|
let total = sessions.len();
|
||||||
let keys: Vec<&str> = sessions.keys().map(|k| k.as_str()).collect();
|
let keys: Vec<&str> = sessions.keys().map(|k| k.as_str()).collect();
|
||||||
tracing::warn!(
|
tracing::debug!(
|
||||||
lookup_task_id = %task_id,
|
lookup_task_id = %task_id,
|
||||||
total_tasks = total,
|
total_tasks = total,
|
||||||
all_keys = ?keys,
|
all_keys = ?keys,
|
||||||
"REPO_LOOKUP: Looking up task session"
|
"Looking up task session"
|
||||||
);
|
);
|
||||||
Ok(sessions.get(task_id).cloned())
|
Ok(sessions.get(task_id).cloned())
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user