Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/core/src/conversation/records.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ pub struct TurnError {
pub code: String,
/// The human-readable error message.
pub message: String,
/// Optional user-facing next step for recovering from this failure.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recovery_hint: Option<String>,
}

/// Stores one persisted item record in the canonical journal.
Expand Down Expand Up @@ -626,6 +629,7 @@ mod tests {
error: Some(TurnError {
code: "PROVIDER_SERVER_ERROR".into(),
message: "provider request failed".into(),
recovery_hint: None,
}),
schema_version: 4,
..make_test_turn(TurnStatus::Running)
Expand Down Expand Up @@ -729,6 +733,7 @@ mod tests {
error: Some(TurnError {
code: "TOOL_EXECUTION_FAILED".into(),
message: "command exited with code 1".into(),
recovery_hint: None,
}),
..make_test_item()
};
Expand Down Expand Up @@ -1036,26 +1041,32 @@ mod tests {
TurnError {
code: "CONTEXT_LIMIT_EXCEEDED".into(),
message: "Too many tokens".into(),
recovery_hint: None,
},
TurnError {
code: "MODEL_RESOLUTION_FAILED".into(),
message: "No valid binding".into(),
recovery_hint: None,
},
TurnError {
code: "PROVIDER_RATE_LIMITED".into(),
message: "Retry after 30s".into(),
recovery_hint: None,
},
TurnError {
code: "PERSISTENCE_FAILURE".into(),
message: "Disk full".into(),
recovery_hint: None,
},
TurnError {
code: "TOOL_EXECUTION_FAILED".into(),
message: "exit code 1".into(),
recovery_hint: None,
},
TurnError {
code: "APPROVAL_TIMEOUT".into(),
message: "User did not respond".into(),
recovery_hint: None,
},
];
for err in &errors {
Expand Down
22 changes: 22 additions & 0 deletions crates/core/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,14 @@ fn classify_error(e: &anyhow::Error) -> ErrorClass {
}
}

if e.chain().any(|cause| {
cause
.downcast_ref::<devo_provider::timeout::StreamIdleTimeoutError>()
.is_some()
}) {
return ErrorClass::NetworkError;
}

if e.chain().any(|cause| {
cause.downcast_ref::<reqwest::Error>().is_some_and(|error| {
error.is_timeout()
Expand Down Expand Up @@ -362,6 +370,7 @@ fn classify_error(e: &anyhow::Error) -> ErrorClass {
|| msg.contains("deadline has elapsed")
|| msg.contains("deadline exceeded")
|| msg.contains("provider timeout")
|| msg.contains("stream idle timeout")
|| msg.contains("network error")
|| msg.contains("network is unreachable")
|| msg.contains("network unreachable")
Expand Down Expand Up @@ -2160,6 +2169,19 @@ mod tests {
message: "provider request timed out".into(),
provider_name: Some("test-provider".into()),
}),
anyhow::Error::new(devo_provider::timeout::stream_idle_timeout_provider_error(
"openai",
"gpt-test",
devo_provider::timeout::StreamIdleTimeoutError {
idle_timeout: std::time::Duration::from_secs(60),
},
)),
anyhow::Error::new(devo_provider::timeout::StreamIdleTimeoutError {
idle_timeout: std::time::Duration::from_secs(60),
}),
anyhow::anyhow!(
"openai stream idle timeout for model gpt-test: provider stream idle timeout after 60s without receiving data"
),
];

for error in cases {
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/tools/edit.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Performs exact string replacements in files.

Usage:
- You must use your `Read` tool at least once on the full file (no offset/limit) in this session before editing. This tool will error if the file has not been read, or if the file changed since it was last read.
- Preferred input field names are `filePath`, `oldString`, `newString`, and `replaceAll`. For compatibility, `path`, `file_path`, `old_string`, `new_string`, and `replace_all` are also accepted.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required. Use `Write` to create new files; `edit` only modifies existing files.
- Prefer `edit` for small, surgical changes. Prefer `apply_patch` for multi-file or large structured edits. Prefer `Write` for full-file rewrites.
Expand Down
Loading
Loading