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.
169 lines
5.0 KiB
Go
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()
|
|
}
|