Looking at the person's screen is quick; a busy model is waited out in words; files go straight from their computer
From the second round of real runs:
- Direct mode no longer asks the model to check "always ask" rules before steps that only look
(screenshot, showing the screen, reading a page or file): none of those rules can be about them,
and the check cost each look at the person's Mac 10-20 s. Clicks and typing are still checked.
- An error inside the model's answer stream ("The model is currently at capacity…") is retried after
8 s and said in Chinese; it used to become the bot's reply, in English, never retried.
- send_file says it sends from any computer, the person's included (the bot copied a file to its own
computer first, which only works for text).
- A shown screen's file name carries no time (a server's clock is UTC).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
This commit is contained in:
parent
e8e5e6fba1
commit
ee92da8519
|
|
@ -1143,8 +1143,12 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
|
|||
let decision = match (decision, &job.review) {
|
||||
// Direct mode: go ahead, unless an "always ask" rule matches.
|
||||
(Decision::Ask { reason, grant }, review) if direct => {
|
||||
// Only looking (a screenshot, a page or file read) changes nothing, so no
|
||||
// "always ask" rule (deleting, paying, posting…) can be about it: no model
|
||||
// call to check (it cost every look at the person's Mac 10–20 s).
|
||||
let looks = matches!(call.name.as_str(), "computer_use_screenshot" | "computer_use_show" | "browser_read" | "read_file" | "list_dir" | "search");
|
||||
let verdict = match review {
|
||||
Some(rv) if !rv.ask.is_empty() => review_action(&job, &Review { allow: vec![], ask: rv.ask.clone() }, &reason, &cancel),
|
||||
Some(rv) if !rv.ask.is_empty() && !looks => review_action(&job, &Review { allow: vec![], ask: rv.ask.clone() }, &reason, &cancel),
|
||||
_ => ReviewVerdict::NoMatch,
|
||||
};
|
||||
match verdict {
|
||||
|
|
@ -1236,7 +1240,8 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc<
|
|||
// 給使用者看畫面: the picture goes into the chat for the person (the bot still sees it).
|
||||
if call.name == "computer_use_show" {
|
||||
if let Some(bytes) = image.as_deref().and_then(|b| base64::engine::general_purpose::STANDARD.decode(b).ok()) {
|
||||
let name = format!("{} 的畫面 {}.jpg", target.as_ref().map(|t| t.name.as_str()).unwrap_or("電腦"), chrono::Local::now().format("%H%M%S"));
|
||||
// (No time in the name: a brain on a server keeps UTC, and the chat shows when.)
|
||||
let name = format!("{} 的畫面.jpg", target.as_ref().map(|t| t.name.as_str()).unwrap_or("電腦"));
|
||||
if let Ok(a) = crate::attach::store_bytes(bot_id, &name, &bytes) {
|
||||
send(StreamEvent::Attach(a));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -754,7 +754,7 @@ impl App {
|
|||
"link": {"type": "string"}}, "required": ["kind", "title"]}}));
|
||||
tool_specs.push(serde_json::json!({"name": "react", "description": "Put an emoji reaction on the person's latest message, like a friend would (e.g. ❤️ for thanks or good news, 👍 for OK, 😂 for a joke, 🎉 for a win, 👀 when you'll look into it). Use it now and then where it fits; you can still answer in words.",
|
||||
"parameters": {"type": "object", "properties": {"emoji": {"type": "string", "description": "One emoji"}}, "required": ["emoji"]}}));
|
||||
tool_specs.push(serde_json::json!({"name": "send_file", "description": "Send a file you made (spreadsheet, document, Markdown, web page, picture, PDF…) into the chat as a file card the person can preview and download. Use this to hand over results instead of only saying where the file is.",
|
||||
tool_specs.push(serde_json::json!({"name": "send_file", "description": "Send a file (spreadsheet, document, Markdown, web page, picture, PDF…) into the chat as a file card the person can preview and download, straight from whichever computer it is on — your own or one of the person's (pass computer; up to 20 MB from theirs). Use this to hand over results instead of only saying where the file is; never copy a file to another computer first just to send it.",
|
||||
"parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "File path (relative to your folder)"}, "computer": {"type": "string", "enum": names.clone()}}, "required": ["path"]}}));
|
||||
tool_specs.push(serde_json::json!({"name": "open_for_person", "description": "Open a file you made (a web page, chart, report, picture, document) on the person's screen so they can see it, or a web address. From the bot's computer it is copied over first.",
|
||||
"parameters": {"type": "object", "properties": {"path": {"type": "string", "description": "File path (relative to your folder) or http(s) address"}, "computer": {"type": "string", "enum": names.clone()}}, "required": ["path"]}}));
|
||||
|
|
|
|||
|
|
@ -336,6 +336,20 @@ fn page_words(body: &str) -> String {
|
|||
out.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
/// An error the model sent inside its answer stream, in words: "at capacity" and the like are
|
||||
/// waited out and tried again (it came through raw English, as the reply, and was never retried).
|
||||
fn stream_failure(msg: &str) -> Failure {
|
||||
let low = msg.to_lowercase();
|
||||
if ["capacity", "overloaded", "rate limit", "too many requests", "try again", "temporarily", "unavailable", "timeout", "timed out"].iter().any(|k| low.contains(k)) {
|
||||
Failure { msg: format!("模型供應商現在太忙,正在等一下再試。\n({msg})"), retryable: true, retry_after_ms: Some(BUSY_RETRY_MS) }
|
||||
} else {
|
||||
Failure::fatal(format!("模型回報錯誤,這一步沒有完成。\n({msg})"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait before asking a busy model again (estimated; it says "a few minutes", 1–4 s was too soon).
|
||||
const BUSY_RETRY_MS: u64 = 8_000;
|
||||
|
||||
fn http_error(status: u16, body: &str, retry_after: Option<u64>) -> Failure {
|
||||
let detail = serde_json::from_str::<Value>(body)
|
||||
.ok()
|
||||
|
|
@ -913,7 +927,7 @@ fn stream_openai_compatible(req: &ChatRequest, cancel: &AtomicBool, send: &dyn F
|
|||
true
|
||||
})?;
|
||||
if let Some(e) = error {
|
||||
return Err(Failure::fatal(e));
|
||||
return Err(stream_failure(&e));
|
||||
}
|
||||
for (i, (id, name, args)) in pending.into_iter().enumerate() {
|
||||
if name.is_empty() {
|
||||
|
|
@ -1105,7 +1119,7 @@ fn stream_anthropic(req: &ChatRequest, cancel: &AtomicBool, send: &dyn Fn(Stream
|
|||
true
|
||||
})?;
|
||||
if let Some(e) = error {
|
||||
return Err(Failure::fatal(e));
|
||||
return Err(stream_failure(&e));
|
||||
}
|
||||
out.blocks = blocks.into_iter().filter(|b| !b.is_null()).collect();
|
||||
Ok(out)
|
||||
|
|
@ -1258,6 +1272,14 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_busy_model_mid_stream_is_waited_out() {
|
||||
let f = stream_failure("The model is currently at capacity due to high demand. Please try again in a few minutes");
|
||||
assert!(f.retryable && f.retry_after_ms == Some(BUSY_RETRY_MS) && f.msg.starts_with("模型供應商現在太忙"), "{}", f.msg);
|
||||
let f = stream_failure("Invalid tool schema");
|
||||
assert!(!f.retryable && f.msg.starts_with("模型回報錯誤"), "{}", f.msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gateways_error_page_reads_as_words() {
|
||||
let page = "<!DOCTYPE HTML>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<style type=\"text/css\">\n:root {\ncolor-scheme: light dark;\n}\n</style>\n<title>Error response</title>\n</head>\n<body>\n<h1>Error response</h1>\n<p>Error code: 502</p>\n<p>Message: Bad Gateway.</p>\n</body>\n</html>\n";
|
||||
|
|
|
|||
Loading…
Reference in New Issue