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
2 changes: 1 addition & 1 deletion contracts/agents-api/sessions-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ A Session stays usable after a Turn fails: new input starts a new Turn. Later re
- **Limits.** The request body is at most 1 MiB, and the stored input of one request at most 512 KiB.
- **Null batch.** An explicit `null` for `events` is invalid.
- **Empty batch.** `{"events": []}` checks that the Session exists and returns 202. It creates no Turn, Item or retry identity.
- **Retries.** An `Idempotency-Key` of up to 128 bytes identifies the whole ordered batch. The same key and batch return 202 again without admitting anything twice; the same key with another batch returns 409 `idempotency_conflict`. A request without a key is always new.
- **Retries.** An `Idempotency-Key` of 1–256 bytes identifies the whole ordered batch; an empty or longer key returns 400. The same key and batch return 202 again without admitting anything twice; the same key with another batch returns 409 `idempotency_conflict`. A request without a key is always new.
- **Messages.** On an idle Session a message batch starts a queued Turn. While a Turn runs, messages join it (steering); they never start a parallel Turn. Each message stays its own user Item, even when the Harness receives several as one prompt.
- **Cancellation.** A queued Turn is cancelled without a live Runtime. A running Turn is cancelled when the Runtime confirms it; completion can win that race. The Turn has stopped when it reads `cancelled`, not when the request returns. A Turn that waits for a function result keeps the Session at `requires_action` with its `required_actions` until then, and the request emits no Session event. A cancellation on an idle Session with no pending input is accepted and has no effect; while an input reservation is pending, it returns 409.
- **Function results.** `turn_id`, `call_id` and `success` are required; `output` and `error` are optional and nullable ([content rules](./message-content.md#function-results)). An identical repeated result returns 202 without another application or event. The result Item appears when the Harness applies the result; a result that cancellation prevents from being applied stays stored but produces no Item.
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/wire-semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ Creation validates the body and metadata types, the request fields and initial i

### Creation retries

Send an `Idempotency-Key` of 1–128 bytes that is not only whitespace; a longer or whitespace-only key returns 400 `invalid_request`. An empty header counts as no key. Without a key, every request creates a new Session. The official service creates a new Session for each request even with the same key; Core returns the original one.
Send an `Idempotency-Key` of 1–256 bytes; a longer key returns 400 `invalid_request`. An empty header counts as no key. Without a key, every request creates a new Session. The official service creates a new Session for each request even with the same key; Core returns the original one.

| Case | Response |
| --- | --- |
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/sessions-events.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Session、事件和历史"
source: contracts/agents-api/sessions-events.md
source_hash: 180e08dbdd9c85ae85005cc202f73316f2b1b04828cca67df13b804206f49f02
source_hash: 1c6e57bbd212f7155906d68bbbcb2e976170512a2aca907e024104667419d720
---

本契约涵盖 Session 内部发生的事情:发送输入、实时事件流,以及读取 Turn、Item 和使用量的持久化历史。Session 资源本身(创建配置、重试标识、更新、列出和删除)见 [API 行为](wire-semantics.md)。消息和函数结果内容见[消息内容](message-content.md)。[Agents API 指南](../../../docs/zh/api/public-agent-api.md)展示了使用 SDK 和 HTTP 的调用方式。
Expand Down Expand Up @@ -50,7 +50,7 @@ Turn 失败后 Session 仍可使用:新输入会启动一个新 Turn。后来
- **限制。** 请求正文最多为 1 MiB,单次请求存储的输入最多为 512 KiB。
- **空值批次。** 将 `events` 显式设为 `null` 无效。
- **空批次。** `{"events": []}` 会检查 Session 是否存在并返回 202。它不会创建 Turn、Item 或重试标识。
- **重试。** 最长 128 字节的 `Idempotency-Key` 标识整个有序批次。相同密钥和批次会再次返回 202,而不会重复接纳任何内容;相同密钥搭配另一个批次会返回 409 `idempotency_conflict`。不带密钥的请求始终是新请求。
- **重试。** 1–256 字节的 `Idempotency-Key` 标识整个有序批次;空键或更长的键返回 400。相同密钥和批次会再次返回 202,而不会重复接纳任何内容;相同密钥搭配另一个批次会返回 409 `idempotency_conflict`。不带密钥的请求始终是新请求。
- **消息。** 在空闲 Session 上,消息批次会启动一个排队的 Turn。Turn 运行时,消息会加入其中(引导);它们绝不会启动并行 Turn。每条消息都保留为独立的用户 Item,即使 Harness 将多条消息作为同一个提示接收。
- **取消。** 排队的 Turn 无需活动 Runtime 即可取消。正在运行的 Turn 只有在 Runtime 确认后才会取消;完成操作可能赢得该竞争。读取到 `cancelled` 时才表示该 Turn 已停止,而不是请求返回时。正在等待函数结果的 Turn 在此之前会让 Session 保持 `requires_action` 及其 `required_actions`,且该请求不会发出 Session 事件。在没有待处理输入的情况下,对空闲 Session 执行的取消会被接受且不产生任何效果;而在输入预留待处理期间,取消会返回 409。
- **函数结果。** `turn_id`、`call_id` 和 `success` 为必填项;`output` 和 `error` 为可选项且可为空([内容规则](message-content.md#function-results))。重复提交完全相同的结果会返回 202,不会再次应用或发出事件。Harness 应用结果时才会出现结果 Item;如果取消操作导致结果无法应用,结果仍会存储,但不会产生 Item。
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/wire-semantics.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "API 行为"
source: contracts/agents-api/wire-semantics.md
source_hash: 29a928dc41487b9e5a11cdeff7305d2f006040822caf66b299e504cb8eab9662
source_hash: abb76393a7bc5669970b2fd7156f6293143b12949f0517fc8061737f51c73d32
---

已锁定版本的 OpenAI Python SDK([upstream.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json))定义了 `/v1` 路由、字段和类型。本页面说明这些类型未作规定之处 Core 的行为,例如状态码、错误字段、默认值和列表边界,以及 Core 与官方服务存在差异的地方。[OpenAI 兼容性](index.md) 列出了这些差异和尚存缺口;[Sessions, events and history](sessions-events.md)、[message content](message-content.md)、[Vaults](vaults.md)、[source Files and Skills](source-files.md) 和 [Environment files and Artifacts](environment-files.md) 分别负责各自资源的规则。
Expand Down Expand Up @@ -241,7 +241,7 @@ Session 创建会将 Agent 的有效配置复制到不可变快照中。使用 `

### 创建重试 {#creation-retries}

发送一个 1–128 字节且不只包含空白字符的 `Idempotency-Key`;更长或仅包含空白字符的键会返回 400 `invalid_request`。空标头视为未提供键。没有键时,每个请求都会创建一个新的 Session。官方服务即使收到相同的键,也会为每个请求创建新的 Session;Core 则返回原有 Session。
发送一个 1–256 字节的 `Idempotency-Key`;更长的键会返回 400 `invalid_request`。空标头视为未提供键。没有键时,每个请求都会创建一个新的 Session。官方服务即使收到相同的键,也会为每个请求创建新的 Session;Core 则返回原有 Session。

| 情况 | 响应 |
| --- | --- |
Expand Down
2 changes: 1 addition & 1 deletion docs/api/public-agent-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Two lists differ: `GET /files` returns up to 10,000 files at once, and workspace

### Idempotency

Send an `Idempotency-Key` (up to 128 bytes) when creating a Session or sending input. A retry with the same key and body returns the original result instead of doing the work twice. The same key with a different body fails with 409 `idempotency_conflict`.
Send an `Idempotency-Key` (1–256 bytes) when creating a Session or sending input. A retry with the same key and body returns the original result instead of doing the work twice. The same key with a different body fails with 409 `idempotency_conflict`.

```python
import uuid
Expand Down
4 changes: 2 additions & 2 deletions docs/zh/api/public-agent-api.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Agents API 指南"
source: docs/api/public-agent-api.md
source_hash: 1db2045ded2e2bf523dd94a3db08c5ac6078bf76489f7cb09de092164e89aa8c
source_hash: bd942f9e71b26493c6d1c3b0dc6fd57e6781a34a6d8597d8559acfdc6c7b8a2a
---

Core 在 `/v1` 提供 OpenAI Agents API。本指南用官方 OpenAI SDK 和普通 HTTP 两种方式演示每项常见任务,并说明 Core 与 OpenAI 的差异。如果尚未启动过 Session,请先阅读[运行第一个 Session](../getting-started/quickstart.md)。
Expand Down Expand Up @@ -111,7 +111,7 @@ agents "/agents/sessions?limit=100&after=$LAST_ID"

### 幂等性 {#idempotency}

创建 Session 或发送输入时,请发送 `Idempotency-Key`(最长 128 字节)。使用相同密钥和正文重试时,会返回原始结果,而不会重复执行工作。相同密钥搭配不同正文会失败,并返回 409 `idempotency_conflict`。
创建 Session 或发送输入时,请发送 `Idempotency-Key`(1–256 字节)。使用相同密钥和正文重试时,会返回原始结果,而不会重复执行工作。相同密钥搭配不同正文会失败,并返回 409 `idempotency_conflict`。

```python
import uuid
Expand Down
4 changes: 2 additions & 2 deletions services/core/internal/api/inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ func (h *Handler) createEvents(w http.ResponseWriter, r *http.Request) {
}
_ = json.Unmarshal(raw, &request)
key := r.Header.Get("Idempotency-Key")
if key == "" {
if len(r.Header.Values("Idempotency-Key")) == 0 {
key = uuid.NewString()
}
if err := sessions.ValidateInputKey(key); err != nil {
if err := sessions.ValidateIdempotencyKey(key); err != nil {
writeSessionsError(w, r, err)
return
}
Expand Down
2 changes: 1 addition & 1 deletion services/core/internal/api/session_semantics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestEmptyEventBatchAuthorizesWithoutExecutionEffects(t *testing.T) {
}
}
reads := sessions.reads
for _, key := range []string{" ", strings.Repeat("x", 129)} {
for _, key := range []string{"", strings.Repeat("x", 257)} {
if w := request("owned", "test-api-key", key); w.Code != http.StatusBadRequest || sessions.reads != reads {
t.Fatalf("invalid identity reached resource: %d %s", w.Code, w.Body)
}
Expand Down
10 changes: 3 additions & 7 deletions services/core/internal/sessions/creation.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ func prepareCreation(input CreateSession, fingerprint func(string) (string, erro
return NewSession{}, nil, nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
}
engine := strings.TrimSpace(input.Engine)
if !ValidEngine(engine) || !validCreationKey(input.IdempotencyKey) {
if !ValidEngine(engine) || ValidateIdempotencyKey(input.IdempotencyKey) != nil {
return NewSession{}, nil, nil, fmt.Errorf("%w: engine and idempotency key are required", ErrInvalidInput)
}
labels := input.Metadata
Expand Down Expand Up @@ -417,10 +417,6 @@ func prepareCreation(input CreateSession, fingerprint func(string) (string, erro
}, batch, encodedInput, nil
}

func validCreationKey(key string) bool {
return strings.TrimSpace(key) != "" && len(key) <= 128
}

// fingerprintedProvider replaces a bundle's key with its keyed fingerprint.
func fingerprintedProvider(provider *v1.ModelProviderInput, fingerprint func(string) (string, error)) (*v1.ModelProviderInput, error) {
if provider == nil {
Expand Down Expand Up @@ -505,8 +501,8 @@ func (s *Service) FindSessionCreation(ctx context.Context, tenant, key string, r
if err := creator.Validate(); err != nil {
return Creation{}, fmt.Errorf("%w: %v", ErrInvalidInput, err)
}
if !validCreationKey(key) {
return Creation{}, ErrInvalidInput
if err := ValidateIdempotencyKey(key); err != nil {
return Creation{}, err
}
hash, err := intentHash(request, s.storage.FingerprintProviderKey)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions services/core/internal/sessions/creation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ func TestPrepareCreation(t *testing.T) {
}{
"invalid creator": {func(input *CreateSession) { input.Creator = identity.Subject{} }, fingerprints, ErrInvalidInput},
"invalid engine": {func(input *CreateSession) { input.Engine = "Codex" }, fingerprints, ErrInvalidInput},
"blank key": {func(input *CreateSession) { input.IdempotencyKey = " " }, fingerprints, ErrInvalidInput},
"long key": {func(input *CreateSession) { input.IdempotencyKey = strings.Repeat("k", 129) }, fingerprints, ErrInvalidInput},
"empty key": {func(input *CreateSession) { input.IdempotencyKey = "" }, fingerprints, ErrInvalidInput},
"long key": {func(input *CreateSession) { input.IdempotencyKey = strings.Repeat("k", 257) }, fingerprints, ErrInvalidInput},
"configuration over 512K": {func(input *CreateSession) {
input.Configuration = json.RawMessage(`"` + strings.Repeat("x", 512*1024) + `"`)
}, fingerprints, ErrInvalidInput},
Expand Down Expand Up @@ -604,7 +604,7 @@ func TestFindSessionCreation(t *testing.T) {
{"missing creation", "create", request, creator, fingerprints, func() (CreationRecord, error) { return CreationRecord{}, ErrNotFound }, ErrNotFound, []string{"FindCreation tenant create"}},
{"another creator", "create", request, identity.Subject{Kind: "user", ID: "stranger"}, fingerprints, found, ErrIdempotencyConflict, []string{"FindCreation tenant create"}},
{"no intent", "create", nil, creator, nil, nil, ErrInvalidInput, nil},
{"invalid key", " ", request, creator, nil, nil, ErrInvalidInput, nil},
{"invalid key", "", request, creator, nil, nil, ErrInvalidInput, nil},
{"invalid creator", "create", request, identity.Subject{}, nil, nil, ErrInvalidInput, nil},
} {
t.Run(test.name, func(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions services/core/internal/sessions/environment_creation.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ func (s *Service) environmentIdentity(key string, creator identity.Subject, requ
if err := creator.Validate(); err != nil {
return EnvironmentIdentity{}, fmt.Errorf("%w: %v", ErrInvalidInput, err)
}
if key == "" || len(key) > 256 {
return EnvironmentIdentity{}, ErrInvalidInput
if err := ValidateIdempotencyKey(key); err != nil {
return EnvironmentIdentity{}, err
}
hash, err := intentHash(request, s.storage.FingerprintProviderKey)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion services/core/internal/sessions/environment_inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func joinsActiveTurn(turn Turn, active bool) bool {
// passed, or the receipts the batch was admitted with. The checks of
// SubmitInputs apply, and a failed or expired Environment rejects new input.
func (s *Service) ReserveEnvironmentInput(ctx context.Context, tenant, session, key string, inputs []Input) (EnvironmentInputReservation, error) {
if err := ValidateInputKey(key); err != nil {
if err := ValidateIdempotencyKey(key); err != nil {
return EnvironmentInputReservation{}, err
}
batch, encoded, err := validateMessageInputs(inputs)
Expand Down
12 changes: 6 additions & 6 deletions services/core/internal/sessions/inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"slices"
"strings"
"time"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject"
Expand All @@ -31,10 +30,11 @@ type TurnInput struct {
CreatedAt time.Time
}

// ValidateInputKey enforces the shared request identity limit, including no-op requests.
func ValidateInputKey(key string) error {
if strings.TrimSpace(key) == "" || len(key) > 128 {
return fmt.Errorf("%w: idempotency key is required and limited to 128 bytes", ErrInvalidInput)
// ValidateIdempotencyKey enforces the pinned Idempotency-Key bound of 1 to 256
// bytes on every request identity, including no-op requests.
func ValidateIdempotencyKey(key string) error {
if key == "" || len(key) > 256 {
return fmt.Errorf("%w: idempotency key must be 1 to 256 bytes", ErrInvalidInput)
}
return nil
}
Expand Down Expand Up @@ -331,7 +331,7 @@ type InputReader interface {
// (ErrInputPending). Internal receipts are not the response body of the
// public events endpoint.
func (s *Service) SubmitInputs(ctx context.Context, tenant, session, key string, inputs []Input) ([]InputReceipt, error) {
if err := ValidateInputKey(key); err != nil {
if err := ValidateIdempotencyKey(key); err != nil {
return nil, err
}
batch, encoded, err := ValidateInputs(inputs)
Expand Down
10 changes: 9 additions & 1 deletion services/core/internal/sessions/inputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,11 +326,19 @@ func TestSubmitInputs(t *testing.T) {
})
t.Run("an invalid request touches no storage", func(t *testing.T) {
service := deviceService(t, &fakeStorage{t: t})
if _, err := service.SubmitInputs(t.Context(), testTenant, testSession, " ", inputs); !errors.Is(err, ErrInvalidInput) {
if _, err := service.SubmitInputs(t.Context(), testTenant, testSession, "", inputs); !errors.Is(err, ErrInvalidInput) {
t.Fatal(err)
}
if _, err := service.SubmitInputs(t.Context(), testTenant, testSession, inputKey, nil); !errors.Is(err, ErrInvalidInput) {
t.Fatal(err)
}
})
}

func TestIdempotencyKeyIsOneTo256Bytes(t *testing.T) {
for key, valid := range map[string]bool{"": false, "k": true, strings.Repeat("k", 256): true, strings.Repeat("k", 257): false} {
if (ValidateIdempotencyKey(key) == nil) != valid {
t.Error("key length", len(key))
}
}
}
Loading