From b23d3ab89f74654f31b5ca1ad8337e0c91bf8297 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Mon, 19 Oct 2026 10:26:13 +0000 Subject: [PATCH] Run MiniMax Code reasoning and MCP natively --- .../internal/agent/mcode/environment_mcp.go | 13 +- .../agent/mcode/environment_mcp_test.go | 30 ++-- apps/daemon/internal/agent/mcode/events.go | 2 +- apps/daemon/internal/agent/mcode/execution.go | 14 +- .../internal/agent/mcode/execution_test.go | 5 +- apps/daemon/internal/agent/mcode/executor.go | 3 + .../internal/agent/mcode/mcp_observations.go | 7 +- .../internal/agent/mcode/model_provider.go | 8 +- .../agent/mcode/model_provider_test.go | 29 ++- .../agent/mcode/native_effort_mcp_test.go | 168 ++++++++++++++++++ apps/daemon/internal/agent/mcode/options.go | 23 +-- .../internal/agent/mcode/options_test.go | 7 +- apps/daemon/internal/agent/mcode/session.go | 17 +- .../internal/agent/mcode/session_test.go | 2 +- .../internal/agent/mcode/subagent_messages.go | 4 +- .../agent/mcode/tool_environment_test.go | 6 +- .../internal/agent/mcode/tool_observations.go | 2 +- apps/daemon/internal/agent/mcode/workspace.go | 7 - contracts/agents-api/harness-capabilities.md | 4 +- contracts/agents-api/index.md | 3 +- contracts/agents-api/model-execution.md | 2 +- .../agents-api/zh/harness-capabilities.md | 6 +- contracts/agents-api/zh/index.md | 5 +- contracts/agents-api/zh/model-execution.md | 4 +- example/parsar/README.md | 2 +- example/parsar/shared/session-profile.mjs | 2 - example/parsar/tests/product.test.mjs | 14 -- .../harnessconfig/builtin/admission_test.go | 2 +- internal/harnessconfig/mcode/declaration.go | 9 +- services/core/deploy/mcode/README.md | 10 +- .../internal/execution/mcode_profile_test.go | 3 +- 31 files changed, 306 insertions(+), 107 deletions(-) create mode 100644 apps/daemon/internal/agent/mcode/native_effort_mcp_test.go diff --git a/apps/daemon/internal/agent/mcode/environment_mcp.go b/apps/daemon/internal/agent/mcode/environment_mcp.go index e63a60d60..2a5d00f40 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp.go @@ -15,13 +15,10 @@ func runtimeMCP(req proto.PromptRequestPayload) ([]map[string]any, error) { if err != nil { return nil, err } - var servers []map[string]any + servers := []map[string]any{} for _, binding := range bindings { - if binding.ConnectionOrigin != "environment" || binding.AllowedTools != nil || binding.Required { - return nil, fmt.Errorf("mcode: unsupported MCP binding") - } if binding.Transport == "http" { - server, err := environmentHTTPMCP(binding) + server, err := httpMCP(binding) if err != nil { return nil, err } @@ -37,15 +34,15 @@ func runtimeMCP(req proto.PromptRequestPayload) ([]map[string]any, error) { // ACP session servers remain in native memory; credentials never enter argv or // persisted native configuration. Custom headers are not qualified because the // pinned native HTTP client can forward them across redirect origins. -func environmentHTTPMCP(item agent.MCPBinding) (map[string]any, error) { +func httpMCP(item agent.MCPBinding) (map[string]any, error) { endpoint, err := url.Parse(item.ServerURL) if err != nil || (endpoint.Scheme != "http" && endpoint.Scheme != "https") || endpoint.Hostname() == "" || endpoint.User != nil || endpoint.Fragment != "" || endpoint.Opaque != "" || len(item.HTTPHeaders) != 0 { - return nil, fmt.Errorf("mcode: unsupported environment HTTP MCP declaration") + return nil, fmt.Errorf("mcode: unsupported HTTP MCP declaration") } headers := []map[string]string{} if item.BearerToken != nil { if endpoint.Scheme != "https" || !agent.ValidMCPHTTPBearerToken(*item.BearerToken) { - return nil, fmt.Errorf("mcode: unsupported environment MCP bearer credential") + return nil, fmt.Errorf("mcode: unsupported MCP bearer credential") } headers = append(headers, map[string]string{"name": "Authorization", "value": "Bearer " + *item.BearerToken}) } diff --git a/apps/daemon/internal/agent/mcode/environment_mcp_test.go b/apps/daemon/internal/agent/mcode/environment_mcp_test.go index 76472a72a..81155ff34 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp_test.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp_test.go @@ -92,7 +92,11 @@ func TestEnvironmentMCPRejectsUnqualifiedAuthorityBeforePreparation(t *testing.T case "duplicate": req.LocalEnvironment.MCP = append(req.LocalEnvironment.MCP, environmentMCPFixture()) } - if _, err := prepareWorkspaceOptions(testStateRoot(t), c, req); err == nil || strings.Contains(err.Error(), "confidential-http-token") { + _, err := prepareWorkspaceOptions(testStateRoot(t), c, req) + if err == nil { + _, err = runtimeMCP(req) + } + if err == nil || strings.Contains(err.Error(), "confidential-http-token") { t.Fatal("unqualified declaration accepted or credential exposed") } }) @@ -232,7 +236,11 @@ func TestPublicEnvironmentHTTPMCPKeepsCredentialTransient(t *testing.T) { if err != nil { t.Fatal(err) } - raw, err := json.Marshal(opts.MCP) + servers, err := runtimeMCP(req) + if err != nil { + t.Fatal(err) + } + raw, err := json.Marshal(servers) if err != nil || !strings.Contains(string(raw), "Bearer "+token) { t.Fatal("selected token not supplied to native ACP") } @@ -255,14 +263,14 @@ func TestPublicEnvironmentHTTPMCPKeepsCredentialTransient(t *testing.T) { if err != nil { t.Fatal(err) } - empty := []string{} - (*req.MCPHTTPServers)[0].AllowedTools = &empty - if _, err := prepareWorkspaceOptions(testStateRoot(t), c, req); err == nil { - t.Fatal("empty allowlist silently treated as all") - } - (*req.MCPHTTPServers)[0].AllowedTools = nil - (*req.MCPHTTPServers)[0].Required = true - if _, err := prepareWorkspaceOptions(testStateRoot(t), c, req); err == nil { - t.Fatal("required initialization silently ignored") +} + +func TestServiceOriginMCPOnNonePlacement(t *testing.T) { + req := testRequest(t) + req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://mcp.example/mcp"}} + servers, err := runtimeMCP(req) + want := []map[string]any{{"name": "fixture", "type": "http", "url": "https://mcp.example/mcp", "headers": []map[string]string{}}} + if err != nil || !reflect.DeepEqual(servers, want) { + t.Fatalf("MCP = %v, %v", servers, err) } } diff --git a/apps/daemon/internal/agent/mcode/events.go b/apps/daemon/internal/agent/mcode/events.go index 1ee505dca..edc9d2c28 100644 --- a/apps/daemon/internal/agent/mcode/events.go +++ b/apps/daemon/internal/agent/mcode/events.go @@ -82,7 +82,7 @@ func (s *Session) emitTool(update toolUpdate) error { } if update.mcp == nil { var err error - update.mcp, err = s.environmentMCPIdentity(update.Name) + update.mcp, err = s.mcpIdentity(update.Name) if err != nil { return err } diff --git a/apps/daemon/internal/agent/mcode/execution.go b/apps/daemon/internal/agent/mcode/execution.go index d9f510088..729d0a3d8 100644 --- a/apps/daemon/internal/agent/mcode/execution.go +++ b/apps/daemon/internal/agent/mcode/execution.go @@ -3,13 +3,14 @@ package mcode import ( "fmt" "os" + "strings" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" declaration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/mcode" ) func validateExecutionRequest(req proto.PromptRequestPayload) error { - if !req.DisableExecutionEnvironment || req.AgentStateKey == "" || req.LocalEnvironment != nil || req.RequireExistingNativeSession || len(req.FunctionTools) != 0 || (req.MCPHTTPServers != nil && len(*req.MCPHTTPServers) != 0) { + if !req.DisableExecutionEnvironment || req.AgentStateKey == "" || req.LocalEnvironment != nil || req.RequireExistingNativeSession { return fmt.Errorf("mcode: unsupported execution configuration") } if req.ExecutionControls == nil || req.ExecutionControls.OutputFormat != nil { @@ -32,8 +33,13 @@ func validateExecutionRequest(req proto.PromptRequestPayload) error { // Harness children use the daemon user's ordinary environment. func executionEnvironment() []string { return os.Environ() } -// ACP commands are only recognized for a single text block. A second, empty -// block keeps public input as user text. +// ACP joins text blocks with "\n\n" and parses slash commands only from a +// single block. A second, empty block keeps input that starts with "/" as user +// text; any other input is sent as one block so the model sees it unchanged. func promptContent(text string) []map[string]string { - return []map[string]string{{"type": "text", "text": text}, {"type": "text", "text": ""}} + content := []map[string]string{{"type": "text", "text": text}} + if strings.HasPrefix(text, "/") { + content = append(content, map[string]string{"type": "text", "text": ""}) + } + return content } diff --git a/apps/daemon/internal/agent/mcode/execution_test.go b/apps/daemon/internal/agent/mcode/execution_test.go index 7ee412b86..df1b2841b 100644 --- a/apps/daemon/internal/agent/mcode/execution_test.go +++ b/apps/daemon/internal/agent/mcode/execution_test.go @@ -43,7 +43,6 @@ func TestExecutionRejectsUnqualifiedAuthority(t *testing.T) { func(r *proto.PromptRequestPayload) { r.DisableExecutionEnvironment = false }, func(r *proto.PromptRequestPayload) { r.DisableSubagents = false }, func(r *proto.PromptRequestPayload) { r.RequireExistingNativeSession = true }, - func(r *proto.PromptRequestPayload) { r.FunctionTools = []proto.FunctionTool{{Name: "f"}} }, func(r *proto.PromptRequestPayload) { r.ExecutionControls.WebSearch = &proto.WebSearchControl{Mode: "live", ContextSize: "medium"} }, @@ -57,9 +56,9 @@ func TestExecutionRejectsUnqualifiedAuthority(t *testing.T) { } func TestPublicTextDoesNotInvokeACPCommands(t *testing.T) { - for _, text := range []string{"/model", "/compact", "hello"} { + for text, want := range map[string]int{"/model": 2, "/compact": 2, "hello": 1} { blocks := promptContent(text) - if len(blocks) != 2 || blocks[0]["text"] != text || blocks[1]["text"] != "" { + if len(blocks) != want || blocks[0]["text"] != text || (want == 2 && blocks[1]["text"] != "") { t.Fatal(blocks) } } diff --git a/apps/daemon/internal/agent/mcode/executor.go b/apps/daemon/internal/agent/mcode/executor.go index ddd00c201..fc0bb177b 100644 --- a/apps/daemon/internal/agent/mcode/executor.go +++ b/apps/daemon/internal/agent/mcode/executor.go @@ -50,6 +50,9 @@ func NewExecutorFactory(runtimeRoot string, config *WorkspaceConfig) agent.Execu binary = frozen.Binary opts, err = prepareWorkspaceOptions(runtimeRoot, *frozen, req) } + if err == nil { + opts.MCP, err = runtimeMCP(req) + } if err != nil { return nil, err } diff --git a/apps/daemon/internal/agent/mcode/mcp_observations.go b/apps/daemon/internal/agent/mcode/mcp_observations.go index fa719802b..00e016db1 100644 --- a/apps/daemon/internal/agent/mcode/mcp_observations.go +++ b/apps/daemon/internal/agent/mcode/mcp_observations.go @@ -16,9 +16,8 @@ type mcpToolIdentity struct{ server, tool string } // MiniMax 0.4.12 atomically writes these assignments before exposing tools. // Read its exact mapping instead of reversing lossy native name normalization. -func (s *Session) environmentMCPIdentity(name string) (*mcpToolIdentity, error) { - if s.req.LocalEnvironment == nil || - !strings.HasPrefix(name, "mcp__") { +func (s *Session) mcpIdentity(name string) (*mcpToolIdentity, error) { + if !strings.HasPrefix(name, "mcp__") { return nil, nil } bindings, err := agent.ResolveMCPBindings(s.req) @@ -68,7 +67,7 @@ func (s *Session) environmentMCPIdentity(name string) (*mcpToolIdentity, error) return found, nil } -func environmentMCPObservation(update toolUpdate, stage string) (*proto.ToolObservation, error) { +func mcpObservation(update toolUpdate, stage string) (*proto.ToolObservation, error) { if update.mcp == nil { return nil, nil } diff --git a/apps/daemon/internal/agent/mcode/model_provider.go b/apps/daemon/internal/agent/mcode/model_provider.go index 3e85d701d..b8546aeb6 100644 --- a/apps/daemon/internal/agent/mcode/model_provider.go +++ b/apps/daemon/internal/agent/mcode/model_provider.go @@ -7,7 +7,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/modelprovider" ) -func modelProviderConfig(raw any, model string) (map[string]any, error) { +func modelProviderConfig(raw any, model, effort string) (map[string]any, error) { provider, err := harnessconfiguration.Declaration.Configuration.ParseProvider(raw) if err != nil { return nil, fmt.Errorf("mcode: %w", err) @@ -21,7 +21,7 @@ func modelProviderConfig(raw any, model string) (map[string]any, error) { case modelprovider.Responses: api = "openai-responses" } - native := map[string]any{"name": model, "tool_call": true} + native := map[string]any{"name": model} // Absent limits are omitted so the native defaults apply. limit := map[string]any{} if provider.ContextWindow > 0 { @@ -33,6 +33,10 @@ func modelProviderConfig(raw any, model string) (map[string]any, error) { if len(limit) > 0 { native["limit"] = limit } + if effort != "" { + // The Session selects this effort through ACP thinkingEffort. + native["thinking"] = map[string]any{"effortOptions": []string{effort}} + } return map[string]any{ "name": "Configured provider", "kind": "custom", "enabled": true, "api": api, "options": map[string]any{"baseURL": provider.BaseURL, "apiKey": provider.APIKey}, diff --git a/apps/daemon/internal/agent/mcode/model_provider_test.go b/apps/daemon/internal/agent/mcode/model_provider_test.go index 8da5b22c9..f3e40b717 100644 --- a/apps/daemon/internal/agent/mcode/model_provider_test.go +++ b/apps/daemon/internal/agent/mcode/model_provider_test.go @@ -32,7 +32,7 @@ func TestOptionsModelProviderProtocols(t *testing.T) { "name": "Configured provider", "kind": "custom", "enabled": true, "api": tc.api, "options": map[string]any{"baseURL": "https://provider.example/v1", "apiKey": "fixture-key"}, "models": map[string]any{"chosen-model": map[string]any{ - "name": "chosen-model", "tool_call": true, + "name": "chosen-model", "limit": map[string]any{"context": float64(64000), "output": float64(4096)}, }}, }} @@ -118,3 +118,30 @@ func TestOptionsAllowLoopbackProviderFixture(t *testing.T) { t.Fatal(err) } } + +func TestOptionsWriteNativeThinkingEffort(t *testing.T) { + req := testRequest(t) + req.ExecutionControls.ReasoningEffort = "high" + opts, err := prepareOptions(testStateRoot(t), req, nil) + if err != nil { + t.Fatal(err) + } + body, err := os.ReadFile(filepath.Join(opts.DataDir, "config.yaml")) + if err != nil { + t.Fatal(err) + } + var config struct { + CustomProvider map[string]struct { + Models map[string]struct { + Thinking map[string]any `json:"thinking"` + } `json:"models"` + } `json:"custom_provider"` + } + if err := json.Unmarshal(body, &config); err != nil { + t.Fatal(err) + } + want := map[string]any{"effortOptions": []any{"high"}} + if got := config.CustomProvider["oac"].Models["fixture"].Thinking; !reflect.DeepEqual(got, want) { + t.Fatalf("thinking = %v", got) + } +} diff --git a/apps/daemon/internal/agent/mcode/native_effort_mcp_test.go b/apps/daemon/internal/agent/mcode/native_effort_mcp_test.go new file mode 100644 index 000000000..60cfa4c94 --- /dev/null +++ b/apps/daemon/internal/agent/mcode/native_effort_mcp_test.go @@ -0,0 +1,168 @@ +package mcode + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" +) + +// This opt-in test makes real model calls. It sends the native model traffic +// through a local recording proxy and a text-placement HTTP MCP fixture, then +// checks the effort and the final user text at the model and the MCP call. +func TestNativeMCodeEffortMCPAndPlainText(t *testing.T) { + binary, options := os.Getenv("OAC_RUNTIME_MCODE_BIN"), os.Getenv("OAC_TEST_MCODE_REAL_OPTIONS") + if binary == "" || options == "" { + t.Skip("native executable and private provider options required") + } + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute) + defer cancel() + raw, err := os.ReadFile(options) + if err != nil { + t.Fatal("private provider options unavailable") + } + req := testRequest(t) + req.RunID, req.Input = "", nil + if json.Unmarshal(raw, &req.AgentOptions) != nil { + t.Fatal("invalid private provider options") + } + provider := req.AgentOptions["model_provider"].(map[string]any) + upstream, err := url.Parse(provider["base_url"].(string)) + if err != nil { + t.Fatal("invalid provider base URL") + } + var mu sync.Mutex + var bodies [][]byte + proxy := httptest.NewServer(&httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(upstream) + if body, err := io.ReadAll(r.In.Body); err == nil { + mu.Lock() + bodies = append(bodies, body) + mu.Unlock() + r.Out.Body = io.NopCloser(bytes.NewReader(body)) + } + }, + }) + defer proxy.Close() + provider["base_url"] = proxy.URL + + var called sync.Map + mcp := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + var rpc struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params struct { + Name string `json:"name"` + Arguments map[string]any `json:"arguments"` + } `json:"params"` + } + _ = json.NewDecoder(r.Body).Decode(&rpc) + if len(rpc.ID) == 0 { + w.WriteHeader(http.StatusAccepted) + return + } + var result any + switch rpc.Method { + case "initialize": + result = map[string]any{"protocolVersion": "2025-03-26", "capabilities": map[string]any{"tools": map[string]any{}}, "serverInfo": map[string]any{"name": "fixture", "version": "1"}} + case "tools/list": + result = map[string]any{"tools": []any{map[string]any{"name": "lookup_code", "description": "Returns the secret access code. Always call it when asked for the access code.", "inputSchema": map[string]any{"type": "object", "properties": map[string]any{}}}}} + case "tools/call": + called.Store(rpc.Params.Name, true) + result = map[string]any{"content": []any{map[string]any{"type": "text", "text": "ACCESS-CODE-4417"}}} + default: + result = map[string]any{} + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": rpc.ID, "result": result}) + })) + defer mcp.Close() + + req.ExecutionControls.ReasoningEffort = "high" + req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: mcp.URL + "/mcp"}} + value, err := NewExecutorFactory(testStateRoot(t), nil)(ctx, req) + if err != nil { + t.Fatal("native Executor preparation failed") + } + e := value.(*executor) + defer func() { + cleanup, stop := context.WithTimeout(context.Background(), 15*time.Second) + defer stop() + _ = e.Close(cleanup) + }() + out := make(chan proto.Envelope, 256) + turn, err := e.StartTurn(ctx, "turn", proto.TextInput("Use the MCP tool to get the access code, then reply with only the code."), out) + if err != nil { + t.Fatal("native Turn admission failed") + } + var content string + observed := false + for event := range out { + switch event.Type { + case proto.TypeError: + t.Fatal("native Turn reported an error") + case proto.TypeToolCall: + var call proto.ToolCallPayload + if json.Unmarshal(event.Payload, &call) == nil && call.Observation != nil && call.Observation.Kind == "mcp" && call.Observation.Server == "fixture" && call.Observation.Name == "lookup_code" { + observed = true + } + case proto.TypeDone: + var done proto.DonePayload + _ = json.Unmarshal(event.Payload, &done) + content = done.Content + } + } + if _, err := turn.AwaitSettlement(ctx); err != nil { + t.Fatal("native Turn did not settle") + } + if _, ok := called.Load("lookup_code"); !ok || !observed || !strings.Contains(content, "ACCESS-CODE-4417") { + t.Fatalf("MCP tool was not listed, called and observed: called=%v observed=%v", ok, observed) + } + mu.Lock() + defer mu.Unlock() + effort, plain := false, false + for _, body := range bodies { + var request struct { + OutputConfig map[string]any `json:"output_config"` + Messages []struct { + Role string `json:"role"` + Content any `json:"content"` + } `json:"messages"` + } + if json.Unmarshal(body, &request) != nil { + continue + } + effort = effort || request.OutputConfig["effort"] == "high" + if len(request.Messages) > 0 && request.Messages[0].Role == "user" { + if blocks, ok := request.Messages[0].Content.([]any); ok { + for _, block := range blocks { + if text, _ := block.(map[string]any)["text"].(string); strings.HasSuffix(text, "reply with only the code.") { + plain = true + } + } + } else if text, _ := request.Messages[0].Content.(string); strings.HasSuffix(text, "reply with only the code.") { + plain = true + } + } + } + t.Logf("model requests=%d effort high on wire=%v user text ends without newlines=%v", len(bodies), effort, plain) + if !effort || !plain { + t.Fatal("model request did not carry the effort and the unmodified user text") + } +} diff --git a/apps/daemon/internal/agent/mcode/options.go b/apps/daemon/internal/agent/mcode/options.go index b210a4d77..fd1480df4 100644 --- a/apps/daemon/internal/agent/mcode/options.go +++ b/apps/daemon/internal/agent/mcode/options.go @@ -56,12 +56,16 @@ func prepareOptions(runtimeRoot string, req proto.PromptRequestPayload, workspac if err := os.WriteFile(filepath.Join(result.DataDir, "AGENTS.md"), []byte(prompt), 0o600); err != nil { return result, err } - config := map[string]any{"logLevel": "error"} + config := map[string]any{} + effort := "" + if req.ExecutionControls != nil { + effort = req.ExecutionControls.ReasoningEffort + } result.Model = optionString(opts, "model") if result.Model == "" { return result, fmt.Errorf("mcode: model is required") } - provider, err := modelProviderConfig(opts["model_provider"], result.Model) + provider, err := modelProviderConfig(opts["model_provider"], result.Model, effort) if err != nil { return result, err } @@ -71,10 +75,8 @@ func prepareOptions(runtimeRoot string, req proto.PromptRequestPayload, workspac // Thread goals schedule continuation Turns after the Turn ends. config["askUser"] = map[string]bool{"enabled": false} config["beta"] = map[string]bool{"mcodeTools": false, "threadGoal": false} - if workspace != nil { - // Unattended: asks are allowed; the few that bypass cannot lift are answered cancelled and fail the tool call. - config["permissionMode"] = "bypassPermissions" - } + // Unattended: tools, including MCP tools on environment none, run without approval prompts. + config["permissionMode"] = "bypassPermissions" data, err := json.Marshal(config) if err != nil { return result, err @@ -98,7 +100,6 @@ func prepareOptions(runtimeRoot string, req proto.PromptRequestPayload, workspac } // The adapter owns the native state location, including after cold resume. result.Env = append(result.Env, "MINIMAX_DATA_DIR="+result.DataDir, "HOME="+result.DataDir, "USERPROFILE="+result.DataDir) - result.MCP = []map[string]any{} return result, nil } @@ -115,20 +116,14 @@ func agentCapabilities(req proto.PromptRequestPayload, workspace *workspaceProfi } capabilities := map[string]any{ "tools": tools, - // Builtin Matrix MCP tools call MiniMax's cloud with an account credential that Core does not hold. - "builtinTools": []string{}, "features": map[string]bool{ - // Mavis adds cron scheduling and memory that run beyond a Turn, and needs a MiniMax account. + // The mavis tool carries cron scheduling that runs beyond a Turn, plus session send, agent create and MCP create. "mavis": false, "delegation": !req.DisableSubagents, // Native web search calls MiniMax's cloud with a MiniMax account credential that Core does not hold. "webSearch": false, }, } - if workspace == nil { - // The none placement reads no Skills; a workspace keeps the native selection with the linked Skills. - capabilities["skills"] = []string{} - } return capabilities } diff --git a/apps/daemon/internal/agent/mcode/options_test.go b/apps/daemon/internal/agent/mcode/options_test.go index 3d7c78c82..989475c9b 100644 --- a/apps/daemon/internal/agent/mcode/options_test.go +++ b/apps/daemon/internal/agent/mcode/options_test.go @@ -50,15 +50,12 @@ func TestOptionsRefreshManagedState(t *testing.T) { if json.Unmarshal(data, &cfg) != nil { t.Fatal("invalid config") } - if _, set := cfg["permissionMode"]; set { - t.Fatal("text profile restated the native permission mode") + if cfg["permissionMode"] != "bypassPermissions" { + t.Fatal("MCP tools on environment none can wait for approval") } if _, set := cfg["skills"]; set { t.Fatal("native Skill discovery was overridden") } - if skills, ok := cfg["agents"].(map[string]any)["default"].(map[string]any)["skills"].([]any); !ok || len(skills) != 0 { - t.Fatal("text profile selected native Skills") - } info, _ := os.Stat(filepath.Join(opts.DataDir, "config.yaml")) if info.Mode().Perm() != 0600 { t.Fatal("native credentials file is not private") diff --git a/apps/daemon/internal/agent/mcode/session.go b/apps/daemon/internal/agent/mcode/session.go index e655d1199..4cd5468d1 100644 --- a/apps/daemon/internal/agent/mcode/session.go +++ b/apps/daemon/internal/agent/mcode/session.go @@ -109,10 +109,15 @@ func (s *Session) prepareNative() error { s.mu.Lock() s.sessionID = session.SessionID s.mu.Unlock() - model, err := advertisedModel(session.ConfigOptions, s.opts.Model) + // A model with effort options is advertised only with its "thinking" variant. + variant := "" + if s.req.ExecutionControls != nil && s.req.ExecutionControls.ReasoningEffort != "" { + variant = "thinking" + } + model, err := advertisedModel(session.ConfigOptions, s.opts.Model, variant) if err != nil && s.req.AgentSessionID != "" && !slices.ContainsFunc(session.ConfigOptions, func(option configOption) bool { return option.ID == "model" }) { // Native load omits the selector when its persisted model was removed; selection still validates against the current catalog. - model = "m:custom_provider%3Aoac:" + strings.ReplaceAll(url.QueryEscape(s.opts.Model), "+", "%20") + ":v:" + model = "m:custom_provider%3Aoac:" + strings.ReplaceAll(url.QueryEscape(s.opts.Model), "+", "%20") + ":v:" + variant err = nil } if err != nil { @@ -122,6 +127,10 @@ func (s *Session) prepareNative() error { if err := s.call("session/set_config_option", map[string]any{"sessionId": session.SessionID, "configId": "model", "value": model}, nil, false); err != nil { return err } + if variant != "" { + // A selected variant takes no model default, so the Session's effort is set explicitly. + return s.call("session/set_config_option", map[string]any{"sessionId": session.SessionID, "configId": "thinkingEffort", "value": s.req.ExecutionControls.ReasoningEffort}, nil, false) + } return nil } @@ -151,7 +160,7 @@ func (s *Session) executePrompt(prompt string) error { } // Select the advertised custom model, rather than using mcode's native default. -func advertisedModel(options []configOption, model string) (string, error) { +func advertisedModel(options []configOption, model, variant string) (string, error) { for _, option := range options { if option.ID != "model" { continue @@ -169,7 +178,7 @@ func advertisedModel(options []configOption, model string) (string, error) { if err != nil { continue } - if provider == "custom_provider:oac" && id == model && (parts[3] == "u" || (len(parts) == 5 && parts[3] == "v" && parts[4] == "")) { + if provider == "custom_provider:oac" && id == model && (parts[3] == "u" || (len(parts) == 5 && parts[3] == "v" && parts[4] == variant)) { return candidate.Value, nil } } diff --git a/apps/daemon/internal/agent/mcode/session_test.go b/apps/daemon/internal/agent/mcode/session_test.go index 22f39b9be..4abb9ae5c 100644 --- a/apps/daemon/internal/agent/mcode/session_test.go +++ b/apps/daemon/internal/agent/mcode/session_test.go @@ -296,7 +296,7 @@ func TestMCodeProcess(t *testing.T) { Prompt []map[string]string `json:"prompt"` } _ = json.Unmarshal(frame.Params, &input) - if len(input.Prompt) != 2 { + if len(input.Prompt) != 1 { os.Exit(9) } if strings.HasPrefix(scenario, "executor") { diff --git a/apps/daemon/internal/agent/mcode/subagent_messages.go b/apps/daemon/internal/agent/mcode/subagent_messages.go index 4b20c429a..d906e5bf7 100644 --- a/apps/daemon/internal/agent/mcode/subagent_messages.go +++ b/apps/daemon/internal/agent/mcode/subagent_messages.go @@ -75,11 +75,11 @@ func (s *Session) projectSubagentMessages(session nativeSubagentSession, turn na observation := s.nativeToolObservation(update, "after") if observation == nil { var err error - update.mcp, err = s.environmentMCPIdentity(update.Name) + update.mcp, err = s.mcpIdentity(update.Name) if err != nil { return err } - observation, err = environmentMCPObservation(update, "after") + observation, err = mcpObservation(update, "after") if err != nil { return err } diff --git a/apps/daemon/internal/agent/mcode/tool_environment_test.go b/apps/daemon/internal/agent/mcode/tool_environment_test.go index a83a201ea..63c34f400 100644 --- a/apps/daemon/internal/agent/mcode/tool_environment_test.go +++ b/apps/daemon/internal/agent/mcode/tool_environment_test.go @@ -69,7 +69,11 @@ func TestWorkspaceCredentialsRemainInRuntimeSnapshotAcrossReconnect(t *testing.T } } // Assert the actual env-selected HTTP credential, not a manually injected token. - headers, ok := opts.MCP[0]["headers"].([]map[string]string) + servers, err := runtimeMCP(prepared) + if err != nil { + t.Fatal(err) + } + headers, ok := servers[0]["headers"].([]map[string]string) if !ok || len(headers) != 1 || headers[0]["name"] != "Authorization" || headers[0]["value"] != "Bearer fixture-bearer-canary" { t.Fatal("frozen MCP credential was lost or replaced") } diff --git a/apps/daemon/internal/agent/mcode/tool_observations.go b/apps/daemon/internal/agent/mcode/tool_observations.go index 252657b27..ffa9c6ee8 100644 --- a/apps/daemon/internal/agent/mcode/tool_observations.go +++ b/apps/daemon/internal/agent/mcode/tool_observations.go @@ -16,7 +16,7 @@ func (s *Session) emitToolStage(update toolUpdate, stage string) error { payload.Observation = s.nativeToolObservation(update, stage) if payload.Observation == nil { var err error - payload.Observation, err = environmentMCPObservation(update, stage) + payload.Observation, err = mcpObservation(update, stage) if err != nil { return err } diff --git a/apps/daemon/internal/agent/mcode/workspace.go b/apps/daemon/internal/agent/mcode/workspace.go index eedf49dc7..334d6c782 100644 --- a/apps/daemon/internal/agent/mcode/workspace.go +++ b/apps/daemon/internal/agent/mcode/workspace.go @@ -44,10 +44,6 @@ func prepareWorkspaceOptions(runtimeRoot string, c WorkspaceConfig, req proto.Pr if req.LocalEnvironment == nil || req.LocalEnvironment.WorkspaceRoot != c.Directory || req.DisableExecutionEnvironment || req.WorkspaceReadOnly { return launchOptions{}, fmt.Errorf("mcode: execution does not match the dedicated workspace") } - servers, err := runtimeMCP(req) - if err != nil { - return launchOptions{}, err - } values, err := localworkspace.ReadOptionalToolEnvironment() if err != nil { return launchOptions{}, err @@ -62,8 +58,6 @@ func prepareWorkspaceOptions(runtimeRoot string, c WorkspaceConfig, req proto.Pr // The native process runs in the declared workspace. private := req private.LocalEnvironment, private.DisableExecutionEnvironment = nil, true - // Public declarations have already been resolved into the transient ACP map. - private.MCPHTTPServers = nil opts, err := prepareOptions(runtimeRoot, private, profile) if err != nil { return opts, err @@ -88,6 +82,5 @@ func prepareWorkspaceOptions(runtimeRoot string, c WorkspaceConfig, req proto.Pr } } } - opts.MCP = servers return opts, nil } diff --git a/contracts/agents-api/harness-capabilities.md b/contracts/agents-api/harness-capabilities.md index e8c3fb349..c845fee27 100644 --- a/contracts/agents-api/harness-capabilities.md +++ b/contracts/agents-api/harness-capabilities.md @@ -22,7 +22,7 @@ Placements are `none` (no Environment), hosted (`openai_hosted`) and self-hosted | [Files and Artifacts](./environment-files.md) | Verified: hosted, self-hosted | Verified: hosted, self-hosted | Verified: hosted, self-hosted | | [Whitespace-only message text](./message-content.md) | Admitted; delivered unchanged | Rejected | Rejected | | [Message images](./message-content.md#images) | PNG and JPEG data URLs: verified on all placements; other data URLs: admitted; remote URLs: rejected | PNG and JPEG data URLs: verified on all placements; remote URLs and GIF and WebP data URLs: admitted | Rejected | -| `reasoning.effort` | `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`: admitted | `low`, `medium`, `high`, `xhigh`, `max`: admitted | Rejected | +| `reasoning.effort` | `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`: admitted | `low`, `medium`, `high`, `xhigh`, `max`: admitted | `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`: admitted | | `reasoning.summary` | `concise`, `detailed`, `auto`: admitted | Rejected | Rejected | | `service_tier` other than `auto` | `default`, `flex`, `priority`: admitted | Rejected | Rejected | | `text.verbosity` other than `medium` | Admitted; the native model decides | Rejected | Rejected | @@ -40,7 +40,7 @@ Native model parameters and provider protocols per Harness are in [model executi | [Deferred function discovery](./execution-tools.md#deferred-function-discovery) | Rejected | Verified: `none`, self-hosted; admitted: hosted | Rejected | | [Disabled web search and programmatic tool calling](./execution-tools.md#web-search-and-programmatic-tool-calling) | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted | | Web search `cached` or `live` and programmatic tool calling `enabled` | Admitted on all placements | `live` admitted on hosted and self-hosted, rejected on `none`; `cached` and `enabled` rejected | Rejected | -| [Service-origin HTTP MCP](./environments.md#public-mcp-connection-origin) | Verified: `none`; rejected elsewhere | Verified: `none`; rejected elsewhere | Rejected | +| [Service-origin HTTP MCP](./environments.md#public-mcp-connection-origin) | Verified: `none`; rejected elsewhere | Verified: `none`; rejected elsewhere | Verified: `none`, with `allowed_tools` null and `required` false only; rejected elsewhere | | [Environment-origin HTTP MCP](./environments.md#public-mcp-connection-origin) | Verified: hosted, self-hosted | Verified: hosted, self-hosted | Verified: hosted, self-hosted; `allowed_tools` null and `required` false only | | [HTTP MCP bearer credentials](./execution-tools.md#http-mcp) | Verified | Verified | Verified | | Required MCP initialization | Verified | Verified | Rejected | diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index b3c611c91..83785f020 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -108,7 +108,7 @@ Each item is Core's deliberate or native behavior where the official service beh **Configuration and tools** - A `reasoning.effort`, `reasoning.summary`, `service_tier`, `web_search` mode or setting or `programmatic_tool_calling` value that the selected Harness does not declare is saved but rejected at Session admission ([Harness capabilities](./harness-capabilities.md#execution-and-input)). -- Harness support for tools, structured output, deferred discovery, subagents and MCP differs by Harness and placement; see the [Harness capabilities](./harness-capabilities.md). MiniMax Code has no public functions, no service-origin MCP and no image input. +- Harness support for tools, structured output, deferred discovery, subagents and MCP differs by Harness and placement; see the [Harness capabilities](./harness-capabilities.md). MiniMax Code has no public functions and no image input. - Model-derived reasoning defaults are not resolved. - MCP tools support the `http` transport only; `stdio` is rejected, and so is an inline `authorization` on a Session MCP transport ([HTTP MCP](./execution-tools.md#http-mcp)). @@ -119,6 +119,7 @@ Each item is Core's deliberate or native behavior where the official service beh - A function result that cancellation prevents from being applied never appears as an Item. - Pinned Codex can lose command output emitted before its stream subscription. - Claude Code and MiniMax Code report no public usage. +- MiniMax Code receives a message that starts with `/` with a trailing blank line, because ACP otherwise runs it as an operator command. - Core gives no crash-safe or exactly-once guarantee for native side effects; claimed work fails on restart without replay. - `file_id` and `detail` on images are rejected; each Harness declares which `image_url` values it accepts ([message content](./message-content.md#images)). diff --git a/contracts/agents-api/model-execution.md b/contracts/agents-api/model-execution.md index 8679d9a21..8e31c33b3 100644 --- a/contracts/agents-api/model-execution.md +++ b/contracts/agents-api/model-execution.md @@ -109,7 +109,7 @@ At dispatch, Core sends the snapshot as one confidential provider bundle over th | --- | --- | --- | | Codex | Empty object only | Reasoning effort belongs to the public `reasoning.effort` | | Claude Code | `thinking`: the SDK's `adaptive`, `enabled` or `disabled` object | SDK `Options.thinking`; the public `reasoning.effort` becomes `Options.effort` | -| MiniMax Code | Empty object only | No native model parameters | +| MiniMax Code | Empty object only | The public `reasoning.effort` becomes the custom model's thinking effort | Claude `thinking` accepts `display` (`summarized` or `omitted`) for `adaptive` and `enabled`, and a positive integer `budgetTokens` only for `enabled`; `maxThinkingTokens` is rejected. These are native settings, not a shared reasoning vocabulary; model availability and provider support are the Harness's responsibility. diff --git a/contracts/agents-api/zh/harness-capabilities.md b/contracts/agents-api/zh/harness-capabilities.md index 0893c2628..be038dd94 100644 --- a/contracts/agents-api/zh/harness-capabilities.md +++ b/contracts/agents-api/zh/harness-capabilities.md @@ -1,7 +1,7 @@ --- title: "Harness 能力" source: contracts/agents-api/harness-capabilities.md -source_hash: 4cea6f45872bd29a38f800f58801a4d271142612fb853a023952323895d6eed3 +source_hash: c51422803ad90e85d6d11b6698a2a70ba606f663ed55be5591faf5fcc701487e --- 本页列出每个 Harness 在每种部署位置支持的能力。Core 根据 `internal/harnessconfig//declaration.go` 中 Harness 的声明决定准入,运行 Session 的 Runtime 也必须声明操作。所链接契约定义各操作;[Harness 接入](harness-onboarding.md#qualify-the-adapter)说明资格验证方法。 @@ -24,7 +24,7 @@ source_hash: 4cea6f45872bd29a38f800f58801a4d271142612fb853a023952323895d6eed3 | [文件与 Artifact](environment-files.md) | 已验证:托管、自托管 | 已验证:托管、自托管 | 已验证:托管、自托管 | | [仅空白消息文本](message-content.md) | 已准入;原样交付 | 已拒绝 | 已拒绝 | | [消息图像](message-content.md#images) | PNG、JPEG data URL:所有位置已验证;其他 data URL:已准入;远程 URL:已拒绝 | PNG、JPEG data URL:所有位置已验证;远程 URL 及 GIF、WebP data URL:已准入 | 已拒绝 | -| `reasoning.effort` | `none`、`minimal`、`low`、`medium`、`high`、`xhigh`、`max`:已准入 | `low`、`medium`、`high`、`xhigh`、`max`:已准入 | 已拒绝 | +| `reasoning.effort` | `none`、`minimal`、`low`、`medium`、`high`、`xhigh`、`max`:已准入 | `low`、`medium`、`high`、`xhigh`、`max`:已准入 | `none`、`minimal`、`low`、`medium`、`high`、`xhigh`、`max`:已准入 | | `reasoning.summary` | `concise`、`detailed`、`auto`:已准入 | 已拒绝 | 已拒绝 | | 非 `auto` 的 `service_tier` | `default`、`flex`、`priority`:已准入 | 已拒绝 | 已拒绝 | | 非 `medium` 的 `text.verbosity` | 已准入;由原生模型决定 | 已拒绝 | 已拒绝 | @@ -42,7 +42,7 @@ source_hash: 4cea6f45872bd29a38f800f58801a4d271142612fb853a023952323895d6eed3 | [延迟函数发现](execution-tools.md#deferred-function-discovery) | 已拒绝 | 已验证:`none`、自托管;已准入:托管 | 已拒绝 | | [禁用 Web 搜索与程序化工具调用](execution-tools.md#web-search-and-programmatic-tool-calling) | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 | | Web 搜索 `cached` 或 `live`,以及程序化工具调用 `enabled` | 所有位置已准入 | `live` 在托管和自托管已准入,在 `none` 已拒绝;`cached` 和 `enabled` 已拒绝 | 已拒绝 | -| [服务端来源 HTTP MCP](environments.md#public-mcp-connection-origin) | 已验证:`none`;其他位置拒绝 | 已验证:`none`;其他位置拒绝 | 已拒绝 | +| [服务端来源 HTTP MCP](environments.md#public-mcp-connection-origin) | 已验证:`none`;其他位置拒绝 | 已验证:`none`;其他位置拒绝 | 已验证:`none`,仅 `allowed_tools` null、`required` false;其他位置拒绝 | | [Environment 来源 HTTP MCP](environments.md#public-mcp-connection-origin) | 已验证:托管、自托管 | 已验证:托管、自托管 | 已验证:托管、自托管;仅 `allowed_tools` null、`required` false | | [HTTP MCP bearer 凭据](execution-tools.md#http-mcp) | 已验证 | 已验证 | 已验证 | | 必需 MCP 初始化 | 已验证 | 已验证 | 已拒绝 | diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index 15ff274b8..cf2eb9b2f 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "OpenAI 兼容性" source: contracts/agents-api/index.md -source_hash: 128b6ec019f014381fe4220afb9399340702294db4c5d260b34887b9a547d384 +source_hash: 7665dc7621643822f5144aa2de85f37128f76a44f5b9fe300441634ca37dc347 --- Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。 @@ -111,7 +111,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh **配置和工具** - 所选 Harness 未声明的 `reasoning.effort`、`reasoning.summary`、`service_tier`、`web_search` 模式或设置、`programmatic_tool_calling` 取值,会被保存,但在 Session 准入时被拒绝([Harness 能力](harness-capabilities.md#execution-and-input))。 -- Harness 对工具、结构化输出、延迟发现、subagents 和 MCP 的支持因 Harness 和部署位置而异;请参阅 [Harness capabilities](harness-capabilities.md)。MiniMax Code 不提供公共 functions、没有服务源 MCP,也不支持图像输入。 +- Harness 对工具、结构化输出、延迟发现、subagents 和 MCP 的支持因 Harness 和部署位置而异;请参阅 [Harness capabilities](harness-capabilities.md)。MiniMax Code 不提供公共 functions,也不支持图像输入。 - 由模型推导出的推理默认值不会被解析确定。 - MCP 工具仅支持 `http` 传输,`stdio` 会被拒绝,Session MCP 传输中的内联 `authorization` 也会被拒绝([HTTP MCP](execution-tools.md#http-mcp))。 @@ -122,6 +122,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - 如果取消导致函数结果无法应用,该结果将永远不会作为 Item 出现。 - 固定版本的 Codex 可能会丢失在订阅其流之前发出的命令输出。 - Claude Code 和 MiniMax Code 都不报告公共用量。 +- 以 `/` 开头的消息到达 MiniMax Code 时末尾多一个空行,否则 ACP 会将其作为操作员命令执行。 - 对于原生副作用,Core 不提供崩溃安全或恰好一次保证;已认领的工作若不重放,会在重启后失败。 - 图像上的 `file_id` 和 `detail` 会被拒绝;每个 Harness 声明自己接受哪些 `image_url`([消息内容](message-content.md#images))。 diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md index a79755733..5bbc565a2 100644 --- a/contracts/agents-api/zh/model-execution.md +++ b/contracts/agents-api/zh/model-execution.md @@ -1,7 +1,7 @@ --- title: "模型执行" source: contracts/agents-api/model-execution.md -source_hash: 4beae38b5287fd8d50509d057e92062f6acf9011cf02170fd8f83ed5e2df5f4f +source_hash: 5486e6c406aa220d5897a2e1980b4f432bd62fe4718317842370d89f66736b0c --- 每个 Session 都运行一个 Harness,并使用一个模型提供商。Core 通过三个固定版本上游协议未定义的 Core 扩展来选择它们:`x_agents_core.harness` 选择 Harness,`x_agents_core.model_provider` 提供端点和密钥,`x_agents_core.harness_config` 携带原生模型参数。Core 没有提供商目录、模型别名解析或产品权限模型;除 Session 和已保存 Agent 配置包外,唯一存储的配置包是每个 Harness 的一个 [deployment default](#deployment-defaults)。本文档定义 Harness—模型提供商协议:[`internal/modelprovider/config.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/modelprovider/config.go) 负责验证冻结的提供商连接,每个 Harness 则通过 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 声明其协议和原生参数。 @@ -111,7 +111,7 @@ Core 从同一个数据库快照读取 Agent 配置和加密配置包;显式 | --- | --- | --- | | Codex | 仅空对象 | 推理强度属于公开的 `reasoning.effort` | | Claude Code | `thinking`:SDK 的 `adaptive`、`enabled` 或 `disabled` 对象 | SDK `Options.thinking`;公开的 `reasoning.effort` 对应 `Options.effort` | -| MiniMax Code | 仅空对象 | 没有原生模型参数 | +| MiniMax Code | 仅空对象 | 公开的 `reasoning.effort` 成为自定义模型的思考强度 | 对于 `adaptive` 和 `enabled`,Claude `thinking` 接受 `display`(`summarized` 或 `omitted`);只有 `enabled` 接受正整数 `budgetTokens`;拒绝 `maxThinkingTokens`。这些是原生设置,而不是通用推理词汇表;模型可用性和提供商支持均由 Harness 负责。 diff --git a/example/parsar/README.md b/example/parsar/README.md index 2dbf5cbe2..a1c0e9b3e 100644 --- a/example/parsar/README.md +++ b/example/parsar/README.md @@ -19,7 +19,7 @@ Open http://127.0.0.1:18180. The Core URL is an origin without `/v1`; remote ori 1. **Models:** enter a Provider name, Base URL and API key, then fetch its `GET /v1/models` list. Select models with checkboxes or add custom model IDs, then save the Provider and selected models together. The dialog shows discovered and selected counts. For manual entry, skip discovery. No default group is created. Keys are stored in the local restricted SQLite file and never returned to the browser; leaving the key blank while editing preserves it. Discovery sends only that Provider's key and does not follow redirects. The list expects the OpenAI-shaped `data: [{id: "..."}]` response. Discovery failures leave manual entry available. Workspace Sessions pass the selected Provider URL/key explicitly to Core, using the harness protocol. These Sessions require an HTTPS Base URL and API key. Text-only Sessions use Core's deployment connection; the creation dialog states that their Provider is only a catalog group. 2. **Skills:** create a SKILL.md resource or upload a ZIP. Inspect versions, upload a new version, and choose the default. Core validates and stores bundles. -3. **MCP:** save anonymous HTTPS endpoints and bind them to Agents. Hosted Sessions install an inline environment Plugin; text-only Sessions use Core's service-origin MCP. Hosted HTTP MCP supports Claude Code and Codex, not MiniMax Code. Authentication, Vault management and OAuth setup are not included. +3. **MCP:** save anonymous HTTPS endpoints and bind them to Agents. Hosted Sessions install an inline environment Plugin; text-only Sessions use Core's service-origin MCP. Authentication, Vault management and OAuth setup are not included. 4. **Runtimes:** choose a Core-managed sandbox, text-only environment, or user machine. User machines select Linux/macOS/Windows and an existing absolute workspace path, plus an optional local capability directory. Each Session gets its own daemon installation and credential; choosing the same workspace path shares the host's files, so use distinct paths for isolated work. These are placement configurations, not online machine identities. Machine enrollment, fixed-node routing and self-hosted registration are omitted. 5. **Agents:** combine a model, harness, instructions, Skills and MCP services. Create, copy and edit configurations without allocating a runtime. 6. **Sessions:** open an Agent, choose a runtime and send the first message. Read streaming replies and tool activity, continue the conversation, cancel a running turn and reopen history. Unsupported Skill/MCP/runtime combinations are explained before creation. The Session's **Files** dialog uploads one file at a time (up to 5 MiB) through Core's public Environment Files API while idle. Each upload gets a unique path under `/workspace/inputs`; a path relative to the working directory is inserted into the message draft, not sent automatically. Unsent drafts, including uploaded file paths, survive reloads and navigation in the same browser tab and remain scoped to the Session. This keeps the API's `/workspace` alias distinct from the physical user-machine directory. Ask the Agent to save generated files under `./outputs`; published Artifacts can be paged and downloaded from the same dialog, even when the live workspace is unavailable. Downloads remain binary; errors stay in the dialog. Text-only Sessions have no file actions. Each hosted Session gets its own workspace; text-only has no workspace. diff --git a/example/parsar/shared/session-profile.mjs b/example/parsar/shared/session-profile.mjs index e4ec5060d..b99a279f3 100644 --- a/example/parsar/shared/session-profile.mjs +++ b/example/parsar/shared/session-profile.mjs @@ -9,6 +9,4 @@ export function sessionRestriction(agent, runtime) { } if (runtime.environment === "none" && agent.skill_ids.length) return "Skills 需要托管运行环境,请选择托管沙箱或移除 Skill 绑定。"; - if (agent.harness === "mcode" && agent.mcp_ids.length) - return "MiniMax Code 暂不支持此处的 HTTP MCP,请选择 Claude Code 或 Codex。"; } diff --git a/example/parsar/tests/product.test.mjs b/example/parsar/tests/product.test.mjs index 7ff07a72d..3a1e59055 100644 --- a/example/parsar/tests/product.test.mjs +++ b/example/parsar/tests/product.test.mjs @@ -439,17 +439,3 @@ test("model editing and moving retain unique IDs within each Provider", async (t }); assert.equal(renamed.model, "three"); }); - - -test("MiniMax MCP bindings reject before Core for text-only and hosted placements", async (t) => { - const store = openStore(":memory:"); - t.after(() => store.close()); - let calls = 0; - const { put, agent } = await setup(store, async () => { calls++; return { id: randomUUID() }; }); - const config = await put("agents", { ...agent, harness: "mcode", skill_ids: [] }, agent.id); - for (const environment of ["none", "openai_hosted"]) { - const runtime = await put("runtimes", { name: environment, environment }); - await assert.rejects(put("sessions", { name: "Rejected", input: "hello", agent_id: config.id, runtime_id: runtime.id }), /MiniMax Code.*MCP/); - } - assert.equal(calls, 0); -}); diff --git a/internal/harnessconfig/builtin/admission_test.go b/internal/harnessconfig/builtin/admission_test.go index c13d87dbd..29f599ac5 100644 --- a/internal/harnessconfig/builtin/admission_test.go +++ b/internal/harnessconfig/builtin/admission_test.go @@ -14,7 +14,7 @@ func TestMCPOriginSupport(t *testing.T) { for _, placement := range []string{"none", "self_hosted", "openai_hosted"} { for _, origin := range []string{"service", "environment", "", "unknown"} { servers := []proto.MCPHTTPServer{{ConnectionOrigin: origin, ServerLabel: "proof", ServerURL: "https://example.test/mcp"}} - allowed := origin == "environment" && placement != "none" || origin == "service" && placement == "none" && kind != "mcode" + allowed := origin == "environment" && placement != "none" || origin == "service" && placement == "none" if err := declaration.ValidateMCPOrigins(&harnessconfig.AdmissionEnvironment{Type: placement}, servers); (err == nil) != allowed { t.Fatalf("%s/%s/%s: %v", kind, placement, origin, err) } diff --git a/internal/harnessconfig/mcode/declaration.go b/internal/harnessconfig/mcode/declaration.go index befbf4b51..650883fc9 100644 --- a/internal/harnessconfig/mcode/declaration.go +++ b/internal/harnessconfig/mcode/declaration.go @@ -50,16 +50,19 @@ var Declaration = harnessconfig.Declaration{ MCPHTTPBearerAuth: proto.CapabilitySupported, }, // Native search needs a MiniMax account credential that Core does not hold, so - // features.webSearch stays false and disabled is the only mode. MiniMax Code has - // no native home for effort, summary, service tier or programmatic tool calling. + // features.webSearch stays false and disabled is the only mode. Effort becomes the + // custom model's only thinking option, and none disables native thinking. + // MiniMax Code has no native home for summary, service tier or programmatic + // tool calling. // Medium is the official default verbosity and needs no setting. Execution: harnessconfig.ExecutionOptions{ WebSearchModes: []string{"disabled"}, + ReasoningEfforts: []string{"none", "minimal", "low", "medium", "high", "xhigh", "max"}, ProgrammaticToolCalling: []bool{false}, TextVerbosities: []string{"medium"}, }, Placements: []string{"none", "openai_hosted", "self_hosted"}, - MCPOrigins: []string{"environment"}, + MCPOrigins: []string{"service", "environment"}, WhitespaceOnlyText: proto.CapabilityUnsupported, ValidateConfiguration: func(admission harnessconfig.Admission) error { environment := admission.Environment diff --git a/services/core/deploy/mcode/README.md b/services/core/deploy/mcode/README.md index a34d4acf1..65f650aa8 100644 --- a/services/core/deploy/mcode/README.md +++ b/services/core/deploy/mcode/README.md @@ -12,7 +12,7 @@ Workspace execution also requires the companion's readiness report: private prot ## Model provider -MiniMax Code accepts the `anthropic`, `responses` and `chat_completions` protocols, and the provider's context window and maximum output tokens are optional ([`harnessconfig/mcode`](../../../../internal/harnessconfig/mcode/declaration.go)). The adapter writes the frozen bundle as the native custom provider for the Session's exact `agent.model`, with the limits that are set, using the native `anthropic-messages`, `openai-responses` or `openai-completions` API ([`model_provider.go`](../../../../apps/daemon/internal/agent/mcode/model_provider.go)). It selects that provider explicitly; there is no native account fallback. A limit that is not set is omitted from the native configuration, so MiniMax Code applies its own defaults of 200000 context tokens and 16384 output tokens. [Model execution](../../../../contracts/agents-api/model-execution.md#deployment-defaults) owns provider selection. +MiniMax Code accepts the `anthropic`, `responses` and `chat_completions` protocols, and the provider's context window and maximum output tokens are optional ([`harnessconfig/mcode`](../../../../internal/harnessconfig/mcode/declaration.go)). The adapter writes the frozen bundle as the native custom provider for the Session's exact `agent.model`, with the limits that are set and, when the Agent sets `reasoning.effort`, that effort as the model's only thinking option (the adapter then selects it with ACP `thinkingEffort`), using the native `anthropic-messages`, `openai-responses` or `openai-completions` API ([`model_provider.go`](../../../../apps/daemon/internal/agent/mcode/model_provider.go)). It selects that provider explicitly; there is no native account fallback. A limit that is not set is omitted from the native configuration, so MiniMax Code applies its own defaults of 200000 context tokens and 16384 output tokens. [Model execution](../../../../contracts/agents-api/model-execution.md#deployment-defaults) owns provider selection. ## Execution profiles @@ -20,18 +20,18 @@ Each Session has its own native data directory, `$OAC_RUNTIME_HOME/runtime/mcode | Profile | Where it runs | Native configuration | | --- | --- | --- | -| Text (`environment: none`) | A private working directory inside the Session's data directory | No native tools, Skills, web search, browser tools, `mcode-tools`, thread goals or user questions. With Subagents enabled, the default agent gets only the task tools (`task`, `task_append`, `task_query`, `task_output`, `task_stop`) and delegation | +| Text (`environment: none`) | A private working directory inside the Session's data directory | No native file, process or fetch tools, web search, `mcode-tools`, thread goals or user questions. The native task controls (`task_query`, `task_output`, `task_stop`) are always present; with Subagents enabled, the default agent also gets delegation. Permission mode `bypassPermissions`, so MCP tools run without approval prompts | | Workspace | The Environment's workspace directory, for the native process, the ACP Session and the native tools | The native `read`, `write`, `edit`, `bash`, `grep`, `glob`, `todowrite` and `web_fetch` tools, permission mode `bypassPermissions`, the selected workspace Skills and the frozen tool environment in the native process environment | -The text profile rejects workspace, function tools and MCP servers. Workspace execution requires Linux or macOS ([`workspace.go`](../../../../apps/daemon/internal/agent/mcode/workspace.go)). +The text profile rejects workspace and function tools; it accepts service-origin HTTP MCP servers, which the adapter passes in the ACP `mcpServers` of the native Session. Workspace execution requires Linux or macOS ([`workspace.go`](../../../../apps/daemon/internal/agent/mcode/workspace.go)). -In the workspace profile, the frozen installation's Skills are linked into the Session's `skills` directory under their exact names and loaded through the native `skill` tool. Native discovery also reads the workspace's `.minimax`, `.claude` and `.agents` Skills, as a local run does. Public MCP servers use the Environment origin only, as the [MCP origin contract](../../../../contracts/agents-api/environments.md#public-mcp-connection-origin) describes. With Subagents enabled, child agents use the same native tools. In both profiles the adapter turns off what needs a MiniMax account or outlives a Turn or asks the user: `askUser`, `beta.mcodeTools`, `beta.threadGoal`, `features.mavis`, `features.webSearch`, the Matrix builtin tools and, on the text profile, the bundled Skills, and it leaves `website_deploy` out of `tools`. `web_search: disabled` also removes `web_fetch`. Background Bash is off: the patched CLI gates it on `OAC_RUNTIME_MCODE_BACKGROUND_BASH=off`, and the adapter rejects a CLI that does not report it off. +In the workspace profile, the frozen installation's Skills are linked into the Session's `skills` directory under their exact names and loaded through the native `skill` tool. Native discovery also reads the workspace's `.minimax`, `.claude` and `.agents` Skills, as a local run does. Public MCP servers use the Environment origin on a workspace and the service origin on the text profile, as the [MCP origin contract](../../../../contracts/agents-api/environments.md#public-mcp-connection-origin) describes. With Subagents enabled, child agents use the same native tools. In both profiles the adapter turns off what needs a MiniMax account or outlives a Turn or asks the user: `askUser`, `beta.mcodeTools`, `beta.threadGoal`, `features.mavis` (its tool carries cron, session send, agent create and MCP create) and `features.webSearch`, and it leaves `website_deploy` out of `tools`. `web_search: disabled` also removes `web_fetch`. Background Bash is off: the patched CLI gates it on `OAC_RUNTIME_MCODE_BACKGROUND_BASH=off`, and the adapter rejects a CLI that does not report it off. ## Turns, cancellation and continuation - Normal Turns reuse one ACP connection and native Session. - Active input uses ACP `mcode/session/steer`. Its receipt confirms that the active native Turn accepted the input, not that the model consumed it. Unknown outcomes are never replayed. -- Public input stays a model message: the adapter adds an empty text block so that ACP does not treat slash text as an operator command. +- Public input stays a model message: ACP joins text blocks with a blank line, so the adapter sends one block, and adds an empty second block only when the text starts with `/`, so that ACP does not treat slash text as an operator command. - A Turn that ends with an error, a cancellation, an unknown input outcome, an unfinished tool call or an unanswered permission or question makes the Executor non-reusable. The adapter stops the native process group and waits for it to exit before the Turn settles; later work loads the same native history in a new Executor. - Continuation requires the exact recorded native Session ID and the same working directory. Recovery without a recorded ID is rejected. - MiniMax Code reports no usage: ACP context occupancy and cumulative cost are not per-Turn usage. diff --git a/services/core/internal/execution/mcode_profile_test.go b/services/core/internal/execution/mcode_profile_test.go index f24dbd8a5..881b8ea54 100644 --- a/services/core/internal/execution/mcode_profile_test.go +++ b/services/core/internal/execution/mcode_profile_test.go @@ -13,7 +13,8 @@ func TestMCodeOperationQualification(t *testing.T) { {"verbosity", `{"agent":{"model":"real-model","text":{"verbosity":"high"}},"environment":{"type":"none"}}`, false}, {"subagents", `{"agent":{"model":"real-model","multi_agent":{"enabled":true}},"environment":{"type":"none"}}`, true}, {"functions", `{"agent":{"model":"real-model","tools":[{"type":"function","name":"f","parameters":{"type":"object"}}]},"environment":{"type":"none"}}`, false}, - {"mcp", `{"agent":{"model":"real-model","tools":[{"type":"mcp","server_label":"x","server_url":"https://example.invalid/mcp"}]},"environment":{"type":"none"}}`, false}, + {"mcp", `{"agent":{"model":"real-model","tools":[{"type":"mcp","server_label":"x","connection_origin":"service","transport":{"type":"http","server_url":"https://example.invalid/mcp"}}]},"environment":{"type":"none"}}`, true}, + {"effort", `{"agent":{"model":"real-model","reasoning":{"effort":"high"}},"environment":{"type":"none"}}`, true}, } { t.Run(tc.name, func(t *testing.T) { err := p.ValidateSessionConfiguration("mcode", []byte(tc.body))