From 790e04278cb12a38a6ca538b5dc4b5ebb96fab13 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 23 Oct 2026 18:25:16 +0000 Subject: [PATCH] Delete unused daemon and shared Go code - claudesdk prepare wrapper, localworkspace.ToolEnvironmentFile - agent.WorkspaceWriter interface, its stub methods, assertions and docs - Codex UserInputLocalImage, UserInput.Path, ThreadStartedNotification, providerConfig Name/HTTPHeaders/QueryParams and their TOML rendering - Write-only fields: codex ModelReasoningEffort, rpc exitCode, subagentEffect source/turn/call, MCPBinding ConnectionOrigin/CredentialAuthority, EnvironmentMCP.WorkspaceRoot, PreparedConfiguration.Provider, transport DialOptions.HTTPHeader - proto DonePayload.Transcript, Usage InputTokens/OutputTokens/CostUSD - Unread Codex decode fields: Thread preview/updatedAt/cliVersion, TurnError additionalDetails, item startedAtMs/completedAtMs - claude-sdk-adapter textInput export (moved into its test) - Correct the OAC_RUNTIME_HOME comment in paths --- .../internal/agent/claudesdk/contracts.go | 2 - .../claudesdk/execution_controls_test.go | 28 +++++----- .../internal/agent/claudesdk/local_test.go | 6 +-- .../agent/claudesdk/mcp_bearer_test.go | 4 +- .../agent/claudesdk/mcp_environment_test.go | 8 +-- .../internal/agent/claudesdk/mcp_test.go | 2 +- .../internal/agent/claudesdk/options.go | 12 ----- .../internal/agent/claudesdk/options_test.go | 2 +- .../agent/claudesdk/subagents_test.go | 8 +-- .../internal/agent/claudesdk/unsupported.go | 15 ------ .../agent/claudesdk/unsupported_test.go | 34 ------------ .../internal/agent/claudesdk/usage_test.go | 4 +- .../agent/claudesdk/workspace_test.go | 8 +-- apps/daemon/internal/agent/codex/contracts.go | 2 - .../agent/codex/error_classification_test.go | 2 +- .../agent/codex/mcp_environment_test.go | 4 +- apps/daemon/internal/agent/codex/options.go | 4 -- apps/daemon/internal/agent/codex/protocol.go | 43 ++++++--------- .../internal/agent/codex/provider_config.go | 52 +------------------ .../agent/codex/provider_config_test.go | 15 +----- .../internal/agent/codex/resume_test.go | 4 +- apps/daemon/internal/agent/codex/rpc.go | 9 +--- .../codex/session_notifications_rpc_test.go | 4 +- .../internal/agent/codex/session_usage.go | 2 +- .../agent/codex/session_usage_test.go | 10 ++-- .../internal/agent/codex/subagent_rollout.go | 3 +- .../internal/agent/codex/unsupported.go | 8 --- .../internal/agent/codex/unsupported_test.go | 7 --- .../agent/contract_declarations_test.go | 1 - apps/daemon/internal/agent/harness.go | 5 -- apps/daemon/internal/agent/mcode/contracts.go | 2 - .../agent/mcode/environment_mcp_test.go | 2 +- .../internal/agent/mcode/session_test.go | 2 +- .../internal/agent/mcode/unsupported.go | 8 --- .../internal/agent/mcode/unsupported_test.go | 7 --- apps/daemon/internal/agent/mcp_binding.go | 26 ++++------ .../daemon/internal/agent/mcp_binding_test.go | 6 +-- .../dispatch/preparation_cancel_test.go | 2 +- .../internal/localworkspace/capabilities.go | 1 - .../internal/localworkspace/initialization.go | 22 ++------ .../internal/localworkspace/mcp_test.go | 2 +- apps/daemon/internal/localworkspace/write.go | 2 - apps/daemon/internal/paths/paths.go | 3 +- apps/daemon/internal/transport/ws.go | 10 +--- apps/daemon/internal/transport/ws_test.go | 5 -- contracts/agents-api/harness-onboarding.md | 2 +- contracts/agents-api/zh/harness-onboarding.md | 4 +- internal/agentdaemon/proto/envelope_test.go | 4 +- internal/agentdaemon/proto/environment.go | 1 - internal/agentdaemon/proto/inbound.go | 12 ++--- internal/harnessconfig/harness.go | 11 ++-- internal/harnessconfig/preparation_test.go | 8 ++- packages/claude-sdk-adapter/README.md | 2 +- .../claude-sdk-adapter/src/message_input.ts | 2 - .../claude-sdk-adapter/tests/inputs.test.mjs | 3 +- .../native_classification_integration_test.go | 4 +- .../internal/execution/engine_failure_test.go | 14 ++--- .../core/internal/execution/journal_test.go | 8 +-- .../execution/runtime_protocol_test.go | 4 +- .../integration/archive_cancellation_test.go | 4 +- .../core/tests/integration/dispatch_test.go | 10 ++-- .../integration/execution_events_test.go | 6 +-- 62 files changed, 133 insertions(+), 374 deletions(-) delete mode 100644 apps/daemon/internal/agent/claudesdk/unsupported.go delete mode 100644 apps/daemon/internal/agent/claudesdk/unsupported_test.go diff --git a/apps/daemon/internal/agent/claudesdk/contracts.go b/apps/daemon/internal/agent/claudesdk/contracts.go index 939607cf9..8027eb5cf 100644 --- a/apps/daemon/internal/agent/claudesdk/contracts.go +++ b/apps/daemon/internal/agent/claudesdk/contracts.go @@ -12,8 +12,6 @@ var ( _ agent.FunctionResultSubmitter = (*session)(nil) _ agent.WorkspaceReader = (*session)(nil) _ agent.WorkspaceDirectoryLister = (*session)(nil) - _ agent.WorkspaceWriter = (*session)(nil) _ agent.WorkspaceReader = (*executor)(nil) _ agent.WorkspaceDirectoryLister = (*executor)(nil) - _ agent.WorkspaceWriter = (*executor)(nil) ) diff --git a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go index e35e48068..ffd8c8230 100644 --- a/apps/daemon/internal/agent/claudesdk/execution_controls_test.go +++ b/apps/daemon/internal/agent/claudesdk/execution_controls_test.go @@ -18,13 +18,13 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) { t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Original input."), AgentSessionID: "native-session", AgentOptions: map[string]any{"model": "native-model", "system_prompt": "Keep these exact instructions.\nDo not replace them."}} - ordinary, _, err := prepare(config, request) + ordinary, _, err := prepareConfiguration(config, request) if err != nil { t.Fatal(err) } request.ExecutionControls = &proto.ExecutionControls{WebSearch: &proto.WebSearchControl{Mode: "disabled", ContextSize: "medium"}, TextVerbosity: "medium"} before, _ := json.Marshal(request) - controlled, _, err := prepare(config, request) + controlled, _, err := prepareConfiguration(config, request) if err != nil { t.Fatal(err) } @@ -38,7 +38,7 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) { } // Without a search control Claude Code keeps its native web tools. request.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium"} - absent, _, err := prepare(config, request) + absent, _, err := prepareConfiguration(config, request) if err != nil || !reflect.DeepEqual(ordinary, absent) { t.Fatal("absent search control changed the native configuration", err) } @@ -48,14 +48,14 @@ func TestLiveWebSearchReachesTheBridgeOnEveryPlacement(t *testing.T) { live := &proto.ExecutionControls{WebSearch: &proto.WebSearchControl{Mode: "live", ContextSize: "medium"}, TextVerbosity: "medium"} root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) - none, _, err := prepare(Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")}, proto.PromptRequestPayload{ + none, _, err := prepareConfiguration(Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")}, proto.PromptRequestPayload{ RunID: "run", Input: proto.TextInput("Search."), ExecutionControls: live, AgentOptions: map[string]any{"model": "native-model"}}) if err != nil || none.WebSearch != "live" { t.Fatal("live search on environment none did not reach the bridge", err) } request := workspaceRequest() request.ExecutionControls = live - workspace, _, err := prepare(workspaceFixture(t), request) + workspace, _, err := prepareConfiguration(workspaceFixture(t), request) if err != nil || workspace.WebSearch != "live" { t.Fatal("live search on a workspace did not reach the bridge", err) } @@ -97,7 +97,7 @@ func TestReasoningEffortReachesNativeOptions(t *testing.T) { config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} for _, effort := range []string{"low", "medium", "high", "xhigh", "max"} { request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium", ReasoningEffort: effort}, AgentOptions: map[string]any{"model": "native-model", "harness_config": map[string]any{"thinking": map[string]any{"type": "adaptive"}}}} - start, _, err := prepare(config, request) + start, _, err := prepareConfiguration(config, request) if err != nil { t.Fatal(effort, err) } @@ -128,21 +128,21 @@ func TestStructuredOutputConfigurationReachesNativeUnchanged(t *testing.T) { config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")} schema := json.RawMessage(`{"type":"object","properties":{"n":{"const":9007199254740992}}}`) request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Original input."), DisableSubagents: true, AgentOptions: map[string]any{"model": "model", "system_prompt": "Original instructions."}, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}} - start, _, err := prepare(config, request) + start, _, err := prepareConfiguration(config, request) if err != nil { t.Fatal(err) } - if start.OutputFormat == nil || string(start.OutputFormat.Schema) != string(schema) || *start.Input[0].Content[0].Text != *request.Input[0].Content[0].Text || start.SystemPrompt != "Original instructions." { + if start.OutputFormat == nil || string(start.OutputFormat.Schema) != string(schema) || start.SystemPrompt != "Original instructions." { t.Fatal("native configuration changed") } request.ExecutionControls.OutputFormat.Schema = json.RawMessage(`{"type":"object","const":9007199254740993}`) - if _, _, err := prepare(config, request); err == nil { + if _, _, err := prepareConfiguration(config, request); err == nil { t.Fatal("lossy schema accepted") } request.ExecutionControls.OutputFormat.Schema = schema request.DisableExecutionEnvironment = true request.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp"}} - if start, _, err := prepare(config, request); err != nil || start.OutputFormat == nil { + if start, _, err := prepareConfiguration(config, request); err != nil || start.OutputFormat == nil { t.Fatal("native structured output was narrowed by MCP", err) } } @@ -155,17 +155,17 @@ func TestToolDiscoveryPreservesFrozenFunctionsBesideOtherProfiles(t *testing.T) {Name: "lookup", Description: "Lookup", Parameters: json.RawMessage(`{"type":"object","properties":{"ticket":{"const":"original"}}}`), DeferLoading: true}, {Name: "clock", Description: "Clock", Parameters: json.RawMessage(`{"type":"object"}`)}, }} - start, _, err := prepare(config, request) + start, _, err := prepareConfiguration(config, request) if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions, request.FunctionTools) { t.Fatal("function discovery changed native definitions", err) } request.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: json.RawMessage(`{"type":"object"}`)}} - if start, _, err := prepare(config, request); err != nil || !start.ToolSearch || start.OutputFormat == nil { + if start, _, err := prepareConfiguration(config, request); err != nil || !start.ToolSearch || start.OutputFormat == nil { t.Fatal("native tool search was narrowed by structured output", err) } request.ExecutionControls = nil request.ToolSearch = false - if _, _, err := prepare(config, request); err == nil { + if _, _, err := prepareConfiguration(config, request); err == nil { t.Fatal("deferred definitions became eager") } } @@ -177,7 +177,7 @@ func TestReasoningSummaryReachesTheBridgeOnlyWhenRequested(t *testing.T) { request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Input."), AgentOptions: map[string]any{"model": "native-model"}} for _, summary := range []string{"", "auto"} { request.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium", ReasoningSummary: summary} - start, _, err := prepare(config, request) + start, _, err := prepareConfiguration(config, request) if err != nil || start.ReasoningSummary != (summary != "") { t.Fatalf("summary %q: reasoning flag %v, err %v", summary, start.ReasoningSummary, err) } diff --git a/apps/daemon/internal/agent/claudesdk/local_test.go b/apps/daemon/internal/agent/claudesdk/local_test.go index 951fa0ae8..09ff0d219 100644 --- a/apps/daemon/internal/agent/claudesdk/local_test.go +++ b/apps/daemon/internal/agent/claudesdk/local_test.go @@ -15,7 +15,7 @@ func TestLocalWorkspaceBindingAndRequiredHistory(t *testing.T) { req := workspaceRequest() req.LocalEnvironment = &proto.LocalEnvironment{ID: "environment", WorkspaceRoot: config.Workspace.Directory} req.RequireExistingNativeSession = true - start, _, err := prepare(config, req) + start, _, err := prepareConfiguration(config, req) if err != nil || !start.RequireHistory { t.Fatal(start, err) } @@ -26,7 +26,7 @@ func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) { original := slices.Clone(config.Env) req := workspaceRequest() req.AgentOptions["model_provider"] = map[string]any{"protocol": "anthropic", "base_url": "https://provider.example/anthropic", "api_key": "selected-secret", "context_window": 64000, "max_output_tokens": 4096} - start, env, err := prepare(config, req) + start, env, err := prepareConfiguration(config, req) if err != nil { t.Fatal(err) } @@ -42,7 +42,7 @@ func TestWorkspaceProviderCredentialsReplaceAmbientSelection(t *testing.T) { } for _, value := range []any{nil, "secret", map[string]any{"protocol": "anthropic", "base_url": "http://provider.example", "api_key": "secret"}, map[string]any{"protocol": "anthropic", "base_url": "https://user:pass@provider.example", "api_key": "secret"}} { req.AgentOptions["model_provider"] = value - if _, _, err := prepare(config, req); err == nil || strings.Contains(err.Error(), "secret") { + if _, _, err := prepareConfiguration(config, req); err == nil || strings.Contains(err.Error(), "secret") { t.Fatal("unsafe provider accepted or disclosed") } } diff --git a/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go b/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go index 69a52785b..6fd5830cd 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_bearer_test.go @@ -25,7 +25,7 @@ func TestMCPBearerUsesFreshOwnedEnvironmentReferences(t *testing.T) { req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, AgentOptions: map[string]any{"model": "fixture"}} seen := map[string]bool{} for range 2 { - start, env, err := prepare(config, req) + start, env, err := prepareConfiguration(config, req) if err != nil { t.Fatal(err) } @@ -67,7 +67,7 @@ func TestMCPBearerRejectsInvalidCredentialBeforeStateCreation(t *testing.T) { config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} servers := []proto.MCPHTTPServer{{ConnectionOrigin: "service", ServerLabel: "fixture", ServerURL: "https://example.invalid/mcp", BearerToken: &token}} req := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), DisableExecutionEnvironment: true, MCPHTTPServers: &servers, AgentOptions: map[string]any{"model": "fixture"}} - if _, _, err := prepare(config, req); err == nil || err.Error() != "claudesdk: unsupported HTTPS MCP bearer credential" { + if _, _, err := prepareConfiguration(config, req); err == nil || err.Error() != "claudesdk: unsupported HTTPS MCP bearer credential" { t.Fatal("invalid bearer accepted or unsafe error returned") } entries, err := os.ReadDir(root) diff --git a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go index d1c87211e..60f09c530 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_environment_test.go @@ -17,10 +17,10 @@ func TestEnvironmentMCPUsesInstalledLauncherAndSelectedCredential(t *testing.T) token := "selected-user-token" t.Setenv("MCP_TOKEN", "unselected-native-token") req.LocalEnvironment = &proto.LocalEnvironment{CapabilityRoot: "/private/runtime/capabilities", WorkspaceRoot: config.Workspace.Directory, MCP: []proto.EnvironmentMCP{ - {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/local", Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: "untrusted-package-command", Args: []string{"package-argument"}, EnvVars: []string{"MCP_TOKEN"}}}, - {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp", BearerTokenEnvVar: "MCP_TOKEN"}, BearerToken: &token}, + {InstallationRoot: "/private/runtime/capabilities", PackageRoot: "plugins/local", Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: "untrusted-package-command", Args: []string{"package-argument"}, EnvVars: []string{"MCP_TOKEN"}}}, + {InstallationRoot: "/private/runtime/capabilities", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp", BearerTokenEnvVar: "MCP_TOKEN"}, BearerToken: &token}, }} - start, env, err := prepare(config, req) + start, env, err := prepareConfiguration(config, req) if err != nil { t.Fatal(err) } @@ -63,7 +63,7 @@ func TestEnvironmentMCPRejectsUnqualifiedCombinations(t *testing.T) { e.MCP[0].Server.URL = "http://example.invalid/mcp" }, } { - environment := &proto.LocalEnvironment{MCP: []proto.EnvironmentMCP{{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"}}}} + environment := &proto.LocalEnvironment{MCP: []proto.EnvironmentMCP{{InstallationRoot: "/private/runtime/capabilities", PackageRoot: "plugins/remote", Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.invalid/mcp"}}}} mutate(environment) if _, _, err := prepareRuntimeMCP(proto.PromptRequestPayload{LocalEnvironment: environment}); err == nil { t.Fatal("unsupported declaration accepted") diff --git a/apps/daemon/internal/agent/claudesdk/mcp_test.go b/apps/daemon/internal/agent/claudesdk/mcp_test.go index f219d101d..25ba5ab6b 100644 --- a/apps/daemon/internal/agent/claudesdk/mcp_test.go +++ b/apps/daemon/internal/agent/claudesdk/mcp_test.go @@ -46,7 +46,7 @@ func TestHTTPMCPDeclaration(t *testing.T) { case "environment": req.DisableExecutionEnvironment = false } - start, _, err := prepare(config, req) + start, _, err := prepareConfiguration(config, req) valid := mode == "unrestricted" || mode == "selected" || mode == "empty" || mode == "nil-slice" || mode == "auth" || mode == "required" || mode == "query" || mode == "wildcard" if (err == nil) != valid { t.Fatalf("unexpected admission: %v", err) diff --git a/apps/daemon/internal/agent/claudesdk/options.go b/apps/daemon/internal/agent/claudesdk/options.go index 665896f47..696006e8b 100644 --- a/apps/daemon/internal/agent/claudesdk/options.go +++ b/apps/daemon/internal/agent/claudesdk/options.go @@ -41,18 +41,6 @@ type startRequest struct { RequireHistory bool `json:"require_history,omitempty"` } -func prepare(config Config, req proto.PromptRequestPayload) (startRequest, []string, error) { - if req.RunID == "" || req.Input.Validate() != nil { - return startRequest{}, nil, fmt.Errorf("claudesdk: run id and prompt are required") - } - start, env, err := prepareConfiguration(config, req) - if err != nil { - return startRequest{}, nil, err - } - start.Input = req.Input - return start, env, nil -} - func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startRequest, []string, error) { start := startRequest{Type: "start", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, Functions: req.FunctionTools} fail := func(reason string) (startRequest, []string, error) { diff --git a/apps/daemon/internal/agent/claudesdk/options_test.go b/apps/daemon/internal/agent/claudesdk/options_test.go index 3eb0a859f..c3dc1305d 100644 --- a/apps/daemon/internal/agent/claudesdk/options_test.go +++ b/apps/daemon/internal/agent/claudesdk/options_test.go @@ -25,7 +25,7 @@ func TestNullableSystemPrompt(t *testing.T) { root := t.TempDir() t.Setenv("OAC_RUNTIME_HOME", root) config := Config{Entrypoint: filepath.Join(root, "main.js"), StateDir: filepath.Join(root, "state")} - start, _, err := prepare(config, proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentOptions: tc.options}) + start, _, err := prepareConfiguration(config, proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("hello"), AgentOptions: tc.options}) if (err != nil) != tc.reject || (!tc.reject && start.SystemPrompt != tc.want) { t.Fatalf("system prompt %q, error %v", start.SystemPrompt, err) } diff --git a/apps/daemon/internal/agent/claudesdk/subagents_test.go b/apps/daemon/internal/agent/claudesdk/subagents_test.go index 0acaef1b4..4293d38f1 100644 --- a/apps/daemon/internal/agent/claudesdk/subagents_test.go +++ b/apps/daemon/internal/agent/claudesdk/subagents_test.go @@ -9,18 +9,18 @@ import ( func TestSubagentConfigurationUsesExplicitRequestAndFrozenLimit(t *testing.T) { config := workspaceFixture(t) req := workspaceRequest() - start, _, err := prepare(config, req) + start, _, err := prepareConfiguration(config, req) if err != nil || start.Subagents != nil { t.Fatal("ordinary execution changed", err) } req.DisableSubagents, req.ObserveSubagentIdentities = false, true - start, _, err = prepare(config, req) + start, _, err = prepareConfiguration(config, req) if err != nil || start.Subagents == nil || start.Subagents.MaxConcurrent != 6 { t.Fatal("missing default native admission limit", err) } limit := 2 req.MaxConcurrentSubagents = &limit - start, _, err = prepare(config, req) + start, _, err = prepareConfiguration(config, req) limit = 4 if err != nil || start.Subagents.MaxConcurrent != 2 { t.Fatal("subagent configuration was not frozen", err) @@ -36,7 +36,7 @@ func TestSubagentConfigurationRejectsUnqualifiedAuthority(t *testing.T) { req := workspaceRequest() req.DisableSubagents, req.ObserveSubagentIdentities = false, true change(&req) - if _, _, err := prepare(config, req); err == nil { + if _, _, err := prepareConfiguration(config, req); err == nil { t.Fatal("unqualified subagent combination accepted") } } diff --git a/apps/daemon/internal/agent/claudesdk/unsupported.go b/apps/daemon/internal/agent/claudesdk/unsupported.go deleted file mode 100644 index 154d904a4..000000000 --- a/apps/daemon/internal/agent/claudesdk/unsupported.go +++ /dev/null @@ -1,15 +0,0 @@ -package claudesdk - -import ( - "context" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" -) - -func (s *executor) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { - return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported -} - -func (s *session) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { - return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported -} diff --git a/apps/daemon/internal/agent/claudesdk/unsupported_test.go b/apps/daemon/internal/agent/claudesdk/unsupported_test.go deleted file mode 100644 index 7fe6048c5..000000000 --- a/apps/daemon/internal/agent/claudesdk/unsupported_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package claudesdk - -import ( - "context" - "errors" - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent" -) - -// Nil concrete receivers prove Unsupported needs no native owner, transport, -// workspace access or input retention. Callers still preserve the separate -// nil-Turn ownership rule on required lifecycle methods. -func TestUnsupportedExtensionsHaveNoNativeEffects(t *testing.T) { - ctx := context.Background() - const secret = "private-fixture-value" - check := func(err error) { - t.Helper() - if !errors.Is(err, agent.ErrUnsupportedOperation) || err.Error() == agent.ErrUnsupportedOperation.Error() { - t.Fatalf("expected explicit unsupported result with reason, got %v", err) - } - if strings.Contains(err.Error(), secret) { - t.Fatal("unsupported error disclosed input") - } - } - for _, owner := range []agent.WorkspaceWriter{(*executor)(nil), (*session)(nil)} { - result, err := owner.WriteWorkspaceFile(ctx, secret, []byte(secret)) - check(err) - if result.SizeBytes != 0 { - t.Fatal("unsupported write fabricated receipt") - } - } -} diff --git a/apps/daemon/internal/agent/claudesdk/usage_test.go b/apps/daemon/internal/agent/claudesdk/usage_test.go index 0c3af818f..d2ad262d5 100644 --- a/apps/daemon/internal/agent/claudesdk/usage_test.go +++ b/apps/daemon/internal/agent/claudesdk/usage_test.go @@ -73,7 +73,7 @@ func TestUsageTransportPreservesSnapshotOnFailureAndDone(t *testing.T) { } var want map[string]any _ = json.Unmarshal([]byte(original), &want) - if observed.Provider != "claude_code" || !reflect.DeepEqual(observed.Raw["claude_sdk_result"], want) || observed.Model != "" || observed.Tokens != nil || observed.CostUSD != 0 || observed.InputTokens != 0 || observed.OutputTokens != 0 { + if observed.Provider != "claude_code" || !reflect.DeepEqual(observed.Raw["claude_sdk_result"], want) || observed.Model != "" || observed.Tokens != nil { t.Fatalf("usage normalized or lost: %+v", observed) } } @@ -137,7 +137,7 @@ func verifyLiveUsageEvents(t *testing.T, events []proto.Envelope) { } } } - if count < 1 || observed.Tokens != nil || observed.Model != "" || observed.CostUSD != 0 { + if count < 1 || observed.Tokens != nil || observed.Model != "" { t.Fatal("live native evidence became unsupported public accounting") } if count > 1 { diff --git a/apps/daemon/internal/agent/claudesdk/workspace_test.go b/apps/daemon/internal/agent/claudesdk/workspace_test.go index c5949c07b..de2eac9a1 100644 --- a/apps/daemon/internal/agent/claudesdk/workspace_test.go +++ b/apps/daemon/internal/agent/claudesdk/workspace_test.go @@ -43,7 +43,7 @@ func TestWorkspaceTrustedBindingAndEnvironment(t *testing.T) { config := workspaceFixture(t) t.Setenv("OAC_TEST_PARENT_SECRET", "parent-only") t.Setenv("ANTHROPIC_API_KEY", "unselected-provider") - start, env, err := prepare(config, workspaceRequest()) + start, env, err := prepareConfiguration(config, workspaceRequest()) if err != nil { t.Fatal(err) } @@ -112,7 +112,7 @@ func TestWorkspaceRejectsConflictsBeforeSideEffects(t *testing.T) { config.Env = append(config.Env, "ANTHROPIC_API_KEY=bad\x00value") } - if _, _, err := prepare(config, req); err == nil { + if _, _, err := prepareConfiguration(config, req); err == nil { t.Fatal("invalid binding or request accepted") } entries, err := os.ReadDir(config.StateDir) @@ -127,7 +127,7 @@ func TestWorkspaceRetainsDeclaredFunctions(t *testing.T) { config := workspaceFixture(t) req := workspaceRequest() req.FunctionTools = []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object"}`)}} - start, _, err := prepare(config, req) + start, _, err := prepareConfiguration(config, req) if err != nil { t.Fatal(err) } @@ -143,7 +143,7 @@ func TestPublicMCPUsesWorkspaceProjectionWithoutCredentialCopy(t *testing.T) { token := "vault-selected-canary" tools := []string{"prove"} req.MCPHTTPServers = &[]proto.MCPHTTPServer{{ConnectionOrigin: "environment", ServerLabel: "remote", ServerURL: "https://example.test/mcp", AllowedTools: &tools, Required: true, BearerToken: &token}} - start, env, err := prepare(config, req) + start, env, err := prepareConfiguration(config, req) if err != nil { t.Fatal(err) } diff --git a/apps/daemon/internal/agent/codex/contracts.go b/apps/daemon/internal/agent/codex/contracts.go index 97859b972..90763a671 100644 --- a/apps/daemon/internal/agent/codex/contracts.go +++ b/apps/daemon/internal/agent/codex/contracts.go @@ -12,8 +12,6 @@ var ( _ agent.FunctionResultSubmitter = (*Session)(nil) _ agent.WorkspaceReader = (*Session)(nil) _ agent.WorkspaceDirectoryLister = (*Session)(nil) - _ agent.WorkspaceWriter = (*Session)(nil) _ agent.WorkspaceReader = (*Executor)(nil) _ agent.WorkspaceDirectoryLister = (*Executor)(nil) - _ agent.WorkspaceWriter = (*Executor)(nil) ) diff --git a/apps/daemon/internal/agent/codex/error_classification_test.go b/apps/daemon/internal/agent/codex/error_classification_test.go index 024a09873..bceba9b8a 100644 --- a/apps/daemon/internal/agent/codex/error_classification_test.go +++ b/apps/daemon/internal/agent/codex/error_classification_test.go @@ -82,7 +82,7 @@ func TestClassificationOnlyFromRootTerminalError(t *testing.T) { done = true var p proto.DonePayload _ = env.DecodePayload(&p) - if p.Metadata[proto.DoneMetaAgentSessionID] != "root" || p.Usage.InputTokens != 7 { + if p.Metadata[proto.DoneMetaAgentSessionID] != "root" { t.Fatal(p) } } diff --git a/apps/daemon/internal/agent/codex/mcp_environment_test.go b/apps/daemon/internal/agent/codex/mcp_environment_test.go index 147a2e7c3..e2674b1ce 100644 --- a/apps/daemon/internal/agent/codex/mcp_environment_test.go +++ b/apps/daemon/internal/agent/codex/mcp_environment_test.go @@ -14,8 +14,8 @@ import ( func TestEnvironmentMCPProjectsIsolatedStdioAndPrivateHTTPReferences(t *testing.T) { token := "user-token" local := &proto.LocalEnvironment{MCP: []proto.EnvironmentMCP{ - {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/0", Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: "must-not-be-native-command", Args: []string{"private-argument"}}}, - {InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/1", BearerToken: &token, Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.com/mcp", HTTPHeaders: map[string]string{"X-Key": "literal-${DO_NOT_EXPAND}"}}}, + {InstallationRoot: "/private/runtime/capabilities", PackageRoot: "plugins/0", Server: agentplugin.MCPServer{Name: "local", Type: "stdio", Command: "must-not-be-native-command", Args: []string{"private-argument"}}}, + {InstallationRoot: "/private/runtime/capabilities", PackageRoot: "plugins/1", BearerToken: &token, Server: agentplugin.MCPServer{Name: "remote", Type: "http", URL: "https://example.com/mcp", HTTPHeaders: map[string]string{"X-Key": "literal-${DO_NOT_EXPAND}"}}}, }} servers, env, err := runtimeMCPServers(proto.PromptRequestPayload{LocalEnvironment: local}) if err != nil || len(servers) != 2 || len(env) != 1 { diff --git a/apps/daemon/internal/agent/codex/options.go b/apps/daemon/internal/agent/codex/options.go index 4e2c4021b..bdd22d4ae 100644 --- a/apps/daemon/internal/agent/codex/options.go +++ b/apps/daemon/internal/agent/codex/options.go @@ -52,9 +52,6 @@ type SessionPlan struct { // SystemPrompt is forwarded as developerInstructions on thread/start. SystemPrompt string - // ModelReasoningEffort is frozen for launch and every native Turn. - ModelReasoningEffort string - // ReasoningSummary reports that the Agent requested reasoning summaries. ReasoningSummary bool @@ -196,7 +193,6 @@ func applyExecutionControls(plan *SessionPlan, controls *proto.ExecutionControls plan.OutputSchema = format.Schema } if controls.ReasoningEffort != "" { - plan.ModelReasoningEffort = controls.ReasoningEffort set("model_reasoning_effort", strconv(controls.ReasoningEffort)) } if controls.ReasoningSummary != "" { diff --git a/apps/daemon/internal/agent/codex/protocol.go b/apps/daemon/internal/agent/codex/protocol.go index 2e9bd8a89..baa591ffd 100644 --- a/apps/daemon/internal/agent/codex/protocol.go +++ b/apps/daemon/internal/agent/codex/protocol.go @@ -112,14 +112,11 @@ type ThreadStartParams struct { } type Thread struct { - ID string `json:"id"` - Cwd string `json:"cwd,omitempty"` - Preview string `json:"preview,omitempty"` - UpdatedAt int64 `json:"updatedAt,omitempty"` - CreatedAt int64 `json:"createdAt,omitempty"` - Status any `json:"status,omitempty"` - Path string `json:"path,omitempty"` - CLIVersion string `json:"cliVersion,omitempty"` + ID string `json:"id"` + Cwd string `json:"cwd,omitempty"` + CreatedAt int64 `json:"createdAt,omitempty"` + Status any `json:"status,omitempty"` + Path string `json:"path,omitempty"` } type ThreadStartResult struct { @@ -144,9 +141,8 @@ type ThreadResumeParams struct { type UserInputType string const ( - UserInputText UserInputType = "text" - UserInputLocalImage UserInputType = "localImage" - UserInputRemoteImg UserInputType = "image" + UserInputText UserInputType = "text" + UserInputRemoteImg UserInputType = "image" ) // UserInput is one element of TurnStartParams.Input. Each variant uses a @@ -157,8 +153,6 @@ type UserInput struct { // Text variant Text string `json:"text,omitempty"` TextElements []any `json:"text_elements,omitempty"` - // LocalImage variant - Path string `json:"path,omitempty"` // Image (remote URL) variant URL string `json:"url,omitempty"` } @@ -197,9 +191,8 @@ type Turn struct { // CodexErrorInfo is a native enum with string and object variants. type TurnError struct { - Message string `json:"message,omitempty"` - CodexErrorInfo json.RawMessage `json:"codexErrorInfo,omitempty"` - AdditionalDetails *string `json:"additionalDetails,omitempty"` + Message string `json:"message,omitempty"` + CodexErrorInfo json.RawMessage `json:"codexErrorInfo,omitempty"` } // --------------------------------------------------------------------------- @@ -247,10 +240,6 @@ type ThreadItem struct { // live as constants in session.go's handler registration). // --------------------------------------------------------------------------- -type ThreadStartedNotification struct { - Thread Thread `json:"thread"` -} - type TurnStartedNotification struct { ThreadID string `json:"threadId"` Turn Turn `json:"turn"` @@ -262,17 +251,15 @@ type TurnCompletedNotification struct { } type ItemStartedNotification struct { - ThreadID string `json:"threadId"` - TurnID string `json:"turnId"` - Item ThreadItem `json:"item"` - StartedAtMs int64 `json:"startedAtMs,omitempty"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` + Item ThreadItem `json:"item"` } type ItemCompletedNotification struct { - ThreadID string `json:"threadId"` - TurnID string `json:"turnId"` - Item ThreadItem `json:"item"` - CompletedAtMs int64 `json:"completedAtMs,omitempty"` + ThreadID string `json:"threadId"` + TurnID string `json:"turnId"` + Item ThreadItem `json:"item"` } type AgentMessageDeltaNotification struct { diff --git a/apps/daemon/internal/agent/codex/provider_config.go b/apps/daemon/internal/agent/codex/provider_config.go index 4f5b95df3..660dafa9f 100644 --- a/apps/daemon/internal/agent/codex/provider_config.go +++ b/apps/daemon/internal/agent/codex/provider_config.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "sort" "strings" ) @@ -25,20 +24,11 @@ const providerAPIKeyEnv = "OAC_CODEX_PROVIDER_API_KEY" // ModelProviderInfo enum (model-provider-info/src/lib.rs) so the // rendered TOML deserialises 1:1 against upstream. type providerConfig struct { - // Name is the human-readable label shown in codex UI; defaults to - // "OpenAgentCore" when empty. - Name string // BaseURL is the model provider's HTTPS endpoint, including any // path prefix (e.g. /v1). Required. BaseURL string // BearerToken is supplied only through the native env_key mechanism. BearerToken string - // HTTPHeaders is forwarded as `[model_providers.oac.http_headers]`. - // Keys / values rendered as TOML basic strings. - HTTPHeaders map[string]string - // QueryParams is forwarded as `[model_providers.oac.query_params]`, - // used by Azure (api-version=2025-04-01-preview). - QueryParams map[string]string // WireAPI is "responses" (codex Responses API) or "chat" (rejected // upstream since codex-rs 0.140). Empty defaults to "responses". WireAPI string @@ -76,12 +66,8 @@ func writeCodexProviderConfig(codexHome string, cfg providerConfig) error { b.WriteString(oacProviderSlug) b.WriteString("]\n") - name := strings.TrimSpace(cfg.Name) - if name == "" { - name = "OpenAgentCore" - } b.WriteString("name = ") - b.WriteString(tomlQuoteString(name)) + b.WriteString(tomlQuoteString("OpenAgentCore")) b.WriteByte('\n') b.WriteString("base_url = ") @@ -103,42 +89,6 @@ func writeCodexProviderConfig(codexHome string, cfg providerConfig) error { b.WriteString(tomlQuoteString(wireAPI)) b.WriteByte('\n') - if len(cfg.HTTPHeaders) > 0 { - keys := make([]string, 0, len(cfg.HTTPHeaders)) - for k := range cfg.HTTPHeaders { - keys = append(keys, k) - } - sort.Strings(keys) - b.WriteString("\n[model_providers.") - b.WriteString(oacProviderSlug) - b.WriteString(".http_headers]\n") - for _, k := range keys { - b.WriteString(tomlQuoteString(k)) - b.WriteString(" = ") - b.WriteString(tomlQuoteString(cfg.HTTPHeaders[k])) - b.WriteByte('\n') - } - } - - if len(cfg.QueryParams) > 0 { - keys := make([]string, 0, len(cfg.QueryParams)) - for k := range cfg.QueryParams { - keys = append(keys, k) - } - sort.Strings(keys) - b.WriteString("\n[model_providers.") - b.WriteString(oacProviderSlug) - b.WriteString(".query_params]\n") - for _, k := range keys { - b.WriteString(tomlQuoteString(k)) - b.WriteString(" = ") - b.WriteString(tomlQuoteString(cfg.QueryParams[k])) - b.WriteByte('\n') - } - } - - b.WriteByte('\n') - path := filepath.Join(codexHome, "config.toml") return appendConfigTOML(path, b.String()) } diff --git a/apps/daemon/internal/agent/codex/provider_config_test.go b/apps/daemon/internal/agent/codex/provider_config_test.go index 7eef92ede..8ab9b71f9 100644 --- a/apps/daemon/internal/agent/codex/provider_config_test.go +++ b/apps/daemon/internal/agent/codex/provider_config_test.go @@ -54,31 +54,18 @@ func TestWriteCodexProviderConfig_PinsResponsesWire(t *testing.T) { func TestWriteCodexProviderConfig_FullProvider(t *testing.T) { dir := t.TempDir() cfg := providerConfig{ - Name: "mygw", BaseURL: "https://platform-api.example.com/v1", BearerToken: "sk-test-fixture", WireAPI: "responses", - HTTPHeaders: map[string]string{ - "X-Sub-Module": "codex-internal", - "X-Request-ID": "abc", - }, - QueryParams: map[string]string{ - "api-version": "2025-04-01-preview", - }, } if err := writeCodexProviderConfig(dir, cfg); err != nil { t.Fatalf("write: %v", err) } body := mustReadFile(t, filepath.Join(dir, "config.toml")) for _, want := range []string{ - `name = "mygw"`, + `name = "OpenAgentCore"`, `base_url = "https://platform-api.example.com/v1"`, `env_key = "OAC_CODEX_PROVIDER_API_KEY"`, - `[model_providers.oac.http_headers]`, - `"X-Sub-Module" = "codex-internal"`, - `"X-Request-ID" = "abc"`, - `[model_providers.oac.query_params]`, - `"api-version" = "2025-04-01-preview"`, } { if !strings.Contains(body, want) { t.Errorf("config.toml missing %q\n---\n%s", want, body) diff --git a/apps/daemon/internal/agent/codex/resume_test.go b/apps/daemon/internal/agent/codex/resume_test.go index 698eb2cc7..b89149c6f 100644 --- a/apps/daemon/internal/agent/codex/resume_test.go +++ b/apps/daemon/internal/agent/codex/resume_test.go @@ -51,9 +51,9 @@ func TestCancellationKeepsConsumedTerminalOutputAndUsage(t *testing.T) { s.setThreadID("native") s.onTurnStarted(json.RawMessage(`{"threadId":"native","turn":{"id":"turn"}}`)) s.appendFinalText("Already produced") - s.onTurnCompleted(json.RawMessage(`{"threadId":"native","turn":{"id":"turn","status":"interrupted","usage":{"inputTokens":31,"outputTokens":7}}}`)) + s.onTurnCompleted(json.RawMessage(`{"threadId":"native","turn":{"id":"turn","status":"interrupted","usage":{"inputTokens":31,"cachedInputTokens":0,"outputTokens":7,"reasoningOutputTokens":0,"totalTokens":38}}}`)) snapshot := s.CancellationOutcome() - if snapshot.Content != "Already produced" || snapshot.Usage.InputTokens != 31 || snapshot.Usage.OutputTokens != 7 || snapshot.Metadata[proto.DoneMetaAgentSessionID] != "native" { + if snapshot.Content != "Already produced" || snapshot.Usage.Tokens.InputTokens != 31 || snapshot.Usage.Tokens.OutputTokens != 7 || snapshot.Metadata[proto.DoneMetaAgentSessionID] != "native" { t.Fatalf("incomplete terminal snapshot: %+v", snapshot) } } diff --git a/apps/daemon/internal/agent/codex/rpc.go b/apps/daemon/internal/agent/codex/rpc.go index 78607a12c..25634f3eb 100644 --- a/apps/daemon/internal/agent/codex/rpc.go +++ b/apps/daemon/internal/agent/codex/rpc.go @@ -87,9 +87,8 @@ type JSONRPCClient struct { stdout io.ReadCloser stderr io.ReadCloser - mu sync.Mutex - alive bool - exitCode *int + mu sync.Mutex + alive bool pendingMu sync.Mutex pending map[string]*pendingRequest @@ -465,10 +464,6 @@ func (c *JSONRPCClient) waitChild() { err := c.process.Wait() c.mu.Lock() c.alive = false - if c.cmd.ProcessState != nil { - code := c.cmd.ProcessState.ExitCode() - c.exitCode = &code - } c.mu.Unlock() _ = c.drainPending(fmt.Errorf("codex app-server exited: %v", err)) } diff --git a/apps/daemon/internal/agent/codex/session_notifications_rpc_test.go b/apps/daemon/internal/agent/codex/session_notifications_rpc_test.go index 516f9bbb6..c14d7cd98 100644 --- a/apps/daemon/internal/agent/codex/session_notifications_rpc_test.go +++ b/apps/daemon/internal/agent/codex/session_notifications_rpc_test.go @@ -109,11 +109,11 @@ func TestRootRPCNotificationOrdering(t *testing.T) { if err := env.DecodePayload(&done); err != nil { t.Fatal(err) } - wantInput := int32(130) + wantInput := int64(130) if resume { wantInput = 30 } - if done.Content != "root answer" || done.Metadata[proto.DoneMetaAgentSessionID] != "root" || done.Usage.InputTokens != wantInput { + if done.Content != "root answer" || done.Metadata[proto.DoneMetaAgentSessionID] != "root" || done.Usage.Tokens.InputTokens != wantInput { t.Fatalf("reply ordering lost root output/usage: %+v", done) } } diff --git a/apps/daemon/internal/agent/codex/session_usage.go b/apps/daemon/internal/agent/codex/session_usage.go index 4d2ea1d5d..3a2581037 100644 --- a/apps/daemon/internal/agent/codex/session_usage.go +++ b/apps/daemon/internal/agent/codex/session_usage.go @@ -115,7 +115,7 @@ func (u TurnUsage) completeTokens() bool { } func (s *Session) usagePayload(u TurnUsage) proto.Usage { - result := proto.Usage{Provider: "openai", Model: s.resolvedModel, InputTokens: int32(u.InputTokens), OutputTokens: int32(u.OutputTokens)} + result := proto.Usage{Provider: "openai", Model: s.resolvedModel} if u.completeTokens() { result.Tokens = &proto.TokenUsage{InputTokens: int64(u.InputTokens), OutputTokens: int64(u.OutputTokens), CachedInputTokens: int64(u.CachedInputTokens), ReasoningOutputTokens: int64(u.ReasoningOutputTokens), TotalTokens: int64(u.TotalTokens)} } diff --git a/apps/daemon/internal/agent/codex/session_usage_test.go b/apps/daemon/internal/agent/codex/session_usage_test.go index b75faf625..7c5e17ada 100644 --- a/apps/daemon/internal/agent/codex/session_usage_test.go +++ b/apps/daemon/internal/agent/codex/session_usage_test.go @@ -16,15 +16,15 @@ func TestNativeTokenUsageAcrossRuns(t *testing.T) { cfg: sessionConfig{logger: slog.New(slog.NewTextHandler(io.Discard, nil))}} s.setThreadID("thread") // A resumed thread replays its previous usage before starting this run. - s.onUsageUpdated(json.RawMessage(`{"threadId":"thread","turnId":"previous","tokenUsage":{"total":{"inputTokens":1000,"outputTokens":100},"last":{"inputTokens":500,"outputTokens":50}}}`)) + s.onUsageUpdated(json.RawMessage(`{"threadId":"thread","turnId":"previous","tokenUsage":{"total":{"inputTokens":1000,"cachedInputTokens":0,"outputTokens":100,"reasoningOutputTokens":0,"totalTokens":1100},"last":{"inputTokens":500,"cachedInputTokens":0,"outputTokens":50,"reasoningOutputTokens":0,"totalTokens":550}}}`)) if s.latestUsage != nil { t.Fatal("restored history was treated as current usage") } s.onTurnStarted(json.RawMessage(`{"threadId":"thread","turn":{"id":"current"}}`)) - s.onUsageUpdated(json.RawMessage(`{"threadId":"thread","turnId":"current","tokenUsage":{"total":{"inputTokens":1200,"outputTokens":120},"last":{"inputTokens":200,"outputTokens":20}}}`)) + s.onUsageUpdated(json.RawMessage(`{"threadId":"thread","turnId":"current","tokenUsage":{"total":{"inputTokens":1200,"cachedInputTokens":0,"outputTokens":120,"reasoningOutputTokens":0,"totalTokens":1320},"last":{"inputTokens":200,"cachedInputTokens":0,"outputTokens":20,"reasoningOutputTokens":0,"totalTokens":220}}}`)) // The second model request follows a tool call; its last counter is not // the whole turn. Repeating the cumulative snapshot must not add usage. - second := json.RawMessage(`{"threadId":"thread","turnId":"current","tokenUsage":{"total":{"inputTokens":1500,"outputTokens":150},"last":{"inputTokens":300,"outputTokens":30}}}`) + second := json.RawMessage(`{"threadId":"thread","turnId":"current","tokenUsage":{"total":{"inputTokens":1500,"cachedInputTokens":0,"outputTokens":150,"reasoningOutputTokens":0,"totalTokens":1650},"last":{"inputTokens":300,"cachedInputTokens":0,"outputTokens":30,"reasoningOutputTokens":0,"totalTokens":330}}}`) s.onUsageUpdated(second) s.onUsageUpdated(second) if s.latestUsage == nil || s.latestUsage.InputTokens != 500 || s.latestUsage.OutputTokens != 50 { @@ -39,7 +39,7 @@ func TestNativeTokenUsageAcrossRuns(t *testing.T) { if s.latestUsage != nil { t.Fatal("new turn retained previous turn usage") } - s.onUsageUpdated(json.RawMessage(`{"threadId":"thread","turnId":"next","tokenUsage":{"total":{"inputTokens":1700,"outputTokens":175},"last":{"inputTokens":200,"outputTokens":25}}}`)) + s.onUsageUpdated(json.RawMessage(`{"threadId":"thread","turnId":"next","tokenUsage":{"total":{"inputTokens":1700,"cachedInputTokens":0,"outputTokens":175,"reasoningOutputTokens":0,"totalTokens":1875},"last":{"inputTokens":200,"cachedInputTokens":0,"outputTokens":25,"reasoningOutputTokens":0,"totalTokens":225}}}`)) s.finalText = "done" s.onTurnCompleted(json.RawMessage(`{"threadId":"thread","turn":{"id":"next","status":"completed"}}`)) var usage proto.UsagePayload @@ -56,7 +56,7 @@ func TestNativeTokenUsageAcrossRuns(t *testing.T) { } } } - if usage.InputTokens != 200 || usage.OutputTokens != 25 || done.Usage.InputTokens != 200 || done.Content != "done" { + if usage.Tokens.InputTokens != 200 || usage.Tokens.OutputTokens != 25 || done.Usage.Tokens.InputTokens != 200 || done.Content != "done" { t.Fatalf("terminal usage=%+v done=%+v", usage, done) } } diff --git a/apps/daemon/internal/agent/codex/subagent_rollout.go b/apps/daemon/internal/agent/codex/subagent_rollout.go index 20e063c2a..d695da7a8 100644 --- a/apps/daemon/internal/agent/codex/subagent_rollout.go +++ b/apps/daemon/internal/agent/codex/subagent_rollout.go @@ -14,7 +14,6 @@ import ( type subagentEffect struct { proto.SubagentLifecyclePayload - source, turn, call string } type rolloutCall struct { Name string `json:"name"` @@ -228,7 +227,7 @@ func readSubagentEffects(home string, h *subagentHistory) ([]subagentEffect, err if json.Unmarshal(output, &text) != nil || json.Unmarshal([]byte(text), &receipt) != nil || len(receipt) != 1 || !validSubagentStatus(receipt[key]) { return nil, errors.New("codex: lifecycle success receipt unavailable") } - effects = append(effects, subagentEffect{proto.SubagentLifecyclePayload{NativeID: target, EffectID: h.ID + ":" + item.ID, Status: status, OccurredAtMS: completion.CompletedAt}, h.ID, turn.ID, item.ID}) + effects = append(effects, subagentEffect{proto.SubagentLifecyclePayload{NativeID: target, EffectID: h.ID + ":" + item.ID, Status: status, OccurredAtMS: completion.CompletedAt}}) } } for call := range calls { diff --git a/apps/daemon/internal/agent/codex/unsupported.go b/apps/daemon/internal/agent/codex/unsupported.go index 3da26fb7f..e9f02276a 100644 --- a/apps/daemon/internal/agent/codex/unsupported.go +++ b/apps/daemon/internal/agent/codex/unsupported.go @@ -13,10 +13,6 @@ func (s *Executor) ListWorkspaceDirectory(context.Context, string, int) (agent.W return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported } -func (s *Executor) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { - return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported -} - func (s *Session) ReadWorkspaceFile(context.Context, string, int) (agent.WorkspaceReadResult, error) { return agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUnsupported } @@ -24,7 +20,3 @@ func (s *Session) ReadWorkspaceFile(context.Context, string, int) (agent.Workspa func (s *Session) ListWorkspaceDirectory(context.Context, string, int) (agent.WorkspaceDirectoryResult, error) { return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported } - -func (s *Session) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { - return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported -} diff --git a/apps/daemon/internal/agent/codex/unsupported_test.go b/apps/daemon/internal/agent/codex/unsupported_test.go index fe2fe3616..903f87422 100644 --- a/apps/daemon/internal/agent/codex/unsupported_test.go +++ b/apps/daemon/internal/agent/codex/unsupported_test.go @@ -38,11 +38,4 @@ func TestUnsupportedExtensionsHaveNoNativeEffects(t *testing.T) { t.Fatal("unsupported listing fabricated entries") } } - for _, owner := range []agent.WorkspaceWriter{(*Executor)(nil), (*Session)(nil)} { - result, err := owner.WriteWorkspaceFile(ctx, secret, []byte(secret)) - check(err) - if result.SizeBytes != 0 { - t.Fatal("unsupported write fabricated receipt") - } - } } diff --git a/apps/daemon/internal/agent/contract_declarations_test.go b/apps/daemon/internal/agent/contract_declarations_test.go index bae330dbc..b3d6dbe39 100644 --- a/apps/daemon/internal/agent/contract_declarations_test.go +++ b/apps/daemon/internal/agent/contract_declarations_test.go @@ -21,7 +21,6 @@ func TestPublicHarnessContractDeclarations(t *testing.T) { "FunctionResultSubmitter": {"session"}, "WorkspaceReader": {"executor", "session"}, "WorkspaceDirectoryLister": {"executor", "session"}, - "WorkspaceWriter": {"executor", "session"}, } files, err := filepath.Glob("*.go") if err != nil { diff --git a/apps/daemon/internal/agent/harness.go b/apps/daemon/internal/agent/harness.go index 19f2364df..3db9bede8 100644 --- a/apps/daemon/internal/agent/harness.go +++ b/apps/daemon/internal/agent/harness.go @@ -212,11 +212,6 @@ type WorkspaceDirectoryLister interface { ListWorkspaceDirectory(context.Context, string, int) (WorkspaceDirectoryResult, error) } -// WorkspaceWriter confirms a native commit on an already authorized prepared owner. -type WorkspaceWriter interface { - WriteWorkspaceFile(context.Context, string, []byte) (WorkspaceWriteResult, error) -} - // Kind registration. // RegisterKind installs the heartbeat descriptor and model configuration for an diff --git a/apps/daemon/internal/agent/mcode/contracts.go b/apps/daemon/internal/agent/mcode/contracts.go index 30e32fa6f..9c8525d16 100644 --- a/apps/daemon/internal/agent/mcode/contracts.go +++ b/apps/daemon/internal/agent/mcode/contracts.go @@ -12,8 +12,6 @@ var ( _ agent.FunctionResultSubmitter = (*Session)(nil) _ agent.WorkspaceReader = (*Session)(nil) _ agent.WorkspaceDirectoryLister = (*Session)(nil) - _ agent.WorkspaceWriter = (*Session)(nil) _ agent.WorkspaceReader = (*executor)(nil) _ agent.WorkspaceDirectoryLister = (*executor)(nil) - _ agent.WorkspaceWriter = (*executor)(nil) ) diff --git a/apps/daemon/internal/agent/mcode/environment_mcp_test.go b/apps/daemon/internal/agent/mcode/environment_mcp_test.go index 677d6ced2..06f2d58b1 100644 --- a/apps/daemon/internal/agent/mcode/environment_mcp_test.go +++ b/apps/daemon/internal/agent/mcode/environment_mcp_test.go @@ -15,7 +15,7 @@ import ( ) func environmentMCPFixture() proto.EnvironmentMCP { - return proto.EnvironmentMCP{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/fixture", Server: agentplugin.MCPServer{ + return proto.EnvironmentMCP{InstallationRoot: "/private/runtime/capabilities", PackageRoot: "plugins/fixture", Server: agentplugin.MCPServer{ Name: "proof.server", Type: "stdio", Command: "never-exec-before-sandbox", Args: []string{"private-argument"}, EnvVars: []string{"USER_SELECTED"}, CWD: "resources", }} diff --git a/apps/daemon/internal/agent/mcode/session_test.go b/apps/daemon/internal/agent/mcode/session_test.go index 6aadc85e9..c080aa50c 100644 --- a/apps/daemon/internal/agent/mcode/session_test.go +++ b/apps/daemon/internal/agent/mcode/session_test.go @@ -117,7 +117,7 @@ func TestSessionStreamsCurrentTurnAndResumes(t *testing.T) { if p.Content != "Hello world" || p.Metadata[proto.DoneMetaAgentSessionID] != "native-1" { t.Fatalf("done = %#v", p) } - if p.Usage.InputTokens != 0 || p.Usage.OutputTokens != 0 || p.Usage.CostUSD != 0 { + if p.Usage.Tokens != nil { t.Fatalf("context occupancy reported as usage: %#v", p.Usage) } } diff --git a/apps/daemon/internal/agent/mcode/unsupported.go b/apps/daemon/internal/agent/mcode/unsupported.go index ac35c4efa..040dcf9b9 100644 --- a/apps/daemon/internal/agent/mcode/unsupported.go +++ b/apps/daemon/internal/agent/mcode/unsupported.go @@ -13,10 +13,6 @@ func (s *executor) ListWorkspaceDirectory(context.Context, string, int) (agent.W return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported } -func (s *executor) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { - return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported -} - func (s *Session) ReadWorkspaceFile(context.Context, string, int) (agent.WorkspaceReadResult, error) { return agent.WorkspaceReadResult{}, agent.ErrWorkspaceReadUnsupported } @@ -24,7 +20,3 @@ func (s *Session) ReadWorkspaceFile(context.Context, string, int) (agent.Workspa func (s *Session) ListWorkspaceDirectory(context.Context, string, int) (agent.WorkspaceDirectoryResult, error) { return agent.WorkspaceDirectoryResult{}, agent.ErrWorkspaceReadUnsupported } - -func (s *Session) WriteWorkspaceFile(context.Context, string, []byte) (agent.WorkspaceWriteResult, error) { - return agent.WorkspaceWriteResult{}, agent.ErrWorkspaceWriteUnsupported -} diff --git a/apps/daemon/internal/agent/mcode/unsupported_test.go b/apps/daemon/internal/agent/mcode/unsupported_test.go index 05a2afb0d..3cc8bb206 100644 --- a/apps/daemon/internal/agent/mcode/unsupported_test.go +++ b/apps/daemon/internal/agent/mcode/unsupported_test.go @@ -38,11 +38,4 @@ func TestUnsupportedExtensionsHaveNoNativeEffects(t *testing.T) { t.Fatal("unsupported listing fabricated entries") } } - for _, owner := range []agent.WorkspaceWriter{(*executor)(nil), (*Session)(nil)} { - result, err := owner.WriteWorkspaceFile(ctx, secret, []byte(secret)) - check(err) - if result.SizeBytes != 0 { - t.Fatal("unsupported write fabricated receipt") - } - } } diff --git a/apps/daemon/internal/agent/mcp_binding.go b/apps/daemon/internal/agent/mcp_binding.go index a1f5413f3..b3922b7dc 100644 --- a/apps/daemon/internal/agent/mcp_binding.go +++ b/apps/daemon/internal/agent/mcp_binding.go @@ -14,15 +14,13 @@ import ( // it into their native configuration without resolving credentials or packages. // Never serialize or log bindings: bearer values are confidential. type MCPBinding struct { - ServerLabel string - ConnectionOrigin string - CredentialAuthority string - Transport string - ServerURL string - AllowedTools *[]string - Required bool - BearerToken *string - HTTPHeaders map[string]string + ServerLabel string + Transport string + ServerURL string + AllowedTools *[]string + Required bool + BearerToken *string + HTTPHeaders map[string]string // Stdio retains the installed package identity for the fixed Runtime launcher. Stdio *proto.EnvironmentMCP // Process is a public stdio declaration, started natively by the Harness. @@ -44,7 +42,7 @@ func ResolveMCPBindings(req proto.PromptRequestPayload) ([]MCPBinding, error) { if server.Stdio != nil { transport = "stdio" } - item := MCPBinding{ServerLabel: server.ServerLabel, ConnectionOrigin: server.ConnectionOrigin, CredentialAuthority: "none", Transport: transport, ServerURL: server.ServerURL, Required: server.Required, BearerToken: server.BearerToken, HTTPHeaders: maps.Clone(server.Headers)} + item := MCPBinding{ServerLabel: server.ServerLabel, Transport: transport, ServerURL: server.ServerURL, Required: server.Required, BearerToken: server.BearerToken, HTTPHeaders: maps.Clone(server.Headers)} if server.Stdio != nil { process := *server.Stdio process.Args, process.EnvVars = slices.Clone(process.Args), slices.Clone(process.EnvVars) @@ -54,22 +52,16 @@ func ResolveMCPBindings(req proto.PromptRequestPayload) ([]MCPBinding, error) { names := append([]string{}, (*server.AllowedTools)...) item.AllowedTools = &names } - if server.BearerToken != nil { - item.CredentialAuthority = "project_vault" - } bindings = append(bindings, item) } } if local := req.LocalEnvironment; local != nil && len(local.MCP) > 0 { for _, installed := range local.MCP { declaration := installed.Server - item := MCPBinding{ServerLabel: declaration.Name, ConnectionOrigin: "environment", CredentialAuthority: "none", Transport: declaration.Type, ServerURL: declaration.URL, BearerToken: installed.BearerToken, HTTPHeaders: maps.Clone(declaration.HTTPHeaders)} + item := MCPBinding{ServerLabel: declaration.Name, Transport: declaration.Type, ServerURL: declaration.URL, BearerToken: installed.BearerToken, HTTPHeaders: maps.Clone(declaration.HTTPHeaders)} if declaration.BearerTokenEnvVar != "" && installed.BearerToken == nil { return nil, errors.New("environment MCP credential unavailable") } - if installed.BearerToken != nil || len(declaration.HTTPHeaders) > 0 || len(declaration.EnvVars) > 0 { - item.CredentialAuthority = "environment_configuration" - } if declaration.Type == "stdio" { value := installed value.Server.Args = slices.Clone(installed.Server.Args) diff --git a/apps/daemon/internal/agent/mcp_binding_test.go b/apps/daemon/internal/agent/mcp_binding_test.go index c205380ed..1b97f2962 100644 --- a/apps/daemon/internal/agent/mcp_binding_test.go +++ b/apps/daemon/internal/agent/mcp_binding_test.go @@ -18,7 +18,7 @@ func TestMCPBindingsPreserveOriginAuthorityAndPolicy(t *testing.T) { t.Fatal("public binding unavailable", err) } b := got[0] - if b.ConnectionOrigin != "service" || b.CredentialAuthority != "project_vault" || b.Transport != "http" || !b.Required || (b.AllowedTools == nil) != (allow == nil) || *b.BearerToken != token { + if b.Transport != "http" || !b.Required || (b.AllowedTools == nil) != (allow == nil) || *b.BearerToken != token { t.Fatal("public policy or authority changed") } *b.BearerToken = "mutated" @@ -34,7 +34,7 @@ func TestMCPBindingsPreserveOriginAuthorityAndPolicy(t *testing.T) { if err != nil || len(got) != 2 { t.Fatal("installed bindings unavailable", err) } - if got[0].ConnectionOrigin != "environment" || got[0].CredentialAuthority != "none" || got[0].Stdio.PackageRoot != "plugins/proof" || got[0].Required || got[0].AllowedTools != nil || got[1].CredentialAuthority != "environment_configuration" { + if got[0].Stdio.PackageRoot != "plugins/proof" || got[0].Required || got[0].AllowedTools != nil { t.Fatal("installed identity or authority changed") } got[0].Stdio.Server.Args[0] = "mutated" @@ -79,7 +79,7 @@ func TestPublicEnvironmentMCPRetainsVaultAuthority(t *testing.T) { if err != nil || len(got) != 1 { t.Fatal("environment binding unavailable", err) } - if got[0].ConnectionOrigin != "environment" || got[0].CredentialAuthority != "project_vault" || !got[0].Required || (*got[0].AllowedTools)[0] != "prove" { + if !got[0].Required || (*got[0].AllowedTools)[0] != "prove" { t.Fatal("public policy changed") } *got[0].BearerToken = "mutated" diff --git a/apps/daemon/internal/dispatch/preparation_cancel_test.go b/apps/daemon/internal/dispatch/preparation_cancel_test.go index 5804650cb..f3977123f 100644 --- a/apps/daemon/internal/dispatch/preparation_cancel_test.go +++ b/apps/daemon/internal/dispatch/preparation_cancel_test.go @@ -206,7 +206,7 @@ func TestPreparedCancellationFailuresRemainConservative(t *testing.T) { p := &cancellationPreparation{controlledPreparation: &controlledPreparation{closed: make(chan struct{})}} p.start = func(_ context.Context, id string, _ proto.MessageInput, out chan<- proto.Envelope) (agent.Session, error) { session = &fakeSession{out: out, closeOutOnCancel: true} - out <- mustEnv(t, proto.TypeUsage, id, proto.UsagePayload{Usage: proto.Usage{InputTokens: 3}}) + out <- mustEnv(t, proto.TypeUsage, id, proto.UsagePayload{Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 3}}}) close(entered) <-allowReturn return session, errors.New("controlled late Start failure") diff --git a/apps/daemon/internal/localworkspace/capabilities.go b/apps/daemon/internal/localworkspace/capabilities.go index 4199aac3f..b94729542 100644 --- a/apps/daemon/internal/localworkspace/capabilities.go +++ b/apps/daemon/internal/localworkspace/capabilities.go @@ -73,7 +73,6 @@ func (b *Binding) Prepare(ctx context.Context, r proto.PromptRequestPayload) (pr local.MCP, err = resolveEnvironmentMCP(manifest.MCP, values) for i := range local.MCP { local.MCP[i].InstallationRoot = b.capabilityRoot - local.MCP[i].WorkspaceRoot = b.workspace } if err != nil { return r, err diff --git a/apps/daemon/internal/localworkspace/initialization.go b/apps/daemon/internal/localworkspace/initialization.go index 4d99c1d40..b3e06af37 100644 --- a/apps/daemon/internal/localworkspace/initialization.go +++ b/apps/daemon/internal/localworkspace/initialization.go @@ -70,30 +70,14 @@ func validToolEnvironment(values map[string]string) bool { // ReadOptionalToolEnvironment permits a Runtime without explicit user variables. func ReadOptionalToolEnvironment() (map[string]string, error) { - values, _, err := readOptionalToolEnvironment() - return values, err -} - -// ToolEnvironmentFile returns the validated Runtime-owned source without copying -// its values into a Harness profile. An unconfigured environment has no file. -func ToolEnvironmentFile() (string, error) { - _, path, err := readOptionalToolEnvironment() - return path, err -} - -func readOptionalToolEnvironment() (map[string]string, string, error) { path, err := toolEnvironmentPath() if err != nil { - return nil, "", err + return nil, err } if _, err = os.Stat(path); errors.Is(err, os.ErrNotExist) && os.Getenv("OAC_RUNTIME_TOOL_ENV_FILE") == "" { - return map[string]string{}, "", nil - } - values, err := readToolEnvironmentFile(path) - if err != nil { - return nil, "", err + return map[string]string{}, nil } - return values, path, nil + return readToolEnvironmentFile(path) } // The prepared snapshot takes precedence over the operator's source file. diff --git a/apps/daemon/internal/localworkspace/mcp_test.go b/apps/daemon/internal/localworkspace/mcp_test.go index 1ff8b336b..8bafbc604 100644 --- a/apps/daemon/internal/localworkspace/mcp_test.go +++ b/apps/daemon/internal/localworkspace/mcp_test.go @@ -37,7 +37,7 @@ func TestDeclaredMCPVariablesNeverFallBackToNativeEnv(t *testing.T) { } func TestMCPStdioLauncherContainsOnlyInstalledIdentity(t *testing.T) { - server := proto.EnvironmentMCP{InstallationRoot: "/private/runtime/capabilities", WorkspaceRoot: "/private/runtime/workspace", PackageRoot: "plugins/0", Server: agentplugin.MCPServer{ + server := proto.EnvironmentMCP{InstallationRoot: "/private/runtime/capabilities", PackageRoot: "plugins/0", Server: agentplugin.MCPServer{ Name: "package_tool", Type: "stdio", Command: "untrusted-command", Args: []string{"private-argument"}, }} command, args := MCPStdioCommand(server) diff --git a/apps/daemon/internal/localworkspace/write.go b/apps/daemon/internal/localworkspace/write.go index 062f49330..69d91895d 100644 --- a/apps/daemon/internal/localworkspace/write.go +++ b/apps/daemon/internal/localworkspace/write.go @@ -13,8 +13,6 @@ import ( // This private transfer bound is distinct from the public inline-file limit. const WriteMaxBytes = proto.WorkspaceWriteMaxBytes -var _ agent.WorkspaceWriter = (*Binding)(nil) - func (b *Binding) AcceptsFileWrite(environment, session string) bool { return b != nil && b.writer != nil && b.environment == environment && b.stateKey == session } diff --git a/apps/daemon/internal/paths/paths.go b/apps/daemon/internal/paths/paths.go index d6af78594..8549d628b 100644 --- a/apps/daemon/internal/paths/paths.go +++ b/apps/daemon/internal/paths/paths.go @@ -34,8 +34,7 @@ func ValidateProfile(name string) error { return nil } -// Root returns ~/.oac. Honours OAC_RUNTIME_HOME for tests / -// sandbox environments without a writable home. +// Root returns ~/.oac, or the operator-selected OAC_RUNTIME_HOME when set. func Root() (string, error) { if override := os.Getenv("OAC_RUNTIME_HOME"); override != "" { if !filepath.IsAbs(override) || filepath.Clean(override) != override { diff --git a/apps/daemon/internal/transport/ws.go b/apps/daemon/internal/transport/ws.go index 845cb45cf..974078bc7 100644 --- a/apps/daemon/internal/transport/ws.go +++ b/apps/daemon/internal/transport/ws.go @@ -19,7 +19,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" ) -// DialOptions is the input to Dial. HTTPHeader is optional. +// DialOptions is the input to Dial. type DialOptions struct { // WSURL is the absolute ws://... or wss://... URL. WSURL string @@ -38,9 +38,6 @@ type DialOptions struct { // HandshakeTimeout caps WS upgrade wait. Zero → 10s. HandshakeTimeout time.Duration - - // HTTPHeader is appended to the upgrade request. Useful in tests. - HTTPHeader http.Header } // Conn is the daemon-side WebSocket. One read goroutine + one write @@ -94,10 +91,7 @@ func Dial(ctx context.Context, opts DialOptions) (*Conn, error) { dialCtx, cancel := context.WithTimeout(ctx, opts.HandshakeTimeout) defer cancel() - headers := opts.HTTPHeader.Clone() - if headers == nil { - headers = make(http.Header) - } + headers := make(http.Header) headers.Set("Authorization", "Bearer "+opts.Credential) wsConn, resp, err := dialer.DialContext(dialCtx, dialURL, headers) if err != nil { diff --git a/apps/daemon/internal/transport/ws_test.go b/apps/daemon/internal/transport/ws_test.go index de2e6d129..a031e8527 100644 --- a/apps/daemon/internal/transport/ws_test.go +++ b/apps/daemon/internal/transport/ws_test.go @@ -91,10 +91,8 @@ func TestDialPassesAuthOnlyInHeader(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(gw.handler)) defer srv.Close() - headers := http.Header{"Authorization": {"Bearer obsolete"}, "X-Test": {"preserved"}} conn, err := transport.Dial(context.Background(), transport.DialOptions{ WSURL: httpToWS(srv.URL) + "/agent-daemon/ws", - HTTPHeader: headers, DeviceID: "rt_abc", Credential: "shh-secret", DaemonVersion: "0.1.0", @@ -117,9 +115,6 @@ func TestDialPassesAuthOnlyInHeader(t *testing.T) { if auth != "Bearer shh-secret" { t.Error("missing credential authorization header") } - if headers.Get("Authorization") != "Bearer obsolete" { - t.Error("Dial mutated caller headers") - } if !strings.Contains(dialURL, "version=0.1.0") { t.Errorf("dial URL %q missing version", dialURL) } diff --git a/contracts/agents-api/harness-onboarding.md b/contracts/agents-api/harness-onboarding.md index a90b8b86e..4890b0c77 100644 --- a/contracts/agents-api/harness-onboarding.md +++ b/contracts/agents-api/harness-onboarding.md @@ -68,7 +68,7 @@ For example, the Codex adapter keeps its app-server and thread, the Claude adapt | `Session`, `Turn`, `CancellationOutcome`, `AwaitSettlement` | Real implementation | Cancel the exact Turn, keep observed results and confirm settlement independently of cancellation requests | | `DurableSteerer` | Real implementation on every Turn | Distinguish a complete write from the native application receipt; keep retry identity | | `FunctionResultSubmitter` | Explicit implementation or Unsupported | Match native call and result identity and acknowledge application | -| `WorkspaceReader`, `WorkspaceDirectoryLister`, `WorkspaceWriter` | Explicit on Turn and Executor owners | Use the authorized workspace, confirm access, commit or close, or return the operation's Unsupported error | +| `WorkspaceReader`, `WorkspaceDirectoryLister` | Explicit on Turn and Executor owners | Use the authorized workspace, confirm access, commit or close, or return the operation's Unsupported error | | Neutral messages, images, MCP, structured output and Subagent observations | Explicit capability decisions | Keep each operation's protocol semantics; reject unsupported input before submission | Each adapter's `contracts.go` holds an individual compile-time assertion for each small interface. Do not embed a default implementation that makes future interfaces appear implemented. Adding a contract also requires a classification in the common completeness check and an explicit assertion in every public adapter; the check follows the authored Harness catalog. diff --git a/contracts/agents-api/zh/harness-onboarding.md b/contracts/agents-api/zh/harness-onboarding.md index 2bc0770c5..1e4799f52 100644 --- a/contracts/agents-api/zh/harness-onboarding.md +++ b/contracts/agents-api/zh/harness-onboarding.md @@ -1,7 +1,7 @@ --- title: "Harness 协议" source: contracts/agents-api/harness-onboarding.md -source_hash: 735078df56cfbe89dc569d36a87d31c7eccd69848f19465d3bf66ac55e5f2238 +source_hash: c5688b7362e0ede4a85200d3a66ee383bd71da23505429755adbc1888ce8dd60 --- **Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code);**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。Harness 协议是 Runtime 与适配器之间的契约:适配器接口及其生命周期义务、声明、注册和验收。本文面向添加或维护 Harness 的开发者。 @@ -70,7 +70,7 @@ Environment 提供执行资源。受管 E2B、Docker 和 microsandbox 机器以 | `Session`、`Turn`、`CancellationOutcome`、`AwaitSettlement` | 真实实现 | 取消精确的 Turn,保留已观察结果,并独立于取消请求确认结算 | | `DurableSteerer` | 每个 Turn 上真实实现 | 区分完整写入与原生应用回执;保留重试身份 | | `FunctionResultSubmitter` | 明确实现或 Unsupported | 匹配原生调用和结果身份,并确认应用 | -| `WorkspaceReader`、`WorkspaceDirectoryLister`、`WorkspaceWriter` | 在 Turn 和 Executor 所有者上明确实现 | 使用授权工作区,确认访问,提交或关闭,或者返回该操作的 Unsupported 错误 | +| `WorkspaceReader`、`WorkspaceDirectoryLister` | 在 Turn 和 Executor 所有者上明确实现 | 使用授权工作区,确认访问,提交或关闭,或者返回该操作的 Unsupported 错误 | | 中立消息、图像、MCP、结构化输出和 Subagent 观察 | 明确作出能力决策 | 保持每项操作的协议语义;在提交前拒绝不受支持的输入 | 每个适配器的 `contracts.go` 都包含针对每个小型接口的单项编译时断言。不要嵌入会让未来接口看起来已经实现的默认实现。添加契约时,还必须在通用完整性检查中进行分类,并在每个公共适配器中添加明确断言;该检查遵循已编写的 Harness 目录。 diff --git a/internal/agentdaemon/proto/envelope_test.go b/internal/agentdaemon/proto/envelope_test.go index cec6e5d6d..1164f5d1c 100644 --- a/internal/agentdaemon/proto/envelope_test.go +++ b/internal/agentdaemon/proto/envelope_test.go @@ -65,7 +65,7 @@ func TestUsagePayloadEmbedsUsage(t *testing.T) { // which translates Usage → store.UsageInput. If we accidentally // renamed the embedded field or stopped embedding, the persistence // path would silently lose all usage fields. Catch that here. - in := UsagePayload{Usage: Usage{Provider: "anthropic", Model: "claude-opus", InputTokens: 100}} + in := UsagePayload{Usage: Usage{Provider: "anthropic", Model: "claude-opus", Tokens: &TokenUsage{InputTokens: 100}}} raw, err := json.Marshal(in) if err != nil { t.Fatalf("Marshal: %v", err) @@ -77,7 +77,7 @@ func TestUsagePayloadEmbedsUsage(t *testing.T) { if err := json.Unmarshal(raw, &out); err != nil { t.Fatalf("Unmarshal: %v", err) } - if out.Provider != "anthropic" || out.InputTokens != 100 { + if out.Provider != "anthropic" || out.Tokens.InputTokens != 100 { t.Fatalf("usage round-trip lost data: %+v", out) } } diff --git a/internal/agentdaemon/proto/environment.go b/internal/agentdaemon/proto/environment.go index c2a917310..e28cf1bb6 100644 --- a/internal/agentdaemon/proto/environment.go +++ b/internal/agentdaemon/proto/environment.go @@ -30,7 +30,6 @@ type LocalEnvironment struct { // and the selected user bearer may be confidential. It is not agent.tools MCP. type EnvironmentMCP struct { InstallationRoot string - WorkspaceRoot string PackageRoot string Server agentplugin.MCPServer BearerToken *string diff --git a/internal/agentdaemon/proto/inbound.go b/internal/agentdaemon/proto/inbound.go index 7ce4d2942..957355d4c 100644 --- a/internal/agentdaemon/proto/inbound.go +++ b/internal/agentdaemon/proto/inbound.go @@ -118,13 +118,10 @@ type TokenUsage struct { // Tokens is absent when a complete, correctly scoped measurement is unavailable. // Core does not reconstruct token counts, prices or provider identity from Raw. type Usage struct { - Tokens *TokenUsage `json:"tokens,omitempty"` - Provider string `json:"provider,omitempty"` - Model string `json:"model,omitempty"` - InputTokens int32 `json:"input_tokens,omitempty"` - OutputTokens int32 `json:"output_tokens,omitempty"` - CostUSD float64 `json:"cost_usd,omitempty"` - Raw map[string]any `json:"raw,omitempty"` + Tokens *TokenUsage `json:"tokens,omitempty"` + Provider string `json:"provider,omitempty"` + Model string `json:"model,omitempty"` + Raw map[string]any `json:"raw,omitempty"` } // UsagePayload carries a Usage update mid-stream. @@ -146,7 +143,6 @@ type DonePayload struct { // SourceCompletedAtMS freezes the native root completion before child settlement. SourceCompletedAtMS *int64 `json:"source_completed_at_ms,omitempty"` Content string `json:"content"` - Transcript string `json:"transcript,omitempty"` Usage Usage `json:"usage,omitzero"` Metadata map[string]any `json:"metadata,omitempty"` } diff --git a/internal/harnessconfig/harness.go b/internal/harnessconfig/harness.go index 17d0d2a98..e83562cdd 100644 --- a/internal/harnessconfig/harness.go +++ b/internal/harnessconfig/harness.go @@ -163,12 +163,11 @@ func validChoices(values []string, allowed ...string) bool { } // PreparedConfiguration owns a validated snapshot without native side effects. -// Model and Provider may be absent only for the existing native-owned connection -// path. A supplied model must be a nonempty string; an explicit provider requires -// an explicit model. HarnessConfig is always an independently owned object. +// Model may be absent only for the existing native-owned connection path. A +// supplied model must be a nonempty string; an explicit provider requires an +// explicit model. HarnessConfig is always an independently owned object. type PreparedConfiguration struct { Model string - Provider *modelprovider.Provider HarnessConfig proto.HarnessConfig } @@ -198,11 +197,9 @@ func (c Configuration) Prepare(options map[string]any) (PreparedConfiguration, e if result.Model == "" { return PreparedConfiguration{}, ErrModel } - provider, err := c.ParseProvider(value) - if err != nil { + if _, err := c.ParseProvider(value); err != nil { return PreparedConfiguration{}, err } - result.Provider = &provider } native, err := c.PrepareHarnessConfig(options) if err != nil { diff --git a/internal/harnessconfig/preparation_test.go b/internal/harnessconfig/preparation_test.go index ac1a4a602..748e7ecbe 100644 --- a/internal/harnessconfig/preparation_test.go +++ b/internal/harnessconfig/preparation_test.go @@ -42,14 +42,12 @@ func TestPrepareModelConfiguration(t *testing.T) { } }) } - raw := provider() - got, err := c.Prepare(map[string]any{"model": "fixture", "model_provider": raw}) + got, err := c.Prepare(map[string]any{"model": "fixture", "model_provider": provider()}) if err != nil { t.Fatal(err) } - raw["api_key"] = "changed" - if got.Model != "fixture" || got.Provider.APIKey != "private-sentinel" { - t.Fatal("prepared provider did not own snapshot") + if got.Model != "fixture" { + t.Fatal("prepared model changed") } if _, err := (Configuration{}).Prepare(map[string]any{"model": "fixture", "model_provider": provider()}); err == nil { t.Fatal("empty declaration inferred provider support") diff --git a/packages/claude-sdk-adapter/README.md b/packages/claude-sdk-adapter/README.md index ac28cf5ae..2e5917ea4 100644 --- a/packages/claude-sdk-adapter/README.md +++ b/packages/claude-sdk-adapter/README.md @@ -84,7 +84,7 @@ Function results remain pending after stdin/MCP delivery. A matching live native ### Usage -Each SDK result supplies one native usage snapshot, including reported failures. `Usage.Raw.claude_sdk_result` holds the latest; queries with multiple native results also retain all snapshots in order under `claude_sdk_results`. Main-loop `usage` is per native turn, while query-pipeline `modelUsage` and estimated `total_cost_usd` are cumulative within the query. Retain subtype/error provenance and earlier snapshots even when a later failure reports zero counters. Reuse the latest full snapshot set in Usage and Done; never sum cumulative measurements. Each Executor owns one SDK query, including cold resume. Raw snapshots explicitly identify per-native-turn usage versus query-cumulative model usage and cost. A new Turn has a fresh snapshot list, but query totals may include earlier Turns; never represent those totals as consumption by the current Turn. Missing native results do not imply zero consumption. SDK estimates stay in raw evidence, outside the billed cost field; do not select an arbitrary model or invent missing public token breakdowns. The API does not parse native counters. The native snapshot is not a complete public Usage: precise public usage projection, unreported costs and crash or partial accounting are not provided. +Each SDK result supplies one native usage snapshot, including reported failures. `Usage.Raw.claude_sdk_result` holds the latest; queries with multiple native results also retain all snapshots in order under `claude_sdk_results`. Main-loop `usage` is per native turn, while query-pipeline `modelUsage` and estimated `total_cost_usd` are cumulative within the query. Retain subtype/error provenance and earlier snapshots even when a later failure reports zero counters. Reuse the latest full snapshot set in Usage and Done; never sum cumulative measurements. Each Executor owns one SDK query, including cold resume. Raw snapshots explicitly identify per-native-turn usage versus query-cumulative model usage and cost. A new Turn has a fresh snapshot list, but query totals may include earlier Turns; never represent those totals as consumption by the current Turn. Missing native results do not imply zero consumption. SDK estimates stay in raw evidence; do not select an arbitrary model or invent missing public token breakdowns. The API does not parse native counters. The native snapshot is not a complete public Usage: precise public usage projection, unreported costs and crash or partial accounting are not provided. ### Active input diff --git a/packages/claude-sdk-adapter/src/message_input.ts b/packages/claude-sdk-adapter/src/message_input.ts index 90ca676bd..04324b790 100644 --- a/packages/claude-sdk-adapter/src/message_input.ts +++ b/packages/claude-sdk-adapter/src/message_input.ts @@ -43,5 +43,3 @@ export function nativeContent(message: MessageInput[number], remote = false): SD ? { type: "text" as const, text: part.text } : { type: "image" as const, source: remote && !part.image_url.startsWith("data:") ? { type: "url" as const, url: part.image_url } : imageSource(part.image_url) }); } - -export function textInput(text: string): MessageInput { return [{ content: [{ type: "input_text", text }] }]; } diff --git a/packages/claude-sdk-adapter/tests/inputs.test.mjs b/packages/claude-sdk-adapter/tests/inputs.test.mjs index 9d197645a..80cf0b290 100644 --- a/packages/claude-sdk-adapter/tests/inputs.test.mjs +++ b/packages/claude-sdk-adapter/tests/inputs.test.mjs @@ -3,6 +3,7 @@ import test from "node:test"; import { Inputs } from "../dist/inputs.js"; const result = (ids, session_id = "native") => ({ type: "result", session_id, user_message_uuids: ids }); +const textInput = text => [{ content: [{ type: "input_text", text }] }]; const steer = (input_id, text = "additional text") => ({ type: "steer", input_id, input: [{ content: [{ type: "input_text", text }] }] }); test("queued input survives the first result and completes only with its own native result", async () => { @@ -65,7 +66,6 @@ test("reject before readiness, repeated identities and native receipt capacity w }); test("ordered image batches keep message boundaries and aggregate native consumption", async () => { - const { textInput } = await import("../dist/message_input.js"); const inputs = new Inputs(textInput("opening")); const stream = inputs[Symbol.asyncIterator](); const first = (await stream.next()).value; @@ -92,7 +92,6 @@ test("ordered image batches keep message boundaries and aggregate native consump }); test("native UUID capacity rejects a whole batch and partial consumption does not apply it", async () => { - const { textInput } = await import("../dist/message_input.js"); const inputs = new Inputs(Array.from({ length: 63 }, () => textInput("opening")[0])); inputs.start("native"); const pair = [...textInput("a"), ...textInput("b")]; diff --git a/services/core/internal/api/native_classification_integration_test.go b/services/core/internal/api/native_classification_integration_test.go index 197922fd5..e9421dd60 100644 --- a/services/core/internal/api/native_classification_integration_test.go +++ b/services/core/internal/api/native_classification_integration_test.go @@ -30,7 +30,7 @@ func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) { receipt := submitMessage(t, pool, tenant, session.ID, "input", "test") transitionTurn(t, pool, tenant, session.ID, receipt.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}) status := 503 - result := execution.Result{ErrorCode: "engine_failed", Error: "Bearer secret-canary https://private.example/key", EngineErrorCode: code, EngineHTTPStatus: &status, Done: proto.DonePayload{Usage: proto.Usage{InputTokens: 7, OutputTokens: 3}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native-secret-canary"}}} + result := execution.Result{ErrorCode: "engine_failed", Error: "Bearer secret-canary https://private.example/key", EngineErrorCode: code, EngineHTTPStatus: &status, Done: proto.DonePayload{Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 7, OutputTokens: 3}}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native-secret-canary"}}} outcome, err := json.Marshal(result) if err != nil { t.Fatal(err) @@ -41,7 +41,7 @@ func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) { t.Fatal(err) } var saved execution.Result - if err = json.Unmarshal(snap.Turn.Outcome, &saved); err != nil || saved.EngineErrorCode != code || saved.EngineHTTPStatus == nil || *saved.EngineHTTPStatus != 503 || saved.Done.Usage.InputTokens != 7 || saved.Done.Usage.OutputTokens != 3 || saved.Done.Metadata[proto.DoneMetaAgentSessionID] != "native-secret-canary" { + if err = json.Unmarshal(snap.Turn.Outcome, &saved); err != nil || saved.EngineErrorCode != code || saved.EngineHTTPStatus == nil || *saved.EngineHTTPStatus != 503 || saved.Done.Usage.Tokens.InputTokens != 7 || saved.Done.Usage.Tokens.OutputTokens != 3 || saved.Done.Metadata[proto.DoneMetaAgentSessionID] != "native-secret-canary" { t.Fatal("outcome round trip lost facts", err) } want := code diff --git a/services/core/internal/execution/engine_failure_test.go b/services/core/internal/execution/engine_failure_test.go index eb5452fa2..a6dce0693 100644 --- a/services/core/internal/execution/engine_failure_test.go +++ b/services/core/internal/execution/engine_failure_test.go @@ -16,7 +16,7 @@ func TestEngineClassificationSurvivesDrainWithoutChangingSettlement(t *testing.T j := journal{writer: writer, next: 1} events := make(chan proto.Envelope, 3) failure, _ := proto.NewEnvelope(proto.TypeError, "run", proto.ErrorPayload{Error: "raw native text", Code: "authentication_error"}) - usage, _ := proto.NewEnvelope(proto.TypeUsage, "run", proto.Usage{InputTokens: 7}) + usage, _ := proto.NewEnvelope(proto.TypeUsage, "run", proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 7}}) done, _ := proto.NewEnvelope(proto.TypeDone, "run", map[string]any{"metadata": map[string]any{proto.DoneMetaAgentSessionID: "native"}}) events <- failure events <- usage @@ -25,14 +25,14 @@ func TestEngineClassificationSurvivesDrainWithoutChangingSettlement(t *testing.T if err := j.drain(events, &result); err != nil { t.Fatal(err) } - if result.ErrorCode != prior || result.EngineErrorCode != "authentication_error" || result.Done.Usage.InputTokens != 7 || result.Done.Metadata[proto.DoneMetaAgentSessionID] != "native" { + if result.ErrorCode != prior || result.EngineErrorCode != "authentication_error" || result.Done.Usage.Tokens.InputTokens != 7 || result.Done.Metadata[proto.DoneMetaAgentSessionID] != "native" { t.Fatal(result) } - reply := cancellationResult{ack: proto.InteractionDecisionAckPayload{Applied: true, Outcome: &proto.DonePayload{Usage: proto.Usage{InputTokens: 9}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native"}}}} + reply := cancellationResult{ack: proto.InteractionDecisionAckPayload{Applied: true, Outcome: &proto.DonePayload{Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 9}}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native"}}}} if err := recordCancellation(context.Background(), &j, reply, &result); err != nil { t.Fatal(err) } - if result.ErrorCode != prior || result.Done.Usage.InputTokens != 9 { + if result.ErrorCode != prior || result.Done.Usage.Tokens.InputTokens != 9 { t.Fatal(result) } raw, _ := json.Marshal(result) @@ -44,7 +44,7 @@ func TestEngineClassificationSurvivesDrainWithoutChangingSettlement(t *testing.T unknown := proto.Envelope{Type: proto.TypeError, Payload: json.RawMessage(`{"error":"new","code":{"arbitrary":"value"},"http_status":"secret"}`)} settlement, _ := proto.NewEnvelope(proto.TypeError, "run", proto.ErrorPayload{Error: "executor Turn settlement failed"}) for _, later := range []proto.Envelope{unknown, settlement} { - if err := result.mergeObservation(later); err != nil || result.EngineErrorCode != "authentication_error" || result.ErrorCode != prior || result.Done.Usage.InputTokens != 9 { + if err := result.mergeObservation(later); err != nil || result.EngineErrorCode != "authentication_error" || result.ErrorCode != prior || result.Done.Usage.Tokens.InputTokens != 9 { t.Fatal(err, result) } } @@ -64,7 +64,7 @@ func TestClassifiedErrorCannotOverrideCancellationReceipt(t *testing.T) { events <- failure close(events) ack := make(chan cancellationResult, 1) - ack <- cancellationResult{ack: proto.InteractionDecisionAckPayload{Applied: true, Outcome: &proto.DonePayload{Usage: proto.Usage{InputTokens: 9}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native"}}}} + ack <- cancellationResult{ack: proto.InteractionDecisionAckPayload{Applied: true, Outcome: &proto.DonePayload{Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 9}}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native"}}}} if drainFirst { if err := j.drain(events, &result); err != nil { t.Fatal(err) @@ -76,7 +76,7 @@ func TestClassifiedErrorCannotOverrideCancellationReceipt(t *testing.T) { t.Fatal(err) } } - if status != sessions.TurnCancelled || result.Done.Usage.InputTokens != 9 || result.Done.Metadata[proto.DoneMetaAgentSessionID] != "native" || result.EngineErrorCode != "rate_limit_exceeded" { + if status != sessions.TurnCancelled || result.Done.Usage.Tokens.InputTokens != 9 || result.Done.Metadata[proto.DoneMetaAgentSessionID] != "native" || result.EngineErrorCode != "rate_limit_exceeded" { t.Fatalf("receipt lost authority: %s %+v", status, result) } } diff --git a/services/core/internal/execution/journal_test.go b/services/core/internal/execution/journal_test.go index 1955836aa..b03bf3465 100644 --- a/services/core/internal/execution/journal_test.go +++ b/services/core/internal/execution/journal_test.go @@ -114,7 +114,7 @@ func TestJournalDrainRetainsTerminalContinuityAndUsageOnFailure(t *testing.T) { env, _ := proto.NewEnvelope(proto.TypeDelta, "run", proto.DeltaPayload{Delta: "partial", Sequence: uint64(i + 1)}) upstream <- env } - done, _ := proto.NewEnvelope(proto.TypeDone, "run", proto.DonePayload{Content: "Final", Usage: proto.Usage{InputTokens: 10, OutputTokens: 4}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native-thread"}}) + done, _ := proto.NewEnvelope(proto.TypeDone, "run", proto.DonePayload{Content: "Final", Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 10, OutputTokens: 4}}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native-thread"}}) upstream <- done close(upstream) result := Result{ErrorCode: "event_persistence_failed"} @@ -124,7 +124,7 @@ func TestJournalDrainRetainsTerminalContinuityAndUsageOnFailure(t *testing.T) { if err := j.flush(context.Background()); err == nil { t.Fatal("flush should fail") } - if result.ErrorCode != "event_persistence_failed" || result.Done.Metadata[proto.DoneMetaAgentSessionID] != "native-thread" || result.Done.Usage.InputTokens != 10 || result.Done.Usage.OutputTokens != 4 { + if result.ErrorCode != "event_persistence_failed" || result.Done.Metadata[proto.DoneMetaAgentSessionID] != "native-thread" || result.Done.Usage.Tokens.InputTokens != 10 || result.Done.Usage.Tokens.OutputTokens != 4 { t.Fatalf("terminal data lost or failure cleared: %+v", result) } if err := j.flush(context.Background()); err != nil { @@ -145,11 +145,11 @@ func TestCancellationReceiptContinuitySurvivesFlushFailure(t *testing.T) { } } result := Result{ErrorCode: "event_persistence_failed"} - reply := cancellationResult{ack: proto.InteractionDecisionAckPayload{Applied: true, Outcome: &proto.DonePayload{Usage: proto.Usage{InputTokens: 9}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "cancel-native"}}}} + reply := cancellationResult{ack: proto.InteractionDecisionAckPayload{Applied: true, Outcome: &proto.DonePayload{Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 9}}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "cancel-native"}}}} if err := recordCancellation(context.Background(), j, reply, &result); err == nil { t.Fatal("flush should fail") } - if result.Done.Metadata[proto.DoneMetaAgentSessionID] != "cancel-native" || result.Done.Usage.InputTokens != 9 || result.ErrorCode != "event_persistence_failed" { + if result.Done.Metadata[proto.DoneMetaAgentSessionID] != "cancel-native" || result.Done.Usage.Tokens.InputTokens != 9 || result.ErrorCode != "event_persistence_failed" { t.Fatalf("lost cancellation outcome: %+v", result) } if err := j.flush(context.Background()); err != nil { diff --git a/services/core/internal/execution/runtime_protocol_test.go b/services/core/internal/execution/runtime_protocol_test.go index 51e926496..8dcfc497f 100644 --- a/services/core/internal/execution/runtime_protocol_test.go +++ b/services/core/internal/execution/runtime_protocol_test.go @@ -18,7 +18,7 @@ func TestRuntimeProtocolUnknownEffectRetainsObservationFailure(t *testing.T) { writer := &recoveringWriter{} j := journal{writer: writer, next: 1} events := make(chan proto.Envelope, 1) - usage, err := proto.NewEnvelope(proto.TypeUsage, "run", proto.Usage{InputTokens: 7}) + usage, err := proto.NewEnvelope(proto.TypeUsage, "run", proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 7}}) if err != nil { t.Fatal(err) } @@ -43,7 +43,7 @@ func TestRuntimeProtocolUnknownEffectRetainsObservationFailure(t *testing.T) { t.Fatal(err) } if persisted.ErrorCode != reason || persisted.EngineErrorCode != "" || persisted.EngineHTTPStatus != nil || - persisted.Done.SourceCompletedAtMS != nil || persisted.Done.Usage.InputTokens != 7 || persisted.AppliedThrough != 1 { + persisted.Done.SourceCompletedAtMS != nil || persisted.Done.Usage.Tokens.InputTokens != 7 || persisted.AppliedThrough != 1 { t.Fatalf("unknown result changed into native terminal evidence: %+v", persisted) } if len(writer.events) != 1 || writer.events[0].Kind != proto.TypeUsage { diff --git a/services/core/tests/integration/archive_cancellation_test.go b/services/core/tests/integration/archive_cancellation_test.go index 820ffb596..ddb4c7ac3 100644 --- a/services/core/tests/integration/archive_cancellation_test.go +++ b/services/core/tests/integration/archive_cancellation_test.go @@ -255,7 +255,7 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{}) } if !closedCase { - ack := proto.InteractionDecisionAckPayload{DeliveryID: request.DeliveryID, Applied: true, Outcome: &proto.DonePayload{Usage: proto.Usage{InputTokens: 17}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "cancelled-native"}}} + ack := proto.InteractionDecisionAckPayload{DeliveryID: request.DeliveryID, Applied: true, Outcome: &proto.DonePayload{Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 17}}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "cancelled-native"}}} if scenario == "negative_ack" { ack.Applied = false ack.ErrorCode = "cancel_failed" @@ -298,7 +298,7 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { t.Fatal(got.run.Turn.Status, got.err, string(got.run.Turn.Outcome)) } var outcome execution.Result - if err := json.Unmarshal(got.run.Turn.Outcome, &outcome); err != nil || (wantStatus == sessions.TurnCancelled && outcome.Done.Usage.InputTokens != 17) { + if err := json.Unmarshal(got.run.Turn.Outcome, &outcome); err != nil || (wantStatus == sessions.TurnCancelled && outcome.Done.Usage.Tokens.InputTokens != 17) { t.Fatal("receipt lost usage", string(got.run.Turn.Outcome), err) } diff --git a/services/core/tests/integration/dispatch_test.go b/services/core/tests/integration/dispatch_test.go index c0871bd57..00104f28b 100644 --- a/services/core/tests/integration/dispatch_test.go +++ b/services/core/tests/integration/dispatch_test.go @@ -253,11 +253,11 @@ func TestExecutionDispatchSteeringAndNativeContinuity(t *testing.T) { t.Fatal("steering retry changed identity") } h.write(first.TurnID, proto.TypePromptSteerAck, proto.PromptSteerAckPayload{InputID: input.InputID, Accepted: true}) - h.write(first.TurnID, proto.TypeDone, proto.DonePayload{Content: "Finished", Usage: proto.Usage{InputTokens: 7, OutputTokens: 3}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native-thread-1"}}) + h.write(first.TurnID, proto.TypeDone, proto.DonePayload{Content: "Finished", Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 7, OutputTokens: 3}}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "native-thread-1"}}) done := h.finished(result, sessions.TurnCompleted) var outcome execution.Result _ = json.Unmarshal(done.Outcome, &outcome) - if outcome.AppliedThrough != second.Sequence || outcome.Done.Usage.InputTokens != 7 { + if outcome.AppliedThrough != second.Sequence || outcome.Done.Usage.Tokens.InputTokens != 7 { t.Fatalf("missing result: %+v", outcome) } // Restart Core: a new Store and execution owner continue the native Session. @@ -307,7 +307,7 @@ func TestExecutionCancellationRequiresReceiptAndSurvivesContextEnd(t *testing.T) if withDone { h.write(first.TurnID, proto.TypeDone, proto.DonePayload{}) } - outcome := &proto.DonePayload{Metadata: map[string]any{proto.DoneMetaAgentSessionID: "cancelled-native"}, Usage: proto.Usage{InputTokens: 10}} + outcome := &proto.DonePayload{Metadata: map[string]any{proto.DoneMetaAgentSessionID: "cancelled-native"}, Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 10}}} h.write(first.TurnID, proto.TypeInteractionDecisionAck, proto.InteractionDecisionAckPayload{DeliveryID: cancel.DeliveryID, Applied: true, Outcome: outcome}) h.finished(result, sessions.TurnCancelled) next := h.message("next", "Continue after cancellation") @@ -347,7 +347,7 @@ func TestExecutionFailureDoesNotBecomeSuccessOrReplay(t *testing.T) { case "disconnect": h.conn.Close() case "engine": - h.write(first.TurnID, proto.TypeUsage, proto.UsagePayload{Usage: proto.Usage{InputTokens: 13, OutputTokens: 7}}) + h.write(first.TurnID, proto.TypeUsage, proto.UsagePayload{Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 13, OutputTokens: 7}}}) h.write(first.TurnID, proto.TypeError, proto.ErrorPayload{Error: "Engine failed"}) h.write(first.TurnID, proto.TypeDone, proto.DonePayload{}) case "late-input": @@ -362,7 +362,7 @@ func TestExecutionFailureDoesNotBecomeSuccessOrReplay(t *testing.T) { if kind == "engine" { var outcome execution.Result _ = json.Unmarshal(done.Outcome, &outcome) - if outcome.Done.Usage.InputTokens != 13 || outcome.Done.Usage.OutputTokens != 7 { + if outcome.Done.Usage.Tokens.InputTokens != 13 || outcome.Done.Usage.Tokens.OutputTokens != 7 { t.Fatal("failed execution lost reported usage") } } diff --git a/services/core/tests/integration/execution_events_test.go b/services/core/tests/integration/execution_events_test.go index 9588652e5..12712a9ca 100644 --- a/services/core/tests/integration/execution_events_test.go +++ b/services/core/tests/integration/execution_events_test.go @@ -21,7 +21,7 @@ func TestExecutionPersistsLiveAndCancelledPartialOutput(t *testing.T) { h.write(input.TurnID, proto.TypeDelta, proto.DeltaPayload{Delta: "已输出", Sequence: 1}) h.write(input.TurnID, proto.TypeToolCall, proto.ToolCallPayload{ID: "tool-1", Name: "Bash", Stage: "before", Observation: &proto.ToolObservation{Kind: "command", Command: "pwd", Status: "in_progress"}}) h.write(input.TurnID, proto.TypeToolCall, proto.ToolCallPayload{ID: "tool-1", Name: "Bash", Stage: "after", Observation: &proto.ToolObservation{Kind: "command", Command: "pwd", Status: "completed"}}) - h.write(input.TurnID, proto.TypeUsage, proto.UsagePayload{Usage: proto.Usage{InputTokens: 11, OutputTokens: 2}}) + h.write(input.TurnID, proto.TypeUsage, proto.UsagePayload{Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 11, OutputTokens: 2}}}) reopened, pool := testStore(t) defer pool.Close() deadline := time.Now().Add(5 * time.Second) @@ -86,7 +86,7 @@ func TestExecutionDoesNotCompleteAfterEventPersistenceFailure(t *testing.T) { t.Fatal(err) } h.write(input.TurnID, proto.TypeDelta, proto.DeltaPayload{Delta: "cannot be stored", Sequence: 1}) - h.write(input.TurnID, proto.TypeDone, proto.DonePayload{Content: "Do not report success", Usage: proto.Usage{InputTokens: 13}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "failed-native"}}) + h.write(input.TurnID, proto.TypeDone, proto.DonePayload{Content: "Do not report success", Usage: proto.Usage{Tokens: &proto.TokenUsage{InputTokens: 13}}, Metadata: map[string]any{proto.DoneMetaAgentSessionID: "failed-native"}}) turn := h.finished(result, sessions.TurnFailed) var outcome execution.Result _ = json.Unmarshal(turn.Outcome, &outcome) @@ -94,7 +94,7 @@ func TestExecutionDoesNotCompleteAfterEventPersistenceFailure(t *testing.T) { t.Fatal(outcome.ErrorCode) } bound, err := sessionAdapter(h.s).GetSessionExecutionBinding(ctx, h.tenant, h.session.ID) - if err != nil || bound.NativeSessionID != "failed-native" || outcome.Done.Usage.InputTokens != 13 { + if err != nil || bound.NativeSessionID != "failed-native" || outcome.Done.Usage.Tokens.InputTokens != 13 { t.Fatalf("terminal failure lost native continuity or usage: %+v %+v %v", bound, outcome, err) } events, err := h.s.ListTurnEvents(ctx, h.tenant, h.session.ID, input.TurnID, 0, 100)