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
1 change: 0 additions & 1 deletion apps/daemon/internal/agent/claudesdk/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ func startExecutor(ctx context.Context, checked *runtimeCheckCache, probe Config
if err = validateExecutorFeatures(info, start); err != nil {
return nil, err
}
start.Type = "executor_prepare"
base, err := run()
if err != nil {
return nil, err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"time"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/clirunner"
Expand Down Expand Up @@ -59,7 +60,7 @@ func startSingleTurn(ctx context.Context, config testBridge, req proto.PromptReq
if err != nil {
return nil, err
}
resource, err := config.factory()(ctx, agent.PrepareRequest{PromptRequestPayload: req, Prepared: configuration})
resource, err := config.factory()(ctx, agent.PrepareRequest{PreparationDeadline: time.Now().Add(time.Minute), PromptRequestPayload: req, Prepared: configuration})
if err != nil {
return nil, err
}
Expand Down
34 changes: 20 additions & 14 deletions apps/daemon/internal/agent/claudesdk/options.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package claudesdk

import (
"context"
"fmt"
"time"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
Expand All @@ -21,26 +23,30 @@ type subagentOptions struct {
}

type startRequest struct {
NativeModelOptions *nativeModelOptions `json:"native_model_options,omitempty"`
ToolSearch bool `json:"tool_search,omitempty"`
Subagents *subagentOptions `json:"subagents,omitempty"`
OutputFormat *proto.OutputFormat `json:"output_format,omitempty"`
Type string `json:"type"`
Model string `json:"model"`
SystemPrompt string `json:"system_prompt"`
Cwd string `json:"cwd"`
Resume string `json:"resume,omitempty"`
Functions []proto.FunctionTool `json:"functions,omitempty"`
MCPHTTPServers *[]mcpHTTPServer `json:"mcp_http_servers,omitempty"`
Workspace *workspaceProfile `json:"workspace,omitempty"`
RequireHistory bool `json:"require_history,omitempty"`
PreparationDeadline int64 `json:"preparation_deadline"`
NativeModelOptions *nativeModelOptions `json:"native_model_options,omitempty"`
ToolSearch bool `json:"tool_search,omitempty"`
Subagents *subagentOptions `json:"subagents,omitempty"`
OutputFormat *proto.OutputFormat `json:"output_format,omitempty"`
Type string `json:"type"`
Model string `json:"model"`
SystemPrompt string `json:"system_prompt"`
Cwd string `json:"cwd"`
Resume string `json:"resume,omitempty"`
Functions []proto.FunctionTool `json:"functions,omitempty"`
MCPHTTPServers *[]mcpHTTPServer `json:"mcp_http_servers,omitempty"`
Workspace *workspaceProfile `json:"workspace,omitempty"`
RequireHistory bool `json:"require_history,omitempty"`
}

// prepareOptions renders the request's execution configuration and the
// selected model provider. The registered factory already admitted the
// selection against the declaration.
func prepareOptions(req agent.PrepareRequest) (startRequest, []string, error) {
start := startRequest{Type: "start", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, ToolSearch: req.ToolSearch}
if req.PreparationDeadline.IsZero() || !time.Now().Before(req.PreparationDeadline) {
return startRequest{}, nil, context.DeadlineExceeded
}
start := startRequest{PreparationDeadline: req.PreparationDeadline.UnixMilli(), Type: "executor_prepare", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, ToolSearch: req.ToolSearch}
fail := func(reason string) (startRequest, []string, error) {
return startRequest{}, nil, fmt.Errorf("claudesdk: %s", reason)
}
Expand Down
20 changes: 19 additions & 1 deletion apps/daemon/internal/agent/claudesdk/options_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package claudesdk

import (
"context"
"errors"
"slices"
"testing"
"time"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/clirunner"
Expand Down Expand Up @@ -55,10 +57,26 @@ func prepared(t testing.TB, req proto.PromptRequestPayload) agent.PrepareRequest
if err != nil {
t.Fatal(err)
}
return agent.PrepareRequest{PromptRequestPayload: req, Prepared: configuration}
return agent.PrepareRequest{PreparationDeadline: time.Now().Add(time.Minute), PromptRequestPayload: req, Prepared: configuration}
}

// fixtureProvider is the provider every Claude request carries.
func fixtureProvider() *modelprovider.Provider {
return &modelprovider.Provider{Protocol: modelprovider.Anthropic, BaseURL: "https://model.example", APIKey: "fixture-key"}
}

func TestPreparationDeadlineReachesBridgeUnchanged(t *testing.T) {
req := prepared(t, proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableExecutionEnvironment: true, Model: "test-model"})
deadline := time.Now().Add(3 * time.Minute).Truncate(time.Millisecond)
req.PreparationDeadline = deadline
start, _, err := prepareTestView(t, req)
if err != nil || start.PreparationDeadline != deadline.UnixMilli() {
t.Fatalf("deadline=%d error=%v", start.PreparationDeadline, err)
}
for _, invalid := range []time.Time{{}, time.Now().Add(-time.Second)} {
req.PreparationDeadline = invalid
if _, _, err := prepareTestView(t, req); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("invalid budget: %v", err)
}
}
}
5 changes: 5 additions & 0 deletions apps/daemon/internal/agent/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"slices"
"strconv"
"strings"
"time"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/clirunner"
"github.com/MiniMax-AI/OpenAgentCore/internal/agentcapabilities"
Expand Down Expand Up @@ -532,6 +533,10 @@ type ExecutorFactory func(context.Context, PrepareRequest) (Executor, error)
// sets Prepared. A Turn's run ID and input arrive in Executor.StartTurn.
type PrepareRequest struct {
proto.PromptRequestPayload
// PreparationDeadline is the dispatch-owned absolute preparation deadline.
// It must be nonzero and preserved through preparation, never restarted.
// The factory context owns the Executor lifetime; this deadline does not.
PreparationDeadline time.Time
// Prepared is the model configuration, validated against the kind's
// declaration. An adapter takes its model, provider and native parameters
// only from here.
Expand Down
3 changes: 3 additions & 0 deletions apps/daemon/internal/agenthost/agenthost_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ func newDaemon(t *testing.T, cfg Config, d deps) *daemon {
}
e, err := dm.host.openExecutor(ctx, req)
if s, ok := e.(*session); ok {
if req.PreparationDeadline.IsZero() || s.plan.request.PreparationDeadline != req.PreparationDeadline {
t.Error("agent host changed the dispatch preparation deadline")
}
dm.mu.Lock()
dm.opened[req.Assignment.SessionID] = s
dm.mu.Unlock()
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/dispatch/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func (r *Router) prepareExecutor(p *preparationState, configuration proto.Prompt
}
var native agent.Executor
var err error
req := agent.PrepareRequest{PromptRequestPayload: configuration, StateKey: "agents-api-" + owner.sessionID, Assignment: p.request.Assignment}
req := agent.PrepareRequest{PromptRequestPayload: configuration, StateKey: "agents-api-" + owner.sessionID, Assignment: p.request.Assignment, PreparationDeadline: p.deadline}
if owner.ctx.Err() == nil {
if environment != nil {
req, err = environment.Prepare(owner.ctx, req)
Expand Down
65 changes: 65 additions & 0 deletions apps/daemon/internal/dispatch/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,68 @@ func (*reusableTurn) SteerWithReceipt(context.Context, proto.PromptSteerPayload,
func (*reusableTurn) SubmitFunctionResult(context.Context, proto.FunctionResultPayload) error {
return agent.ErrUnknownFunctionCall
}

func TestExecutorPreparationDeadlineIsPreservedWithoutOwningWarmLifetime(t *testing.T) {
environment := newTestOwner(preparationEnvironmentID, preparationSessionID)
beforeEnvironment, afterEnvironment := make(chan agent.PrepareRequest, 1), make(chan agent.PrepareRequest, 1)
proceed := make(chan struct{})
environment.prepare = func(req agent.PrepareRequest) (agent.PrepareRequest, error) {
beforeEnvironment <- req
<-proceed
req.WorkspaceRoot = "/workspace"
return req, nil
}
owner := &reusableExecutor{starts: make(chan *reusableTurn, 2)}
ownerContext := make(chan context.Context, 1)
reg := agent.NewRegistry()
reg.RegisterKind(proto.SupportedAgentKind{Kind: "prepared", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported, FunctionResultImages: proto.CapabilitySupported, NativeSessionRecovery: proto.CapabilitySupported})}, prototest.ModelConfiguration())
reg.RegisterExecutor("prepared", func(ctx context.Context, req agent.PrepareRequest) (agent.Executor, error) {
ownerContext <- ctx
afterEnvironment <- req
return owner, nil
})
sender := &recSender{}
r, err := dispatch.New(dispatch.Config{Registry: reg, Sender: sender, Environments: environment.Resolve, PreparationTimeout: time.Second, IdleTimeout: time.Minute})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := r.Shutdown(ctx); err != nil {
t.Error(err)
}
})
assign(t, r, preparationSessionID, preparationEnvironmentID)
req := preparationRequest()
if err := r.Handle(t.Context(), mustEnv(t, proto.TypeExecutionPrepare, "deadline", req)); err != nil {
t.Fatal(err)
}
admitted := waitPreparationStatus(t, sender, "deadline", "preparing", "")
before := <-beforeEnvironment
close(proceed)
after := <-afterEnvironment
ctx := <-ownerContext
if before.PreparationDeadline.IsZero() || before.PreparationDeadline != after.PreparationDeadline || before.PreparationDeadline.UnixMilli() != admitted.ExpiresAt || after.WorkspaceRoot != "/workspace" || after.Prepared.Model == "" {
t.Fatalf("preparation handoff changed: before=%v after=%v admission=%d", before.PreparationDeadline, after.PreparationDeadline, admitted.ExpiresAt)
}
if _, hasDeadline := ctx.Deadline(); hasDeadline {
t.Fatal("preparation deadline attached to Executor context")
}
ready := waitPreparationStatus(t, sender, "deadline", "ready", "")
startExecutorTurn(t, r, sender, "deadline", "first", ready)
first := <-owner.starts
<-time.After(time.Until(before.PreparationDeadline))
if ctx.Err() != nil || owner.closes.Load() != 0 {
t.Fatal("adopted Executor ended at preparation deadline")
}
first.finish()
waitFor(t, func() bool { return r.ActiveRuns() == 0 }, "first settled")
req.Configuration.AgentSessionID = "native-session"
next := executorAdmission(t, r, sender, "next", req)
if !next.Reused || next.ExecutorID != ready.ExecutorID {
t.Fatal("warm owner not reused after deadline")
}
startExecutorTurn(t, r, sender, "next", "second", next)
(<-owner.starts).finish()
}
2 changes: 1 addition & 1 deletion contracts/agents-api/harness-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Implement the mandatory text lifecycle and handle every extension explicitly. Qu

[`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go) is the interface entry point. The required lifecycle is `ViewExecutorFactory`, `Executor`, `Turn` and `TurnSettlement`. `Turn` is one interface: `Cancel`, `CancellationOutcome`, `SteerWithReceipt`, `SubmitFunctionResult` and `AwaitSettlement`. Required methods perform their native obligations; returning Unsupported is not an implementation of cancellation, receipts, settlement or cleanup. An operation the adapter does not support returns Unsupported, and the capability declaration, not the method, decides whether the Runtime calls it. All use the neutral protocol types.

The view's `ViewExecutorFactory` takes one `agent.PrepareRequest`: the Session's configuration as `execution_prepare` carries it, the model configuration that the Registry prepared once from the kind's declaration (`Prepared`), the Session's native state key (`StateKey`), and the Environment's workspace and installed Capabilities (`WorkspaceRoot`, `CapabilityRoot`, `Skills`, `MCP`), which its owner fills. The adapter takes its model, provider and native parameters only from `Prepared` and never parses `model` or `model_provider` itself. A Turn's Run ID and input arrive in `Executor.StartTurn`.
The view's `ViewExecutorFactory` takes one `agent.PrepareRequest`: the Session's configuration as `execution_prepare` carries it, the model configuration that the Registry prepared once from the kind's declaration (`Prepared`), the Session's native state key (`StateKey`), and the Environment's workspace and installed Capabilities (`WorkspaceRoot`, `CapabilityRoot`, `Skills`, `MCP`), which its owner fills. The adapter takes its model, provider and native parameters only from `Prepared` and never parses `model` or `model_provider` itself. Dispatch also supplies the required absolute `PreparationDeadline`, created once for the admission. Environment preparation, Registry and View preserve it unchanged. Native initialization must finish before it; an adapter that passes an initialization budget to its SDK derives it from the remaining time and rejects an exhausted budget. The factory context owns the Executor lifetime independently: adopting a prepared Executor must not attach the preparation deadline to its later Turns. A Turn's Run ID and input arrive in `Executor.StartTurn`.

For example, the Codex adapter keeps its app-server and thread, the Claude adapter one streaming Query, and the MiniMax adapter its ACP connection and native session. All expose the same Executor and Turn contract. Native callbacks and resources stay inside the adapter; the Runtime owns admission, idle expiry and replacement. Cancellation targets the exact Turn through `Turn.Cancel`, and the adapter supplies native completion evidence to the Runtime.

Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/harness-onboarding.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "添加 Harness"
source: contracts/agents-api/harness-onboarding.md
source_hash: 56b2a40b814062de7d636af33ad2eb7671b8b3eb11781e32caacdf3d3985ae73
source_hash: a18a2221b28aa01596574a1db63e1bf643dc43fb73c15261fb8932dd447f499b
---

**Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code)。**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。本文档定义 Runtime–Harness 协议:适配器接口及其生命周期义务、注册、支持声明和验收。
Expand Down Expand Up @@ -61,7 +61,7 @@ Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以

[`agent/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/daemon/internal/agent/harness.go) 是接口入口。必需的生命周期包括 `ViewExecutorFactory`、`Executor`、`Turn` 和 `TurnSettlement`。`Turn` 是一个接口:`Cancel`、`CancellationOutcome`、`SteerWithReceipt`、`SubmitFunctionResult` 和 `AwaitSettlement`。必需方法必须履行其原生义务;返回 Unsupported 并不构成对取消、回执、结算或清理的实现。适配器不支持的操作返回 Unsupported,由能力声明而不是方法决定 Runtime 是否调用它。所有接口都使用中立协议类型。

视图的 `ViewExecutorFactory` 接收一个 `agent.PrepareRequest`:`execution_prepare` 携带的 Session 配置、Registry 按 kind 的声明一次性准备好的模型配置(`Prepared`)、Session 的原生状态键(`StateKey`),以及由 Environment owner 填写的 Environment 工作区和已安装 Capabilities(`WorkspaceRoot`、`CapabilityRoot`、`Skills`、`MCP`)。适配器只从 `Prepared` 获取模型、提供商和原生参数,从不自行解析 `model` 或 `model_provider`。Turn 的 Run ID 和输入通过 `Executor.StartTurn` 传入。
视图的 `ViewExecutorFactory` 接收一个 `agent.PrepareRequest`:`execution_prepare` 携带的 Session 配置、Registry 按 kind 的声明一次性准备好的模型配置(`Prepared`)、Session 的原生状态键(`StateKey`),以及由 Environment owner 填写的 Environment 工作区和已安装 Capabilities(`WorkspaceRoot`、`CapabilityRoot`、`Skills`、`MCP`)。适配器只从 `Prepared` 获取模型、提供商和原生参数,从不自行解析 `model` 或 `model_provider`。Dispatch 还提供 admission 创建时唯一确定的必填绝对截止时间 `PreparationDeadline`。Environment 准备、Registry 与 View 原样传递它。原生初始化必须在截止时间前结束;适配器向 SDK 传递初始化预算时,必须从剩余时间推导,并拒绝已耗尽的预算。工厂上下文独立拥有 Executor 生命周期:接管准备完成的 Executor 后,不得让准备截止时间约束后续 Turn。Turn 的 Run ID 和输入通过 `Executor.StartTurn` 传入。

例如,Codex 适配器保留其 app-server 和 thread,Claude 适配器保留一个流式 Query,MiniMax 适配器保留其 ACP 连接和原生 session。它们都公开相同的 Executor 和 Turn 契约。原生回调和资源保留在适配器内部;Runtime 负责准入、空闲过期和替换。取消通过 `Turn.Cancel` 精确定位到目标 Turn,适配器则向 Runtime 提供原生完成证据。

Expand Down
2 changes: 1 addition & 1 deletion packages/claude-sdk-adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The Harness environment is closed: the Session home's native directories, the ga

### Executor preparation and Turns

The private bridge accepts `executor_prepare` without model input. It freezes validated configuration and resume identity, checks required history, and retains one native process and SDK Query across Turns. Preparation requires initialization and acknowledgement of required hooks while the input iterator remains empty. The pinned SDK owns the initialization deadline through its default `startup` behavior. An `executor_ready` receipt permits later `turn_start` messages containing only Turn identity and ordered input; configuration replacement and concurrent starts are rejected. Every Turn event carries its originating `turn_id`. Native Session identity and actual tool inventory are checked before `input_ready`.
The private bridge accepts `executor_prepare` without model input. It freezes validated configuration and resume identity, checks required history, and retains one native process and SDK Query across Turns. Preparation requires initialization and acknowledgement of required hooks while the input iterator remains empty. The required `preparation_deadline` is dispatch’s absolute Unix millisecond deadline from `PrepareRequest.PreparationDeadline`. Immediately before `startup`, the bridge passes the remaining milliseconds to the pinned SDK’s `initializeTimeoutMs`; an exhausted budget fails without starting native initialization. Readiness must still precede that deadline. The budget applies only to preparation and does not terminate an adopted warm Query. An `executor_ready` receipt permits later `turn_start` messages containing only Turn identity and ordered input; configuration replacement and concurrent starts are rejected. Every Turn event carries its originating `turn_id`. Native Session identity and actual tool inventory are checked before `input_ready`.

Each Turn ends with a result/error and `turn_settled`, independently of process exit. The outer input iterator remains open for later Turns. `turn_cancel` invokes the native interrupt control for that exact Turn. Unconfirmed input, native child work or queue state invalidates the Executor and requires close before replacement. EOF, owner signals and invalid control input close owned resources. Preparation may write native metadata and perform startup traffic; readiness does not prove provider authentication, complete sandbox health or placement authorization.

Expand Down
Loading
Loading