Split plugin install discovery into list and request tools (#23372)

## Summary
- Add `list_available_plugins_to_install` as the inventory step for
plugin and connector install suggestions.
- Slim `request_plugin_install` so it only handles the actual
elicitation, instead of carrying the full discoverable list in its
prompt.
- Emit send-time telemetry when an install elicitation is dispatched,
including requested tool identity in the event payload.
- Emit install-result telemetry through `SessionTelemetry`, including
tool type, user response action, and completion status.
- Update registration and tests to cover the new two-step flow while
keeping the existing `tool_suggest` feature gate unchanged.

## Testing
- `just fmt`
- `cargo test -p codex-tools`
- `cargo test -p codex-core request_plugin_install`
- `cargo test -p codex-core list_available_plugins_to_install`
- `cargo test -p codex-core
install_suggestion_tools_can_be_registered_without_search_tool`
- `cargo test -p codex-otel
manager_records_plugin_install_suggestion_metric`
- `cargo test -p codex-otel
manager_records_plugin_install_elicitation_sent_metric`
- `just fix -p codex-core`
- `just fix -p codex-tools`
- `just fix -p codex-otel`
- `cargo check -p codex-core`
This commit is contained in:
Matthew Zeng authored and GitHub committed 2026-05-19 14:45:37 -07:00
1 parent 1509ae6d8d
commit 8335b56c33
17 files changed
+667 -158

No files matched your search

@@ -22,6 +22,7 @@ use core_test_support::test_codex::test_codex;
use serde_json::Value;
const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
const LIST_AVAILABLE_PLUGINS_TO_INSTALL_TOOL_NAME: &str = "list_available_plugins_to_install";
const REQUEST_PLUGIN_INSTALL_TOOL_NAME: &str = "request_plugin_install";
const DISCOVERABLE_GMAIL_ID: &str = "connector_68df038e0ba48191908c8434991bbac2";
@@ -125,6 +126,12 @@ async fn request_plugin_install_is_available_without_search_tool_after_discovery
!tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME),
"tools list should not include {TOOL_SEARCH_TOOL_NAME}: {tools:?}"
);
assert!(
tools
.iter()
.any(|name| name == LIST_AVAILABLE_PLUGINS_TO_INSTALL_TOOL_NAME),
"tools list should include {LIST_AVAILABLE_PLUGINS_TO_INSTALL_TOOL_NAME}: {tools:?}"
);
assert!(
tools
.iter()
@@ -132,18 +139,26 @@ async fn request_plugin_install_is_available_without_search_tool_after_discovery
"tools list should include {REQUEST_PLUGIN_INSTALL_TOOL_NAME}: {tools:?}"
);
let list_description =
function_tool_description(&body, LIST_AVAILABLE_PLUGINS_TO_INSTALL_TOOL_NAME)
.expect("description");
assert!(list_description.contains(
"The user explicitly asks to use a specific plugin or connector that is not already available in the current context or active `tools` list."
));
assert!(list_description.contains(
"`tool_search` is not available, or it has already been called and did not find or make the requested tool callable."
));
assert!(list_description.contains(
"When both a plugin and a connector match, prefer the plugin; use the connector only when its corresponding plugin is already installed."
));
let description =
function_tool_description(&body, REQUEST_PLUGIN_INSTALL_TOOL_NAME).expect("description");
assert!(description.contains(
"Use this tool only to ask the user to install one known plugin or connector from the list below"
));
assert!(description.contains(
"`tool_search` is not available, or it has already been called and did not find or make the requested tool callable."
));
assert!(description.contains(
"Only use when the user explicitly asks to use that exact listed plugin or connector."
"Use this tool only after `list_available_plugins_to_install` returns a plugin or connector that exactly matches the user's explicit request."
));
assert!(description.contains("IMPORTANT: DO NOT call this tool in parallel with other tools."));
assert!(!description.contains(DISCOVERABLE_GMAIL_ID));
assert!(!description.contains("tool_search fails to find a good match"));
Ok(())