mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add goal model tools (3 / 5) (#18075)
Adds the model-facing goal tools on top of the app-server API from PR 2. ## Why Once goals are persisted and exposed to clients, the model needs a small, constrained tool surface for goal workflows. The tool contract should let the model inspect goals, create them only when explicitly requested, and mark them complete without giving it broad control over user/runtime-owned state. ## What changed - Added `get_goal`, `create_goal`, and `update_goal` tool specs behind the `goals` feature flag. - Added core goal tool handlers that validate objectives and token budgets before mutating persisted state. - Constrained `create_goal` to create only when no goal exists, with optional `token_budget` only when a budget is explicitly provided. - Tightened the `create_goal` instructions so the model does not infer goals from ordinary task requests. - Constrained `update_goal` to expose only goal completion; pause, resume, clear, and budget-limited transitions remain user- or runtime-controlled. - Registered the goal tools in the tool registry and kept them out of review contexts where they should not appear. ## Verification - Added tool-registry coverage for feature gating and tool availability. - Added core session tests for create/get/update behavior, duplicate goal rejection, budget validation, and completion-only updates.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
//! Responses API tool definitions for persisted thread goals.
|
||||
//!
|
||||
//! These specs expose goal read/update primitives to the model while keeping
|
||||
//! usage accounting system-managed.
|
||||
|
||||
use crate::JsonSchema;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolSpec;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub const GET_GOAL_TOOL_NAME: &str = "get_goal";
|
||||
pub const CREATE_GOAL_TOOL_NAME: &str = "create_goal";
|
||||
pub const UPDATE_GOAL_TOOL_NAME: &str = "update_goal";
|
||||
|
||||
pub fn create_get_goal_tool() -> ToolSpec {
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: GET_GOAL_TOOL_NAME.to_string(),
|
||||
description: "Get the current goal for this thread, including status, budgets, token and elapsed-time usage, and remaining token budget."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(BTreeMap::new(), Some(Vec::new()), Some(false.into())),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_create_goal_tool() -> ToolSpec {
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"objective".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Required. The concrete objective to start pursuing. This starts a new active goal only when no goal is currently defined; if a goal already exists, this tool fails."
|
||||
.to_string(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
"token_budget".to_string(),
|
||||
JsonSchema::integer(Some(
|
||||
"Optional positive token budget for the new active goal.".to_string(),
|
||||
)),
|
||||
),
|
||||
]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: CREATE_GOAL_TOOL_NAME.to_string(),
|
||||
description: format!(
|
||||
r#"Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks.
|
||||
Set token_budget only when an explicit token budget is requested. Fails if a goal exists; use {UPDATE_GOAL_TOOL_NAME} only for status."#
|
||||
),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(
|
||||
properties,
|
||||
/*required*/ Some(vec!["objective".to_string()]),
|
||||
Some(false.into()),
|
||||
),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_update_goal_tool() -> ToolSpec {
|
||||
let properties = BTreeMap::from([(
|
||||
"status".to_string(),
|
||||
JsonSchema::string_enum(
|
||||
vec![json!("complete")],
|
||||
Some(
|
||||
"Required. Set to complete only when the objective is achieved and no required work remains."
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
)]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: UPDATE_GOAL_TOOL_NAME.to_string(),
|
||||
description: r#"Update the existing goal.
|
||||
Use this tool only to mark the goal achieved.
|
||||
Set status to `complete` only when the objective has actually been achieved and no required work remains.
|
||||
Do not mark a goal complete merely because its budget is nearly exhausted or because you are stopping work.
|
||||
You cannot use this tool to pause, resume, or budget-limit a goal; those status changes are controlled by the user or system.
|
||||
When marking a budgeted goal achieved with status `complete`, report the final token usage from the tool result to the user."#
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(
|
||||
properties,
|
||||
/*required*/ Some(vec!["status".to_string()]),
|
||||
Some(false.into()),
|
||||
),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn update_goal_tool_only_exposes_complete_status() {
|
||||
let ToolSpec::Function(tool) = create_update_goal_tool() else {
|
||||
panic!("update_goal should be a function tool");
|
||||
};
|
||||
let status = tool
|
||||
.parameters
|
||||
.properties
|
||||
.as_ref()
|
||||
.and_then(|properties| properties.get("status"))
|
||||
.expect("status property should exist");
|
||||
|
||||
assert_eq!(status.enum_values, Some(vec![json!("complete")]));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ mod agent_tool;
|
||||
mod apply_patch_tool;
|
||||
mod code_mode;
|
||||
mod dynamic_tool;
|
||||
mod goal_tool;
|
||||
mod image_detail;
|
||||
mod json_schema;
|
||||
mod local_tool;
|
||||
@@ -51,6 +52,12 @@ pub use code_mode::create_wait_tool;
|
||||
pub use code_mode::tool_spec_to_code_mode_tool_definition;
|
||||
pub use codex_protocol::ToolName;
|
||||
pub use dynamic_tool::parse_dynamic_tool;
|
||||
pub use goal_tool::CREATE_GOAL_TOOL_NAME;
|
||||
pub use goal_tool::GET_GOAL_TOOL_NAME;
|
||||
pub use goal_tool::UPDATE_GOAL_TOOL_NAME;
|
||||
pub use goal_tool::create_create_goal_tool;
|
||||
pub use goal_tool::create_get_goal_tool;
|
||||
pub use goal_tool::create_update_goal_tool;
|
||||
pub use image_detail::can_request_original_image_detail;
|
||||
pub use image_detail::normalize_output_image_detail;
|
||||
pub use image_detail::sanitize_original_image_detail;
|
||||
|
||||
@@ -101,6 +101,7 @@ pub struct ToolsConfig {
|
||||
pub code_mode_only_enabled: bool,
|
||||
pub can_request_original_image_detail: bool,
|
||||
pub collab_tools: bool,
|
||||
pub goal_tools: bool,
|
||||
pub multi_agent_v2: bool,
|
||||
pub hide_spawn_agent_metadata: bool,
|
||||
pub spawn_agent_usage_hint: bool,
|
||||
@@ -140,6 +141,7 @@ impl ToolsConfig {
|
||||
let include_code_mode = features.enabled(Feature::CodeMode);
|
||||
let include_code_mode_only = include_code_mode && features.enabled(Feature::CodeModeOnly);
|
||||
let include_collab_tools = features.enabled(Feature::Collab);
|
||||
let include_goal_tools = features.enabled(Feature::Goals);
|
||||
let include_multi_agent_v2 = features.enabled(Feature::MultiAgentV2);
|
||||
let include_agent_jobs = features.enabled(Feature::SpawnCsv);
|
||||
let include_default_mode_request_user_input =
|
||||
@@ -218,6 +220,7 @@ impl ToolsConfig {
|
||||
code_mode_only_enabled: include_code_mode_only,
|
||||
can_request_original_image_detail: include_original_image_detail,
|
||||
collab_tools: include_collab_tools,
|
||||
goal_tools: include_goal_tools,
|
||||
multi_agent_v2: include_multi_agent_v2,
|
||||
hide_spawn_agent_metadata: false,
|
||||
spawn_agent_usage_hint: true,
|
||||
@@ -254,6 +257,11 @@ impl ToolsConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_goal_tools_allowed(mut self, allowed: bool) -> Self {
|
||||
self.goal_tools = self.goal_tools && allowed;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_concurrent_threads_per_session(
|
||||
mut self,
|
||||
max_concurrent_threads_per_session: Option<usize>,
|
||||
|
||||
@@ -26,8 +26,10 @@ use crate::create_apply_patch_json_tool;
|
||||
use crate::create_close_agent_tool_v1;
|
||||
use crate::create_close_agent_tool_v2;
|
||||
use crate::create_code_mode_tool;
|
||||
use crate::create_create_goal_tool;
|
||||
use crate::create_exec_command_tool;
|
||||
use crate::create_followup_task_tool;
|
||||
use crate::create_get_goal_tool;
|
||||
use crate::create_image_generation_tool;
|
||||
use crate::create_list_agents_tool;
|
||||
use crate::create_list_dir_tool;
|
||||
@@ -49,6 +51,7 @@ use crate::create_spawn_agents_on_csv_tool;
|
||||
use crate::create_test_sync_tool;
|
||||
use crate::create_tool_search_tool;
|
||||
use crate::create_tool_suggest_tool;
|
||||
use crate::create_update_goal_tool;
|
||||
use crate::create_update_plan_tool;
|
||||
use crate::create_view_image_tool;
|
||||
use crate::create_wait_agent_tool_v1;
|
||||
@@ -215,6 +218,26 @@ pub fn build_tool_registry_plan(
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler("update_plan", ToolHandlerKind::Plan);
|
||||
if config.goal_tools {
|
||||
plan.push_spec(
|
||||
create_get_goal_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler("get_goal", ToolHandlerKind::Goal);
|
||||
plan.push_spec(
|
||||
create_create_goal_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler("create_goal", ToolHandlerKind::Goal);
|
||||
plan.push_spec(
|
||||
create_update_goal_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler("update_goal", ToolHandlerKind::Goal);
|
||||
}
|
||||
|
||||
plan.push_spec(
|
||||
create_request_user_input_tool(request_user_input_tool_description(
|
||||
|
||||
@@ -104,6 +104,15 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
|
||||
] {
|
||||
expected.insert(spec.name().to_string(), spec);
|
||||
}
|
||||
if config.goal_tools {
|
||||
for spec in [
|
||||
create_get_goal_tool(),
|
||||
create_create_goal_tool(),
|
||||
create_update_goal_tool(),
|
||||
] {
|
||||
expected.insert(spec.name().to_string(), spec);
|
||||
}
|
||||
}
|
||||
let collab_specs = if config.multi_agent_v2 {
|
||||
vec![
|
||||
create_spawn_agent_tool_v2(spawn_agent_tool_options(&config)),
|
||||
@@ -186,6 +195,51 @@ fn test_build_specs_collab_tools_enabled() {
|
||||
assert!(!properties.contains_key("fork_turns"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goal_tools_require_goals_feature() {
|
||||
let model_info = model_info();
|
||||
let available_models = Vec::new();
|
||||
let mut features = Features::with_defaults();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
assert_lacks_tool_name(&tools, "get_goal");
|
||||
assert_lacks_tool_name(&tools, "create_goal");
|
||||
assert_lacks_tool_name(&tools, "update_goal");
|
||||
|
||||
features.enable(Feature::Goals);
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
assert_contains_tool_names(&tools, &["get_goal", "create_goal", "update_goal"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_multi_agent_v2_uses_task_names_and_hides_resume() {
|
||||
let model_info = model_info();
|
||||
|
||||
@@ -18,6 +18,7 @@ pub enum ToolHandlerKind {
|
||||
CodeModeWait,
|
||||
DynamicTool,
|
||||
FollowupTaskV2,
|
||||
Goal,
|
||||
ListAgentsV2,
|
||||
ListDir,
|
||||
Mcp,
|
||||
|
||||
Reference in New Issue
Block a user