Files
weichen 6032dbaa16 merge: integrate go-agentx runtime into local_dev candidate
Bring pinative/agentx workspace registry and A-B harness onto current
local_dev; renumber goose agentx registry 00062→00066 to avoid clash
with chat-summary/provenance migrations. Keep CURRENT/MATRIX as ours
for later docs apply; union module-boundaries agentx import tree.
Path literals use workspace constants for arch-check.
2026-09-23 13:25:32 +00:00

169 lines
5.0 KiB
Go

package agent
import (
"context"
"os"
"path/filepath"
"strings"
"chat-agent-system/internal/agentx"
"chat-agent-system/internal/agentx/llm"
"chat-agent-system/internal/agentx/session"
"chat-agent-system/internal/agentx/tools"
"chat-agent-system/internal/config"
"chat-agent-system/internal/workspace"
)
// PiNativeBackend runs the in-process Go agent loop. The container pi-agent backend stays available.
type PiNativeBackend struct {
cfg *config.Config
stream llm.StreamFunc
}
func NewPiNativeBackend(cfg *config.Config) *PiNativeBackend {
return &PiNativeBackend{cfg: cfg}
}
func (p *PiNativeBackend) Name() string { return "pi-native" }
func (p *PiNativeBackend) Execute(ctx context.Context, req Request) (*Result, error) {
if len(req.Targets) == 0 {
req.Targets = []string{workspace.DeliverableKey}
}
profile := strings.TrimSpace(req.ToolProfile)
if profile == "" {
profile = ToolProfileWorkspace
}
if profile != ToolProfileWorkspace && profile != ToolProfileReasoning {
return &Result{Success: false, ErrorReason: "unknown tool profile"}, nil
}
if p.stream == nil && (p.cfg == nil || strings.TrimSpace(p.cfg.AgentXAPIKey) == "") {
return &Result{Success: false, ErrorReason: "agentx api key is not configured"}, nil
}
workDir := req.WorkDir
if !filepath.IsAbs(workDir) {
wd, _ := os.Getwd()
workDir = filepath.Join(wd, workDir)
}
if err := os.MkdirAll(workDir, 0o755); err != nil {
return &Result{Success: false, ErrorReason: err.Error()}, nil
}
if p.cfg != nil {
_ = workspace.NoteWorkspace(p.cfg.DataDir, workDir, filepath.Base(workDir))
}
if strings.TrimSpace(req.Instructions) != "" {
_ = os.WriteFile(filepath.Join(workDir, "INSTRUCTIONS.md"), []byte(req.Instructions), 0o644)
_ = os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte(req.Instructions), 0o644)
}
if strings.TrimSpace(req.Prompt) != "" {
_ = os.WriteFile(filepath.Join(workDir, "TASK.md"), []byte(req.Prompt), 0o644)
}
var reg *tools.Registry
if profile == ToolProfileWorkspace {
reg = tools.WorkspaceTools(workDir)
}
store, err := session.Open(filepath.Join(workDir, ".agentx", "transcript.jsonl"))
if err != nil {
return &Result{Success: false, ErrorReason: err.Error()}, nil
}
stream := p.stream
model := "MiniMax-M2.7-highspeed"
if p.cfg != nil && p.cfg.AgentXModel != "" {
model = p.cfg.AgentXModel
}
if stream == nil {
client := &llm.Client{BaseURL: p.cfg.AgentXAPIBase, APIKey: p.cfg.AgentXAPIKey}
stream = client.Stream
}
timeout := req.Timeout
if timeout <= 0 && p.cfg != nil {
timeout = p.cfg.AgentXTimeout
}
if timeout <= 0 && p.cfg != nil {
timeout = p.cfg.AgentTimeout
}
runCtx := ctx
cancel := func() {}
if timeout > 0 {
runCtx, cancel = context.WithTimeout(ctx, timeout)
}
defer cancel()
var logs strings.Builder
runRes, err := agentx.Run(runCtx, agentx.Config{
Model: model,
SystemPrompt: nativeSystemPrompt(req, profile),
Tools: reg,
Stream: stream,
Session: store,
OnEvent: func(ev agentx.Event) {
logs.WriteString(ev.Type)
if ev.Tool != "" {
logs.WriteString(" ")
logs.WriteString(ev.Tool)
}
if ev.Text != "" {
logs.WriteString(" ")
logs.WriteString(ev.Text)
}
logs.WriteByte('\n')
},
}, llm.Message{Role: llm.RoleUser, Content: req.Prompt})
if err != nil {
return &Result{Success: false, ErrorReason: err.Error(), Logs: logs.String()}, nil
}
_ = os.WriteFile(filepath.Join(workDir, "execution.log"), []byte(logs.String()), 0o644)
if profile == ToolProfileReasoning && strings.TrimSpace(runRes.Text) != "" {
for _, target := range req.Targets {
path := filepath.Join(workDir, filepath.FromSlash(target))
if b, err := os.ReadFile(path); err == nil && len(strings.TrimSpace(string(b))) > 0 {
continue
}
_ = os.MkdirAll(filepath.Dir(path), 0o755)
_ = os.WriteFile(path, []byte(runRes.Text), 0o644)
}
}
if runRes.Err != nil {
reason := runRes.Err.Error()
res := &Result{Success: false, ErrorReason: reason, Logs: logs.String(), Retryable: runRes.Retryable}
if runRes.StopReason == llm.StopAborted {
res.Retryable = true
}
return res, nil
}
patches, matErr := MaterializeTargets(workDir, req.Targets, nil)
if matErr != nil {
return &Result{Success: false, ErrorReason: matErr.Error(), Patches: patches, Logs: logs.String()}, nil
}
return &Result{
Success: true,
Patches: patches,
Logs: logs.String(),
PromptTokens: runRes.Usage.PromptTokens,
CompletionTokens: runRes.Usage.CompletionTokens,
}, nil
}
func nativeSystemPrompt(req Request, profile string) string {
var b strings.Builder
if strings.TrimSpace(req.Instructions) != "" {
b.WriteString(req.Instructions)
b.WriteString("\n\n")
}
b.WriteString("Deliverable files: ")
b.WriteString(strings.Join(req.Targets, ", "))
b.WriteString(".")
if profile == ToolProfileWorkspace {
b.WriteString(" Use workspace tools to write those files. Do not invent paths outside the workspace.")
} else {
b.WriteString(" Reply with the deliverable text only. Tools are disabled.")
}
return b.String()
}