Make goals feature on by default and no longer experimental (#23732)

## Why

The `goals` feature is ready to be available without requiring users to
opt into experimental features. Keeping it behind the beta flag leaves
persisted thread goals and automatic goal continuation disabled by
default.

This PR also marks the goal-related app server APIs and events as no
longer experimental.

## What changed

- Mark `goals` as `Stage::Stable`.
- Enable `goals` by default in `codex-rs/features/src/lib.rs`.
This commit is contained in:
Eric Traut
2026-05-20 15:07:35 -07:00
committed by GitHub
parent 3075061bdd
commit 0e9d222178
21 changed files with 782 additions and 38 deletions

View File

@@ -3156,6 +3156,62 @@
],
"type": "object"
},
"ThreadGoalClearParams": {
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadGoalGetParams": {
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadGoalSetParams": {
"properties": {
"objective": {
"type": [
"string",
"null"
]
},
"status": {
"anyOf": [
{
"$ref": "#/definitions/ThreadGoalStatus"
},
{
"type": "null"
}
]
},
"threadId": {
"type": "string"
},
"tokenBudget": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"required": [
"threadId"
],
"type": "object"
},
"ThreadGoalStatus": {
"enum": [
"active",
@@ -4316,6 +4372,78 @@
"title": "Thread/name/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/goal/set"
],
"title": "Thread/goal/setRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadGoalSetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/goal/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/goal/get"
],
"title": "Thread/goal/getRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadGoalGetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/goal/getRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/goal/clear"
],
"title": "Thread/goal/clearRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadGoalClearParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/goal/clearRequest",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -372,6 +372,78 @@
"title": "Thread/name/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"thread/goal/set"
],
"title": "Thread/goal/setRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadGoalSetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/goal/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"thread/goal/get"
],
"title": "Thread/goal/getRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadGoalGetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/goal/getRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"thread/goal/clear"
],
"title": "Thread/goal/clearRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/ThreadGoalClearParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/goal/clearRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -15754,6 +15826,32 @@
],
"type": "object"
},
"ThreadGoalClearParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadGoalClearParams",
"type": "object"
},
"ThreadGoalClearResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cleared": {
"type": "boolean"
}
},
"required": [
"cleared"
],
"title": "ThreadGoalClearResponse",
"type": "object"
},
"ThreadGoalClearedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -15767,6 +15865,85 @@
"title": "ThreadGoalClearedNotification",
"type": "object"
},
"ThreadGoalGetParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadGoalGetParams",
"type": "object"
},
"ThreadGoalGetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"goal": {
"anyOf": [
{
"$ref": "#/definitions/v2/ThreadGoal"
},
{
"type": "null"
}
]
}
},
"title": "ThreadGoalGetResponse",
"type": "object"
},
"ThreadGoalSetParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"objective": {
"type": [
"string",
"null"
]
},
"status": {
"anyOf": [
{
"$ref": "#/definitions/v2/ThreadGoalStatus"
},
{
"type": "null"
}
]
},
"threadId": {
"type": "string"
},
"tokenBudget": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"required": [
"threadId"
],
"title": "ThreadGoalSetParams",
"type": "object"
},
"ThreadGoalSetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"goal": {
"$ref": "#/definitions/v2/ThreadGoal"
}
},
"required": [
"goal"
],
"title": "ThreadGoalSetResponse",
"type": "object"
},
"ThreadGoalStatus": {
"enum": [
"active",

View File

@@ -1098,6 +1098,78 @@
"title": "Thread/name/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/goal/set"
],
"title": "Thread/goal/setRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadGoalSetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/goal/setRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/goal/get"
],
"title": "Thread/goal/getRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadGoalGetParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/goal/getRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"thread/goal/clear"
],
"title": "Thread/goal/clearRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/ThreadGoalClearParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Thread/goal/clearRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -13578,6 +13650,32 @@
],
"type": "object"
},
"ThreadGoalClearParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadGoalClearParams",
"type": "object"
},
"ThreadGoalClearResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cleared": {
"type": "boolean"
}
},
"required": [
"cleared"
],
"title": "ThreadGoalClearResponse",
"type": "object"
},
"ThreadGoalClearedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -13591,6 +13689,85 @@
"title": "ThreadGoalClearedNotification",
"type": "object"
},
"ThreadGoalGetParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadGoalGetParams",
"type": "object"
},
"ThreadGoalGetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"goal": {
"anyOf": [
{
"$ref": "#/definitions/ThreadGoal"
},
{
"type": "null"
}
]
}
},
"title": "ThreadGoalGetResponse",
"type": "object"
},
"ThreadGoalSetParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"objective": {
"type": [
"string",
"null"
]
},
"status": {
"anyOf": [
{
"$ref": "#/definitions/ThreadGoalStatus"
},
{
"type": "null"
}
]
},
"threadId": {
"type": "string"
},
"tokenBudget": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"required": [
"threadId"
],
"title": "ThreadGoalSetParams",
"type": "object"
},
"ThreadGoalSetResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"goal": {
"$ref": "#/definitions/ThreadGoal"
}
},
"required": [
"goal"
],
"title": "ThreadGoalSetResponse",
"type": "object"
},
"ThreadGoalStatus": {
"enum": [
"active",

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadGoalClearParams",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"cleared": {
"type": "boolean"
}
},
"required": [
"cleared"
],
"title": "ThreadGoalClearResponse",
"type": "object"
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"threadId": {
"type": "string"
}
},
"required": [
"threadId"
],
"title": "ThreadGoalGetParams",
"type": "object"
}

