UX: live terminal progress; conclusion only when done
This commit is contained in:
parent
c94a32ea72
commit
d965bb4b4a
|
|
@ -28,7 +28,7 @@ export GROKBOY_API_KEY=your_key # or XAI_API_KEY
|
||||||
# export GROKBOY_CONTEXT_CHARS=100000
|
# export GROKBOY_CONTEXT_CHARS=100000
|
||||||
# export GROKBOY_MAX_ROUNDS=12 # rounds per chunk
|
# export GROKBOY_MAX_ROUNDS=12 # rounds per chunk
|
||||||
# export GROKBOY_MAX_ROUNDS_TOTAL=48 # absolute ceiling across auto-continues
|
# export GROKBOY_MAX_ROUNDS_TOTAL=48 # absolute ceiling across auto-continues
|
||||||
# export GROKBOY_PROGRESS=0 # silence 〔續跑〕 progress beats
|
# export GROKBOY_PROGRESS=0 # silence live progress (思考/工具/續跑/結束)
|
||||||
# export GROKBOY_BROWSER_HEADED=1 # visible Chromium (recommended for handoff / agent)
|
# export GROKBOY_BROWSER_HEADED=1 # visible Chromium (recommended for handoff / agent)
|
||||||
|
|
||||||
cd ~/GrokBoy
|
cd ~/GrokBoy
|
||||||
|
|
@ -103,7 +103,7 @@ Tools: `shell`, `list_dir`, `read_file`, `write_file`, `report_done`, `report_bl
|
||||||
Sessions are stored under `~/.grokboy/sessions/<id>.json` (may include `last_browser_url`).
|
Sessions are stored under `~/.grokboy/sessions/<id>.json` (may include `last_browser_url`).
|
||||||
|
|
||||||
The agent stops on `report_done` / `report_blocked`, blocks identical tool rounds (×3), and truncates old context when over budget.
|
The agent stops on `report_done` / `report_blocked`, blocks identical tool rounds (×3), and truncates old context when over budget.
|
||||||
Long tasks **auto-continue** across chunks (`GROKBOY_MAX_ROUNDS` per beat, up to `GROKBOY_MAX_ROUNDS_TOTAL`) with `〔續跑〕` progress on stderr — like Grok Bot — instead of hard-stopping for a user re-prompt after every chunk.
|
Long tasks **auto-continue** across chunks (`GROKBOY_MAX_ROUNDS` per beat, up to `GROKBOY_MAX_ROUNDS_TOTAL`) with live stderr progress (`〔思考中〕` / `〔工具〕` / `〔進度|尚未完成〕〔續跑〕` / …) — like Grok Bot — instead of hard-stopping for a user re-prompt after every chunk. Final Done/Answer prints as `〔結論〕`; mid-task progress is never the conclusion.
|
||||||
|
|
||||||
## Traditional Chinese
|
## Traditional Chinese
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,10 @@ For web pages prefer DOM snapshot + selector/role click/type — not screenshots
|
||||||
If you hit a login / OTP / captcha wall you cannot pass alone, call browser_handoff with a clear reason so the human can help in the visible browser, then continue from the returned snapshot. Prefer browser_handoff only for auth walls.
|
If you hit a login / OTP / captcha wall you cannot pass alone, call browser_handoff with a clear reason so the human can help in the visible browser, then continue from the returned snapshot. Prefer browser_handoff only for auth walls.
|
||||||
Never publish/send social posts (Threads, Facebook, Instagram, X/Twitter, etc.) or take other irreversible public actions without either (a) an explicit user message this turn approving the exact draft, or (b) request_user_confirm returning approved. Prefer draft → confirm → then act. If approval is missing, call request_user_confirm (with the draft in prompt) or report_blocked — never post unilaterally.
|
Never publish/send social posts (Threads, Facebook, Instagram, X/Twitter, etc.) or take other irreversible public actions without either (a) an explicit user message this turn approving the exact draft, or (b) request_user_confirm returning approved. Prefer draft → confirm → then act. If approval is missing, call request_user_confirm (with the draft in prompt) or report_blocked — never post unilaterally.
|
||||||
For greetings, small talk, clarifying questions, or when no tools are needed: reply with normal assistant text and stop (do not call report_done).
|
For greetings, small talk, clarifying questions, or when no tools are needed: reply with normal assistant text and stop (do not call report_done).
|
||||||
Call report_done only when a real task/tool workflow is actually finished, with a short summary.
|
Do not call report_done or give a final wrap-up answer until the user's task is actually complete.
|
||||||
|
If still researching/browsing, keep using tools; live progress is shown by the runtime on stderr — you do not need to narrate every step as a conclusion.
|
||||||
|
If you must speak mid-flight without tools, say it is partial progress only — prefer continuing with tools instead.
|
||||||
|
report_done = final delivery only (short summary when a real task/tool workflow is actually finished).
|
||||||
Call report_blocked when stuck or cannot proceed — do not invent results or loop.
|
Call report_blocked when stuck or cannot proceed — do not invent results or loop.
|
||||||
If work is large, keep using tools across the session; the runtime may continue in chunks — still call report_done when truly finished; don't stop early just to 'save rounds'.
|
If work is large, keep using tools across the session; the runtime may continue in chunks — still call report_done when truly finished; don't stop early just to 'save rounds'.
|
||||||
Do not invent tool results — call the tools.";
|
Do not invent tool results — call the tools.";
|
||||||
|
|
@ -83,13 +86,43 @@ pub fn max_rounds_total_budget() -> usize {
|
||||||
.unwrap_or(DEFAULT_MAX_ROUNDS_TOTAL)
|
.unwrap_or(DEFAULT_MAX_ROUNDS_TOTAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Progress beats on stderr unless `GROKBOY_PROGRESS=0`.
|
/// Progress beats on stderr unless `GROKBOY_PROGRESS=0`. Always newline + flush.
|
||||||
fn emit_progress(msg: &str) {
|
fn emit_progress(msg: &str) {
|
||||||
match std::env::var("GROKBOY_PROGRESS") {
|
match std::env::var("GROKBOY_PROGRESS") {
|
||||||
Ok(v) if v == "0" => {}
|
Ok(v) if v == "0" => {}
|
||||||
_ => eprintln!("{msg}"),
|
_ => {
|
||||||
|
use std::io::Write;
|
||||||
|
let mut err = std::io::stderr();
|
||||||
|
let _ = writeln!(err, "{msg}");
|
||||||
|
let _ = err.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_progress_line(msg: &str, on_progress: &mut impl FnMut(&str)) {
|
||||||
|
emit_progress(msg);
|
||||||
|
on_progress(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Short success/fail line for a finished tool invocation.
|
||||||
|
fn tool_progress_line(name: &str, result_json: &str) -> String {
|
||||||
|
let v: Value = serde_json::from_str(result_json).unwrap_or(Value::Null);
|
||||||
|
if let Some(err) = v.get("error") {
|
||||||
|
let s = err
|
||||||
|
.as_str()
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_else(|| err.to_string());
|
||||||
|
return format!("〔失敗〕{name}: {}", preview_progress(&s, 80));
|
||||||
|
}
|
||||||
|
if v.get("blocked").and_then(|b| b.as_bool()) == Some(true) {
|
||||||
|
let reason = v
|
||||||
|
.get("reason")
|
||||||
|
.and_then(|r| r.as_str())
|
||||||
|
.unwrap_or("blocked");
|
||||||
|
return format!("〔失敗〕{name}: {}", preview_progress(reason, 80));
|
||||||
|
}
|
||||||
|
format!("〔完成〕{name}")
|
||||||
|
}
|
||||||
|
|
||||||
fn preview_progress(text: &str, max_chars: usize) -> String {
|
fn preview_progress(text: &str, max_chars: usize) -> String {
|
||||||
let t = text.trim();
|
let t = text.trim();
|
||||||
|
|
@ -252,11 +285,39 @@ pub async fn run_agent_with<F, Fut>(
|
||||||
max_rounds: usize,
|
max_rounds: usize,
|
||||||
max_rounds_total: usize,
|
max_rounds_total: usize,
|
||||||
context_budget: usize,
|
context_budget: usize,
|
||||||
mut complete: F,
|
complete: F,
|
||||||
) -> Result<AgentVerdict>
|
) -> Result<AgentVerdict>
|
||||||
where
|
where
|
||||||
F: FnMut(Vec<ChatMessage>, Option<Value>) -> Fut,
|
F: FnMut(Vec<ChatMessage>, Option<Value>) -> Fut,
|
||||||
Fut: Future<Output = Result<ChatMessage>>,
|
Fut: Future<Output = Result<ChatMessage>>,
|
||||||
|
{
|
||||||
|
run_agent_with_progress(
|
||||||
|
messages,
|
||||||
|
tool_ctx,
|
||||||
|
max_rounds,
|
||||||
|
max_rounds_total,
|
||||||
|
context_budget,
|
||||||
|
complete,
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Like [`run_agent_with`], but also invokes `on_progress` for every live progress line
|
||||||
|
/// (stderr still gated by `GROKBOY_PROGRESS`).
|
||||||
|
pub async fn run_agent_with_progress<F, Fut, P>(
|
||||||
|
messages: &mut Vec<ChatMessage>,
|
||||||
|
tool_ctx: &ToolContext,
|
||||||
|
max_rounds: usize,
|
||||||
|
max_rounds_total: usize,
|
||||||
|
context_budget: usize,
|
||||||
|
mut complete: F,
|
||||||
|
mut on_progress: P,
|
||||||
|
) -> Result<AgentVerdict>
|
||||||
|
where
|
||||||
|
F: FnMut(Vec<ChatMessage>, Option<Value>) -> Fut,
|
||||||
|
Fut: Future<Output = Result<ChatMessage>>,
|
||||||
|
P: FnMut(&str),
|
||||||
{
|
{
|
||||||
let tools = tool_definitions();
|
let tools = tool_definitions();
|
||||||
let mut prev_round_sig: Option<String> = None;
|
let mut prev_round_sig: Option<String> = None;
|
||||||
|
|
@ -267,21 +328,36 @@ where
|
||||||
let max_rounds = max_rounds.max(1);
|
let max_rounds = max_rounds.max(1);
|
||||||
let max_rounds_total = max_rounds_total.max(1);
|
let max_rounds_total = max_rounds_total.max(1);
|
||||||
|
|
||||||
|
emit_progress_line(
|
||||||
|
&format!("〔開始〕最多 {max_rounds_total} 輪(每段 {max_rounds})"),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
chunk_idx += 1;
|
chunk_idx += 1;
|
||||||
let remaining = max_rounds_total.saturating_sub(total_used);
|
let remaining = max_rounds_total.saturating_sub(total_used);
|
||||||
if remaining == 0 {
|
if remaining == 0 {
|
||||||
let progress = summarize_progress(messages, context_budget, &mut complete).await;
|
let progress = summarize_progress(messages, context_budget, &mut complete).await;
|
||||||
return Ok(AgentVerdict::Blocked(format_total_exhausted(
|
let verdict = AgentVerdict::Blocked(format_total_exhausted(
|
||||||
max_rounds_total,
|
max_rounds_total,
|
||||||
&progress,
|
&progress,
|
||||||
)));
|
));
|
||||||
|
emit_progress_line(
|
||||||
|
&format!("〔結束〕verdict={}", verdict.kind()),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
|
return Ok(verdict);
|
||||||
}
|
}
|
||||||
let chunk_limit = max_rounds.min(remaining);
|
let chunk_limit = max_rounds.min(remaining);
|
||||||
|
|
||||||
for _round_in_chunk in 0..chunk_limit {
|
for _round_in_chunk in 0..chunk_limit {
|
||||||
truncate_messages(messages, context_budget);
|
truncate_messages(messages, context_budget);
|
||||||
|
|
||||||
|
emit_progress_line(
|
||||||
|
&format!("〔思考中〕第 {}/{} 輪…", total_used + 1, max_rounds_total),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
|
|
||||||
let reply = complete(messages.clone(), Some(tools.clone())).await?;
|
let reply = complete(messages.clone(), Some(tools.clone())).await?;
|
||||||
let tool_calls = reply.tool_calls.clone().unwrap_or_default();
|
let tool_calls = reply.tool_calls.clone().unwrap_or_default();
|
||||||
total_used += 1;
|
total_used += 1;
|
||||||
|
|
@ -290,13 +366,32 @@ where
|
||||||
let last_text = reply.text().to_string();
|
let last_text = reply.text().to_string();
|
||||||
messages.push(reply);
|
messages.push(reply);
|
||||||
if last_text.trim().is_empty() {
|
if last_text.trim().is_empty() {
|
||||||
return Ok(AgentVerdict::Blocked(
|
let verdict = AgentVerdict::Blocked(
|
||||||
"model returned empty final answer".into(),
|
"model returned empty final answer".into(),
|
||||||
));
|
);
|
||||||
|
emit_progress_line(
|
||||||
|
&format!("〔結束〕verdict={}", verdict.kind()),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
|
return Ok(verdict);
|
||||||
}
|
}
|
||||||
return Ok(AgentVerdict::Answer(last_text));
|
let verdict = AgentVerdict::Answer(last_text);
|
||||||
|
emit_progress_line(
|
||||||
|
&format!("〔結束〕verdict={}", verdict.kind()),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
|
return Ok(verdict);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let names: Vec<&str> = tool_calls
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.function.name.as_str())
|
||||||
|
.collect();
|
||||||
|
emit_progress_line(
|
||||||
|
&format!("〔工具〕round {total_used}: {}", names.join(", ")),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
|
|
||||||
// Loop guard: identical tool-call round repeated N times → fail closed (no auto-continue).
|
// Loop guard: identical tool-call round repeated N times → fail closed (no auto-continue).
|
||||||
let sig = round_signature(&tool_calls);
|
let sig = round_signature(&tool_calls);
|
||||||
if prev_round_sig.as_deref() == Some(sig.as_str()) {
|
if prev_round_sig.as_deref() == Some(sig.as_str()) {
|
||||||
|
|
@ -310,7 +405,12 @@ where
|
||||||
let reason = format!(
|
let reason = format!(
|
||||||
"loop guard: identical tool calls repeated {LOOP_GUARD_REPEAT} times (round {total_used})"
|
"loop guard: identical tool calls repeated {LOOP_GUARD_REPEAT} times (round {total_used})"
|
||||||
);
|
);
|
||||||
return Ok(AgentVerdict::Blocked(reason));
|
let verdict = AgentVerdict::Blocked(reason);
|
||||||
|
emit_progress_line(
|
||||||
|
&format!("〔結束〕verdict={}", verdict.kind()),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
|
return Ok(verdict);
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.push(reply);
|
messages.push(reply);
|
||||||
|
|
@ -319,6 +419,10 @@ where
|
||||||
for call in &tool_calls {
|
for call in &tool_calls {
|
||||||
let result =
|
let result =
|
||||||
execute_tool(tool_ctx, &call.function.name, &call.function.arguments).await;
|
execute_tool(tool_ctx, &call.function.name, &call.function.arguments).await;
|
||||||
|
emit_progress_line(
|
||||||
|
&tool_progress_line(&call.function.name, &result),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
if completion.is_none() && is_completion_tool(&call.function.name) {
|
if completion.is_none() && is_completion_tool(&call.function.name) {
|
||||||
completion = parse_completion_verdict(&call.function.name, &result);
|
completion = parse_completion_verdict(&call.function.name, &result);
|
||||||
}
|
}
|
||||||
|
|
@ -326,6 +430,10 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(verdict) = completion {
|
if let Some(verdict) = completion {
|
||||||
|
emit_progress_line(
|
||||||
|
&format!("〔結束〕verdict={}", verdict.kind()),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
return Ok(verdict);
|
return Ok(verdict);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -334,17 +442,25 @@ where
|
||||||
let progress = summarize_progress(messages, context_budget, &mut complete).await;
|
let progress = summarize_progress(messages, context_budget, &mut complete).await;
|
||||||
|
|
||||||
if total_used >= max_rounds_total {
|
if total_used >= max_rounds_total {
|
||||||
return Ok(AgentVerdict::Blocked(format_total_exhausted(
|
let verdict = AgentVerdict::Blocked(format_total_exhausted(
|
||||||
max_rounds_total,
|
max_rounds_total,
|
||||||
&progress,
|
&progress,
|
||||||
)));
|
));
|
||||||
|
emit_progress_line(
|
||||||
|
&format!("〔結束〕verdict={}", verdict.kind()),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
|
return Ok(verdict);
|
||||||
}
|
}
|
||||||
|
|
||||||
let next_chunk = chunk_idx + 1;
|
let next_chunk = chunk_idx + 1;
|
||||||
let preview = preview_progress(&progress, 160);
|
let preview = preview_progress(&progress, 160);
|
||||||
emit_progress(&format!(
|
emit_progress_line(
|
||||||
"〔續跑〕第 {next_chunk} 段(已用 {total_used}/{max_rounds_total} 輪)進度:{preview}"
|
&format!(
|
||||||
));
|
"〔進度|尚未完成〕〔續跑〕第 {next_chunk} 段(已用 {total_used}/{max_rounds_total} 輪)進度:{preview}"
|
||||||
|
),
|
||||||
|
&mut on_progress,
|
||||||
|
);
|
||||||
// Auto-continue another chunk in the same run_agent invocation.
|
// Auto-continue another chunk in the same run_agent invocation.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -447,11 +563,20 @@ mod tests {
|
||||||
assert!(AGENT_SYSTEM.contains("irreversible") || AGENT_SYSTEM.contains("Never publish"));
|
assert!(AGENT_SYSTEM.contains("irreversible") || AGENT_SYSTEM.contains("Never publish"));
|
||||||
assert!(AGENT_SYSTEM.contains("Traditional Chinese") || AGENT_SYSTEM.contains("Chinese"));
|
assert!(AGENT_SYSTEM.contains("Traditional Chinese") || AGENT_SYSTEM.contains("Chinese"));
|
||||||
assert!(AGENT_SYSTEM.contains("do not call report_done"));
|
assert!(AGENT_SYSTEM.contains("do not call report_done"));
|
||||||
assert!(AGENT_SYSTEM.contains("real task/tool workflow"));
|
assert!(AGENT_SYSTEM.contains("real task/tool workflow") || AGENT_SYSTEM.contains("final delivery"));
|
||||||
assert!(
|
assert!(
|
||||||
AGENT_SYSTEM.contains("continue in chunks") || AGENT_SYSTEM.contains("save rounds"),
|
AGENT_SYSTEM.contains("continue in chunks") || AGENT_SYSTEM.contains("save rounds"),
|
||||||
"P8 chunk continue guidance missing"
|
"P8 chunk continue guidance missing"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
AGENT_SYSTEM.contains("actually complete")
|
||||||
|
|| AGENT_SYSTEM.contains("partial progress"),
|
||||||
|
"no mid-task conclusion guidance missing"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
AGENT_SYSTEM.contains("final delivery"),
|
||||||
|
"report_done = final delivery guidance missing"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -865,6 +990,100 @@ mod tests {
|
||||||
unsafe { std::env::remove_var("GROKBOY_PROGRESS") };
|
unsafe { std::env::remove_var("GROKBOY_PROGRESS") };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Progress callback fires on start / think / tools / done (offline).
|
||||||
|
#[tokio::test]
|
||||||
|
async fn progress_callback_invoked_on_rounds() {
|
||||||
|
let _env = crate::test_env::lock();
|
||||||
|
unsafe { std::env::set_var("GROKBOY_PROGRESS", "0") };
|
||||||
|
|
||||||
|
let dir = std::env::temp_dir().join(format!(
|
||||||
|
"grokboy-agent-progress-{}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = std::fs::create_dir_all(&dir);
|
||||||
|
std::fs::write(dir.join("a.txt"), "1").unwrap();
|
||||||
|
let ctx = ToolContext::new(dir.clone());
|
||||||
|
let mut messages = vec![ChatMessage::user("go")];
|
||||||
|
let captured = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||||
|
let cap = captured.clone();
|
||||||
|
let n = Arc::new(Mutex::new(0usize));
|
||||||
|
let n2 = n.clone();
|
||||||
|
|
||||||
|
let verdict = run_agent_with_progress(
|
||||||
|
&mut messages,
|
||||||
|
&ctx,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
100_000,
|
||||||
|
move |_msgs, tools| {
|
||||||
|
let n2 = n2.clone();
|
||||||
|
async move {
|
||||||
|
if tools.is_none() {
|
||||||
|
return Ok(ChatMessage::assistant("should not summarize"));
|
||||||
|
}
|
||||||
|
let i = {
|
||||||
|
let mut g = n2.lock().unwrap();
|
||||||
|
*g += 1;
|
||||||
|
*g
|
||||||
|
};
|
||||||
|
if i == 1 {
|
||||||
|
Ok(ChatMessage::assistant_tool_calls(
|
||||||
|
None,
|
||||||
|
vec![tc(
|
||||||
|
"c1",
|
||||||
|
"read_file",
|
||||||
|
&json!({"path": "a.txt"}).to_string(),
|
||||||
|
)],
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(ChatMessage::assistant_tool_calls(
|
||||||
|
None,
|
||||||
|
vec![tc(
|
||||||
|
"done1",
|
||||||
|
"report_done",
|
||||||
|
&json!({"message": "讀完了"}).to_string(),
|
||||||
|
)],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move |msg| {
|
||||||
|
cap.lock().unwrap().push(msg.to_string());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(verdict, AgentVerdict::Done("讀完了".into()));
|
||||||
|
let lines = captured.lock().unwrap().clone();
|
||||||
|
assert!(
|
||||||
|
lines.iter().any(|l| l.contains("〔開始〕") && l.contains("最多")),
|
||||||
|
"missing start: {lines:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
lines.iter().any(|l| l.contains("〔思考中〕") && l.contains("輪")),
|
||||||
|
"missing thinking: {lines:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
lines
|
||||||
|
.iter()
|
||||||
|
.any(|l| l.contains("〔工具〕") && l.contains("read_file")),
|
||||||
|
"missing tools: {lines:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
lines.iter().any(|l| l.contains("〔完成〕read_file")),
|
||||||
|
"missing tool done: {lines:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
lines
|
||||||
|
.iter()
|
||||||
|
.any(|l| l.contains("〔結束〕") && l.contains("done")),
|
||||||
|
"missing end: {lines:?}"
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
unsafe { std::env::remove_var("GROKBOY_PROGRESS") };
|
||||||
|
}
|
||||||
|
|
||||||
/// Loop guard still fail-closes without auto-continuing forever.
|
/// Loop guard still fail-closes without auto-continuing forever.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn loop_guard_does_not_auto_continue() {
|
async fn loop_guard_does_not_auto_continue() {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ ENV:
|
||||||
GROKBOY_CONTEXT_CHARS context budget (default 100000)
|
GROKBOY_CONTEXT_CHARS context budget (default 100000)
|
||||||
GROKBOY_MAX_ROUNDS ReAct tool rounds per chunk (default 12)
|
GROKBOY_MAX_ROUNDS ReAct tool rounds per chunk (default 12)
|
||||||
GROKBOY_MAX_ROUNDS_TOTAL Absolute round ceiling across chunks (default 48)
|
GROKBOY_MAX_ROUNDS_TOTAL Absolute round ceiling across chunks (default 48)
|
||||||
GROKBOY_PROGRESS 0 = silence 〔續跑〕 progress beats on stderr
|
GROKBOY_PROGRESS 0 = silence live progress on stderr (思考/工具/續跑/結束)
|
||||||
GROKBOY_BROWSER_HEADED 1 = always launch Chromium headed (visible; for run/agent)
|
GROKBOY_BROWSER_HEADED 1 = always launch Chromium headed (visible; for run/agent)
|
||||||
GROKBOY_HANDOFF_AUTO 1 = auto-resume handoff (tests); abort = auto-abort
|
GROKBOY_HANDOFF_AUTO 1 = auto-resume handoff (tests); abort = auto-abort
|
||||||
GROKBOY_CONFIRM_AUTO 1 = auto-approve confirm (tests); abort = auto-deny
|
GROKBOY_CONFIRM_AUTO 1 = auto-approve confirm (tests); abort = auto-deny
|
||||||
|
|
@ -175,7 +175,7 @@ async fn cmd_run(args: &[String]) -> Result<()> {
|
||||||
}
|
}
|
||||||
session.touch();
|
session.touch();
|
||||||
let path = save_session(&session)?;
|
let path = save_session(&session)?;
|
||||||
println!("{}", verdict.message());
|
print_verdict(&verdict);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"\n[verdict: {} | session {} → {}]",
|
"\n[verdict: {} | session {} → {}]",
|
||||||
verdict.kind(),
|
verdict.kind(),
|
||||||
|
|
@ -297,7 +297,7 @@ async fn cmd_agent(args: &[String]) -> Result<()> {
|
||||||
session.touch();
|
session.touch();
|
||||||
let path = save_session(&session)?;
|
let path = save_session(&session)?;
|
||||||
|
|
||||||
println!("{}", verdict.message());
|
print_verdict(&verdict);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"[verdict: {} | session {} → {}]",
|
"[verdict: {} | session {} → {}]",
|
||||||
verdict.kind(),
|
verdict.kind(),
|
||||||
|
|
@ -624,6 +624,21 @@ async fn cmd_smoke() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// User-facing final answer: blank line + 〔結論〕 for Done/Answer.
|
||||||
|
/// Blocked prints the message as-is (recovery hint follows separately).
|
||||||
|
fn print_verdict(verdict: &AgentVerdict) {
|
||||||
|
println!();
|
||||||
|
match verdict {
|
||||||
|
AgentVerdict::Done(msg) | AgentVerdict::Answer(msg) => {
|
||||||
|
println!("〔結論〕{msg}");
|
||||||
|
}
|
||||||
|
AgentVerdict::Blocked(msg) => {
|
||||||
|
println!("{msg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn eprintln_blocked_recovery_hint_agent() {
|
fn eprintln_blocked_recovery_hint_agent() {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"提示:這回合被擋下了,session 還在。請換更短、更具體的指令繼續(不要把 blocked 原文貼回來)。例:直接開 https://affiliate.shopee.tw/ ,需要登入就 browser_handoff,找到就 report_done。"
|
"提示:這回合被擋下了,session 還在。請換更短、更具體的指令繼續(不要把 blocked 原文貼回來)。例:直接開 https://affiliate.shopee.tw/ ,需要登入就 browser_handoff,找到就 report_done。"
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
|
|
||||||
## P8 — auto-continue chunks like Grok Bot
|
## P8 — auto-continue chunks like Grok Bot
|
||||||
- [x] `GROKBOY_MAX_ROUNDS` = rounds per chunk (default 12); auto-continue another chunk in same `run_agent` when chunk ends without completion
|
- [x] `GROKBOY_MAX_ROUNDS` = rounds per chunk (default 12); auto-continue another chunk in same `run_agent` when chunk ends without completion
|
||||||
- [x] Progress summary between chunks (no-tools); stderr `〔續跑〕…` unless `GROKBOY_PROGRESS=0`
|
- [x] Progress summary between chunks (no-tools); stderr live progress unless `GROKBOY_PROGRESS=0`
|
||||||
- [x] Absolute ceiling `GROKBOY_MAX_ROUNDS_TOTAL` (default 48) → Blocked + progress + exhausted note
|
- [x] Absolute ceiling `GROKBOY_MAX_ROUNDS_TOTAL` (default 48) → Blocked + progress + exhausted note
|
||||||
- [x] Loop guard / identical tool rounds ×3 still Blocked without auto-continue
|
- [x] Loop guard / identical tool rounds ×3 still Blocked without auto-continue
|
||||||
- [x] AGENT_SYSTEM: large work may continue in chunks; still `report_done` when finished; don't stop early to "save rounds"
|
- [x] AGENT_SYSTEM: large work may continue in chunks; still `report_done` when finished; don't stop early to "save rounds"
|
||||||
|
|
@ -78,3 +78,9 @@
|
||||||
- [x] Offline tests: >chunk then Done; total ceiling Blocked; loop guard unchanged
|
- [x] Offline tests: >chunk then Done; total ceiling Blocked; loop guard unchanged
|
||||||
- [x] Docs PRODUCT / ACCEPTANCE / README; `cargo test` / `grokboy smoke` green without API key
|
- [x] Docs PRODUCT / ACCEPTANCE / README; `cargo test` / `grokboy smoke` green without API key
|
||||||
|
|
||||||
|
## Live terminal progress + conclusion only when done
|
||||||
|
- [x] Stderr progress always on unless `GROKBOY_PROGRESS=0`: `〔開始〕` / `〔思考中〕` / `〔工具〕` / `〔完成〕|〔失敗〕` / `〔進度|尚未完成〕〔續跑〕` / `〔結束〕` (flushed; no long blank waits)
|
||||||
|
- [x] AGENT_SYSTEM: no mid-task `report_done` or final wrap-up; keep tools while researching; partial mid-flight text only if needed; `report_done` = final delivery
|
||||||
|
- [x] CLI: Done/Answer → blank line + `〔結論〕`; Blocked keeps recovery hint; chunk progress is stderr-only (not the final answer)
|
||||||
|
- [x] Offline unit test: progress callback invoked on rounds; `cargo test` / `grokboy smoke` green without API key
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,14 +86,20 @@ Long legitimate work should **not** hard-stop at max rounds with only `blocked:
|
||||||
1. **`GROKBOY_MAX_ROUNDS`** (default 12) = rounds **per chunk** (one progress beat).
|
1. **`GROKBOY_MAX_ROUNDS`** (default 12) = rounds **per chunk** (one progress beat).
|
||||||
2. When a chunk ends without `report_done` / `report_blocked` / final Answer → short progress summary (no-tools) → **auto-continue** another chunk in the **same** `run_agent` invocation.
|
2. When a chunk ends without `report_done` / `report_blocked` / final Answer → short progress summary (no-tools) → **auto-continue** another chunk in the **same** `run_agent` invocation.
|
||||||
3. **Stop conditions:** completion tools / empty-tools Answer; identical-tool loop guard ×3 (no continue); absolute ceiling **`GROKBOY_MAX_ROUNDS_TOTAL`** (default 48) → Blocked with progress + total-budget-exhausted note.
|
3. **Stop conditions:** completion tools / empty-tools Answer; identical-tool loop guard ×3 (no continue); absolute ceiling **`GROKBOY_MAX_ROUNDS_TOTAL`** (default 48) → Blocked with progress + total-budget-exhausted note.
|
||||||
4. Progress on stderr: `〔續跑〕第 N 段(已用 X/Y 輪)進度:…` (silence with `GROKBOY_PROGRESS=0`).
|
4. Progress on stderr (always on unless `GROKBOY_PROGRESS=0`): live beats so the terminal is never blank during API/tool waits — `〔開始〕`, `〔思考中〕`, `〔工具〕`, `〔完成〕`/`〔失敗〕`, `〔進度|尚未完成〕〔續跑〕…`, `〔結束〕`.
|
||||||
5. Blocked recovery hint only when truly stopped (loop guard or total ceiling), not after every chunk.
|
5. Blocked recovery hint only when truly stopped (loop guard or total ceiling), not after every chunk.
|
||||||
|
|
||||||
|
### Live terminal progress + conclusion only when done
|
||||||
|
|
||||||
|
- Mid-task: runtime shows where work is (`做到哪`); do **not** treat chunk progress as the user-facing final answer.
|
||||||
|
- `AGENT_SYSTEM`: no `report_done` / final wrap-up until the task is actually complete; mid-flight plain text must be labeled partial — prefer continuing tools.
|
||||||
|
- CLI `run` / `agent`: final Done/Answer printed as blank line + `〔結論〕…`; Blocked keeps recovery hint. Progress stays on stderr.
|
||||||
|
|
||||||
### Env (P8)
|
### Env (P8)
|
||||||
|
|
||||||
- `GROKBOY_MAX_ROUNDS` — rounds per chunk (default 12)
|
- `GROKBOY_MAX_ROUNDS` — rounds per chunk (default 12)
|
||||||
- `GROKBOY_MAX_ROUNDS_TOTAL` — absolute ceiling (default 48)
|
- `GROKBOY_MAX_ROUNDS_TOTAL` — absolute ceiling (default 48)
|
||||||
- `GROKBOY_PROGRESS=0` — silence continuation progress beats
|
- `GROKBOY_PROGRESS=0` — silence all live progress lines on stderr
|
||||||
|
|
||||||
## Roadmap hint (later)
|
## Roadmap hint (later)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue