Files
wehub-resource-sync 26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:16 +08:00

162 lines
3.8 KiB
Go

package agent
import (
"context"
"fmt"
"strings"
"time"
)
type fallbackAgent struct {
agents []Agent
}
// NewFallback returns an Agent that tries each agent in order when an
// invocation fails because the current agent process is unavailable.
func NewFallback(agents []Agent) Agent {
switch len(agents) {
case 0:
return nil
case 1:
return agents[0]
default:
copied := make([]Agent, len(agents))
copy(copied, agents)
return &fallbackAgent{agents: copied}
}
}
func (a *fallbackAgent) Name() string {
if len(a.agents) == 0 {
return ""
}
return a.agents[0].Name()
}
func (a *fallbackAgent) SupportsSessionResume() bool {
for _, current := range a.agents {
if SupportsSessionResume(current) {
return true
}
}
return false
}
func (a *fallbackAgent) SupportsSessionProvider(provider string) bool {
for _, current := range a.agents {
if SupportsSessionProvider(current, provider) {
return true
}
}
return false
}
func (a *fallbackAgent) ReportsAgentAttempts() bool { return true }
// NeutralizesGateInstructions fails closed over the whole fallback set: the
// wrapper may invoke any member, so it neutralizes the target repo's project
// agent-instruction files only if EVERY member does. A single unverified member
// makes the wrapper report false so the gate is refused rather than risk that
// member running unneutralized.
func (a *fallbackAgent) NeutralizesGateInstructions() bool {
if len(a.agents) == 0 {
return false
}
for _, current := range a.agents {
if !NeutralizesGateInstructions(current) {
return false
}
}
return true
}
func (a *fallbackAgent) Run(ctx context.Context, opts RunOpts) (*Result, error) {
candidates := a.agents
if opts.Session != nil && opts.Session.ID != "" && opts.Session.Agent != "" {
candidates = nil
for _, current := range a.agents {
if SupportsSessionProvider(current, opts.Session.Agent) {
candidates = append(candidates, current)
break
}
}
if len(candidates) == 0 {
return nil, fmt.Errorf("session provider %q is not configured", opts.Session.Agent)
}
}
var lastErr error
for i, current := range candidates {
currentOpts := opts
if currentOpts.Session != nil && currentOpts.Session.ID == "" && !SupportsSessionResume(current) {
currentOpts.Session = nil
currentOpts.SessionFallback = false
}
startedAt := time.Now()
result, err := current.Run(ctx, currentOpts)
if !ReportsAgentAttempts(current) {
emitAgentAttempt(currentOpts, current.Name(), result, err, startedAt, time.Now())
}
if err == nil {
if result != nil && result.Provider == "" {
result.Provider = current.Name()
}
return result, nil
}
lastErr = err
if i == len(candidates)-1 || !isAgentUnavailableError(err) {
return nil, err
}
next := candidates[i+1]
if opts.OnChunk != nil {
opts.OnChunk(fmt.Sprintf("\nagent %s failed (%s); falling back to %s\n", current.Name(), fallbackReason(err), next.Name()))
}
}
return nil, lastErr
}
func (a *fallbackAgent) Close() error {
var errs []string
for _, ag := range a.agents {
if err := ag.Close(); err != nil {
errs = append(errs, fmt.Sprintf("%s: %v", ag.Name(), err))
}
}
if len(errs) > 0 {
return fmt.Errorf("close fallback agents: %s", strings.Join(errs, "; "))
}
return nil
}
func isAgentUnavailableError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
unavailable := []string{
" start:",
"start server ",
" server: start server ",
" exited:",
" reported exit code ",
}
for _, needle := range unavailable {
if strings.Contains(msg, needle) {
return true
}
}
return false
}
func fallbackReason(err error) string {
if err == nil {
return "unknown error"
}
text := strings.Join(strings.Fields(err.Error()), " ")
const max = 160
if len([]rune(text)) <= max {
return text
}
runes := []rune(text)
return string(runes[:max]) + "..."
}