diff --git a/apps/daemon/internal/agent/mcode/execution.go b/apps/daemon/internal/agent/mcode/execution.go index a54bfc742..14282fce4 100644 --- a/apps/daemon/internal/agent/mcode/execution.go +++ b/apps/daemon/internal/agent/mcode/execution.go @@ -19,13 +19,8 @@ func validateExecutionRequest(req proto.PromptRequestPayload) error { if req.ExecutionControls == nil || req.ExecutionControls.OutputFormat != nil || req.ExecutionControls.WebSearch != "disabled" || (req.ExecutionControls.TextVerbosity != "" && req.ExecutionControls.TextVerbosity != "medium") { return fmt.Errorf("mcode: unsupported execution controls") } - if !req.DisableSubagents { - if req.MaxConcurrentSubagents == nil || *req.MaxConcurrentSubagents < 1 { - return fmt.Errorf("mcode: Subagent concurrency limit is required") - } - if _, _, err := subagentReader(); err != nil { - return err - } + if !req.DisableSubagents && (req.MaxConcurrentSubagents == nil || *req.MaxConcurrentSubagents < 1) { + return fmt.Errorf("mcode: Subagent concurrency limit is required") } if mode := optionString(req.AgentOptions, "mode"); mode != "" { return fmt.Errorf("mcode: text execution uses default native permissions") diff --git a/apps/daemon/internal/agent/mcode/options.go b/apps/daemon/internal/agent/mcode/options.go index 4f2975062..5da808d31 100644 --- a/apps/daemon/internal/agent/mcode/options.go +++ b/apps/daemon/internal/agent/mcode/options.go @@ -29,6 +29,12 @@ type launchOptions struct { // the CLI entry when the binary is node rather than the CLI itself. start func(clirunner.StartOptions) (*clirunner.Process, error) script string + // spawn runs the Subagent history reader beside the native process in an + // agent-host view, with reader's node, script and data directory as the + // view presents them. nil runs the installed reader on the host over + // DataDir. + spawn func(clirunner.StartOptions) (*clirunner.Process, error) + reader clirunner.StartOptions // home is an agent-host view's Session home on the host. It contains // DataDir and belongs to the Session user, so the daemon reads DataDir // only within it. @@ -44,6 +50,11 @@ func prepareOptionsWithSkills(ctx context.Context, req proto.PromptRequestPayloa if err := validateOptions(req); err != nil { return result, err } + if req.StrictResume && !req.DisableSubagents { + if _, _, err := subagentReader(); err != nil { + return result, err + } + } root, err := agent.ManagedSkillsRoot("mcode", req.AgentStateKey, req.ConversationID, req.RunID) if err != nil { return result, err diff --git a/apps/daemon/internal/agent/mcode/subagents.go b/apps/daemon/internal/agent/mcode/subagents.go index 65c05a7b2..9905cbf5b 100644 --- a/apps/daemon/internal/agent/mcode/subagents.go +++ b/apps/daemon/internal/agent/mcode/subagents.go @@ -6,10 +6,11 @@ import ( "fmt" "io" "os" - "os/exec" "path/filepath" + "slices" "time" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/clirunner" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) @@ -66,26 +67,27 @@ func subagentReader() (string, string, error) { func (s *Session) readSubagents(ctx context.Context) (nativeSubagentSnapshot, error) { var snapshot nativeSubagentSnapshot - node, reader, err := subagentReader() - if err != nil { - return snapshot, err + reader, start := s.opts.reader, s.opts.spawn + if start == nil { + node, script, err := subagentReader() + if err != nil { + return snapshot, err + } + reader, start = clirunner.StartOptions{Binary: node, Args: []string{script, s.opts.DataDir}, Env: executionEnvironment()}, clirunner.Start } ctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() - command := exec.CommandContext(ctx, node, "--disable-warning=ExperimentalWarning", reader, s.opts.DataDir, s.sessionID) - command.Env = executionEnvironment() - stdout, err := command.StdoutPipe() + reader.Parent, reader.Args = ctx, slices.Concat([]string{"--disable-warning=ExperimentalWarning"}, reader.Args, []string{s.sessionID}) + process, err := start(reader) if err != nil { return snapshot, fmt.Errorf("mcode: child history reader is unavailable") } - if err := command.Start(); err != nil { - return snapshot, fmt.Errorf("mcode: child history reader is unavailable") - } - raw, readErr := io.ReadAll(io.LimitReader(stdout, 64*1024*1024+1)) + go io.Copy(io.Discard, process.Stderr) + raw, readErr := io.ReadAll(io.LimitReader(process.Stdout, 64*1024*1024+1)) if readErr != nil || len(raw) > 64*1024*1024 { - _ = command.Process.Kill() + process.Cancel() } - err = command.Wait() + err = process.Wait() if err != nil || readErr != nil || len(raw) > 64*1024*1024 { return snapshot, fmt.Errorf("mcode: complete child history is unavailable") } diff --git a/apps/daemon/internal/agent/mcode/view.go b/apps/daemon/internal/agent/mcode/view.go index 0651cc89d..8e3c703ed 100644 --- a/apps/daemon/internal/agent/mcode/view.go +++ b/apps/daemon/internal/agent/mcode/view.go @@ -10,13 +10,15 @@ import ( "slices" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" + "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/clirunner" "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/viewloader" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) // In an agent-host view, node runs the CLI from the closure and the native -// data directory lives in the Session home. The workspace worker runs beside -// the CLI in the same view; bash, rg and git run in the sandbox. +// data directory lives in the Session home. The workspace worker and the +// Subagent history reader run beside the CLI in the same view; bash, rg and +// git run in the sandbox. // Closure names and Session home directories. const ( @@ -154,11 +156,6 @@ func (i viewInstall) prepare(_ context.Context, req proto.PromptRequestPayload, if len(local.Skills) != 0 { return launchOptions{}, fmt.Errorf("%w: a MiniMax Code view does not install Capabilities", agent.ErrUnsupportedOperation) } - // Subagent settlement reads native history with a second process while - // the CLI runs, and a Session has one live view, which runs only the CLI. - if !req.DisableSubagents { - return launchOptions{}, fmt.Errorf("%w: a MiniMax Code view does not run Subagents", agent.ErrUnsupportedOperation) - } workspace := local.WorkspaceRoot if !path.IsAbs(workspace) || path.Clean(workspace) != workspace || workspace == "/" { return launchOptions{}, errors.New("mcode: the workspace is not a canonical absolute path") @@ -209,6 +206,8 @@ func (i viewInstall) prepare(_ context.Context, req proto.PromptRequestPayload, opts.Env = append(opts.Env, "LD_LIBRARY_PATH="+i.loader.LibraryPath) } opts.Env = append(opts.Env, nativeEnvironment(private, dataDir)...) + opts.spawn = session.Spawn + opts.reader = clirunner.StartOptions{Binary: i.node, Args: []string{path.Join(path.Dir(i.bridge), "subagent-snapshot.mjs"), dataDir}, Dir: dataDir, Env: opts.Env, OwnProcessGroup: true} profile := map[string]any{"workspace": workspace, "scratch": tempDir, "network": "enabled"} tools := workspaceTools{node: i.node, bridge: i.bridge, profile: path.Join(dataDir, "workspace-profile.json")} return opts, writeWorkspaceTools(&opts, data, req, tools, profile, nil, servers) diff --git a/apps/daemon/internal/agent/mcode/view_test.go b/apps/daemon/internal/agent/mcode/view_test.go index 5b20c1882..e0d0965ca 100644 --- a/apps/daemon/internal/agent/mcode/view_test.go +++ b/apps/daemon/internal/agent/mcode/view_test.go @@ -75,11 +75,6 @@ func TestViewLaunchesNodeWithGatewayOnly(t *testing.T) { var launched clirunner.StartOptions session := viewSession(&launched) session.Home = agent.ViewDir{Host: t.TempDir(), View: path.Join(agent.ViewPrivateRoot, agent.ViewHomeName)} - subagents := req - subagents.DisableSubagents, subagents.MaxConcurrentSubagents = false, new(2) - if _, err := view.Executor(t.Context(), subagents, session); !errors.Is(err, agent.ErrUnsupportedOperation) || launched.Binary != "" { - t.Fatalf("Executor with Subagents = %v, launched %q", err, launched.Binary) - } if _, err := view.Executor(t.Context(), req, session); err == nil || err.Error() != "launch recorded" { t.Fatalf("Executor = %v", err) } @@ -124,6 +119,31 @@ func TestViewLaunchesNodeWithGatewayOnly(t *testing.T) { } } +func TestViewReadsSubagentsBesideTheCLI(t *testing.T) { + install, _, req := viewFixture(t) + req.DisableSubagents, req.MaxConcurrentSubagents = false, new(2) + var launched, spawned clirunner.StartOptions + session := viewSession(&launched) + session.Home = agent.ViewDir{Host: t.TempDir(), View: path.Join(agent.ViewPrivateRoot, agent.ViewHomeName)} + session.Spawn = func(options clirunner.StartOptions) (*clirunner.Process, error) { + spawned = options + return clirunner.Start(clirunner.StartOptions{Parent: options.Parent, Binary: "/bin/echo", Args: []string{`{"version":1,"complete":true,"rootSessionId":"root"}`}}) + } + opts, err := install.prepare(t.Context(), req, session) + if err != nil { + t.Fatal(err) + } + s := &Session{sessionID: "root", opts: opts} + if _, err := s.readSubagents(t.Context()); err != nil { + t.Fatal(err) + } + data := path.Join(session.Home.View, viewDataName) + args := []string{"--disable-warning=ExperimentalWarning", path.Join(path.Dir(install.bridge), "subagent-snapshot.mjs"), data, "root"} + if spawned.Binary != install.node || !slices.Equal(spawned.Args, args) || spawned.Dir != data || !spawned.OwnProcessGroup || !slices.Contains(spawned.Env, "LD_LIBRARY_PATH="+install.loader.LibraryPath) { + t.Fatalf("spawned %+v", spawned) + } +} + func TestViewDoesNotFollowHomeLinks(t *testing.T) { _, view, req := viewFixture(t) outside := filepath.Join(t.TempDir(), "outside")