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:
Snider 2026-03-15 14:35:47 +00:00
parent 5d5646248e
commit 9587ea4d49
2 changed files with 50 additions and 40 deletions

View file

@ -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
}

View file

@ -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},
}
}