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() }