refactor(agentic): use go-process for agent lifecycle management
Replace raw exec.Command with process.StartWithOptions() from go-process. Agents get proper PID tracking and output capture via RingBuffer. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
5d5646248e
commit
9587ea4d49
2 changed files with 50 additions and 40 deletions
|
|
@ -6,10 +6,10 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
process "forge.lthn.ai/core/go-process"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
|
|
@ -34,6 +34,7 @@ type DispatchOutput struct {
|
|||
WorkspaceDir string `json:"workspace_dir"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
PID int `json:"pid,omitempty"`
|
||||
ProcessID string `json:"process_id,omitempty"` // go-process ID for lifecycle management
|
||||
OutputFile string `json:"output_file,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +45,20 @@ func (s *PrepSubsystem) registerDispatchTool(server *mcp.Server) {
|
|||
}, s.dispatch)
|
||||
}
|
||||
|
||||
// agentCommand returns the command and args for a given agent type.
|
||||
func agentCommand(agent, prompt string) (string, []string, error) {
|
||||
switch agent {
|
||||
case "gemini":
|
||||
return "gemini", []string{"-p", prompt, "--yolo"}, nil
|
||||
case "codex":
|
||||
return "codex", []string{"--approval-mode", "full-auto", "-q", prompt}, nil
|
||||
case "claude":
|
||||
return "claude", []string{"-p", prompt, "--dangerously-skip-permissions"}, nil
|
||||
default:
|
||||
return "", nil, fmt.Errorf("unknown agent: %s", agent)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest, input DispatchInput) (*mcp.CallToolResult, DispatchOutput, error) {
|
||||
if input.Repo == "" {
|
||||
return nil, DispatchOutput{}, fmt.Errorf("repo is required")
|
||||
|
|
@ -94,46 +109,40 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Step 2: Spawn agent in src/ directory
|
||||
outputFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", input.Agent))
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch input.Agent {
|
||||
case "gemini":
|
||||
cmd = exec.Command("gemini", "-p", prompt, "--yolo")
|
||||
case "codex":
|
||||
cmd = exec.Command("codex", "--approval-mode", "full-auto", "-q", prompt)
|
||||
case "claude":
|
||||
cmd = exec.Command("claude", "-p", prompt, "--dangerously-skip-permissions")
|
||||
default:
|
||||
return nil, DispatchOutput{}, fmt.Errorf("unknown agent: %s", input.Agent)
|
||||
// Step 2: Spawn agent via go-process
|
||||
command, args, err := agentCommand(input.Agent, prompt)
|
||||
if err != nil {
|
||||
return nil, DispatchOutput{}, err
|
||||
}
|
||||
|
||||
cmd.Dir = srcDir
|
||||
|
||||
outFile, _ := os.Create(outputFile)
|
||||
cmd.Stdout = outFile
|
||||
cmd.Stderr = outFile
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
outFile.Close()
|
||||
proc, err := process.StartWithOptions(ctx, process.RunOptions{
|
||||
Command: command,
|
||||
Args: args,
|
||||
Dir: srcDir,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, DispatchOutput{}, fmt.Errorf("failed to spawn %s: %w", input.Agent, err)
|
||||
}
|
||||
|
||||
info := proc.Info()
|
||||
|
||||
// Write initial status
|
||||
writeStatus(wsDir, &WorkspaceStatus{
|
||||
Status: "running",
|
||||
Agent: input.Agent,
|
||||
Repo: input.Repo,
|
||||
Org: input.Org,
|
||||
Task: input.Task,
|
||||
PID: cmd.Process.Pid,
|
||||
PID: info.PID,
|
||||
StartedAt: time.Now(),
|
||||
Runs: 1,
|
||||
})
|
||||
|
||||
// Write output to log file when process completes
|
||||
outputFile := filepath.Join(wsDir, fmt.Sprintf("agent-%s.log", input.Agent))
|
||||
go func() {
|
||||
cmd.Wait()
|
||||
outFile.Close()
|
||||
proc.Wait()
|
||||
os.WriteFile(outputFile, proc.OutputBytes(), 0644)
|
||||
}()
|
||||
|
||||
return nil, DispatchOutput{
|
||||
|
|
@ -141,7 +150,8 @@ func (s *PrepSubsystem) dispatch(ctx context.Context, req *mcp.CallToolRequest,
|
|||
Agent: input.Agent,
|
||||
Repo: input.Repo,
|
||||
WorkspaceDir: wsDir,
|
||||
PID: cmd.Process.Pid,
|
||||
PID: info.PID,
|
||||
ProcessID: info.ID,
|
||||
OutputFile: outputFile,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ import (
|
|||
|
||||
// PrepSubsystem provides agentic MCP tools.
|
||||
type PrepSubsystem struct {
|
||||
forgeURL string
|
||||
forgeToken string
|
||||
brainURL string
|
||||
brainKey string
|
||||
specsPath string
|
||||
codePath string
|
||||
client *http.Client
|
||||
forgeURL string
|
||||
forgeToken string
|
||||
brainURL string
|
||||
brainKey string
|
||||
specsPath string
|
||||
codePath string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewPrep creates an agentic subsystem.
|
||||
|
|
@ -49,13 +49,13 @@ func NewPrep() *PrepSubsystem {
|
|||
}
|
||||
|
||||
return &PrepSubsystem{
|
||||
forgeURL: envOr("FORGE_URL", "https://forge.lthn.ai"),
|
||||
forgeToken: forgeToken,
|
||||
brainURL: envOr("CORE_BRAIN_URL", "https://api.lthn.sh"),
|
||||
brainKey: brainKey,
|
||||
specsPath: envOr("SPECS_PATH", filepath.Join(home, "Code", "host-uk", "specs")),
|
||||
codePath: envOr("CODE_PATH", filepath.Join(home, "Code")),
|
||||
client: &http.Client{Timeout: 30 * time.Second},
|
||||
forgeURL: envOr("FORGE_URL", "https://forge.lthn.ai"),
|
||||
forgeToken: forgeToken,
|
||||
brainURL: envOr("CORE_BRAIN_URL", "https://api.lthn.sh"),
|
||||
brainKey: brainKey,
|
||||
specsPath: envOr("SPECS_PATH", filepath.Join(home, "Code", "host-uk", "specs")),
|
||||
codePath: envOr("CODE_PATH", filepath.Join(home, "Code")),
|
||||
client: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue