diff --git a/contracts/agents-api/environments.md b/contracts/agents-api/environments.md index e99afc829..0a40df7db 100644 --- a/contracts/agents-api/environments.md +++ b/contracts/agents-api/environments.md @@ -255,7 +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. +- Every `openai_hosted` Environment starts `pending`, including one with no files or setup, and 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. Core journals `agent.session.environment.connected` when the Runtime connects, which is usually before initialization starts. A `self_hosted` Environment emits neither `pending` nor `ready`, and starts `complete` unless it has files or setup. 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: diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index 4cb29a291..e7d4da9d3 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -111,6 +111,7 @@ Each item is Core's deliberate or native behavior where the official service beh - `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. +- The usual hosted order is `connected`, `pending`, `ready`, because Core journals `connected` when the Runtime connects, before it starts initialization; the last Environment event then reads `ready` while the Environment is `connected`. **Files, Skills, Environment files and Artifacts** ([Files and Skills](./source-files.md), [Environment files and Artifacts](./environment-files.md)) diff --git a/contracts/agents-api/zh/environments.md b/contracts/agents-api/zh/environments.md index 933cffb75..75f1f11dc 100644 --- a/contracts/agents-api/zh/environments.md +++ b/contracts/agents-api/zh/environments.md @@ -1,7 +1,7 @@ --- title: "Environment 与模板" source: contracts/agents-api/environments.md -source_hash: fa17eba20ecb905da36dfb1c0ffb8d9a86f49560f0688ddc6b9a3b59009fc4b4 +source_hash: 5cd6355e98456fc2b65dd8097080a8bce391d577bd9a746370d281c379d5e48b --- Environment 是 Session 的执行资源,包括 Harness 运行所在的机器、工作区以及已完成准备的能力。Session 通过其 `environment` 配置创建 Environment;不存在独立的 create 调用。Environment 模板是 Session 创建时解析的可复用准备配置。本契约涵盖这两类资源、两种放置方式、输入接纳、能力准备、Skills、Plugins 和 MCP 连接来源。 @@ -257,7 +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 两者都不发出。 +- 每个 `openai_hosted` Environment 都从 `pending` 开始,包括没有文件和初始化设置的 Environment,并在开始初始化的事务中记录 `agent.session.environment.pending`,在完成初始化的事务中记录 `agent.session.environment.ready`,各恰好一次。Core 在 Runtime 连接时记录 `agent.session.environment.connected`,通常早于初始化开始。`self_hosted` Environment 不发出 `pending` 和 `ready`,且除非有文件或初始化设置,否则从 `complete` 开始。 初始化失败时记录的事件和 Session 状态由 [Environment 初始化失败](sessions-events.md#environment-initialization-failure)定义。原因只指明步骤及其退出状态: diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index 43691403b..e7f48629f 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "OpenAI 兼容性" source: contracts/agents-api/index.md -source_hash: b6322e1f2733cd1e5b1486488c4341190ce72b3cd29bb4547ef3fc244893ff7c +source_hash: 27f86ff815bee7a3bfe69a5fb4706cea7f5c5103e5718b5a5b227822e3e259ac --- Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。 @@ -114,6 +114,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - `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 没有。 +- hosted Environment 通常的顺序是 `connected`、`pending`、`ready`,因为 Core 在 Runtime 连接时、开始初始化之前就记录 `connected`;此时最后一个 Environment 事件显示 `ready`,而 Environment 处于 `connected`。 **Files、Skills、Environment files 和 Artifacts**([Files and Skills](source-files.md)、[Environment files and Artifacts](environment-files.md)) diff --git a/services/core/internal/db/queries/environments.sql b/services/core/internal/db/queries/environments.sql index 2e122d372..1e843a5a2 100644 --- a/services/core/internal/db/queries/environments.sql +++ b/services/core/internal/db/queries/environments.sql @@ -1,6 +1,7 @@ -- name: CreateEnvironment :exec INSERT INTO environments (id, session_id, initialization) -VALUES ($1, $2, CASE WHEN EXISTS (SELECT 1 FROM initial_environment_files WHERE session_id = $2) +VALUES ($1, $2, CASE WHEN (SELECT configuration->'environment'->>'type' FROM sessions WHERE id = $2) = 'openai_hosted' + OR EXISTS (SELECT 1 FROM initial_environment_files WHERE session_id = $2) OR EXISTS (SELECT 1 FROM environment_setups WHERE session_id = $2) THEN 'pending' ELSE 'complete' END); -- name: GetEnvironment :one diff --git a/services/core/internal/db/sqlc/environments.sql.go b/services/core/internal/db/sqlc/environments.sql.go index 6a8097c94..3f0004e28 100644 --- a/services/core/internal/db/sqlc/environments.sql.go +++ b/services/core/internal/db/sqlc/environments.sql.go @@ -13,7 +13,8 @@ import ( const createEnvironment = `-- name: CreateEnvironment :exec INSERT INTO environments (id, session_id, initialization) -VALUES ($1, $2, CASE WHEN EXISTS (SELECT 1 FROM initial_environment_files WHERE session_id = $2) +VALUES ($1, $2, CASE WHEN (SELECT configuration->'environment'->>'type' FROM sessions WHERE id = $2) = 'openai_hosted' + OR EXISTS (SELECT 1 FROM initial_environment_files WHERE session_id = $2) OR EXISTS (SELECT 1 FROM environment_setups WHERE session_id = $2) THEN 'pending' ELSE 'complete' END) ` diff --git a/services/core/internal/execution/archive_cancellation_cleanup_test.go b/services/core/internal/execution/archive_cancellation_cleanup_test.go index 6dddee6bc..8a4307025 100644 --- a/services/core/internal/execution/archive_cancellation_cleanup_test.go +++ b/services/core/internal/execution/archive_cancellation_cleanup_test.go @@ -83,6 +83,9 @@ func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) { t.Fatal(err) } session := created.Session + if _, err := pool.Exec(t.Context(), `UPDATE environments SET initialization='complete' WHERE id=$1`, session.Environment.ID); err != nil { + t.Fatal(err) + } secret := uuid.NewString() key := deployment.AllocationKey{TenantID: project.TenantID, EnvironmentID: session.Environment.ID} owner, err := leased.Deployment.ReserveAllocation(t.Context(), key, installation, runtimedevice.HashCredential(secret)) diff --git a/services/core/internal/execution/runtime_workspace_settlement_test.go b/services/core/internal/execution/runtime_workspace_settlement_test.go index 13ad9efd2..824fc5afa 100644 --- a/services/core/internal/execution/runtime_workspace_settlement_test.go +++ b/services/core/internal/execution/runtime_workspace_settlement_test.go @@ -110,6 +110,10 @@ func workspaceSettlementFixtureWithSetup(t *testing.T, provider sandbox.SandboxP t.Fatal(err) } session := created.Session + // The fixture's Environment is already prepared. + if _, err := pool.Exec(t.Context(), `UPDATE environments SET initialization='complete' WHERE id=$1`, session.Environment.ID); err != nil { + t.Fatal(err) + } if nodeID != "" { if _, err := operations.EnsurePlacement(t.Context(), deployment.AllocationKey{TenantID: session.TenantID, EnvironmentID: session.Environment.ID}, installation); err != nil { t.Fatal(err) diff --git a/services/core/tests/integration/archive_cancellation_test.go b/services/core/tests/integration/archive_cancellation_test.go index 6f3bb7a73..d1b05c5ec 100644 --- a/services/core/tests/integration/archive_cancellation_test.go +++ b/services/core/tests/integration/archive_cancellation_test.go @@ -62,6 +62,9 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { if err != nil { t.Fatal(err) } + if _, err := s.pool.Exec(t.Context(), `UPDATE environments SET initialization='complete' WHERE id=$1`, session.Environment.ID); err != nil { + t.Fatal(err) + } secret := uuid.NewString() owner, err := leased.Deployment.ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: project.TenantID, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(secret)) if err != nil { diff --git a/services/core/tests/integration/local_environment_devices_test.go b/services/core/tests/integration/local_environment_devices_test.go index 9a146faad..feda04d56 100644 --- a/services/core/tests/integration/local_environment_devices_test.go +++ b/services/core/tests/integration/local_environment_devices_test.go @@ -24,6 +24,10 @@ func localEnvironment(t *testing.T, s *Store, tenant string) (sessions.Session, if err != nil { t.Fatal(err) } + // The fixture's Environment is already prepared. + if _, err := s.pool.Exec(t.Context(), `UPDATE environments SET initialization='complete' WHERE id=$1`, environment.ID); err != nil { + t.Fatal(err) + } return session, environment } diff --git a/services/core/tests/integration/root_fixture_test.go b/services/core/tests/integration/root_fixture_test.go index 4d4367215..6f00f0e9f 100644 --- a/services/core/tests/integration/root_fixture_test.go +++ b/services/core/tests/integration/root_fixture_test.go @@ -57,6 +57,11 @@ func (s *Store) SetPlacement(rules *placement.Rules) { s.placement = rules } // CreateSession creates a Session through the Session service on s. func (s *Store) CreateSession(ctx context.Context, tenant string, input sessions.CreateSession) (sessions.Session, error) { creation, err := createSession(ctx, s, tenant, input) + // A hosted fixture with nothing to set up stands for a prepared Environment; + // the initialization tests give theirs files or a setup. + if err == nil && creation.Session.Environment != nil && len(input.InitialFiles) == 0 && input.Initialization.Empty() { + _, err = s.pool.Exec(ctx, `UPDATE environments SET initialization='complete' WHERE id=$1`, creation.Session.Environment.ID) + } return creation.Session, err } diff --git a/services/core/tests/integration/runtime_initialization_test.go b/services/core/tests/integration/runtime_initialization_test.go index 3fed7317b..65aabfaca 100644 --- a/services/core/tests/integration/runtime_initialization_test.go +++ b/services/core/tests/integration/runtime_initialization_test.go @@ -216,3 +216,42 @@ func TestManagedRuntimePreparationAllOperationsUsePeer(t *testing.T) { t.Fatal("initialization events", lifecycle) } } + +func TestHostedEnvironmentWithoutSetupIsInitialized(t *testing.T) { + s, key := configuredStore(t) + tenant := uuid.NewString() + creation, err := createSession(t.Context(), s, tenant, WithFixtureModelProvider(sessions.CreateSession{ + Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), + Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), + })) + if err != nil { + t.Fatal(err) + } + session := creation.Session + if initializationState(t, s, tenant, session.Environment.ID) != "pending" { + t.Fatal("hosted Environment without setup did not start pending") + } + provider := &initializingProvider{lifecycleProvider: lifecycleProvider{resources: map[string]sandbox.Info{}}} + provider.apply = func(request proto.RuntimePreparePayload, _ []byte) proto.RuntimePrepareResultPayload { + t.Error("empty initialization sent a Runtime operation", request.Action) + return completedInitialization(request, nil) + } + worker, _ := managedWorkerMode(t, s, key, provider, true) + if _, err := worker.ProvisionEnvironment(t.Context(), tenant, session.Environment.ID, key); err != nil { + t.Fatal(err) + } + awaitInitialization(t, s, tenant, session.Environment.ID, "complete") + 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) + } +}