144 lines
4 KiB
Go
144 lines
4 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
const failureSchemaVersion = 1
|
|
|
|
// FailureCode is the stable, provider-neutral classification of an execution
|
|
// failure. Provider-specific diagnostics belong in Message or Metadata.
|
|
type FailureCode string
|
|
|
|
const (
|
|
FailureCodeUnknown FailureCode = "unknown"
|
|
FailureCodeCancelled FailureCode = "cancelled"
|
|
FailureCodeDeadlineExceeded FailureCode = "deadline_exceeded"
|
|
FailureCodeInvalidRequest FailureCode = "invalid_request"
|
|
FailureCodeSessionNotFound FailureCode = "session_not_found"
|
|
FailureCodeUnavailable FailureCode = "unavailable"
|
|
FailureCodeQuotaExhausted FailureCode = "quota_exhausted"
|
|
FailureCodeProcessExit FailureCode = "process_exit"
|
|
FailureCodeProvider FailureCode = "provider_error"
|
|
FailureCodeInternal FailureCode = "internal"
|
|
)
|
|
|
|
// Failure is the public typed failure value shared by runtime hosts.
|
|
type Failure struct {
|
|
Code FailureCode `json:"code"`
|
|
Message string `json:"message,omitempty"`
|
|
Retryable bool `json:"retryable"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
}
|
|
|
|
func (f *Failure) Error() string {
|
|
if f == nil {
|
|
return ""
|
|
}
|
|
if f.Message != "" {
|
|
return f.Message
|
|
}
|
|
return string(f.Code)
|
|
}
|
|
|
|
type failureEnvelope struct {
|
|
SchemaVersion int `json:"schema_version"`
|
|
Failure *Failure `json:"failure"`
|
|
}
|
|
|
|
// EncodeFailure serializes a typed failure for durable host boundaries.
|
|
func EncodeFailure(f *Failure) (string, error) {
|
|
if f == nil {
|
|
return "", errors.New("agentruntime: nil failure")
|
|
}
|
|
normalized := normalizeFailure(*f)
|
|
payload, err := json.Marshal(failureEnvelope{
|
|
SchemaVersion: failureSchemaVersion,
|
|
Failure: &normalized,
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("agentruntime: encode failure: %w", err)
|
|
}
|
|
return string(payload), nil
|
|
}
|
|
|
|
// DecodeFailure parses a durable failure envelope. Unknown future codes are
|
|
// preserved in metadata and normalized to FailureCodeUnknown.
|
|
func DecodeFailure(payload string) (*Failure, error) {
|
|
var envelope failureEnvelope
|
|
if err := json.Unmarshal([]byte(payload), &envelope); err != nil {
|
|
return nil, fmt.Errorf("agentruntime: decode failure: %w", err)
|
|
}
|
|
if envelope.SchemaVersion != failureSchemaVersion {
|
|
return nil, fmt.Errorf("agentruntime: unsupported failure schema version %d", envelope.SchemaVersion)
|
|
}
|
|
if envelope.Failure == nil {
|
|
return nil, errors.New("agentruntime: failure payload is missing")
|
|
}
|
|
normalized := normalizeFailure(*envelope.Failure)
|
|
return &normalized, nil
|
|
}
|
|
|
|
// FailureFromError maps cancellation boundaries and otherwise returns a
|
|
// provider-neutral unknown failure without guessing provider policy.
|
|
func FailureFromError(err error) *Failure {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
switch {
|
|
case errors.Is(err, ErrRunCancelled), errors.Is(err, context.Canceled):
|
|
return &Failure{Code: FailureCodeCancelled, Message: err.Error()}
|
|
case errors.Is(err, context.DeadlineExceeded):
|
|
return &Failure{Code: FailureCodeDeadlineExceeded, Message: err.Error(), Retryable: true}
|
|
default:
|
|
return &Failure{Code: FailureCodeUnknown, Message: err.Error()}
|
|
}
|
|
}
|
|
|
|
func normalizeFailure(f Failure) Failure {
|
|
if isKnownFailureCode(f.Code) {
|
|
return f
|
|
}
|
|
metadata := cloneStringMap(f.Metadata)
|
|
if metadata == nil {
|
|
metadata = make(map[string]string, 1)
|
|
}
|
|
if f.Code != "" {
|
|
metadata["original_code"] = string(f.Code)
|
|
}
|
|
f.Code = FailureCodeUnknown
|
|
f.Metadata = metadata
|
|
return f
|
|
}
|
|
|
|
func isKnownFailureCode(code FailureCode) bool {
|
|
switch code {
|
|
case FailureCodeUnknown,
|
|
FailureCodeCancelled,
|
|
FailureCodeDeadlineExceeded,
|
|
FailureCodeInvalidRequest,
|
|
FailureCodeSessionNotFound,
|
|
FailureCodeUnavailable,
|
|
FailureCodeQuotaExhausted,
|
|
FailureCodeProcessExit,
|
|
FailureCodeProvider,
|
|
FailureCodeInternal:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func cloneStringMap(input map[string]string) map[string]string {
|
|
if len(input) == 0 {
|
|
return nil
|
|
}
|
|
cloned := make(map[string]string, len(input))
|
|
for key, value := range input {
|
|
cloned[key] = value
|
|
}
|
|
return cloned
|
|
}
|