View File

@@ -0,0 +1,76 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ThreadGoal": {
"properties": {
"createdAt": {
"format": "int64",
"type": "integer"
},
"objective": {
"type": "string"
},
"status": {
"$ref": "#/definitions/ThreadGoalStatus"
},
"threadId": {
"type": "string"
},
"timeUsedSeconds": {
"format": "int64",
"type": "integer"
},
"tokenBudget": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"tokensUsed": {
"format": "int64",
"type": "integer"
},
"updatedAt": {
"format": "int64",
"type": "integer"
}
},
"required": [
"createdAt",
"objective",
"status",
"threadId",
"timeUsedSeconds",
"tokensUsed",
"updatedAt"
],
"type": "object"
},
"ThreadGoalStatus": {
"enum": [
"active",
"paused",
"blocked",
"usageLimited",
"budgetLimited",
"complete"
],
"type": "string"
}
},
"properties": {
"goal": {
"anyOf": [
{
"$ref": "#/definitions/ThreadGoal"
},
{
"type": "null"
}
]
}
},
"title": "ThreadGoalGetResponse",
"type": "object"
}

View File

@@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ThreadGoalStatus": {
"enum": [
"active",
"paused",
"blocked",
"usageLimited",
"budgetLimited",
"complete"
],
"type": "string"
}
},
"properties": {
"objective": {
"type": [
"string",
"null"
]
},
"status": {
"anyOf": [
{
"$ref": "#/definitions/ThreadGoalStatus"
},
{
"type": "null"
}
]
},
"threadId": {
"type": "string"
},
"tokenBudget": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"required": [
"threadId"
],
"title": "ThreadGoalSetParams",
"type": "object"
}

View File

