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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contracts/agents-api/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ stateDiagram-v2
- Each file, Skill or Plugin transfer has a two-minute staging budget. Configure, package installation, setup commands and snapshot finalization share the whole initialization’s 30-minute budget, whose remaining duration Core carries on each operation and the Runtime enforces. Initial input keeps its five-minute admission deadline, so large installations should start from an idle Session.
- A running initialization whose owner is lost, including across a Core restart, fails as unconfirmed; nothing is replayed. A completed Environment never reinstalls on reconnect or native recovery, so later user changes survive.
- Failure is terminal for the Session but destroys neither compute nor files.
- An `openai_hosted` Environment journals `agent.session.environment.pending` in the transaction that starts its initialization and `agent.session.environment.ready` in the one that completes it, each exactly once. A `self_hosted` Environment emits neither.

A failed initialization records the events and Session state that [Environment initialization failure](./sessions-events.md#environment-initialization-failure) defines. The reason names only the step and its exit status:

Expand Down
4 changes: 3 additions & 1 deletion contracts/agents-api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Each item is Core's deliberate or native behavior where the official service beh
- `openai_hosted` and Template `capability_directories` must lie inside `/workspace`; the official service also accepts system paths such as `/etc` ([capability directories](./environments.md#plugins-and-capability-directories)).
- `env` cannot set `OPENAI_API_KEY`, which Core reserves ([env](./environments.md#env-and-setup-commands)); the official service accepts it.
- A `self_hosted` Session without pending input requests no connection ([activity](./environments.md#activity-and-required-actions)); one unused official Session switched to `requires_action` with `environment_connection` about five minutes after creation, and its deletion then returned 409.
- A hosted Environment emits `ready` once, when its initialization completes, and not again before each later Turn as one official Session did. It also emits `connected` and `disconnected`, which that Session did not.

**Files, Skills, Environment files and Artifacts** ([Files and Skills](./source-files.md), [Environment files and Artifacts](./environment-files.md))

Expand Down Expand Up @@ -141,7 +142,7 @@ Each item is Core's deliberate or native behavior where the official service beh

**Execution and history**

- The stream does not emit Environment `pending` or `ready` events, or every pinned interim tool-output variant.
- The stream does not emit every pinned interim tool-output variant.
- Native Item variants beyond those listed under [Turns and Items](./sessions-events.md#turns-and-items) are not projected, and Items cannot be modified.
- A function result that cancellation prevents from being applied never appears as an Item.
- Pinned Codex can lose command output emitted before its stream subscription.
Expand Down Expand Up @@ -183,6 +184,7 @@ Each item is Core's deliberate or native behavior where the official service beh

- The order of errors when one request has several faults, and error, default and payload-limit parity in general.
- A Subagent's function call: Core makes it a required action of the root Turn that runs the child, with `turn_id` set to that Turn, and publishes the child's `function_call` Item once as `agent.session.turn.item.added` with the child Turn ID and a null `output_index`; a result may name either Turn. The Turn is `waiting` and the Session `requires_action` while root output may continue, and the `call_id` equals the `call_id` of the child's `function_call` Item. Claude Code publishes the child's Items when the root Turn settles, after the required action.
- Whether the official service emits Environment `pending` and when: Core emits it when a hosted Environment's initialization starts, and the one official sample showed only `ready`, about 29 s after creation.
- Whether Subagent child Turns and pending Environment file writes block Session deletion.
- The official order between the pending-input error and an unknown result target.
- Whether a Session whose latest Turn failed accepts new input: Core starts a new Turn, and one official sample rejected the input.
Expand Down
3 changes: 2 additions & 1 deletion contracts/agents-api/zh/environments.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Environment 与模板"
source: contracts/agents-api/environments.md
source_hash: 15550304b85596e09cbeb4cc6d7253a4bdd64ffd6adc58a539511e0c939f7ad6
source_hash: fa17eba20ecb905da36dfb1c0ffb8d9a86f49560f0688ddc6b9a3b59009fc4b4
---

Environment 是 Session 的执行资源,包括 Harness 运行所在的机器、工作区以及已完成准备的能力。Session 通过其 `environment` 配置创建 Environment;不存在独立的 create 调用。Environment 模板是 Session 创建时解析的可复用准备配置。本契约涵盖这两类资源、两种放置方式、输入接纳、能力准备、Skills、Plugins 和 MCP 连接来源。
Expand Down Expand Up @@ -257,6 +257,7 @@ stateDiagram-v2
- 每个文件、Skill 或 Plugin 传输的暂存预算为两分钟。configure、软件包安装、设置命令和快照最终确定共享整个初始化过程的 30 分钟预算,Core 在每项操作中携带剩余时长,由 Runtime 执行。初始输入仍保留其五分钟接纳期限,因此大型安装应从空闲 Session 开始。
- 正在运行且所有权丧失的初始化,包括跨 Core 重启丧失所有权,会作为未确认而失败;不会重播任何内容。已完成的 Environment 在重连或原生恢复时绝不会重新安装,因此用户后续更改会保留下来。
- 失败对 Session 而言是终止状态,但不会销毁计算资源或文件。
- `openai_hosted` Environment 在开始初始化的事务中记录 `agent.session.environment.pending`,在完成初始化的事务中记录 `agent.session.environment.ready`,各恰好一次。`self_hosted` Environment 两者都不发出。

初始化失败时记录的事件和 Session 状态由 [Environment 初始化失败](sessions-events.md#environment-initialization-failure)定义。原因只指明步骤及其退出状态:

Expand Down
6 changes: 4 additions & 2 deletions contracts/agents-api/zh/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "OpenAI 兼容性"
source: contracts/agents-api/index.md
source_hash: 09fcf64102a376e53b2881621c21647afb7187e3899ef6c4b9f350fa7a2d95fe
source_hash: b6322e1f2733cd1e5b1486488c4341190ce72b3cd29bb4547ef3fc244893ff7c
---

Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。
Expand Down Expand Up @@ -113,6 +113,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh
- `openai_hosted` 和模板的 `capability_directories` 必须位于 `/workspace` 内;官方服务也接受 `/etc` 等系统路径([能力目录](environments.md#plugins-and-capability-directories))。
- `env` 不能设置 Core 保留的 `OPENAI_API_KEY`([env](environments.md#env-and-setup-commands));官方服务接受该变量。
- 没有待处理输入的 `self_hosted` Session 不会请求连接([活动状态](environments.md#activity-and-required-actions));一个未使用的官方 Session 在创建约五分钟后转为 `requires_action`,原因为 `environment_connection`,之后删除它返回 409。
- hosted Environment 只在初始化完成时发出一次 `ready`,不会像一个官方 Session 那样在之后每个 Turn 之前再次发出;它还会发出 `connected` 和 `disconnected`,而该官方 Session 没有。

**Files、Skills、Environment files 和 Artifacts**([Files and Skills](source-files.md)、[Environment files and Artifacts](environment-files.md))

Expand Down Expand Up @@ -144,7 +145,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh

**执行和历史**

- 流不会发出 Environment 的 `pending` 或 `ready` 事件,也不会覆盖固定版本中的所有临时 tool-output 变体。
- 流不会覆盖固定版本中的所有临时 tool-output 变体。
- 除 [Turns and Items](sessions-events.md#turns-and-items) 中列出的变体外,其他原生 Item 变体不会被投影,而且 Items 无法修改。
- 如果取消导致函数结果无法应用,该结果将永远不会作为 Item 出现。
- 固定版本的 Codex 可能会丢失在订阅其流之前发出的命令输出。
Expand Down Expand Up @@ -186,6 +187,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh

- 当一个请求存在多项故障时的错误顺序,以及错误处理、默认值和载荷限制的总体一致性。
- Subagent 的函数调用:Core 将其作为运行该子级的根 Turn 的待执行操作,`turn_id` 为该 Turn,并把子级的 `function_call` Item 以 `agent.session.turn.item.added` 发布一次,`turn_id` 为子级 Turn ID,`output_index` 为 null;结果可以指定其中任一 Turn。该 Turn 为 `waiting`、Session 为 `requires_action`,根输出可继续,`call_id` 等于该子级 `function_call` Item 的 `call_id`。Claude Code 在根 Turn 收敛时才发布子级的 Item,晚于该待执行操作。
- 官方服务是否发出 Environment `pending` 以及何时发出:Core 在 hosted Environment 初始化开始时发出,而仅有的一个官方样本只显示了创建约 29 秒后的 `ready`。
- Subagent 子级 Turns 和待处理的 Environment file 写入是否会阻止 Session 删除。
- 官方服务在待处理输入错误与未知结果目标之间的先后顺序。
- 最新 Turn 失败的 Session 是否接受新输入:Core 会开始新的 Turn,而一个官方样本拒绝了该输入。
Expand Down
20 changes: 18 additions & 2 deletions services/core/internal/sessions/execution_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,17 +175,33 @@ func (o *ExecutionOperations) advanceInitialization(ctx context.Context, owner E
// device.
func (o *ExecutionOperations) ClaimEnvironmentInitialization(ctx context.Context, owner EnvironmentInitialization) error {
return o.advanceInitialization(ctx, owner, func(ctx context.Context, tx InitializationTx, environment Environment) (bool, error) {
return tx.ClaimInitialization(ctx, environment.ID)
return journalInitialization(ctx, tx, environment, "pending", tx.ClaimInitialization)
})
}

// CompleteEnvironmentInitialization completes the claimed preparation.
func (o *ExecutionOperations) CompleteEnvironmentInitialization(ctx context.Context, owner EnvironmentInitialization) error {
return o.advanceInitialization(ctx, owner, func(ctx context.Context, tx InitializationTx, environment Environment) (bool, error) {
return tx.CompleteInitialization(ctx, environment.ID)
return journalInitialization(ctx, tx, environment, "ready", tx.CompleteInitialization)
})
}

// journalInitialization applies one initialization transition and, when it
// applied to a hosted Environment, journals the matching
// agent.session.environment.<status> change in the same transaction. Self-hosted
// Environments are prepared by the user and emit neither event.
func journalInitialization(ctx context.Context, tx InitializationTx, environment Environment, status string, transition func(context.Context, string) (bool, error)) (bool, error) {
kind, err := EnvironmentType(environment.Configuration)
if err != nil {
return false, err
}
applied, err := transition(ctx, environment.ID)
if err != nil || !applied || kind != "openai_hosted" {
return applied, err
}
return true, tx.AppendChanges(ctx, EnvironmentStateChange(environment.ID, kind, status))
}

// FailEnvironmentInitialization records the failure of a preparation that did
// not complete and fails the Environment, as FailEnvironment reports it. It
// records state and settles work; it never destroys compute or a workspace.
Expand Down
10 changes: 7 additions & 3 deletions services/core/internal/sessions/execution_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ func TestInitializationOwnerAndDeviceRules(t *testing.T) {
}

func TestInitializationTransitionsRunUnderTheListedDevice(t *testing.T) {
var journaled []SessionChange
owner := EnvironmentInitialization{TenantID: "tenant", SessionID: "session", EnvironmentID: "environment", DeviceID: "device"}
listed := func() (ExecutionDevice, bool, error) {
return ExecutionDevice{ID: "device", EnvironmentID: "environment"}, true, nil
Expand All @@ -176,15 +177,18 @@ func TestInitializationTransitionsRunUnderTheListedDevice(t *testing.T) {
want error
calls []string
}{
{"claim", fakeTx{loadEnvironment: returns(hostedEnvironment), loadSessionDevice: listed, claimInitialization: returns(true)},
{"claim", fakeTx{loadEnvironment: returns(hostedEnvironment), loadSessionDevice: listed, claimInitialization: returns(true), appendChanges: collect(&journaled)},
LockedSession{}, func(o *ExecutionOperations) error { return o.ClaimEnvironmentInitialization(t.Context(), owner) },
nil, append(checked, "ClaimInitialization environment", "AppendChanges agent.session.environment.pending")},
{"claim of a self-hosted Environment", fakeTx{loadEnvironment: returns(Environment{ID: "environment", Status: "pending", Configuration: []byte(`{"type":"self_hosted"}`)}), loadSessionDevice: listed, claimInitialization: returns(true)},
LockedSession{}, func(o *ExecutionOperations) error { return o.ClaimEnvironmentInitialization(t.Context(), owner) },
nil, append(checked, "ClaimInitialization environment")},
{"claim that did not apply", fakeTx{loadEnvironment: returns(hostedEnvironment), loadSessionDevice: listed, claimInitialization: returns(false)},
LockedSession{}, func(o *ExecutionOperations) error { return o.ClaimEnvironmentInitialization(t.Context(), owner) },
ErrTurnConflict, append(checked, "ClaimInitialization environment")},
{"complete", fakeTx{loadEnvironment: returns(hostedEnvironment), loadSessionDevice: listed, completeInitialization: returns(true)},
{"complete", fakeTx{loadEnvironment: returns(hostedEnvironment), loadSessionDevice: listed, completeInitialization: returns(true), appendChanges: collect(&journaled)},
LockedSession{}, func(o *ExecutionOperations) error { return o.CompleteEnvironmentInitialization(t.Context(), owner) },
nil, append(checked, "CompleteInitialization environment")},
nil, append(checked, "CompleteInitialization environment", "AppendChanges agent.session.environment.ready")},
{"device moved", fakeTx{loadEnvironment: returns(hostedEnvironment), loadSessionDevice: loads(ExecutionDevice{ID: "other", EnvironmentID: "environment"}, true)},
LockedSession{}, func(o *ExecutionOperations) error { return o.CompleteEnvironmentInitialization(t.Context(), owner) },
ErrTurnConflict, checked},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ func TestHostedInitializationFailureRecordsSafeSessionFailure(t *testing.T) {
t.Fatal("Session list", page, err)
}
events, err := sessionAdapter(s).ListSessionEvents(t.Context(), tenant, session.ID, 0)
// Transport may connect while initialization is still running.
if len(events) > 0 && events[0].Event.Type == "agent.session.environment.connected" {
// Initialization starts and transport may connect before the failure.
for len(events) > 0 && (events[0].Event.Type == "agent.session.environment.pending" || events[0].Event.Type == "agent.session.environment.connected") {
events = events[1:]
}

Expand Down Expand Up @@ -352,7 +352,7 @@ func TestHostedInitializationFailurePublicHTTP(t *testing.T) {
t.Fatal("invalid frame", line)
}
bodies = append(bodies, data)
if frame["type"] == "agent.session.environment.connected" && len(frames) == 0 && len(names) == 1 {
if (frame["type"] == "agent.session.environment.pending" || frame["type"] == "agent.session.environment.connected") && len(frames) == 0 && len(names) == 1 {
names = nil
continue
}
Expand Down
13 changes: 13 additions & 0 deletions services/core/tests/integration/runtime_initialization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,17 @@ func TestManagedRuntimePreparationAllOperationsUsePeer(t *testing.T) {
if err != nil || initializationState(t, s, allocation.TenantID, allocation.EnvironmentID) != "complete" {
t.Fatal("initialization incomplete", allocation, err)
}
changes, err := sessionAdapter(s).ListSessionEvents(t.Context(), tenant, session.ID, 0)
if err != nil {
t.Fatal(err)
}
var lifecycle []string
for _, change := range changes {
if change.Event.Type == "agent.session.environment.pending" || change.Event.Type == "agent.session.environment.ready" {
lifecycle = append(lifecycle, change.Event.Type)
}
}
if !reflect.DeepEqual(lifecycle, []string{"agent.session.environment.pending", "agent.session.environment.ready"}) {
t.Fatal("initialization events", lifecycle)
}
}
Loading