Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions apps/daemon/internal/agent/mcode/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
11 changes: 11 additions & 0 deletions apps/daemon/internal/agent/mcode/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
28 changes: 15 additions & 13 deletions apps/daemon/internal/agent/mcode/subagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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")
}
Expand Down
13 changes: 6 additions & 7 deletions apps/daemon/internal/agent/mcode/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
30 changes: 25 additions & 5 deletions apps/daemon/internal/agent/mcode/view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
Expand Down
Loading