@@ -0,0 +1,72 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ThreadGoal": {
"properties": {
"createdAt": {
"format": "int64",
"type": "integer"
},
"objective": {
"type": "string"
},
"status": {
"$ref": "#/definitions/ThreadGoalStatus"
},
"threadId": {
"type": "string"
},
"timeUsedSeconds": {
"format": "int64",
"type": "integer"
},
"tokenBudget": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"tokensUsed": {
"format": "int64",
"type": "integer"
},
"updatedAt": {
"format": "int64",
"type": "integer"
}
},
"required": [
"createdAt",
"objective",
"status",
"threadId",
"timeUsedSeconds",
"tokensUsed",
"updatedAt"
],
"type": "object"
},
"ThreadGoalStatus": {
"enum": [
"active",
"paused",
"blocked",
"usageLimited",
"budgetLimited",
"complete"
],
"type": "string"
}
},
"properties": {
"goal": {
"$ref": "#/definitions/ThreadGoal"
}
},
"required": [
"goal"
],
"title": "ThreadGoalSetResponse",
"type": "object"
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadGoalClearParams = { threadId: string, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadGoalClearResponse = { cleared: boolean, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ThreadGoalGetParams = { threadId: string, };

View File

@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadGoal } from "./ThreadGoal";
export type ThreadGoalGetResponse = { goal: ThreadGoal | null, };

View File

@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadGoalStatus } from "./ThreadGoalStatus";
export type ThreadGoalSetParams = { threadId: string, objective?: string | null, status?: ThreadGoalStatus | null, tokenBudget?: number | null, };

View File

@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadGoal } from "./ThreadGoal";
export type ThreadGoalSetResponse = { goal: ThreadGoal, };

View File

@@ -365,7 +365,13 @@ export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse";
export type { ThreadForkParams } from "./ThreadForkParams";
export type { ThreadForkResponse } from "./ThreadForkResponse";
export type { ThreadGoal } from "./ThreadGoal";
export type { ThreadGoalClearParams } from "./ThreadGoalClearParams";
export type { ThreadGoalClearResponse } from "./ThreadGoalClearResponse";
export type { ThreadGoalClearedNotification } from "./ThreadGoalClearedNotification";
export type { ThreadGoalGetParams } from "./ThreadGoalGetParams";
export type { ThreadGoalGetResponse } from "./ThreadGoalGetResponse";
export type { ThreadGoalSetParams } from "./ThreadGoalSetParams";
export type { ThreadGoalSetResponse } from "./ThreadGoalSetResponse";
export type { ThreadGoalStatus } from "./ThreadGoalStatus";
export type { ThreadGoalUpdatedNotification } from "./ThreadGoalUpdatedNotification";
export type { ThreadInjectItemsParams } from "./ThreadInjectItemsParams";

View File

