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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions apps/daemon/internal/agent/codex/completion_evidence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package codex

import (
"strings"
"sync"
)

// completionEvidence observes root native items, even when reasoning is hidden.
// It does not decide business success or require artifacts from tool-only tasks.
type completionEvidence struct {
mu sync.Mutex
seen map[string]bool
last string
final bool
}

func (e *completionEvidence) observe(item ThreadItem) {
e.mu.Lock()
defer e.mu.Unlock()
if e.seen[item.ID] {
return
}
if e.seen == nil {
e.seen = make(map[string]bool)
}
e.seen[item.ID] = true
// Only finite types reach logs; an unknown item prevents a positive diagnosis.
switch item.Type {
case "reasoning", "agentMessage", "commandExecution", "fileChange", "mcpToolCall", "dynamicToolCall", "webSearch":
e.last = item.Type
default:
e.last = "other"
}
if item.Type == "agentMessage" && (item.Phase == "final_answer" || item.Phase == "") && strings.TrimSpace(item.Text) != "" {
e.final = true
}
}

func (e *completionEvidence) snapshot() (string, bool) {
e.mu.Lock()
defer e.mu.Unlock()
return e.last, e.final
}
80 changes: 80 additions & 0 deletions apps/daemon/internal/agent/codex/completion_evidence_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package codex

import (
"bytes"
"context"
"encoding/json"
"log/slog"
"strings"
"testing"

"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
)

