1use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum ErrorKind {
25 RateLimit,
26 AuthError,
27 TokenLimit,
28 ServerError500,
34 BadGateway502,
37 ServiceUnavailable503,
42 GatewayTimeout504,
47 NetworkError,
48 ParseError,
49 Cancelled,
50 Timeout,
59 ScriptError,
60 AuthorRaise,
68 ScriptDepthExceeded,
71 Panic,
77 Internal,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "kebab-case")]
89pub enum SuggestedAction {
90 Retry,
93 FixConfig,
96 FixScript,
99 FixInput,
102 HandleAuthorFailure,
106 None,
108 Report,
111}
112
113impl ErrorKind {
114 pub fn is_transient(&self) -> bool {
118 matches!(
119 self,
120 ErrorKind::RateLimit
121 | ErrorKind::ServerError500
122 | ErrorKind::BadGateway502
123 | ErrorKind::ServiceUnavailable503
124 | ErrorKind::GatewayTimeout504
125 | ErrorKind::NetworkError
126 )
127 }
128
129 pub fn is_server_error(&self) -> bool {
134 matches!(
135 self,
136 ErrorKind::ServerError500
137 | ErrorKind::BadGateway502
138 | ErrorKind::ServiceUnavailable503
139 | ErrorKind::GatewayTimeout504
140 )
141 }
142
143 pub fn base_backoff_ms(&self) -> Option<u64> {
157 Some(match self {
158 ErrorKind::RateLimit => 2_000,
159 ErrorKind::ServerError500 => 1_000,
160 ErrorKind::BadGateway502 => 1_000,
161 ErrorKind::ServiceUnavailable503 => 2_000,
162 ErrorKind::GatewayTimeout504 => 4_000,
163 ErrorKind::NetworkError => 1_000,
164 _ => return None,
165 })
166 }
167
168 pub fn is_user_actionable(&self) -> bool {
172 matches!(
173 self,
174 ErrorKind::AuthError
175 | ErrorKind::TokenLimit
176 | ErrorKind::Timeout
177 | ErrorKind::ScriptError
178 | ErrorKind::ScriptDepthExceeded
179 | ErrorKind::AuthorRaise
180 )
181 }
182
183 pub fn as_wire(&self) -> &'static str {
189 match self {
190 ErrorKind::RateLimit => "RateLimit",
191 ErrorKind::AuthError => "AuthError",
192 ErrorKind::TokenLimit => "TokenLimit",
193 ErrorKind::ServerError500 => "ServerError500",
194 ErrorKind::BadGateway502 => "BadGateway502",
195 ErrorKind::ServiceUnavailable503 => "ServiceUnavailable503",
196 ErrorKind::GatewayTimeout504 => "GatewayTimeout504",
197 ErrorKind::NetworkError => "NetworkError",
198 ErrorKind::ParseError => "ParseError",
199 ErrorKind::Cancelled => "Cancelled",
200 ErrorKind::Timeout => "Timeout",
201 ErrorKind::ScriptError => "ScriptError",
202 ErrorKind::AuthorRaise => "AuthorRaise",
203 ErrorKind::ScriptDepthExceeded => "ScriptDepthExceeded",
204 ErrorKind::Panic => "Panic",
205 ErrorKind::Internal => "Internal",
206 }
207 }
208
209 pub fn suggested_action(&self) -> SuggestedAction {
211 match self {
212 ErrorKind::RateLimit
213 | ErrorKind::ServerError500
214 | ErrorKind::BadGateway502
215 | ErrorKind::ServiceUnavailable503
216 | ErrorKind::GatewayTimeout504
217 | ErrorKind::NetworkError => {
218 SuggestedAction::Retry
219 }
220 ErrorKind::AuthError => SuggestedAction::FixConfig,
221 ErrorKind::TokenLimit => SuggestedAction::FixInput,
222 ErrorKind::Timeout => SuggestedAction::FixInput,
223 ErrorKind::ScriptError | ErrorKind::ScriptDepthExceeded | ErrorKind::ParseError => {
224 SuggestedAction::FixScript
225 }
226 ErrorKind::AuthorRaise => SuggestedAction::HandleAuthorFailure,
227 ErrorKind::Cancelled => SuggestedAction::None,
228 ErrorKind::Panic | ErrorKind::Internal => SuggestedAction::Report,
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
242pub enum ErrorCode {
243 UserCancelled,
245 ExecutionTimeout,
247 CheckpointTimeout,
249 ProviderRateLimit,
251 ProviderAuth,
253 ProviderTokenLimit,
255 ProviderServer,
262 ProviderServer500,
264 ProviderBadGateway502,
266 ProviderServiceUnavailable503,
269 ProviderGatewayTimeout504,
272 ProviderNetwork,
275 ProviderParse,
277 ProviderOther,
279 InternalPanic,
281 InternalDroppedChannel,
286 InternalDeadlock,
289 InternalTaskAborted,
293 InternalOther,
296 ScriptError,
298 ScriptDepthExceeded,
300 PartialRetryExhausted,
303 AuthorRaise,
306 ToolBudgetExceeded,
308 ToolApprovalProtocol,
311 ToolNoRegistry,
314 ToolError,
317 AgentToolsDoubleDispatch,
325 ConfigMissing,
327 LoopOutputBudgetExceeded,
333 LoopMultiCheckpoint,
340 ContextOverflow,
346 ContextNativeUnsupported,
350 ContextCompactionExhausted,
355 CompactionThresholdInvalid,
358 CompactorSignature,
362 CompactionLoopOnly,
366 StdFormatMissing,
369 StdFormatSyntax,
372 StdJsonParse,
375 StdJsonStringify,
379 StdRegexInvalid,
382 Other,
385}
386
387impl ErrorCode {
388 pub fn parse_retry_after_ms(msg: &str) -> Option<u64> {
402 let needle = "retry-after";
405 let bytes = msg.as_bytes();
409 let n_len = needle.len();
410 let start = if bytes.len() < n_len {
411 return None;
412 } else {
413 (0..=bytes.len() - n_len)
414 .find(|&i| bytes[i..i + n_len].eq_ignore_ascii_case(needle.as_bytes()))?
415 };
416 let after = &msg[start + needle.len()..];
417 let after = after.trim_start_matches(|c: char| c == ':' || c == '=' || c.is_whitespace());
419 let end = after
420 .find(|c: char| !c.is_ascii_digit() && c != '.')
421 .unwrap_or(after.len());
422 let head = &after[..end];
423 if !head.is_empty() {
424 if let Ok(secs) = head.parse::<u64>() {
425 return Some(secs.saturating_mul(1000));
426 }
427 if let Ok(secs_f) = head.parse::<f64>() {
428 if secs_f.is_finite() && secs_f >= 0.0 {
429 return Some((secs_f * 1000.0) as u64);
430 }
431 }
432 }
433 let date_slice = after
435 .split(|c: char| c == '\n' || c == '\r')
436 .next()
437 .unwrap_or(after)
438 .trim()
439 .trim_end_matches(|c: char| matches!(c, ',' | ';' | '.'));
440 if date_slice.is_empty() {
441 return None;
442 }
443 if let Ok(then) = httpdate::parse_http_date(date_slice) {
444 let now = std::time::SystemTime::now();
445 match then.duration_since(now) {
446 Ok(d) => return Some(d.as_millis().min(u64::MAX as u128) as u64),
447 Err(_) => return Some(0),
448 }
449 }
450 None
451 }
452}
453
454impl ErrorCode {
455 pub fn kind(&self) -> ErrorKind {
459 match self {
460 ErrorCode::UserCancelled => ErrorKind::Cancelled,
461 ErrorCode::ExecutionTimeout | ErrorCode::CheckpointTimeout => ErrorKind::Timeout,
462 ErrorCode::ProviderRateLimit => ErrorKind::RateLimit,
463 ErrorCode::ProviderAuth | ErrorCode::ConfigMissing => ErrorKind::AuthError,
464 ErrorCode::ProviderTokenLimit => ErrorKind::TokenLimit,
465 ErrorCode::ProviderServer => ErrorKind::ServerError500,
466 ErrorCode::ProviderServer500 => ErrorKind::ServerError500,
467 ErrorCode::ProviderBadGateway502 => ErrorKind::BadGateway502,
468 ErrorCode::ProviderServiceUnavailable503 => ErrorKind::ServiceUnavailable503,
469 ErrorCode::ProviderGatewayTimeout504 => ErrorKind::GatewayTimeout504,
470 ErrorCode::ProviderNetwork => ErrorKind::NetworkError,
471 ErrorCode::ProviderParse => ErrorKind::ParseError,
472 ErrorCode::ProviderOther => ErrorKind::ServerError500,
473 ErrorCode::InternalPanic => ErrorKind::Panic,
474 ErrorCode::InternalDroppedChannel
475 | ErrorCode::InternalDeadlock
476 | ErrorCode::InternalTaskAborted
477 | ErrorCode::InternalOther => ErrorKind::Internal,
478 ErrorCode::ScriptError
479 | ErrorCode::ToolBudgetExceeded
480 | ErrorCode::ToolApprovalProtocol
481 | ErrorCode::ToolNoRegistry
482 | ErrorCode::ToolError
483 | ErrorCode::AgentToolsDoubleDispatch
484 | ErrorCode::LoopOutputBudgetExceeded
485 | ErrorCode::LoopMultiCheckpoint
486 | ErrorCode::ContextOverflow
487 | ErrorCode::ContextNativeUnsupported
488 | ErrorCode::ContextCompactionExhausted
489 | ErrorCode::CompactionThresholdInvalid
490 | ErrorCode::CompactorSignature
491 | ErrorCode::CompactionLoopOnly
492 | ErrorCode::PartialRetryExhausted
493 | ErrorCode::StdFormatMissing
494 | ErrorCode::StdFormatSyntax
495 | ErrorCode::StdJsonParse
496 | ErrorCode::StdJsonStringify
497 | ErrorCode::StdRegexInvalid
498 | ErrorCode::Other => ErrorKind::ScriptError,
499 ErrorCode::ScriptDepthExceeded => ErrorKind::ScriptDepthExceeded,
500 ErrorCode::AuthorRaise => ErrorKind::AuthorRaise,
501 }
502 }
503
504 pub fn as_wire(&self) -> &'static str {
507 match self {
508 ErrorCode::UserCancelled => "AKRIBES-E-USER-CANCELLED",
509 ErrorCode::ExecutionTimeout => "AKRIBES-E-EXECUTION-TIMEOUT",
510 ErrorCode::CheckpointTimeout => "AKRIBES-E-CHECKPOINT-TIMEOUT",
511 ErrorCode::ProviderRateLimit => "AKRIBES-E-PROVIDER-RATE-LIMIT",
512 ErrorCode::ProviderAuth => "AKRIBES-E-PROVIDER-AUTH",
513 ErrorCode::ProviderTokenLimit => "AKRIBES-E-PROVIDER-TOKEN-LIMIT",
514 ErrorCode::ProviderServer => "AKRIBES-E-PROVIDER-SERVER",
515 ErrorCode::ProviderServer500 => "AKRIBES-E-PROVIDER-SERVER-500",
516 ErrorCode::ProviderBadGateway502 => "AKRIBES-E-PROVIDER-BAD-GATEWAY-502",
517 ErrorCode::ProviderServiceUnavailable503 => "AKRIBES-E-PROVIDER-SERVICE-UNAVAILABLE-503",
518 ErrorCode::ProviderGatewayTimeout504 => "AKRIBES-E-PROVIDER-GATEWAY-TIMEOUT-504",
519 ErrorCode::ProviderNetwork => "AKRIBES-E-PROVIDER-NETWORK",
520 ErrorCode::ProviderParse => "AKRIBES-E-PROVIDER-PARSE",
521 ErrorCode::ProviderOther => "AKRIBES-E-PROVIDER-OTHER",
522 ErrorCode::InternalPanic => "AKRIBES-E-INTERNAL-PANIC",
523 ErrorCode::InternalDroppedChannel => "AKRIBES-E-INTERNAL-DROPPED-CHANNEL",
524 ErrorCode::InternalDeadlock => "AKRIBES-E-INTERNAL-DEADLOCK",
525 ErrorCode::InternalTaskAborted => "AKRIBES-E-INTERNAL-TASK-ABORTED",
526 ErrorCode::InternalOther => "AKRIBES-E-INTERNAL-OTHER",
527 ErrorCode::ScriptError => "AKRIBES-E-SCRIPT-ERROR",
528 ErrorCode::ScriptDepthExceeded => "AKRIBES-E-SCRIPT-DEPTH",
529 ErrorCode::PartialRetryExhausted => "AKRIBES-E-RETRY-PARTIAL-EXHAUSTED",
530 ErrorCode::AuthorRaise => "AKRIBES-E-AUTHOR-RAISE",
531 ErrorCode::ToolBudgetExceeded => "AKRIBES-E-TOOL-BUDGET",
532 ErrorCode::ToolApprovalProtocol => "AKRIBES-E-TOOL-APPROVAL-PROTOCOL",
533 ErrorCode::ToolNoRegistry => "AKRIBES-E-TOOL-NO-REGISTRY",
534 ErrorCode::ToolError => "AKRIBES-E-TOOL-ERROR",
535 ErrorCode::AgentToolsDoubleDispatch => "AKRIBES-E-AGENT-TOOLS-DOUBLE-DISPATCH",
536 ErrorCode::ConfigMissing => "AKRIBES-E-CONFIG-MISSING",
537 ErrorCode::LoopOutputBudgetExceeded => "AKRIBES-E-LOOP-OUTPUT-BUDGET-EXCEEDED",
538 ErrorCode::LoopMultiCheckpoint => "AKRIBES-E-LOOP-MULTI-CHECKPOINT",
539 ErrorCode::ContextOverflow => "AKRIBES-E-CONTEXT-OVERFLOW",
540 ErrorCode::ContextNativeUnsupported => "AKRIBES-E-CONTEXT-NATIVE-UNSUPPORTED",
541 ErrorCode::ContextCompactionExhausted => "AKRIBES-E-CONTEXT-COMPACTION-EXHAUSTED",
542 ErrorCode::CompactionThresholdInvalid => "AKRIBES-E-COMPACTION-THRESHOLD-INVALID",
543 ErrorCode::CompactorSignature => "AKRIBES-E-COMPACTOR-SIGNATURE",
544 ErrorCode::CompactionLoopOnly => "AKRIBES-E-COMPACTION-LOOP-ONLY",
545 ErrorCode::StdFormatMissing => "AKRIBES-E-STD-FORMAT-MISS-001",
546 ErrorCode::StdFormatSyntax => "AKRIBES-E-STD-FORMAT-SYNTAX-001",
547 ErrorCode::StdJsonParse => "AKRIBES-E-STD-JSON-PARSE-001",
548 ErrorCode::StdJsonStringify => "AKRIBES-E-STD-JSON-STRINGIFY-001",
549 ErrorCode::StdRegexInvalid => "AKRIBES-E-STD-REGEX-001",
550 ErrorCode::Other => "AKRIBES-E-OTHER",
551 }
552 }
553
554 pub fn from_wire(s: &str) -> Option<Self> {
560 let code = match s {
561 "AKRIBES-E-USER-CANCELLED" => ErrorCode::UserCancelled,
562 "AKRIBES-E-EXECUTION-TIMEOUT" => ErrorCode::ExecutionTimeout,
563 "AKRIBES-E-CHECKPOINT-TIMEOUT" => ErrorCode::CheckpointTimeout,
564 "AKRIBES-E-PROVIDER-RATE-LIMIT" => ErrorCode::ProviderRateLimit,
565 "AKRIBES-E-PROVIDER-AUTH" => ErrorCode::ProviderAuth,
566 "AKRIBES-E-PROVIDER-TOKEN-LIMIT" => ErrorCode::ProviderTokenLimit,
567 "AKRIBES-E-PROVIDER-SERVER" => ErrorCode::ProviderServer,
568 "AKRIBES-E-PROVIDER-SERVER-500" => ErrorCode::ProviderServer500,
569 "AKRIBES-E-PROVIDER-BAD-GATEWAY-502" => ErrorCode::ProviderBadGateway502,
570 "AKRIBES-E-PROVIDER-SERVICE-UNAVAILABLE-503" => ErrorCode::ProviderServiceUnavailable503,
571 "AKRIBES-E-PROVIDER-GATEWAY-TIMEOUT-504" => ErrorCode::ProviderGatewayTimeout504,
572 "AKRIBES-E-PROVIDER-NETWORK" => ErrorCode::ProviderNetwork,
573 "AKRIBES-E-PROVIDER-PARSE" => ErrorCode::ProviderParse,
574 "AKRIBES-E-PROVIDER-OTHER" => ErrorCode::ProviderOther,
575 "AKRIBES-E-INTERNAL-PANIC" => ErrorCode::InternalPanic,
576 "AKRIBES-E-INTERNAL-DROPPED-CHANNEL" => ErrorCode::InternalDroppedChannel,
577 "AKRIBES-E-INTERNAL-DEADLOCK" => ErrorCode::InternalDeadlock,
578 "AKRIBES-E-INTERNAL-TASK-ABORTED" => ErrorCode::InternalTaskAborted,
579 "AKRIBES-E-INTERNAL-OTHER" => ErrorCode::InternalOther,
580 "AKRIBES-E-SCRIPT-ERROR" => ErrorCode::ScriptError,
581 "AKRIBES-E-SCRIPT-DEPTH" => ErrorCode::ScriptDepthExceeded,
582 "AKRIBES-E-RETRY-PARTIAL-EXHAUSTED" => ErrorCode::PartialRetryExhausted,
583 "AKRIBES-E-AUTHOR-RAISE" => ErrorCode::AuthorRaise,
584 "AKRIBES-E-TOOL-BUDGET" => ErrorCode::ToolBudgetExceeded,
585 "AKRIBES-E-TOOL-APPROVAL-PROTOCOL" => ErrorCode::ToolApprovalProtocol,
586 "AKRIBES-E-TOOL-NO-REGISTRY" => ErrorCode::ToolNoRegistry,
587 "AKRIBES-E-TOOL-ERROR" => ErrorCode::ToolError,
588 "AKRIBES-E-AGENT-TOOLS-DOUBLE-DISPATCH" => ErrorCode::AgentToolsDoubleDispatch,
589 "AKRIBES-E-CONFIG-MISSING" => ErrorCode::ConfigMissing,
590 "AKRIBES-E-LOOP-OUTPUT-BUDGET-EXCEEDED" => ErrorCode::LoopOutputBudgetExceeded,
591 "AKRIBES-E-LOOP-MULTI-CHECKPOINT" => ErrorCode::LoopMultiCheckpoint,
592 "AKRIBES-E-CONTEXT-OVERFLOW" => ErrorCode::ContextOverflow,
593 "AKRIBES-E-CONTEXT-NATIVE-UNSUPPORTED" => ErrorCode::ContextNativeUnsupported,
594 "AKRIBES-E-CONTEXT-COMPACTION-EXHAUSTED" => ErrorCode::ContextCompactionExhausted,
595 "AKRIBES-E-COMPACTION-THRESHOLD-INVALID" => ErrorCode::CompactionThresholdInvalid,
596 "AKRIBES-E-COMPACTOR-SIGNATURE" => ErrorCode::CompactorSignature,
597 "AKRIBES-E-COMPACTION-LOOP-ONLY" => ErrorCode::CompactionLoopOnly,
598 "AKRIBES-E-STD-FORMAT-MISS-001" => ErrorCode::StdFormatMissing,
599 "AKRIBES-E-STD-FORMAT-SYNTAX-001" => ErrorCode::StdFormatSyntax,
600 "AKRIBES-E-STD-JSON-PARSE-001" => ErrorCode::StdJsonParse,
601 "AKRIBES-E-STD-JSON-STRINGIFY-001" => ErrorCode::StdJsonStringify,
602 "AKRIBES-E-STD-REGEX-001" => ErrorCode::StdRegexInvalid,
603 "AKRIBES-E-OTHER" => {
604 tracing::warn!(
612 target: "akribes_types::error",
613 wire_code = "AKRIBES-E-OTHER",
614 "decoded fallback ErrorCode::Other from wire payload — the producing component skipped a more specific AKRIBES-E-* code"
615 );
616 ErrorCode::Other
617 }
618 _ => return None,
619 };
620 Some(code)
621 }
622
623 pub fn default_user_message(&self) -> &'static str {
628 match self {
629 ErrorCode::UserCancelled => {
630 "The execution was cancelled."
631 }
632 ErrorCode::ExecutionTimeout => {
633 "The workflow ran past its time budget. Try a smaller input, simplify the workflow, or raise AKRIBES_EXECUTION_TIMEOUT."
634 }
635 ErrorCode::CheckpointTimeout => {
636 "A checkpoint waited longer than its on_timeout window without a resume."
637 }
638 ErrorCode::ProviderRateLimit => {
639 "The model provider rate-limited the request. Wait a moment and retry; consider lowering concurrency."
640 }
641 ErrorCode::ProviderAuth => {
642 "The model provider rejected our credentials. Check the provider's API key and that the configured model is enabled."
643 }
644 ErrorCode::ProviderTokenLimit => {
645 "The prompt exceeds the model's context window. Reduce input length, use a larger-context model, or split the work."
646 }
647 ErrorCode::ProviderServer => {
648 "The model provider returned a server-side error. Retrying is usually appropriate."
649 }
650 ErrorCode::ProviderServer500 => {
651 "The model provider returned HTTP 500. The origin reported an internal error; a retry with a short backoff is usually appropriate."
652 }
653 ErrorCode::ProviderBadGateway502 => {
654 "The model provider returned HTTP 502 (bad gateway). The edge fronted a failing origin; retry with a short backoff."
655 }
656 ErrorCode::ProviderServiceUnavailable503 => {
657 "The model provider returned HTTP 503 (service unavailable). This is rate-limit-adjacent — honour Retry-After if the provider sent one, otherwise back off."
658 }
659 ErrorCode::ProviderGatewayTimeout504 => {
660 "The model provider returned HTTP 504 (gateway timeout). The upstream is slow or stuck; retry with a longer backoff before alerting."
661 }
662 ErrorCode::ProviderNetwork => {
663 "Could not reach the model provider (network/DNS/TLS/timeout). Retry; check connectivity if it persists."
664 }
665 ErrorCode::ProviderParse => {
666 "The model produced output that didn't fit the declared schema. Check the prompt and the type definition."
667 }
668 ErrorCode::ProviderOther => {
669 "The model provider failed with an unclassified error."
670 }
671 ErrorCode::InternalPanic => {
672 "An internal Akribes task crashed (AKRIBES-E-INTERNAL-PANIC). \
673 This is a bug. Report with the execution id at \
674 https://github.com/PodestaAI/akribes-sdks/issues."
675 }
676 ErrorCode::InternalDroppedChannel => {
677 "An internal Akribes channel was closed unexpectedly (AKRIBES-E-INTERNAL-DROPPED-CHANNEL). \
678 This is usually a bug. Report with the execution id at \
679 https://github.com/PodestaAI/akribes-sdks/issues."
680 }
681 ErrorCode::InternalDeadlock => {
682 "Akribes detected a stuck workflow graph (AKRIBES-E-INTERNAL-DEADLOCK). \
683 This is a compiler/engine bug. Report at \
684 https://github.com/PodestaAI/akribes-sdks/issues."
685 }
686 ErrorCode::InternalTaskAborted => {
687 "An internal task was aborted unexpectedly (AKRIBES-E-INTERNAL-TASK-ABORTED). \
688 This is usually a bug. Report at \
689 https://github.com/PodestaAI/akribes-sdks/issues."
690 }
691 ErrorCode::InternalOther => {
692 "An unspecified internal error occurred (AKRIBES-E-INTERNAL-OTHER). \
693 Report with the execution id at \
694 https://github.com/PodestaAI/akribes-sdks/issues."
695 }
696 ErrorCode::ScriptError => {
697 "The workflow encountered a runtime error. Check task logic, types, and inputs."
698 }
699 ErrorCode::ScriptDepthExceeded => {
700 "Workflow call(...) chain exceeded the recursion cap. Refactor to reduce nesting."
701 }
702 ErrorCode::PartialRetryExhausted => {
703 "All validation retries on a partial-retry task were exhausted."
704 }
705 ErrorCode::AuthorRaise => {
706 "The workflow's failure path fired (the LLM returned an Unable or non-success variant the script mapped to fail)."
707 }
708 ErrorCode::ToolBudgetExceeded => {
709 "An agent exceeded its tool_budget cap. Increase the cap or reduce tool use."
710 }
711 ErrorCode::ToolApprovalProtocol => {
712 "Tool approval received an unexpected payload. This is a host-integration bug."
713 }
714 ErrorCode::ToolNoRegistry => {
715 "A tool call was attempted but no MCP registry is attached. Configure mcp_server / mcp_registry, or run via a host that wires the registry."
716 }
717 ErrorCode::ToolError => {
718 "An MCP tool returned an error. Check tool configuration and the upstream service."
719 }
720 ErrorCode::AgentToolsDoubleDispatch => {
721 "An agent invoked tools more than once in a single dispatch. Agents are single-round-trip — use a `loop` block for multi-turn tool use."
722 }
723 ErrorCode::ConfigMissing => {
724 "Required configuration is missing (API key, env var, or provider setup)."
725 }
726 ErrorCode::LoopOutputBudgetExceeded => {
727 "A `loop` block exceeded its `max_total_output_tokens` cap. Raise the cap or shorten per-turn output."
728 }
729 ErrorCode::LoopMultiCheckpoint => {
730 "A loop turn fired more than one checkpoint. One checkpoint per turn is the supported envelope — split them across turns or move one outside the loop."
731 }
732 ErrorCode::ContextOverflow => {
733 "The conversation exceeds the model's context window. Configure `compaction:` on the agent (e.g. `compaction: at 80%`) or pick a model with a larger window."
734 }
735 ErrorCode::ContextNativeUnsupported => {
736 "This model doesn't support server-side native compaction. Pick a capable model (opus_4_7, opus_4_6, sonnet_4_6, gpt_5_3_codex, gpt_5_5) or switch to a custom compaction chain."
737 }
738 ErrorCode::ContextCompactionExhausted => {
739 "The compaction chain ran every configured step and the conversation still exceeds the configured cap. Add a terminal step (truncate or native) or raise the cap."
740 }
741 ErrorCode::CompactionThresholdInvalid => {
742 "A compaction threshold is invalid. Use 1..=100 with `%`, or a positive absolute token count."
743 }
744 ErrorCode::CompactorSignature => {
745 "User-defined compactor must have signature `(history: str | list[message]) -> str | list[message]`."
746 }
747 ErrorCode::CompactionLoopOnly => {
748 "`compact_to_state(...)` may only appear inside a loop's `compaction:` block — move it under the loop, or use a different primitive on the agent."
749 }
750 ErrorCode::StdFormatMissing => {
751 "`std.format` is missing a placeholder key. Pass every `{name}` in the template via the `args` map."
752 }
753 ErrorCode::StdFormatSyntax => {
754 "`std.format` template has malformed brace syntax. Use `{name}` for placeholders, `{{` / `}}` for literal braces."
755 }
756 ErrorCode::StdJsonParse => {
757 "`std.json_parse` could not parse the input as JSON."
758 }
759 ErrorCode::StdJsonStringify => {
760 "`std.json_stringify` could not serialise the value. Check for control-plane values (FatalError) and non-JSON shapes."
761 }
762 ErrorCode::StdRegexInvalid => {
763 "`std.regex_extract` was given an invalid regex pattern. Check the syntax against the Rust `regex` crate's rules."
764 }
765 ErrorCode::Other => {
766 "An error occurred. See the developer message for detail."
767 }
768 }
769 }
770}
771
772#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
777pub struct ErrorSource {
778 #[serde(skip_serializing_if = "Option::is_none")]
780 pub task: Option<String>,
781 #[serde(skip_serializing_if = "Option::is_none")]
783 pub agent: Option<String>,
784 #[serde(skip_serializing_if = "Option::is_none")]
787 pub provider: Option<String>,
788 #[serde(skip_serializing_if = "Option::is_none")]
790 pub model: Option<String>,
791 #[serde(skip_serializing_if = "Option::is_none")]
793 pub tool_ref: Option<String>,
794 #[serde(skip_serializing_if = "Option::is_none")]
796 pub script: Option<String>,
797 #[serde(skip_serializing_if = "Option::is_none")]
799 pub line: Option<u32>,
800}
801
802impl ErrorSource {
803 pub fn empty() -> Self {
804 Self::default()
805 }
806
807 pub fn is_empty(&self) -> bool {
808 self == &Self::default()
809 }
810
811 pub fn with_task(mut self, task: impl Into<String>) -> Self {
813 self.task = Some(task.into());
814 self
815 }
816 pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
817 self.agent = Some(agent.into());
818 self
819 }
820 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
821 self.provider = Some(provider.into());
822 self
823 }
824 pub fn with_model(mut self, model: impl Into<String>) -> Self {
825 self.model = Some(model.into());
826 self
827 }
828 pub fn with_tool_ref(mut self, tool_ref: impl Into<String>) -> Self {
829 self.tool_ref = Some(tool_ref.into());
830 self
831 }
832 pub fn with_script(mut self, script: impl Into<String>) -> Self {
833 self.script = Some(script.into());
834 self
835 }
836 pub fn with_line(mut self, line: u32) -> Self {
837 self.line = Some(line);
838 self
839 }
840}
841
842#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
857pub struct ErrorDetail {
858 pub kind: ErrorKind,
859 pub code: ErrorCode,
860 pub message: String,
863 pub user_message: String,
866 #[serde(skip_serializing_if = "Option::is_none")]
869 pub retry_after_ms: Option<u64>,
870 #[serde(skip_serializing_if = "ErrorSource::is_empty", default)]
873 pub source: ErrorSource,
874}
875
876impl ErrorDetail {
877 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
880 Self {
881 kind: code.kind(),
882 code,
883 message: message.into(),
884 user_message: code.default_user_message().to_string(),
885 retry_after_ms: None,
886 source: ErrorSource::default(),
887 }
888 }
889
890 pub fn from_kind(kind: ErrorKind, message: impl Into<String>) -> Self {
895 let message = message.into();
896 let code = match kind {
897 ErrorKind::RateLimit => ErrorCode::ProviderRateLimit,
898 ErrorKind::AuthError => ErrorCode::ProviderAuth,
899 ErrorKind::TokenLimit => ErrorCode::ProviderTokenLimit,
900 ErrorKind::ServerError500 => ErrorCode::ProviderServer500,
901 ErrorKind::BadGateway502 => ErrorCode::ProviderBadGateway502,
902 ErrorKind::ServiceUnavailable503 => ErrorCode::ProviderServiceUnavailable503,
903 ErrorKind::GatewayTimeout504 => ErrorCode::ProviderGatewayTimeout504,
904 ErrorKind::NetworkError => ErrorCode::ProviderNetwork,
905 ErrorKind::ParseError => ErrorCode::ProviderParse,
906 ErrorKind::Cancelled => ErrorCode::UserCancelled,
907 ErrorKind::Timeout => ErrorCode::ExecutionTimeout,
908 ErrorKind::ScriptError => ErrorCode::ScriptError,
909 ErrorKind::AuthorRaise => ErrorCode::AuthorRaise,
910 ErrorKind::ScriptDepthExceeded => ErrorCode::ScriptDepthExceeded,
911 ErrorKind::Panic => ErrorCode::InternalPanic,
912 ErrorKind::Internal => ErrorCode::InternalOther,
913 };
914 let retry_after_ms = if matches!(
918 kind,
919 ErrorKind::RateLimit
920 | ErrorKind::ServerError500
921 | ErrorKind::BadGateway502
922 | ErrorKind::ServiceUnavailable503
923 | ErrorKind::GatewayTimeout504
924 | ErrorKind::NetworkError
925 ) {
926 ErrorCode::parse_retry_after_ms(&message)
927 } else {
928 None
929 };
930 Self {
931 kind,
932 code,
933 message,
934 user_message: code.default_user_message().to_string(),
935 retry_after_ms,
936 source: ErrorSource::default(),
937 }
938 }
939
940 pub fn with_user_message(mut self, msg: impl Into<String>) -> Self {
943 self.user_message = msg.into();
944 self
945 }
946
947 pub fn with_retry_after_ms(mut self, ms: u64) -> Self {
948 self.retry_after_ms = Some(ms);
949 self
950 }
951
952 pub fn with_source(mut self, source: ErrorSource) -> Self {
953 self.source = source;
954 self
955 }
956
957 pub fn with_task(mut self, task: impl Into<String>) -> Self {
959 self.source.task = Some(task.into());
960 self
961 }
962
963 pub fn is_retryable(&self) -> bool {
966 self.retry_after_ms.is_some() || self.kind.is_transient()
967 }
968
969 pub fn suggested_action(&self) -> SuggestedAction {
970 self.kind.suggested_action()
971 }
972}
973
974impl std::fmt::Display for ErrorDetail {
975 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
979 write!(f, "{}: {}", self.code.as_wire(), self.message)
980 }
981}
982
983impl std::fmt::Display for ErrorKind {
984 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
985 match self {
986 ErrorKind::RateLimit => write!(f, "rate limit"),
987 ErrorKind::AuthError => write!(f, "authentication error"),
988 ErrorKind::TokenLimit => write!(f, "token limit"),
989 ErrorKind::ServerError500 => write!(f, "server error (HTTP 500)"),
990 ErrorKind::BadGateway502 => write!(f, "bad gateway (HTTP 502)"),
991 ErrorKind::ServiceUnavailable503 => write!(f, "service unavailable (HTTP 503)"),
992 ErrorKind::GatewayTimeout504 => write!(f, "gateway timeout (HTTP 504)"),
993 ErrorKind::NetworkError => write!(f, "network error"),
994 ErrorKind::ParseError => write!(f, "parse error"),
995 ErrorKind::Cancelled => write!(f, "cancelled"),
996 ErrorKind::Timeout => write!(f, "timeout"),
997 ErrorKind::ScriptError => write!(f, "script error"),
998 ErrorKind::AuthorRaise => write!(f, "author raise"),
999 ErrorKind::ScriptDepthExceeded => write!(f, "script depth exceeded"),
1000 ErrorKind::Panic => write!(f, "panic"),
1001 ErrorKind::Internal => write!(f, "internal error"),
1002 }
1003 }
1004}