@@ -494,19 +494,16 @@ client_request_definitions! {
serialization: thread_id(params.thread_id),
response: v2::ThreadSetNameResponse,
},
#[experimental("thread/goal/set")]
ThreadGoalSet => "thread/goal/set" {
params: v2::ThreadGoalSetParams,
serialization: thread_id(params.thread_id),
response: v2::ThreadGoalSetResponse,
},
#[experimental("thread/goal/get")]
ThreadGoalGet => "thread/goal/get" {
params: v2::ThreadGoalGetParams,
serialization: thread_id(params.thread_id),
response: v2::ThreadGoalGetResponse,
},
#[experimental("thread/goal/clear")]
ThreadGoalClear => "thread/goal/clear" {
params: v2::ThreadGoalClearParams,
serialization: thread_id(params.thread_id),
@@ -1473,9 +1470,7 @@ server_notification_definitions! {
ThreadClosed => "thread/closed" (v2::ThreadClosedNotification),
SkillsChanged => "skills/changed" (v2::SkillsChangedNotification),
ThreadNameUpdated => "thread/name/updated" (v2::ThreadNameUpdatedNotification),
#[experimental("thread/goal/updated")]
ThreadGoalUpdated => "thread/goal/updated" (v2::ThreadGoalUpdatedNotification),
#[experimental("thread/goal/cleared")]
ThreadGoalCleared => "thread/goal/cleared" (v2::ThreadGoalClearedNotification),
#[experimental("thread/settings/updated")]
ThreadSettingsUpdated => "thread/settings/updated" (v2::ThreadSettingsUpdatedNotification),
@@ -3035,7 +3030,7 @@ mod tests {
}
#[test]
fn thread_goal_methods_are_marked_experimental() {
fn thread_goal_methods_are_not_marked_experimental() {
let set_request = ClientRequest::ThreadGoalSet {
request_id: RequestId::Integer(1),
params: v2::ThreadGoalSetParams {
@@ -3060,20 +3055,20 @@ mod tests {
assert_eq!(
crate::experimental_api::ExperimentalApi::experimental_reason(&set_request),
Some("thread/goal/set")
None
);
assert_eq!(
crate::experimental_api::ExperimentalApi::experimental_reason(&get_request),
Some("thread/goal/get")
None
);
assert_eq!(
crate::experimental_api::ExperimentalApi::experimental_reason(&clear_request),
Some("thread/goal/clear")
None
);
}
#[test]
fn thread_goal_notifications_are_marked_experimental() {
fn thread_goal_notifications_are_not_marked_experimental() {
let goal = v2::ThreadGoal {
thread_id: "thr_123".to_string(),
objective: "ship goal mode".to_string(),
@@ -3095,11 +3090,11 @@ mod tests {
assert_eq!(
crate::experimental_api::ExperimentalApi::experimental_reason(&updated),
Some("thread/goal/updated")
None
);
assert_eq!(
crate::experimental_api::ExperimentalApi::experimental_reason(&cleared),
Some("thread/goal/cleared")
None
);
}

View File

@@ -2,9 +2,8 @@ use super::*;
use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ThreadGoal;
use codex_app_server_protocol::ThreadGoalStatus;
use codex_app_server_protocol::ThreadGoalUpdatedNotification;
use codex_app_server_protocol::ThreadRealtimeStartedNotification;
use codex_protocol::protocol::RealtimeConversationVersion;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -15,20 +14,11 @@ fn absolute_path(path: &str) -> AbsolutePathBuf {
AbsolutePathBuf::from_absolute_path(path).expect("absolute path")
}
fn thread_goal_updated_notification() -> ServerNotification {
ServerNotification::ThreadGoalUpdated(ThreadGoalUpdatedNotification {
fn thread_realtime_started_notification() -> ServerNotification {
ServerNotification::ThreadRealtimeStarted(ThreadRealtimeStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: None,
goal: ThreadGoal {
thread_id: "thread-1".to_string(),
objective: "ship goal mode".to_string(),
status: ThreadGoalStatus::Active,
token_budget: None,
tokens_used: 0,
time_used_seconds: 0,
created_at: 1,
updated_at: 1,
},
realtime_session_id: None,
version: RealtimeConversationVersion::V1,
})
}
@@ -182,7 +172,7 @@ async fn experimental_notifications_are_dropped_without_capability() {
&mut connections,
OutgoingEnvelope::ToConnection {
connection_id,
message: OutgoingMessage::AppServerNotification(thread_goal_updated_notification()),
message: OutgoingMessage::AppServerNotification(thread_realtime_started_notification()),
write_complete_tx: None,
},
)
@@ -215,7 +205,7 @@ async fn experimental_notifications_are_preserved_with_capability() {
&mut connections,
OutgoingEnvelope::ToConnection {
connection_id,
message: OutgoingMessage::AppServerNotification(thread_goal_updated_notification()),
message: OutgoingMessage::AppServerNotification(thread_realtime_started_notification()),
write_complete_tx: None,
},
)
@@ -227,7 +217,7 @@ async fn experimental_notifications_are_preserved_with_capability() {
.expect("experimental notification should reach opted-in client");
assert!(matches!(
message.message,
OutgoingMessage::AppServerNotification(ServerNotification::ThreadGoalUpdated(_))
OutgoingMessage::AppServerNotification(ServerNotification::ThreadRealtimeStarted(_))
));
}

View File

@@ -179,6 +179,9 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
};
expected_tools_names.extend([
"update_plan",
"get_goal",
"create_goal",
"update_goal",
"request_user_input",
"apply_patch",
"view_image",

View File

@@ -1077,12 +1077,8 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::Goals,
key: "goals",
stage: Stage::Experimental {
name: "Goals",
menu_description: "Set a persistent goal Codex can continue over time",
announcement: "",
},
default_enabled: false,
stage: Stage::Stable,
default_enabled: true,
},
FeatureSpec {
id: Feature::CollaborationModes,