func TestNativeCompletionEvidence(t *testing.T) {
for _, tc := range []struct {
name string
items []ThreadItem
status string
fail bool
}{
{"reasoning only", []ThreadItem{{ID: "r", Type: "reasoning"}}, "completed", true},
{"early commentary", []ThreadItem{{ID: "a", Type: "agentMessage", Phase: "commentary", Text: "SECRET"}, {ID: "r", Type: "reasoning"}}, "completed", true},
{"tool then reasoning", []ThreadItem{{ID: "t", Type: "commandExecution", Command: "true", Status: "completed", ExitCode: new(int)}, {ID: "r", Type: "reasoning"}}, "completed", true},
{"final answer", []ThreadItem{{ID: "r", Type: "reasoning"}, {ID: "a", Type: "agentMessage", Phase: "final_answer", Text: "SECRET"}}, "completed", false},
{"unphased answer", []ThreadItem{{ID: "a", Type: "agentMessage", Text: "SECRET"}, {ID: "r", Type: "reasoning"}}, "completed", false},
{"post final reasoning", []ThreadItem{{ID: "a", Type: "agentMessage", Phase: "final_answer", Text: "SECRET"}, {ID: "r", Type: "reasoning"}}, "completed", false},
{"tool only", []ThreadItem{{ID: "t", Type: "commandExecution", Command: "true", Status: "completed", ExitCode: new(int)}}, "completed", false},
{"empty", nil, "completed", false},
{"unknown", []ThreadItem{{ID: "r", Type: "reasoning"}, {ID: "x", Type: "SECRET"}}, "completed", false},
{"duplicate reasoning", []ThreadItem{{ID: "r", Type: "reasoning"}, {ID: "a", Type: "agentMessage", Text: "SECRET"}, {ID: "r", Type: "reasoning"}}, "completed", false},
{"interrupted", []ThreadItem{{ID: "r", Type: "reasoning"}}, "interrupted", false},
} {
for _, visible := range []bool{true, false} {
t.Run(tc.name+map[bool]string{true: " visible", false: " hidden"}[visible], func(t *testing.T) {
var logs bytes.Buffer
cfg := defaultSessionConfig(testStateRoot(t))
cfg.logger = slog.New(slog.NewJSONHandler(&logs, nil))
out := make(chan proto.Envelope, 64)
s := &Session{runID: "run", out: out, cancelCtx: context.Background(), cfg: cfg}
s.reasoningSummary = visible
s.setThreadID("thread")
s.beginRootTurn("thread", "turn")
for _, item := range tc.items {
raw, _ := json.Marshal(ItemCompletedNotification{ThreadID: "thread", TurnID: "turn", Item: item})
s.onItemCompleted(raw)
s.beginRootTurn("thread", "turn") // duplicate start must retain evidence
}
// Child and stale turn evidence cannot change the root outcome.
s.onItemCompleted(json.RawMessage(`{"threadId":"child","turnId":"turn","item":{"id":"child","type":"reasoning"}}`))
s.onItemCompleted(json.RawMessage(`{"threadId":"thread","turnId":"old","item":{"id":"old","type":"agentMessage","phase":"final_answer","text":"SECRET"}}`))
raw, _ := json.Marshal(TurnCompletedNotification{ThreadID: "thread", Turn: Turn{ID: "turn", Status: tc.status}})
s.onTurnCompleted(raw)
s.onTurnCompleted(raw)
failures, done := 0, 0
for env := range out {
if env.Type == proto.TypeError {
var p proto.ErrorPayload
_ = env.DecodePayload(&p)
if p.Code != "incomplete_response" {
t.Fatalf("code %q", p.Code)
}
failures++
}
if env.Type == proto.TypeDone {
done++
}
}
if (failures == 1) != tc.fail || done != 1 {
t.Fatalf("failures=%d done=%d", failures, done)
}
if strings.Contains(logs.String(), "SECRET") {
t.Fatal("content leaked")
}
if !strings.Contains(logs.String(), `"decision_source":"codex_adapter"`) {
t.Fatal("missing decision source")
}
})
}
}
}
18 changes: 18 additions & 0 deletions apps/daemon/internal/agent/codex/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ type Session struct {
// reasoningSummary reports that the Agent requested reasoning summaries.
reasoningSummary bool

completion completionEvidence
finalTextMu sync.Mutex
finalText string
lastErrText string
Expand Down Expand Up @@ -226,6 +227,14 @@ func (s *Session) onTurnCompleted(raw json.RawMessage) {
}

status := strings.ToLower(p.Turn.Status)
lastItem, hasFinal := s.completion.snapshot()
reason := "native_terminal"
incomplete := status == "completed" && lastItem == "reasoning" && !hasFinal
if incomplete {
reason = "reasoning_only_tail"
} else if status == "failed" {
reason = "native_failed"
}
finalText := s.takeFinalText()
errText := s.takeLastErrText()
// Always log the turn outcome — operators need this when a prompt
Expand All @@ -237,9 +246,18 @@ func (s *Session) onTurnCompleted(raw json.RawMessage) {
"run_id", s.runID,
"turn_id", p.Turn.ID,
"status", p.Turn.Status,
"decision_source", "codex_adapter",
"decision_reason", reason,
"last_completed_item_type", lastItem,
"has_final_answer", hasFinal,
"final_text_len", len(finalText),
"buffered_err_text_len", len(errText),
"has_error", p.Turn.Error != nil)
if incomplete {
s.emitTerminalFailure("Native execution ended with reasoning but no final response or subsequent action.", true, proto.ErrorPayload{Code: "incomplete_response"})
s.finishAfterTerminal()
return
}
if status == "failed" {
// Body precedence on failure:
// 1. agent's final text (rare on hard failures but exists for
Expand Down
4 changes: 4 additions & 0 deletions apps/daemon/internal/agent/codex/session_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func (s *Session) onItemCompleted(raw json.RawMessage) {
return
}
if p.Item.Type == "reasoning" {
s.completion.observe(p.Item)
s.completeReasoning(p.Item)
return
}
Expand All @@ -72,6 +73,9 @@ func (s *Session) onItemCompleted(raw json.RawMessage) {
if messageText == "" {
messageText = text
}
completedItem := p.Item
completedItem.Text = messageText
s.completion.observe(completedItem)
first := false
if p.Item.Type == "agentMessage" && messageText != "" {
first = s.boundary.text(received, p.TurnID)
Expand Down
1 change: 1 addition & 0 deletions contracts/agents-api/core-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ The [Session diagnostics](./session-diagnostics.md) routes return these categori
| `request_timeout` | Native provider timeout classification |
| `context_length_exceeded` | Native context limit classification |
| `cyber_policy` | Native cyber policy rejection |
| `incomplete_response` | The executor terminated without the expected response; params are empty and the public Turn error maps to `internal_error` |
| `connection_failed` | Native connection failure; params contain `http_status`, an integer in 100–599 or null |
| `model_provider_required` | Missing frozen model provider |
| `runtime_unavailable` | `execution_device_unavailable`, `execution_unavailable` |
Expand Down
3 changes: 2 additions & 1 deletion contracts/agents-api/zh/core-errors.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Core 管理错误"
source: contracts/agents-api/core-errors.md
source_hash: eeaba03a33dd1f82943139292035873d8139a5dd862d82e1f9cf1267f1ad10c6
source_hash: 320fe54d2db2fb022f2d6069daf74c5b189c87db3575a36c58242bf98d7fea61
---

`/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。
Expand Down Expand Up @@ -124,6 +124,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封
| `request_timeout` | 原生提供商超时分类 |
| `context_length_exceeded` | 原生上下文限制分类 |
| `cyber_policy` | 原生网络安全策略拒绝 |
| `incomplete_response` | 执行器已终止但缺少预期响应;params 为空,公共 Turn 错误映射为 `internal_error` |
| `connection_failed` | 原生连接故障;params 包含 `http_status`,其值为 100–599 范围内的整数或 null |
| `model_provider_required` | 缺少已冻结的模型提供商 |
| `runtime_unavailable` | `execution_device_unavailable`、`execution_unavailable` |
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Model providers are not process settings; see [Default models](#default-models).
| `OAC_EXECUTION_CONCURRENCY` | `4` | Concurrent execution work, from 1 to 1024 |
| `OAC_SANDBOX_MAX_ACTIVE` | `100` | Active sandbox limit for direct Providers with suspension, from 1 to 100000. Independent of execution concurrency and node capacity |
| `OAC_SANDBOX_MAX_RETAINED` | `400` | Retained sandbox limit for direct Providers, from 1 to 100000, including active, suspended and unconfirmed cleanup. Must be at least the active limit |
| `OAC_TURN_AUTO_RETRIES` | `3` | Automatic retries of a Turn that failed on a recoverable model or tool error, from 0 to 5. `0` turns it off |
| `OAC_TURN_AUTO_RETRIES` | `3` | Automatic retries of a Turn that failed on a recoverable model or tool error, or because its Runtime connection closed, from 0 to 5. `0` turns it off |
| `OAC_DEFAULT_HARNESS` | `codex` | Harness used when a request does not name one |
| `OAC_HARNESSES` | Every registered Harness | Comma-separated Harnesses to enable besides the default one. Unknown names stop startup |
| `OAC_WRITE_AUDIT_RETENTION` | `2160h` | Minimum `1h` |
Expand Down
4 changes: 3 additions & 1 deletion docs/runtime-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,12 @@ Retry only an operation whose own contract makes retry safe. A retired preparati

An adapter may add `code` and `http_status` to a Run's `error` frame. They are optional neutral metadata, not a terminal event; the frame's text, Usage, `done`, native Session identity and cancellation receipts keep their order and meaning.

The accepted codes are `authentication_error`, `rate_limit_exceeded`, `usage_limit_exceeded`, `server_overloaded`, `server_error`, `invalid_request`, `resource_not_found`, `request_timeout`, `context_length_exceeded`, `cyber_policy`, `provider_billing_unavailable` and `connection_failed` ([`engine_failure.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/engine_failure.go)). `connection_failed` keeps an integer `http_status` from 100 to 599; status 402 normalizes to `provider_billing_unavailable`, which keeps only status 402. This category describes the model endpoint billing rejection, not the application user balance, and does not authorize automatic retries. Every other status is discarded. A missing, malformed or unknown value leaves the error unclassified without discarding Usage or `done`.
The accepted codes are `authentication_error`, `rate_limit_exceeded`, `usage_limit_exceeded`, `server_overloaded`, `server_error`, `invalid_request`, `resource_not_found`, `request_timeout`, `context_length_exceeded`, `cyber_policy`, `provider_billing_unavailable`, `incomplete_response` and `connection_failed` ([`engine_failure.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/engine_failure.go)). `connection_failed` keeps an integer `http_status` from 100 to 599; status 402 normalizes to `provider_billing_unavailable`, which keeps only status 402. This category describes the model endpoint billing rejection, not the application user balance, and does not authorize automatic retries. Every other status is discarded. A missing, malformed or unknown value leaves the error unclassified without discarding Usage or `done`.

Core stores accepted values in the Turn outcome as `engine_error_code` and `engine_http_status`. Within a Turn, a later accepted classification replaces the earlier classification and its status; an unclassified error frame does not erase them. Classification does not carry over to another Turn. The optional `upstream_request_id` is accepted only as a UUID and stored privately as `engine_request_id`; it never enters Project diagnostics, public Session/Turn responses or SSE. The classification is subordinate to the terminal status and Core's `error_code`: it cannot turn a completed or cancelled Turn into a failure, hide an incomplete event stream, or override a persistence or cancellation-receipt failure. Normal delivery and terminal journal draining use the same extraction. [Session diagnostics](../contracts/agents-api/session-diagnostics.md) expose the category only for a failed Turn whose Core error is `engine_failed`. The [Codex](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/README.md) and [Claude Code](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/claude/README.md) adapter guides give each Harness's mapping; adapters classify structured Harness errors and model provider JSON error bodies using their accepted `error.code` or `error.type`, never arbitrary error prose. Codex alone recognizes its exact terminal HTTP 402 envelope when structured information is absent or `other`; see its adapter guide for the bounded rule.

`incomplete_response` means the adapter observed a native terminal sequence that ended without the expected response or subsequent action. It is not a business-delivery verdict. Core preserves this typed failure, allows at most one automatic continuation within the configured Turn retry budget, and retains cancellation and newest-Turn checks. The continuation asks the Harness to inspect existing results and avoid repeating completed side effects; external operations still require application-owned idempotency. Codex detects a completed root Turn whose last unique completed item is reasoning and which has no nonempty `final_answer` or unphased assistant message; earlier commentary does not satisfy that check. Hidden reasoning is still observed. Unknown tail types, tool-only completions and ordinary empty completions do not activate this rule.

## Workspace operations

A workspace read that needs no running Turn uses the read-only preparation profile: `execution_prepare` with `workspace_read_only`, which requires the `workspace_read_preparation` capability. It accepts only the bound Environment and resource identity; execution options, model and MCP credentials, native Session continuation and model or tool input are excluded, and the owner rejects `execution_start`. The Runtime serves it from its bound local workspace without starting a Harness process. The profile publishes `released` after the Runtime drops the preparation's ownership; a stale status snapshot never publishes success. A release request, HTTP disconnect or remote socket closure alone does not confirm the release.
Expand Down
4 changes: 2 additions & 2 deletions docs/zh/configuration.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "配置参考"
source: docs/configuration.md
source_hash: 7e6594fcab4b4fbf7d44d99e21745d106f200af902e2e7efacad5a7c19eea439
source_hash: 900d0f3b1586911057c69291c8f17c951712297db26182a38d1b96a5563b5e12
---

Core 安装的每项设置、其归属位置以及修改方式。安装程序写入初始值;之后请使用本参考。每项设置都恰好只有一个归属位置,分属以下三类:
Expand Down Expand Up @@ -39,7 +39,7 @@ Core 安装的每项设置、其归属位置以及修改方式。安装程序写
| `OAC_EXECUTION_CONCURRENCY` | `4` | Concurrent execution work, from 1 to 1024 |
| `OAC_SANDBOX_MAX_ACTIVE` | `100` | 启用暂停能力的直接 Provider 的活跃沙箱上限,范围 1–100000;独立于执行并发度和节点容量 |
| `OAC_SANDBOX_MAX_RETAINED` | `400` | 直接 Provider 的保留沙箱上限,范围 1–100000,包括活跃、已暂停以及尚未确认清理完成的分配;必须不小于活跃上限 |
| `OAC_TURN_AUTO_RETRIES` | `3` | Turn 因可恢复的模型或工具错误失败后的自动重试次数,0 到 5。设为 `0` 关闭 |
| `OAC_TURN_AUTO_RETRIES` | `3` | Turn 因可恢复的模型或工具错误、或因 Runtime 连接关闭而失败后的自动重试次数,0 到 5。设为 `0` 关闭 |
| `OAC_DEFAULT_HARNESS` | `codex` | Harness used when a request does not name one |
| `OAC_HARNESSES` | Every registered Harness | Comma-separated Harnesses to enable besides the default one. Unknown names stop startup |
| `OAC_WRITE_AUDIT_RETENTION` | `2160h` | Minimum `1h` |
Expand Down
Loading
Loading