Files
Agent-System/internal/agent/pinative.go
T
weichen 265616e997 feat(stream): 打通小队过程事件到持久化 detail 帧
pi-native 与 runner NDJSON 把推理和工具调用投影为 detail,任务流始终落库;非 200 的流式尝试回落到一次性 /v1/run,避免改变既有错误分类。
2026-09-28 10:30:52 +00:00

178 lines
5.2 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
onProcess := req.OnEvent
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')
if onProcess != nil {
onProcess(ProcessEvent{
Type: ev.Type,
Text: ev.Text,
Tool: ev.Tool,
Detail: ev.Detail,
})
}
},
}, 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()
}