Files
Agent-System/internal/agent/piagent.go
T
weichenandCursor 98619c7b56 feat(agent): classify settled-failure as bounded retryable
Treat process-0 plus terminated/Connection error/auto_retry_end as a
provider transient. Permanent missing deliverables stay fail-closed and
are never reconstructed from truncated write arguments.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 19:16:46 +00:00

232 lines
5.9 KiB
Go

package agent
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"chat-agent-system/internal/config"
)
type PiAgentBackend struct {
cfg *config.Config
}
func (p *PiAgentBackend) Name() string { return "pi-agent" }
func (p *PiAgentBackend) Execute(ctx context.Context, req Request) (*Result, error) {
if len(req.Targets) == 0 {
req.Targets = []string{"output.md"}
}
workDir := req.WorkDir
if !filepath.IsAbs(workDir) {
wd, _ := os.Getwd()
workDir = filepath.Join(wd, workDir)
}
payload, _ := json.Marshal(map[string]any{
"workspace": workDir,
"model": p.cfg.PiAgentModel,
"provider": p.cfg.PiAgentProvider,
"prompt": req.Prompt,
"instructions": req.Instructions,
"targets": req.Targets,
})
bridgeBin := p.cfg.PiAgentBridgeBin
if !filepath.IsAbs(bridgeBin) {
wd, _ := os.Getwd()
bridgeBin = filepath.Join(wd, bridgeBin)
}
timeout := req.Timeout
if timeout <= 0 {
timeout = p.cfg.AgentTimeout
}
runCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
if runnerURL := strings.TrimRight(strings.TrimSpace(p.cfg.PiAgentRunnerURL), "/"); runnerURL != "" {
return p.executeRemote(runCtx, runnerURL, req, workDir, timeout)
}
cmd := exec.CommandContext(runCtx, bridgeBin)
cmd.Dir = workDir
cmd.Stdin = bytes.NewReader(payload)
cmd.Env = append(os.Environ(),
"PI_AGENT_BIN="+p.cfg.PiAgentBin,
"PI_AGENT_MODEL="+p.cfg.PiAgentModel,
"PI_AGENT_PROVIDER="+p.cfg.PiAgentProvider,
"PI_AGENT_SYSTEM_PROMPT_FILE=AGENTS.md",
)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
res := &Result{
Success: false,
ErrorReason: fmt.Sprintf("pi execution error: %v, stderr: %s", err, stderr.String()),
}
res.Retryable = AssessStepFailure(StepFailureSignals{Result: res, WorkDir: workDir}).Class == FailureTransient
return res, nil
}
type bridgeResponse struct {
OK bool `json:"ok"`
FallbackReason string `json:"fallback_reason"`
Stderr string `json:"stderr"`
Retryable bool `json:"retryable"`
}
var bResp bridgeResponse
lines := strings.Split(strings.TrimSpace(stdout.String()), "\n")
if len(lines) > 0 {
_ = json.Unmarshal([]byte(lines[len(lines)-1]), &bResp)
}
if !bResp.OK {
errMsg := bResp.FallbackReason
if bResp.Stderr != "" {
errMsg += " (" + bResp.Stderr + ")"
}
if errMsg == "" {
errMsg = "pi bridge reported task failed"
}
res := &Result{
Success: false,
ErrorReason: errMsg,
Logs: stdout.String(),
Retryable: bResp.Retryable,
}
if !res.Retryable {
res.Retryable = AssessStepFailure(StepFailureSignals{Result: res, WorkDir: workDir}).Class == FailureTransient
}
return res, nil
}
patches, err := MaterializeTargets(workDir, req.Targets, nil)
if err != nil {
res := &Result{
Success: false,
ErrorReason: err.Error(),
Patches: patches,
Logs: stdout.String(),
}
res.Retryable = AssessStepFailure(StepFailureSignals{
Result: res,
WorkDir: workDir,
TargetMissing: true,
}).Class == FailureTransient
return res, nil
}
return &Result{
Success: true,
Patches: patches,
Logs: stdout.String(),
}, nil
}
func (p *PiAgentBackend) executeRemote(ctx context.Context, runnerURL string, req Request, workDir string, timeout time.Duration) (*Result, error) {
reqBody, _ := json.Marshal(map[string]any{
"workspace": workDir,
"model": p.cfg.PiAgentModel,
"provider": p.cfg.PiAgentProvider,
"prompt": req.Prompt,
"instructions": req.Instructions,
"targets": req.Targets,
"timeout_ms": timeout.Milliseconds(),
})
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, runnerURL+"/v1/run", bytes.NewReader(reqBody))
if err != nil {
return &Result{
Success: false,
ErrorReason: fmt.Sprintf("failed to build remote request: %v", err),
}, nil
}
httpReq.Header.Set("Content-Type", "application/json")
ApplyRunnerAuth(httpReq, p.cfg.PiAgentRunnerAuthToken)
client := &http.Client{Timeout: timeout}
resp, err := client.Do(httpReq)
if err != nil {
return &Result{
Success: false,
ErrorReason: fmt.Sprintf("pi remote runner call failed: %v", err),
Retryable: true,
}, nil
}
defer resp.Body.Close()
var remoteResp struct {
OK bool `json:"ok"`
FallbackReason string `json:"fallback_reason"`
Stderr string `json:"stderr"`
Retryable bool `json:"retryable"`
Patches []struct {
RelPath string `json:"rel_path"`
Content string `json:"content"`
} `json:"patches"`
}
if err := json.NewDecoder(resp.Body).Decode(&remoteResp); err != nil {
return &Result{
Success: false,
ErrorReason: fmt.Sprintf("failed to decode remote response: %v", err),
}, nil
}
if !remoteResp.OK {
reason := remoteResp.FallbackReason
if remoteResp.Stderr != "" {
reason += " (" + remoteResp.Stderr + ")"
}
if reason == "" {
reason = "remote pi-agent reported task failure"
}
res := &Result{
Success: false,
ErrorReason: reason,
Retryable: remoteResp.Retryable,
}
if !res.Retryable {
res.Retryable = AssessStepFailure(StepFailureSignals{Result: res, WorkDir: workDir}).Class == FailureTransient
}
return res, nil
}
incoming := make(map[string][]byte, len(remoteResp.Patches))
for _, pt := range remoteResp.Patches {
incoming[pt.RelPath] = []byte(pt.Content)
}
patches, matErr := MaterializeTargets(workDir, req.Targets, incoming)
if matErr != nil {
res := &Result{
Success: false,
ErrorReason: matErr.Error(),
Patches: patches,
}
res.Retryable = remoteResp.Retryable || AssessStepFailure(StepFailureSignals{
Result: res,
WorkDir: workDir,
TargetMissing: true,
}).Class == FailureTransient
return res, nil
}
return &Result{
Success: true,
Patches: patches,
}, nil
}