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
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,6 @@ func TestExecutionControlsRejectUnsupportedProfilesBeforeLaunch(t *testing.T) {
"high-verbosity": {Turn: model, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "high"}},
"unknown-verbosity": {Turn: model, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "invalid"}},
"summary": {Turn: model, ExecutionControls: &proto.ExecutionControls{ReasoningSummary: "concise"}},
"no-effort": {Turn: proto.TurnSettings{Model: "native-model", ReasoningEffort: "none"}},
"minimal-effort": {Turn: proto.TurnSettings{Model: "native-model", ReasoningEffort: "minimal"}},
"service-tier": {Turn: proto.TurnSettings{Model: "native-model", ServiceTier: "priority"}},
}
for name, request := range cases {
t.Run(name, func(t *testing.T) {
Expand Down
3 changes: 0 additions & 3 deletions apps/daemon/internal/agent/claudesdk/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR
if err := harnessconfiguration.Declaration.ValidateExecutionControls(req.ExecutionControls); err != nil {
return fail(err.Error())
}
if err := harnessconfiguration.Declaration.ValidateTurnSettings(req.Turn); err != nil {
return fail(err.Error())
}
start.Model = req.Turn.Model
start.NativeModelOptions = compileNativeModelOptions(modelConfiguration.HarnessConfig, req.Turn.ReasoningEffort)
if err := req.ValidateToolSearch(true); err != nil {
Expand Down
8 changes: 1 addition & 7 deletions apps/daemon/internal/agent/codex/execution_controls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func TestExecutionControlsRejectUndeclaredValues(t *testing.T) {
}
}

func TestTurnSettingsReachNativeConfigAndRejectUndeclaredValues(t *testing.T) {
func TestTurnSettingsReachNativeConfig(t *testing.T) {
t.Setenv("OAC_RUNTIME_HOME", t.TempDir())
plan, err := BuildSessionPlan(testStateRoot(t), "state", nil, proto.TurnSettings{Model: "m", ReasoningEffort: "max", ServiceTier: "priority"}, nil)
if err != nil {
Expand All @@ -74,10 +74,4 @@ func TestTurnSettingsReachNativeConfigAndRejectUndeclaredValues(t *testing.T) {
if plan.Model != "m" || !slices.Contains(plan.ExtraConfig, [2]string{"model_reasoning_effort", `"max"`}) || !slices.Contains(plan.ExtraConfig, [2]string{"service_tier", `"priority"`}) {
t.Fatalf("turn settings not applied: %v", plan.ExtraConfig)
}
for _, turn := range []proto.TurnSettings{{ReasoningEffort: "ultra"}, {ServiceTier: "scale"}} {
if plan, err := BuildSessionPlan(testStateRoot(t), "state", nil, turn, nil); err == nil {
plan.Cleanup()
t.Fatal("undeclared turn setting accepted", turn)
}
}
}
4 changes: 2 additions & 2 deletions apps/daemon/internal/agent/codex/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func newExecutor(ctx context.Context, req proto.PromptRequestPayload, cfg sessio
return &Executor{prepared: p}, nil
}

func (e *Executor) StartTurn(ctx context.Context, runID string, _ proto.TurnSettings, input proto.MessageInput, out chan<- proto.Envelope) (agent.Turn, error) {
func (e *Executor) StartTurn(ctx context.Context, runID string, turn proto.TurnSettings, input proto.MessageInput, out chan<- proto.Envelope) (agent.Turn, error) {
if out == nil || strings.TrimSpace(runID) == "" || input.Validate() != nil {
return nil, errors.New("codex: start requires a run identity, input and output")
}
Expand Down Expand Up @@ -96,7 +96,7 @@ func (e *Executor) StartTurn(ctx context.Context, runID string, _ proto.TurnSett
req := proto.PromptRequestPayload{RunID: runID, Input: input, AgentSessionID: e.prepared.resumeID, RequireExistingNativeSession: e.prepared.requireExistingNativeSession}
// Ownership precedes any native submission. Even an uncertain start returns the
// exact Turn so its caller can await settlement without replaying the input.
err := s.startNative(ctx, e.prepared.plan, req)
err := s.startNative(ctx, e.prepared.plan, turn, req)
if err != nil {
s.emitTerminal("codex: native start failed", true)
s.finishAfterTerminal()
Expand Down
37 changes: 37 additions & 0 deletions apps/daemon/internal/agent/codex/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,40 @@ func TestExecutorCloseTerminatesAfterMissingCancellationTerminal(t *testing.T) {
t.Fatal("original cancellation waiter was abandoned")
}
}

func TestTurnStartCarriesTheTurnSettings(t *testing.T) {
e, root := executorFixture(t, "complete")
turns := []proto.TurnSettings{
{Model: "one", ReasoningEffort: "high", ServiceTier: "priority"},
{Model: "two", Review: "auto_review"},
}
for i, settings := range turns {
out := make(chan proto.Envelope, 20)
turn, err := e.StartTurn(t.Context(), "run", settings, proto.TextInput("answer"), out)
if err != nil {
t.Fatal(err)
}
awaitExecutorTurn(t, turn, out)
var sent []string
for _, frame := range preparationFrames(t, root) {
if frame.Method == "turn/start" {
var p struct {
Model, Effort, ApprovalPolicy, ApprovalsReviewer string
ServiceTier *string
}
if err := json.Unmarshal(frame.Params, &p); err != nil {
t.Fatal(err)
}
tier := "null"
if p.ServiceTier != nil {
tier = *p.ServiceTier
}
sent = append(sent, strings.Join([]string{p.Model, p.Effort, tier, p.ApprovalPolicy, p.ApprovalsReviewer}, " "))
}
}
want := []string{"one high priority never user", "two null on-request auto_review"}
if len(sent) != i+1 || sent[i] != want[i] {
t.Fatalf("turn/start %d = %q, want %q", i, sent, want[i])
}
}
}
18 changes: 18 additions & 0 deletions apps/daemon/internal/agent/codex/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,21 @@ func TestEndedChildTurnWithdrawsItsPendingCall(t *testing.T) {
default:
}
}

func TestApprovalEscalationsAreDeniedWithMethodNotFound(t *testing.T) {
tc, srv, cleanup := NewTestClient()
defer cleanup()
newFunctionTestSession(tc.JSONRPCClient)
if err := SendServerRequest(srv, "approval", "item/commandExecution/requestApproval", map[string]any{"threadId": "thread"}); err != nil {
t.Fatal(err)
}
var reply struct {
ID string `json:"id"`
Error struct {
Code int `json:"code"`
} `json:"error"`
}
if err := json.NewDecoder(srv.FromClient).Decode(&reply); err != nil || reply.ID != "approval" || reply.Error.Code != -32601 {
t.Fatal(reply, err)
}
}
4 changes: 0 additions & 4 deletions apps/daemon/internal/agent/codex/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,6 @@ func BuildSessionPlan(stateRoot, agentStateKey string, opts map[string]any, turn
return plan, err
}

if err := harnessconfiguration.Declaration.ValidateTurnSettings(turn); err != nil {
return plan, err
}

plan.Model = turn.Model
plan.SystemPrompt = stringOpt(opts, "system_prompt")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestPreparationRouterRetainsActualNativeChild(t *testing.T) {
req.DisableExecutionEnvironment = false
req.LocalEnvironment = &proto.LocalEnvironment{ID: environment, WorkspaceDirectory: "/workspace", CapabilitySources: &proto.CapabilitySources{}}
registry := agent.NewRegistry()
registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, harnessconfig.Configuration{})
registry.RegisterKind(proto.SupportedAgentKind{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{LocalEnvironment: proto.CapabilitySupported, FunctionTools: proto.CapabilitySupported})}, harnessconfig.Declaration{})
prepared := make(chan *Prepared, 1)
registry.RegisterExecutor("codex", func(ctx context.Context, req proto.PromptRequestPayload) (agent.Executor, error) {
e, err := newExecutor(ctx, req, cfg)
Expand Down
8 changes: 8 additions & 0 deletions apps/daemon/internal/agent/codex/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ type TurnStartParams struct {
Input []UserInput `json:"input"`
// OutputSchema constrains this Turn's final assistant message natively.
OutputSchema json.RawMessage `json:"outputSchema,omitempty"`
// The settings below apply to this and later Turns. Every Turn sends them,
// because a cold thread/resume restores the persisted values.
Model string `json:"model,omitempty"`
Effort string `json:"effort,omitempty"`
ServiceTier any `json:"serviceTier"` // a tier, or null for auto
ApprovalPolicy string `json:"approvalPolicy"`
// ApprovalsReviewer is "user" or "auto_review".
ApprovalsReviewer string `json:"approvalsReviewer"`
}

type TurnInterruptParams struct {
Expand Down
6 changes: 3 additions & 3 deletions apps/daemon/internal/agent/codex/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ func (s *Session) registerHandlers() {
rpc.OnNotification("thread/tokenUsage/updated", s.onUsageUpdated)
rpc.OnNotification("error", s.onErrorNotif)

// Harnesses run unattended. Approval policy never and the session plan keep
// native asks from being raised; any other server request gets the
// client's method-not-found reply.
// Harnesses run unattended. Approval policy never raises no native ask, and
// under auto_review the reviewer answers. Any server request that still
// reaches the client gets method-not-found, which Codex turns into a denial.
s.onServerRequest("item/tool/call", s.handleFunctionCall)
}

Expand Down
20 changes: 16 additions & 4 deletions apps/daemon/internal/agent/codex/session_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
)

func (s *Session) startNative(ctx context.Context, plan SessionPlan, req proto.PromptRequestPayload) error {
func (s *Session) startNative(ctx context.Context, plan SessionPlan, turn proto.TurnSettings, req proto.PromptRequestPayload) error {
if s.currentThreadID() == "" {
if err := s.resolveThread(req, plan); err != nil {
return err
Expand All @@ -20,9 +20,21 @@ func (s *Session) startNative(ctx context.Context, plan SessionPlan, req proto.P
return err
}
turnParams := TurnStartParams{
ThreadID: s.currentThreadID(),
Input: input,
OutputSchema: plan.OutputSchema,
ThreadID: s.currentThreadID(),
Input: input,
OutputSchema: plan.OutputSchema,
Model: turn.Model,
Effort: turn.ReasoningEffort,
ApprovalPolicy: approvalPolicyNever, ApprovalsReviewer: approvalsReviewerUser,
}
if turn.ServiceTier != "" {
turnParams.ServiceTier = turn.ServiceTier
}
if turn.Review == "auto_review" {
turnParams.ApprovalPolicy, turnParams.ApprovalsReviewer = approvalPolicyOnRequest, turn.Review
}
if turn.Model != "" {
s.resolvedModel = turn.Model
}
turnCtx, turnCancel := context.WithTimeout(ctx, 10*time.Second)
_, ackErr := s.rpc.requestWithResult(turnCtx, "turn/start", turnParams, s.bindTurnResult)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func TestBlockedSteeringWriteEndsRunWithTerminalFrames(t *testing.T) {
ready <- nil
}()
// Executor.StartTurn starts and settles each Turn through these two steps.
go s.settleExecutorTurn(s.startNative(ctx, SessionPlan{Model: "synthetic"}, proto.PromptRequestPayload{Input: proto.TextInput("first")}))
go s.settleExecutorTurn(s.startNative(ctx, SessionPlan{Model: "synthetic"}, proto.TurnSettings{Model: "synthetic"}, proto.PromptRequestPayload{Input: proto.TextInput("first")}))
if err := <-ready; err != nil {
t.Fatal(err)
}
Expand Down
7 changes: 5 additions & 2 deletions apps/daemon/internal/agent/codex/session_thread.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import (
"fmt"
)

// Harnesses run unattended with the launching user's permissions: Codex never
// raises a native approval request and applies no inner sandbox.
// Harnesses run unattended with the launching user's permissions and no inner
// sandbox. Codex asks for approval only under review auto_review, which sends
// each request to its native reviewer.
const (
approvalPolicyNever = "never"
approvalPolicyOnRequest = "on-request"
approvalsReviewerUser = "user"
sandboxDangerFullAccess = "danger-full-access"
)

Expand Down
6 changes: 3 additions & 3 deletions apps/daemon/internal/agent/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ func TestEveryRegistryEntryPreparesTheBoundModelConfiguration(t *testing.T) {
configuration := harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "responses"}}}
calls := 0
expected := errors.New("native entry reached")
registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, configuration)
registry.RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Declaration{Configuration: configuration})
registry.RegisterExecutor("fixture", func(context.Context, proto.PromptRequestPayload) (agent.Executor, error) {
calls++
return nil, expected
})
configuration.Providers[0].Protocol = "anthropic"
executor, _ := registry.ResolveExecutor("fixture")
executor, _, _ := registry.ResolveExecutor("fixture")
for _, request := range []proto.PromptRequestPayload{
{Turn: proto.TurnSettings{Model: " "}},
{Turn: proto.TurnSettings{Model: "fixture"}, AgentOptions: map[string]any{"model_provider": map[string]any{"protocol": "anthropic", "base_url": "https://provider.example", "api_key": "private-sentinel"}}},
Expand All @@ -46,5 +46,5 @@ func TestRegistryRejectsInvalidConfigurationDeclaration(t *testing.T) {
t.Fatal("invalid configuration registered")
}
}()
agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "unknown"}}})
agent.NewRegistry().RegisterKind(proto.SupportedAgentKind{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}, harnessconfig.Declaration{Configuration: harnessconfig.Configuration{Providers: []harnessconfig.Provider{{Protocol: "unknown"}}}})
}
14 changes: 7 additions & 7 deletions apps/daemon/internal/agent/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (r *Registry) Register(declaration Declaration, runtime Runtime) {
if widened := Widened(declaration.Capabilities, advertised); len(widened) > 0 {
panic("agent.Registry.Register: discovery widened " + strings.Join(widened, ", "))
}
r.RegisterKind(runtime.Info, declaration.Configuration)
r.RegisterKind(runtime.Info, declaration.Declaration)
if runtime.Executor != nil {
r.RegisterExecutor(runtime.Info.Kind, runtime.Executor)
}
Expand Down Expand Up @@ -225,21 +225,21 @@ type Installation struct {
// RegisterKind installs the heartbeat descriptor and model configuration for an
// agent_kind. Callers may set Available=false when an adapter exists but its
// underlying CLI is not usable.
func (r *Registry) RegisterKind(info proto.SupportedAgentKind, configuration harnessconfig.Configuration) {
func (r *Registry) RegisterKind(info proto.SupportedAgentKind, declaration harnessconfig.Declaration) {
kind := info.Kind
if kind == "" {
panic("agent.Registry.Register: empty kind")
}
if err := configuration.ValidateDeclaration(); err != nil {
if err := declaration.Configuration.ValidateDeclaration(); err != nil {
panic(err)
}
if err := info.ValidateDeclaration(); err != nil {
panic(err)
}
configuration = configuration.Clone()
declaration.Configuration = declaration.Configuration.Clone()
r.mu.Lock()
defer r.mu.Unlock()
r.configurations[kind] = configuration
r.declarations[kind] = declaration
delete(r.executors, kind)
info.Capabilities.Preparation = proto.CapabilityUnsupported
r.kinds[kind] = info
Expand All @@ -254,12 +254,12 @@ func (r *Registry) RegisterExecutor(kind string, factory ExecutorFactory) {
if !exists || factory == nil {
panic("agent.Registry.RegisterExecutor: registered kind and factory required")
}
configuration, declared := r.configurations[kind]
declaration, declared := r.declarations[kind]
if !declared {
panic("agent.Registry.RegisterExecutor: configuration required")
}
r.executors[kind] = func(ctx context.Context, req proto.PromptRequestPayload) (Executor, error) {
if _, err := configuration.Prepare(req.Turn.Model, req.AgentOptions); err != nil {
if _, err := declaration.Configuration.Prepare(req.Turn.Model, req.AgentOptions); err != nil {
return nil, err
}
return factory(ctx, req)
Expand Down
3 changes: 0 additions & 3 deletions apps/daemon/internal/agent/mcode/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ func validateExecutionRequest(req proto.PromptRequestPayload) error {
if err := declaration.Declaration.ValidateExecutionControls(req.ExecutionControls); err != nil {
return err
}
if err := declaration.Declaration.ValidateTurnSettings(req.Turn); err != nil {
return err
}
if !req.DisableSubagents {
if req.MaxConcurrentSubagents == nil || *req.MaxConcurrentSubagents < 1 {
return fmt.Errorf("mcode: Subagent concurrency limit is required")
Expand Down
22 changes: 12 additions & 10 deletions apps/daemon/internal/agent/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import (
// Registry keeps the daemon-advertised capability descriptor and execution
// factories for each agent_kind. Safe for concurrent use.
type Registry struct {
mu sync.RWMutex
executors map[string]ExecutorFactory
kinds map[string]proto.SupportedAgentKind
configurations map[string]harnessconfig.Configuration
mu sync.RWMutex
executors map[string]ExecutorFactory
kinds map[string]proto.SupportedAgentKind
declarations map[string]harnessconfig.Declaration
}

func NewRegistry() *Registry {
return &Registry{
executors: make(map[string]ExecutorFactory),
kinds: make(map[string]proto.SupportedAgentKind),
configurations: make(map[string]harnessconfig.Configuration),
executors: make(map[string]ExecutorFactory),
kinds: make(map[string]proto.SupportedAgentKind),
declarations: make(map[string]harnessconfig.Declaration),
}
}

Expand All @@ -47,12 +47,14 @@ func (r *Registry) SupportedAgentKinds() []proto.SupportedAgentKind {
return out
}

func (r *Registry) ResolveExecutor(kind string) (ExecutorFactory, error) {
// ResolveExecutor returns the factory of a kind and the declaration it was
// registered with.
func (r *Registry) ResolveExecutor(kind string) (ExecutorFactory, harnessconfig.Declaration, error) {
r.mu.RLock()
defer r.mu.RUnlock()
factory := r.executors[kind]
if factory == nil {
return nil, fmt.Errorf("agent: executor unavailable for %q", kind)
return nil, harnessconfig.Declaration{}, fmt.Errorf("agent: executor unavailable for %q", kind)
}
return factory, nil
return factory, r.declarations[kind], nil
}
Loading
Loading