Files
codex/codex-rs/ext/agent-message-board/src/extension.rs
T
felixxia-oai 8d06d62e15 Allow consumers to select message-board tools (#55973)
## Why

`message_board_tools` always exposed all nine discussion tools. Consumers need a way to expose a subset, such as `read_thread` and `list_threads`, over the same board.

## What changed

Accept an ordered slice of the new `MessageBoardTool` enum and optional description overrides in `message_board_tools`. Use the enum for schemas and dispatch, and keep the agent extension's existing tool set and order through `MessageBoardTool::DISCUSSION`.

## Testing

Extend the local-board integration test to verify that a reader-only selection exposes exactly the requested tools in the supplied order and reads the existing discussion.

GitOrigin-RevId: 60026d73768330ff4b694c3a9e99480e3ed7af27
2026-10-21 19:37:29 +00:00

170 lines
6.5 KiB
Rust

//! Installs caller-bound tools using host identities and optional local connections.
//!
//! Backend selection is a host concern. Startup failures leave tools unavailable
//! and emit a warning; a disabled factory does not open storage.
//! Tool namespaces follow the host's configuration at runtime startup.
use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionEventSink;
use codex_extension_api::ExtensionFuture;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::ExtensionWarning;
use codex_extension_api::ThreadLifecycleContributor;
use codex_extension_api::ThreadStartInput;
use codex_extension_api::ToolContributor;
use codex_message_board::AgentMessageBoard;
use codex_message_board::LocalBoardConnection;
use codex_message_board::MessageBoardTool;
use codex_message_board::message_board_tools;
use codex_protocol::AgentPath;
use codex_protocol::SessionId;
use codex_protocol::ThreadId;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result;
use codex_protocol::openai_models::MultiAgentToolMessages;
use codex_tools::ToolCall;
use codex_tools::ToolExecutor;
use futures::future::BoxFuture;
use std::sync::Arc;
type BoardFactory<C> = dyn for<'a> Fn(
&'a ThreadStartInput<'_, C>,
SessionId,
ThreadId,
) -> BoxFuture<'a, Result<Option<Arc<dyn AgentMessageBoard>>>>
+ Send
+ Sync;
type NamespaceResolver<C> = dyn Fn(&C) -> Option<String> + Send + Sync;
struct BoardExtension<C> {
open: Box<BoardFactory<C>>,
tool_namespace: Box<NamespaceResolver<C>>,
namespace_description: &'static str,
events: Arc<dyn ExtensionEventSink>,
}
struct Binding {
board: Arc<dyn AgentMessageBoard>,
caller: ThreadId,
path: AgentPath,
namespace: Option<String>,
}
impl<C: Sync> ThreadLifecycleContributor<C> for BoardExtension<C> {
fn on_thread_start<'a>(&'a self, input: ThreadStartInput<'a, C>) -> ExtensionFuture<'a, ()> {
Box::pin(async move {
let result = async {
let tree =
SessionId::from_string(input.session_store.level_id()).map_err(|_| {
CodexErr::InvalidRequest("invalid board session identity".into())
})?;
let caller =
ThreadId::from_string(input.thread_store.level_id()).map_err(|_| {
CodexErr::InvalidRequest("invalid board caller identity".into())
})?;
if let Some(board) = (self.open)(&input, tree, caller).await? {
let connection = input.thread_store.get::<LocalBoardConnection>();
let expected = connection
.as_ref()
.map_or(tree, |connection| connection.identity());
if board.identity() != expected {
return Err(CodexErr::InvalidRequest(
"message-board factory returned another tree".into(),
));
}
input.thread_store.insert(Binding {
board,
caller,
namespace: (self.tool_namespace)(input.config),
path: match connection {
Some(connection) => connection.caller_path(caller)?,
None => input
.session_source
.get_agent_path()
.unwrap_or_else(AgentPath::root),
},
});
}
Ok::<(), CodexErr>(())
}
.await;
if result.is_err() {
self.events.emit_warning(ExtensionWarning {
thread_id: input.thread_store.level_id().into(), turn_id: None,
message: "Agent message-board initialization failed; its tools are unavailable for this runtime.".into(),
});
}
})
}
}
impl<C: Sync> ToolContributor for BoardExtension<C> {
fn tools(
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
) -> Vec<Arc<dyn for<'call> ToolExecutor<ToolCall<'call>>>> {
self.tools_with_descriptions(thread_store, /*tool_messages*/ None)
}
fn tools_for_step(
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
step_store: &ExtensionData,
) -> Vec<Arc<dyn for<'call> ToolExecutor<ToolCall<'call>>>> {
let tool_messages = step_store.get::<MultiAgentToolMessages>();
self.tools_with_descriptions(thread_store, tool_messages.as_deref())
}
}
impl<C> BoardExtension<C> {
fn tools_with_descriptions(
&self,
thread_store: &ExtensionData,
tool_messages: Option<&MultiAgentToolMessages>,
) -> Vec<Arc<dyn for<'call> ToolExecutor<ToolCall<'call>>>> {
thread_store
.get::<Binding>()
.map_or_else(Vec::new, |binding| {
message_board_tools(
&MessageBoardTool::DISCUSSION,
binding.board.clone(),
binding.caller,
binding.path.clone(),
binding.namespace.as_deref(),
self.namespace_description,
tool_messages,
)
})
}
}
/// Installs message-board lifecycle and tool contributions. The factory selects
/// a local or remote backend, or returns None when disabled. Configuration is
/// read at runtime startup, including resume; no board is created by installation.
/// The host supplies its shared namespace description and resolves the namespace
/// name from the runtime's startup configuration. The factory may retain its
/// backend in the existing thread store for other extension contributions.
pub fn install<C: Sync + 'static>(
registry: &mut ExtensionRegistryBuilder<C>,
namespace_description: &'static str,
tool_namespace: impl Fn(&C) -> Option<String> + Send + Sync + 'static,
open: impl for<'a> Fn(
&'a ThreadStartInput<'_, C>,
SessionId,
ThreadId,
) -> BoxFuture<'a, Result<Option<Arc<dyn AgentMessageBoard>>>>
+ Send
+ Sync
+ 'static,
) {
let extension = Arc::new(BoardExtension {
open: Box::new(open),
tool_namespace: Box::new(tool_namespace),
namespace_description,
events: registry.event_sink(),
});
registry.thread_lifecycle_contributor(extension.clone());
registry.tool_contributor(extension);
}