feat(agent): fall back to the container backend when pi-native fails
Dev starts on pi-native and keeps pi-agent available for a failed turn. Latency tuning stays deferred.
This commit is contained in:
@@ -46,7 +46,10 @@ func NewBackend(name string, cfg *config.Config) Backend {
|
||||
case "pi-agent":
|
||||
inner = &PiAgentBackend{cfg: cfg}
|
||||
case "pi-native":
|
||||
inner = &PiNativeBackend{cfg: cfg}
|
||||
inner = &fallbackBackend{
|
||||
primary: NewPiNativeBackend(cfg),
|
||||
fallback: &PiAgentBackend{cfg: cfg},
|
||||
}
|
||||
default:
|
||||
inner = &StubBackend{}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fallbackBackend runs pi-native first. A failed turn is retried on the container pi-agent.
|
||||
type fallbackBackend struct {
|
||||
primary Backend
|
||||
fallback Backend
|
||||
}
|
||||
|
||||
func (f *fallbackBackend) Name() string {
|
||||
if f.primary == nil {
|
||||
return "pi-native"
|
||||
}
|
||||
return f.primary.Name()
|
||||
}
|
||||
|
||||
func (f *fallbackBackend) Execute(ctx context.Context, req Request) (*Result, error) {
|
||||
res, err := f.primary.Execute(ctx, req)
|
||||
if ctx.Err() != nil || succeeded(res, err) || f.fallback == nil {
|
||||
return res, err
|
||||
}
|
||||
fb, fbErr := f.fallback.Execute(ctx, req)
|
||||
if !succeeded(fb, fbErr) {
|
||||
return res, err
|
||||
}
|
||||
note := "pi-native failed; served by pi-agent"
|
||||
if res != nil && strings.TrimSpace(res.ErrorReason) != "" {
|
||||
note += ": " + strings.TrimSpace(res.ErrorReason)
|
||||
} else if err != nil {
|
||||
note += ": " + err.Error()
|
||||
}
|
||||
if strings.TrimSpace(fb.Logs) == "" {
|
||||
fb.Logs = note
|
||||
} else {
|
||||
fb.Logs = note + "\n" + fb.Logs
|
||||
}
|
||||
return fb, nil
|
||||
}
|
||||
|
||||
func succeeded(res *Result, err error) bool {
|
||||
return err == nil && res != nil && res.Success
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type scriptBackend struct {
|
||||
name string
|
||||
res *Result
|
||||
err error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *scriptBackend) Name() string { return s.name }
|
||||
|
||||
func (s *scriptBackend) Execute(ctx context.Context, req Request) (*Result, error) {
|
||||
s.calls++
|
||||
return s.res, s.err
|
||||
}
|
||||
|
||||
func TestFallbackKeepsNativeSuccess(t *testing.T) {
|
||||
primary := &scriptBackend{name: "pi-native", res: &Result{Success: true, Logs: "native"}}
|
||||
spare := &scriptBackend{name: "pi-agent", res: &Result{Success: true}}
|
||||
got, err := (&fallbackBackend{primary: primary, fallback: spare}).Execute(context.Background(), Request{})
|
||||
if err != nil || !got.Success || got.Logs != "native" || spare.calls != 0 {
|
||||
t.Fatalf("got %#v err=%v spare=%d", got, err, spare.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackUsesContainerAfterNativeFailure(t *testing.T) {
|
||||
primary := &scriptBackend{name: "pi-native", res: &Result{Success: false, ErrorReason: "missing target deliverable"}}
|
||||
spare := &scriptBackend{name: "pi-agent", res: &Result{Success: true, Logs: "container"}}
|
||||
got, err := (&fallbackBackend{primary: primary, fallback: spare}).Execute(context.Background(), Request{})
|
||||
if err != nil || !got.Success || spare.calls != 1 {
|
||||
t.Fatalf("got %#v err=%v calls=%d", got, err, spare.calls)
|
||||
}
|
||||
if !strings.Contains(got.Logs, "served by pi-agent") || !strings.Contains(got.Logs, "missing target deliverable") {
|
||||
t.Fatalf("logs %q", got.Logs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackReturnsNativeWhenContainerAlsoFails(t *testing.T) {
|
||||
primary := &scriptBackend{name: "pi-native", err: errors.New("native down")}
|
||||
spare := &scriptBackend{name: "pi-agent", res: &Result{Success: false, ErrorReason: "runner down"}}
|
||||
got, err := (&fallbackBackend{primary: primary, fallback: spare}).Execute(context.Background(), Request{})
|
||||
if err == nil || err.Error() != "native down" || got != nil || spare.calls != 1 {
|
||||
t.Fatalf("got %#v err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackStopsWhenContextCancelled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
primary := &scriptBackend{name: "pi-native", err: context.Canceled}
|
||||
spare := &scriptBackend{name: "pi-agent", res: &Result{Success: true}}
|
||||
_, err := (&fallbackBackend{primary: primary, fallback: spare}).Execute(ctx, Request{})
|
||||
if !errors.Is(err, context.Canceled) || spare.calls != 0 {
|
||||
t.Fatalf("err=%v spare=%d", err, spare.calls)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user