P5: interactive multi-turn agent REPL with tools
This commit is contained in:
parent
5507e5b4b5
commit
5b6fe82945
16
README.md
16
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# GrokBoy
|
# GrokBoy
|
||||||
|
|
||||||
Minimal local **GrokBot-like** CLI agent. Phase **P4**: human browser handoff (login/OTP/captcha).
|
Minimal local **GrokBot-like** CLI agent. Phase **P5**: interactive multi-turn agent REPL (dialogue + tools).
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ Minimal local **GrokBot-like** CLI agent. Phase **P4**: human browser handoff (l
|
||||||
| P2 completion / loop guard / truncation | done |
|
| P2 completion / loop guard / truncation | done |
|
||||||
| P3 browser (Playwright DOM) | done (optional) |
|
| P3 browser (Playwright DOM) | done (optional) |
|
||||||
| P4 human handoff | done |
|
| P4 human handoff | done |
|
||||||
|
| P5 interactive agent REPL | done |
|
||||||
|
|
||||||
No Docker desktop, no Codex/LazyBoy fork. Product notes: [`docs/PRODUCT.md`](docs/PRODUCT.md).
|
No Docker desktop, no Codex/LazyBoy fork. Product notes: [`docs/PRODUCT.md`](docs/PRODUCT.md).
|
||||||
|
|
||||||
|
|
@ -22,7 +23,7 @@ export GROKBOY_API_KEY=your_key # or XAI_API_KEY
|
||||||
# export GROKBOY_BASE_URL=https://api.x.ai/v1
|
# export GROKBOY_BASE_URL=https://api.x.ai/v1
|
||||||
# export GROKBOY_MODEL=grok-4.6
|
# export GROKBOY_MODEL=grok-4.6
|
||||||
# export GROKBOY_CONTEXT_CHARS=100000
|
# export GROKBOY_CONTEXT_CHARS=100000
|
||||||
# export GROKBOY_BROWSER_HEADED=1 # visible Chromium (recommended for handoff)
|
# export GROKBOY_BROWSER_HEADED=1 # visible Chromium (recommended for handoff / agent)
|
||||||
|
|
||||||
cd ~/GrokBoy
|
cd ~/GrokBoy
|
||||||
cargo run -p grokboy -- chat
|
cargo run -p grokboy -- chat
|
||||||
|
|
@ -53,6 +54,8 @@ When the agent hits a login / OTP / captcha wall it calls `browser_handoff`:
|
||||||
# Prefer headed for runs that may need handoff:
|
# Prefer headed for runs that may need handoff:
|
||||||
export GROKBOY_BROWSER_HEADED=1
|
export GROKBOY_BROWSER_HEADED=1
|
||||||
cargo run -p grokboy -- run "打開需要登入的頁面並完成任務"
|
cargo run -p grokboy -- run "打開需要登入的頁面並完成任務"
|
||||||
|
# or interactive:
|
||||||
|
cargo run -p grokboy -- agent
|
||||||
```
|
```
|
||||||
|
|
||||||
Tests / CI: `GROKBOY_HANDOFF_AUTO=1` auto-resumes (no interactive Enter).
|
Tests / CI: `GROKBOY_HANDOFF_AUTO=1` auto-resumes (no interactive Enter).
|
||||||
|
|
@ -61,10 +64,14 @@ Tests / CI: `GROKBOY_HANDOFF_AUTO=1` auto-resumes (no interactive Enter).
|
||||||
|
|
||||||
- `grokboy chat` — interactive streaming chat (no tools)
|
- `grokboy chat` — interactive streaming chat (no tools)
|
||||||
- `grokboy run "<prompt>"` — one-shot agent with tools
|
- `grokboy run "<prompt>"` — one-shot agent with tools
|
||||||
- `grokboy run --session <id> "<prompt>"` — continue a saved session
|
- `grokboy run --session <id> "<prompt>"` — continue a saved session (one shot)
|
||||||
|
- `grokboy agent` — **interactive multi-turn** agent REPL with tools (auto session)
|
||||||
|
- `grokboy agent --session <id>` — resume an agent session
|
||||||
- `grokboy smoke` — offline checks (no API key / no interactive handoff)
|
- `grokboy smoke` — offline checks (no API key / no interactive handoff)
|
||||||
- `grokboy help`
|
- `grokboy help`
|
||||||
|
|
||||||
|
In `agent` REPL: `/exit` or `/quit` leave; `/session` show id; empty line ignored.
|
||||||
|
|
||||||
Tools: `shell`, `list_dir`, `read_file`, `write_file`, `report_done`, `report_blocked`,
|
Tools: `shell`, `list_dir`, `read_file`, `write_file`, `report_done`, `report_blocked`,
|
||||||
`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_eval`,
|
`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_eval`,
|
||||||
`browser_handoff`.
|
`browser_handoff`.
|
||||||
|
|
@ -75,7 +82,7 @@ The agent stops on `report_done` / `report_blocked`, blocks identical tool round
|
||||||
|
|
||||||
## Traditional Chinese
|
## Traditional Chinese
|
||||||
|
|
||||||
本機終端機 coding assistant。P4 支援瀏覽器人工接手(登入/OTP/驗證碼):代理暫停 → 你在可見 Chromium 完成 → 終端機按 Enter 繼續。
|
本機終端機 coding assistant。P5 支援互動式多輪代理(含工具):`grokboy agent`。P4 瀏覽器人工接手(登入/OTP/驗證碼)在 `run` / `agent` 內皆可用。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export GROKBOY_API_KEY=你的金鑰
|
export GROKBOY_API_KEY=你的金鑰
|
||||||
|
|
@ -84,6 +91,7 @@ cd ~/GrokBoy
|
||||||
cargo run -p grokboy -- smoke
|
cargo run -p grokboy -- smoke
|
||||||
# 可選瀏覽器:
|
# 可選瀏覽器:
|
||||||
cd tools/playwright && npm install && npx playwright install chromium
|
cd tools/playwright && npm install && npx playwright install chromium
|
||||||
|
cargo run -p grokboy -- agent
|
||||||
cargo run -p grokboy -- run "打開 example.com 並 snapshot"
|
cargo run -p grokboy -- run "打開 example.com 並 snapshot"
|
||||||
cargo run -p grokboy -- chat
|
cargo run -p grokboy -- chat
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ async fn run() -> Result<()> {
|
||||||
match cmd.as_str() {
|
match cmd.as_str() {
|
||||||
"chat" => cmd_chat().await,
|
"chat" => cmd_chat().await,
|
||||||
"run" => cmd_run(&args).await,
|
"run" => cmd_run(&args).await,
|
||||||
|
"agent" => cmd_agent(&args).await,
|
||||||
"smoke" => cmd_smoke().await,
|
"smoke" => cmd_smoke().await,
|
||||||
"version" | "-V" | "--version" => {
|
"version" | "-V" | "--version" => {
|
||||||
println!("grokboy {}", env!("CARGO_PKG_VERSION"));
|
println!("grokboy {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
@ -50,12 +51,14 @@ async fn run() -> Result<()> {
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
println!(
|
println!(
|
||||||
"\
|
"\
|
||||||
GrokBoy — minimal local CLI agent (P4: human browser handoff)
|
GrokBoy — minimal local CLI agent (P5: interactive multi-turn agent)
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
grokboy chat Interactive streaming chat (no tools)
|
grokboy chat Interactive streaming chat (no tools)
|
||||||
grokboy run \"<prompt>\" One-shot agent with tools
|
grokboy run \"<prompt>\" One-shot agent with tools
|
||||||
grokboy run --session <id> \"...\" Continue a saved session
|
grokboy run --session <id> \"...\" Continue a saved session
|
||||||
|
grokboy agent Interactive multi-turn agent REPL (with tools)
|
||||||
|
grokboy agent --session <id> Resume an agent session
|
||||||
grokboy smoke Offline checks (no API key required)
|
grokboy smoke Offline checks (no API key required)
|
||||||
grokboy version
|
grokboy version
|
||||||
grokboy help
|
grokboy help
|
||||||
|
|
@ -65,7 +68,7 @@ ENV:
|
||||||
GROKBOY_BASE_URL default https://api.x.ai/v1
|
GROKBOY_BASE_URL default https://api.x.ai/v1
|
||||||
GROKBOY_MODEL default grok-4.6
|
GROKBOY_MODEL default grok-4.6
|
||||||
GROKBOY_CONTEXT_CHARS context budget (default 100000)
|
GROKBOY_CONTEXT_CHARS context budget (default 100000)
|
||||||
GROKBOY_BROWSER_HEADED 1 = always launch Chromium headed (visible)
|
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
|
||||||
|
|
||||||
Tools: shell, list_dir, read_file, write_file, report_done, report_blocked,
|
Tools: shell, list_dir, read_file, write_file, report_done, report_blocked,
|
||||||
|
|
@ -74,6 +77,7 @@ Tools: shell, list_dir, read_file, write_file, report_done, report_blocked,
|
||||||
Sessions: ~/.grokboy/sessions/<id>.json
|
Sessions: ~/.grokboy/sessions/<id>.json
|
||||||
Browser (optional): cd tools/playwright && npm i && npx playwright install chromium
|
Browser (optional): cd tools/playwright && npm i && npx playwright install chromium
|
||||||
Handoff: agent pauses on login/OTP/captcha → you fix in headed Chromium → Enter
|
Handoff: agent pauses on login/OTP/captcha → you fix in headed Chromium → Enter
|
||||||
|
REPL: /exit /quit leave; /session show id; empty line ignored
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -185,8 +189,135 @@ async fn cmd_run(args: &[String]) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Parse `agent` CLI flags. Returns (session_id, show_help).
|
||||||
|
fn parse_agent_args(args: &[String]) -> Result<(Option<String>, bool)> {
|
||||||
|
let mut session_id: Option<String> = None;
|
||||||
|
let mut i = 0;
|
||||||
|
while i < args.len() {
|
||||||
|
match args[i].as_str() {
|
||||||
|
"--session" | "-s" => {
|
||||||
|
i += 1;
|
||||||
|
let id = args
|
||||||
|
.get(i)
|
||||||
|
.ok_or_else(|| anyhow!("--session requires an id"))?;
|
||||||
|
session_id = Some(id.clone());
|
||||||
|
}
|
||||||
|
"--help" | "-h" => return Ok((session_id, true)),
|
||||||
|
other => {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"unexpected argument: {other}\nusage: grokboy agent [--session <id>]"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
Ok((session_id, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cmd_agent(args: &[String]) -> Result<()> {
|
||||||
|
let (session_id, show_help) = parse_agent_args(args)?;
|
||||||
|
if show_help {
|
||||||
|
println!(
|
||||||
|
"Usage: grokboy agent [--session <id>]\n\
|
||||||
|
Interactive multi-turn ReAct agent with tools.\n\
|
||||||
|
Creates/resumes a session under ~/.grokboy/sessions/.\n\
|
||||||
|
Commands: /exit /quit leave; /session show id; empty line ignored.\n\
|
||||||
|
Tip: GROKBOY_BROWSER_HEADED=1 for visible Chromium (handoff)."
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Config::from_env().map_err(anyhow::Error::msg)?;
|
||||||
|
let cwd = std::env::current_dir().context("cwd")?;
|
||||||
|
let created_new = session_id.is_none();
|
||||||
|
let mut session = load_or_create(session_id.as_deref(), &cwd)?;
|
||||||
|
|
||||||
|
if session.messages.is_empty() {
|
||||||
|
session.push(ChatMessage::system(AGENT_SYSTEM));
|
||||||
|
}
|
||||||
|
session.cwd = cwd.clone();
|
||||||
|
|
||||||
|
// One ToolContext for the whole REPL so browser URL/state carries across turns.
|
||||||
|
let tool_ctx = ToolContext::new(session.cwd.clone());
|
||||||
|
if let Some(url) = &session.last_browser_url {
|
||||||
|
if let Ok(mut g) = tool_ctx.last_browser_url.lock() {
|
||||||
|
*g = Some(url.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist early so the printed session id is on disk.
|
||||||
|
let path = save_session(&session)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"GrokBoy agent model={} base={}",
|
||||||
|
config.model, config.base_url
|
||||||
|
);
|
||||||
|
println!("session {}", session.id);
|
||||||
|
if created_new {
|
||||||
|
eprintln!("[new session → {}]", path.display());
|
||||||
|
} else {
|
||||||
|
eprintln!("[resumed → {}]", path.display());
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"互動式多輪代理(含工具)。輸入訊息後會跑 ReAct;/exit 或 /quit 離開;/session 顯示 id。\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
let stdin = io::stdin();
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
print!("you> ");
|
||||||
|
stdout.flush().ok();
|
||||||
|
let mut line = String::new();
|
||||||
|
if stdin.read_line(&mut line).context("stdin")? == 0 {
|
||||||
|
println!();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let input = line.trim();
|
||||||
|
if input.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if input == "/exit" || input == "/quit" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if input == "/session" {
|
||||||
|
println!("session {}", session.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.push(ChatMessage::user(input));
|
||||||
|
let verdict = run_agent(
|
||||||
|
&config,
|
||||||
|
&mut session.messages,
|
||||||
|
&tool_ctx,
|
||||||
|
DEFAULT_MAX_ROUNDS,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(url) = tool_ctx.last_browser_url_value() {
|
||||||
|
session.last_browser_url = Some(url);
|
||||||
|
}
|
||||||
|
session.touch();
|
||||||
|
let path = save_session(&session)?;
|
||||||
|
|
||||||
|
println!("{}", verdict.message());
|
||||||
|
eprintln!(
|
||||||
|
"[verdict: {} | session {} → {}]",
|
||||||
|
verdict.kind(),
|
||||||
|
session.id,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
// Interactive: blocked does not exit the REPL — user can continue.
|
||||||
|
}
|
||||||
|
|
||||||
|
session.touch();
|
||||||
|
let _ = save_session(&session)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn cmd_smoke() -> Result<()> {
|
async fn cmd_smoke() -> Result<()> {
|
||||||
println!("GrokBoy smoke (offline P4)…");
|
println!("GrokBoy smoke (offline P5)…");
|
||||||
let stamp = uuid_like();
|
let stamp = uuid_like();
|
||||||
let dir = std::env::temp_dir().join(format!("grokboy-smoke-{stamp}"));
|
let dir = std::env::temp_dir().join(format!("grokboy-smoke-{stamp}"));
|
||||||
std::fs::create_dir_all(&dir).context("temp dir")?;
|
std::fs::create_dir_all(&dir).context("temp dir")?;
|
||||||
|
|
@ -393,6 +524,40 @@ async fn cmd_smoke() -> Result<()> {
|
||||||
}
|
}
|
||||||
println!(" session ok");
|
println!(" session ok");
|
||||||
|
|
||||||
|
// agent CLI parse (offline)
|
||||||
|
let (sid, help) = parse_agent_args(&[]).expect("empty agent args");
|
||||||
|
if sid.is_some() || help {
|
||||||
|
return Err(anyhow!("parse_agent_args([]) unexpected"));
|
||||||
|
}
|
||||||
|
let sess_args = vec!["--session".to_string(), "abc-123".to_string()];
|
||||||
|
let (sid, help) = parse_agent_args(&sess_args)?;
|
||||||
|
if sid.as_deref() != Some("abc-123") || help {
|
||||||
|
return Err(anyhow!("parse_agent_args --session failed"));
|
||||||
|
}
|
||||||
|
let help_args = vec!["--help".to_string()];
|
||||||
|
let (_, help) = parse_agent_args(&help_args)?;
|
||||||
|
if !help {
|
||||||
|
return Err(anyhow!("parse_agent_args --help failed"));
|
||||||
|
}
|
||||||
|
// one-turn session plumbing without API: system + user + fake assistant, save/load
|
||||||
|
let mut agent_sess = Session::new(dir.clone());
|
||||||
|
agent_sess.push(ChatMessage::system(AGENT_SYSTEM));
|
||||||
|
agent_sess.push(ChatMessage::user("第一輪"));
|
||||||
|
agent_sess.push(ChatMessage::assistant("回覆一"));
|
||||||
|
agent_sess.push(ChatMessage::user("第二輪"));
|
||||||
|
let agent_path = dir.join("agent-turn.json");
|
||||||
|
let data = serde_json::to_vec_pretty(&agent_sess)?;
|
||||||
|
std::fs::write(&agent_path, data)?;
|
||||||
|
let loaded_agent: Session = serde_json::from_slice(&std::fs::read(&agent_path)?)?;
|
||||||
|
if loaded_agent.messages.len() != 4 {
|
||||||
|
return Err(anyhow!("agent multi-turn session plumbing failed"));
|
||||||
|
}
|
||||||
|
if loaded_agent.messages[0].text() != AGENT_SYSTEM {
|
||||||
|
return Err(anyhow!("agent session missing system prompt"));
|
||||||
|
}
|
||||||
|
println!(" agent parse ok");
|
||||||
|
println!(" agent session ok");
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
|
||||||
if Config::from_env().is_ok() {
|
if Config::from_env().is_ok() {
|
||||||
|
|
@ -420,3 +585,36 @@ fn uuid_like() -> String {
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
format!("{n}")
|
format!("{n}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::parse_agent_args;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_args_empty() {
|
||||||
|
let (sid, help) = parse_agent_args(&[]).unwrap();
|
||||||
|
assert!(sid.is_none());
|
||||||
|
assert!(!help);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_args_session() {
|
||||||
|
let args = vec!["--session".into(), "sess-1".into()];
|
||||||
|
let (sid, help) = parse_agent_args(&args).unwrap();
|
||||||
|
assert_eq!(sid.as_deref(), Some("sess-1"));
|
||||||
|
assert!(!help);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_args_help() {
|
||||||
|
let args = vec!["-h".into()];
|
||||||
|
let (_, help) = parse_agent_args(&args).unwrap();
|
||||||
|
assert!(help);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_args_rejects_extra() {
|
||||||
|
let args = vec!["nope".into()];
|
||||||
|
assert!(parse_agent_args(&args).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,3 +37,14 @@
|
||||||
- [x] Wired into tool defs + agent system prompt; fail-closed if no browser
|
- [x] Wired into tool defs + agent system prompt; fail-closed if no browser
|
||||||
- [x] Offline tests / smoke without API key or interactive stdin (`GROKBOY_HANDOFF_AUTO`)
|
- [x] Offline tests / smoke without API key or interactive stdin (`GROKBOY_HANDOFF_AUTO`)
|
||||||
- [x] README status table updated
|
- [x] README status table updated
|
||||||
|
|
||||||
|
## P5 — interactive multi-turn agent
|
||||||
|
- [x] `grokboy agent` REPL: read line → ReAct with tools → print verdict → save session
|
||||||
|
- [x] `/exit` `/quit` leave; `/session` show id; empty line ignored
|
||||||
|
- [x] `--session <id>` resume; auto-create + print session id when omitted
|
||||||
|
- [x] Keep `run` one-shot; keep `chat` streaming no-tools
|
||||||
|
- [x] Same tool-capable system prompt (`AGENT_SYSTEM`); Traditional Chinese welcome
|
||||||
|
- [x] Docs: ACCEPTANCE P5, PRODUCT.md note, README commands/status
|
||||||
|
- [x] Offline smoke/tests: agent parses / help lists it; multi-turn session plumbing without API
|
||||||
|
- [x] `cargo test` green without API key
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
**North star:** a local **Grok Bot–like** agent — thin CLI core, tool-using ReAct loop, optional Playwright **DOM** browser (not screenshot-first). GrokBoy is the sole main line; LazyBoy is reference only (no fork).
|
**North star:** a local **Grok Bot–like** agent — thin CLI core, tool-using ReAct loop, optional Playwright **DOM** browser (not screenshot-first). GrokBoy is the sole main line; LazyBoy is reference only (no fork).
|
||||||
|
|
||||||
## Done (P0–P3)
|
## Done (P0–P5)
|
||||||
|
|
||||||
| Phase | What |
|
| Phase | What |
|
||||||
|-------|------|
|
|-------|------|
|
||||||
|
|
@ -10,20 +10,22 @@
|
||||||
| **P1** | Tools + ReAct (`shell`, files), sessions under `~/.grokboy/sessions/` |
|
| **P1** | Tools + ReAct (`shell`, files), sessions under `~/.grokboy/sessions/` |
|
||||||
| **P2** | Completion contract (`report_done` / `report_blocked`), loop guard, context truncation |
|
| **P2** | Completion contract (`report_done` / `report_blocked`), loop guard, context truncation |
|
||||||
| **P3** | Optional Playwright DOM tools: navigate / snapshot / click / type / eval (fail-closed) |
|
| **P3** | Optional Playwright DOM tools: navigate / snapshot / click / type / eval (fail-closed) |
|
||||||
|
| **P4** | Human browser handoff (`browser_handoff`) for login / OTP / captcha |
|
||||||
|
| **P5** | Interactive multi-turn agent REPL (`grokboy agent`) with tools + session persist |
|
||||||
|
|
||||||
## P4 — Human handoff (this slice)
|
## P5 — Interactive multi-turn agent (this slice)
|
||||||
|
|
||||||
Auth walls (login, OTP, captcha) often cannot be automated safely. P4 adds **`browser_handoff`**:
|
Gap after P4: `chat` streams but has no tools; `run` has tools but is one-shot. P5 adds **`grokboy agent`**:
|
||||||
|
|
||||||
1. Agent calls `browser_handoff` with a `reason` (optional `timeout_secs`).
|
1. REPL reads a user line (ignore empty; `/exit` `/quit` leave; `/session` prints id).
|
||||||
2. Helper ensures Chromium is **headed** (visible); may relaunch from headless and restore URL.
|
2. Each turn runs the **same** ReAct loop as `run` (tools + handoff inherited).
|
||||||
3. Terminal prints bilingual (繁中 + English) instructions.
|
3. Prints the verdict / assistant answer; **saves** under `~/.grokboy/sessions/` after every turn.
|
||||||
4. Loop **blocks** until you press **Enter** (continue) or type **`abort`**, or timeout → fail-closed blocked.
|
4. `--session <id>` resumes; omitting id auto-creates and prints the session id.
|
||||||
5. On resume, a **DOM snapshot** is returned so the model can continue.
|
5. `chat` stays streaming no-tools; `run` stays one-shot.
|
||||||
|
|
||||||
### Env
|
### Env
|
||||||
|
|
||||||
- `GROKBOY_BROWSER_HEADED=1` — always launch Chromium headed (recommended when handoff is likely).
|
- `GROKBOY_BROWSER_HEADED=1` — always launch Chromium headed (recommended for `run` / `agent` when handoff is likely).
|
||||||
- `GROKBOY_HANDOFF_AUTO=1` — auto-resume (tests / CI); `abort` to auto-abort.
|
- `GROKBOY_HANDOFF_AUTO=1` — auto-resume (tests / CI); `abort` to auto-abort.
|
||||||
|
|
||||||
### Non-goals (this slice)
|
### Non-goals (this slice)
|
||||||
|
|
@ -32,17 +34,12 @@ Auth walls (login, OTP, captcha) often cannot be automated safely. P4 adds **`br
|
||||||
- Desktop accessibility / native UI automation
|
- Desktop accessibility / native UI automation
|
||||||
- External connectors / SaaS integrations
|
- External connectors / SaaS integrations
|
||||||
- Forking LazyBoy or Codex
|
- Forking LazyBoy or Codex
|
||||||
|
- Turning `chat` into a tools REPL (kept simple on purpose)
|
||||||
|
|
||||||
### Acceptance (summary)
|
### Acceptance (summary)
|
||||||
|
|
||||||
See `docs/ACCEPTANCE.md` section P4. Short list:
|
See `docs/ACCEPTANCE.md` section P5.
|
||||||
|
|
||||||
- `browser_handoff` registered and wired into the agent loop
|
|
||||||
- Headed Chromium for handoff; JSONL daemon keeps state within one run when already headed
|
|
||||||
- Bilingual terminal prompt; Enter / abort / timeout fail-closed
|
|
||||||
- Post-resume DOM snapshot as tool result
|
|
||||||
- Offline `cargo test` / `grokboy smoke` without API key or interactive stdin
|
|
||||||
|
|
||||||
## Roadmap hint (later)
|
## Roadmap hint (later)
|
||||||
|
|
||||||
P5+ may deepen persistence, richer session UX, or more tools — still thin core, DOM-first browser.
|
P6+ may deepen session UX, richer browser persistence across process restarts, or more tools — still thin core, DOM-first browser.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue