From 1af814fb51e569bad3eede32a25d1c74d5247e82 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Thu, 7 May 2026 13:23:11 +0100 Subject: [PATCH] v1 --- codex-rs/Cargo.lock | 35 ++++++ codex-rs/Cargo.toml | 8 ++ codex-rs/ext/extension-api/BUILD.bazel | 6 + codex-rs/ext/extension-api/Cargo.toml | 23 ++++ .../examples/enabled_extensions.rs | 113 ++++++++++++++++++ .../ext/extension-api/src/contributors.rs | 36 ++++++ .../extension-api/src/contributors/prompt.rs | 50 ++++++++ codex-rs/ext/extension-api/src/extension.rs | 12 ++ codex-rs/ext/extension-api/src/lib.rs | 20 ++++ codex-rs/ext/extension-api/src/registry.rs | 81 +++++++++++++ codex-rs/ext/git-attribution/BUILD.bazel | 6 + codex-rs/ext/git-attribution/Cargo.toml | 15 +++ codex-rs/ext/git-attribution/src/lib.rs | 79 ++++++++++++ codex-rs/ext/memories/BUILD.bazel | 6 + codex-rs/ext/memories/Cargo.toml | 22 ++++ codex-rs/ext/memories/src/lib.rs | 103 ++++++++++++++++ codex-rs/ext/multi-agent-v2/BUILD.bazel | 6 + codex-rs/ext/multi-agent-v2/Cargo.toml | 15 +++ codex-rs/ext/multi-agent-v2/src/lib.rs | 74 ++++++++++++ 19 files changed, 710 insertions(+) create mode 100644 codex-rs/ext/extension-api/BUILD.bazel create mode 100644 codex-rs/ext/extension-api/Cargo.toml create mode 100644 codex-rs/ext/extension-api/examples/enabled_extensions.rs create mode 100644 codex-rs/ext/extension-api/src/contributors.rs create mode 100644 codex-rs/ext/extension-api/src/contributors/prompt.rs create mode 100644 codex-rs/ext/extension-api/src/extension.rs create mode 100644 codex-rs/ext/extension-api/src/lib.rs create mode 100644 codex-rs/ext/extension-api/src/registry.rs create mode 100644 codex-rs/ext/git-attribution/BUILD.bazel create mode 100644 codex-rs/ext/git-attribution/Cargo.toml create mode 100644 codex-rs/ext/git-attribution/src/lib.rs create mode 100644 codex-rs/ext/memories/BUILD.bazel create mode 100644 codex-rs/ext/memories/Cargo.toml create mode 100644 codex-rs/ext/memories/src/lib.rs create mode 100644 codex-rs/ext/multi-agent-v2/BUILD.bazel create mode 100644 codex-rs/ext/multi-agent-v2/Cargo.toml create mode 100644 codex-rs/ext/multi-agent-v2/src/lib.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b4ea560336..42b1a89b68 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2780,6 +2780,16 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "codex-extension-api" +version = "0.0.0" +dependencies = [ + "codex-git-attribution", + "codex-memories", + "codex-multi-agent-v2", + "rmcp", +] + [[package]] name = "codex-external-agent-migration" version = "0.0.0" @@ -2858,6 +2868,13 @@ dependencies = [ "serde", ] +[[package]] +name = "codex-git-attribution" +version = "0.0.0" +dependencies = [ + "codex-extension-api", +] + [[package]] name = "codex-git-utils" version = "0.0.0" @@ -3065,6 +3082,17 @@ dependencies = [ "wiremock", ] +[[package]] +name = "codex-memories" +version = "0.0.0" +dependencies = [ + "codex-extension-api", + "codex-memories-read", + "codex-utils-absolute-path", + "rmcp", + "serde_json", +] + [[package]] name = "codex-memories-mcp" version = "0.0.0" @@ -3206,6 +3234,13 @@ dependencies = [ "tracing", ] +[[package]] +name = "codex-multi-agent-v2" +version = "0.0.0" +dependencies = [ + "codex-extension-api", +] + [[package]] name = "codex-network-proxy" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index f1673d3bab..f461f16eb0 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -45,6 +45,10 @@ members = [ "exec-server", "execpolicy", "execpolicy-legacy", + "ext/extension-api", + "ext/git-attribution", + "ext/memories", + "ext/multi-agent-v2", "external-agent-migration", "external-agent-sessions", "keyring-store", @@ -159,10 +163,14 @@ codex-exec = { path = "exec" } codex-file-system = { path = "file-system" } codex-exec-server = { path = "exec-server" } codex-execpolicy = { path = "execpolicy" } +codex-extension-api = { path = "ext/extension-api" } codex-external-agent-migration = { path = "external-agent-migration" } codex-external-agent-sessions = { path = "external-agent-sessions" } codex-experimental-api-macros = { path = "codex-experimental-api-macros" } codex-features = { path = "features" } +codex-git-attribution = { path = "ext/git-attribution" } +codex-memories = { path = "ext/memories" } +codex-multi-agent-v2 = { path = "ext/multi-agent-v2" } codex-feedback = { path = "feedback" } codex-install-context = { path = "install-context" } codex-file-search = { path = "file-search" } diff --git a/codex-rs/ext/extension-api/BUILD.bazel b/codex-rs/ext/extension-api/BUILD.bazel new file mode 100644 index 0000000000..c79ad601a4 --- /dev/null +++ b/codex-rs/ext/extension-api/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "extension-api", + crate_name = "codex_extension_api", +) diff --git a/codex-rs/ext/extension-api/Cargo.toml b/codex-rs/ext/extension-api/Cargo.toml new file mode 100644 index 0000000000..7745ed3233 --- /dev/null +++ b/codex-rs/ext/extension-api/Cargo.toml @@ -0,0 +1,23 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-extension-api" +version.workspace = true + +[lib] +name = "codex_extension_api" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +rmcp = { workspace = true, default-features = false, features = [ + "schemars", + "server", +] } + +[dev-dependencies] +codex-git-attribution = { workspace = true } +codex-memories = { workspace = true } +codex-multi-agent-v2 = { workspace = true } diff --git a/codex-rs/ext/extension-api/examples/enabled_extensions.rs b/codex-rs/ext/extension-api/examples/enabled_extensions.rs new file mode 100644 index 0000000000..96df0f447c --- /dev/null +++ b/codex-rs/ext/extension-api/examples/enabled_extensions.rs @@ -0,0 +1,113 @@ +use std::sync::Arc; + +use codex_extension_api::ExtensionRegistryBuilder; +use codex_git_attribution as git_attribution; +use codex_git_attribution::GitAttributionContext; +use codex_memories::MemoriesContext; +use codex_memories::MemoriesExtension; +use codex_multi_agent_v2 as multi_agent_v2; +use codex_multi_agent_v2::MultiAgentV2Context; +use codex_multi_agent_v2::UsageHintAudience; + +fn main() { + let registry = ExtensionRegistryBuilder::::new() + .with_extension(git_attribution::extension()) + .with_extension(Arc::new(MemoriesExtension::new(Some( + "Please use FS access bla bla bla.".to_string(), + )))) + .with_extension(multi_agent_v2::extension()) + .build(); + + let root_context = ctx::RuntimeContext { + commit_attribution: None, + memory_tool_enabled: true, + use_memories: true, + multi_agent_v2_enabled: true, + multi_agent_v2_usage_hint_audience: UsageHintAudience::RootAgent, + root_agent_usage_hint_text: Some("Root-agent usage hint.".to_string()), + subagent_usage_hint_text: Some("Subagent usage hint.".to_string()), + }; + let memories_disabled_context = ctx::RuntimeContext { + use_memories: false, + ..root_context.clone() + }; + + // Build the prompt + let prompt_fragments = registry + .prompt_contributors() + .iter() + .flat_map(|contributor| contributor.contribute(&root_context)) + .collect::>(); + + // Get tools (MCP here but this should shift to handlers) + let tools = registry + .mcp_tool_contributors() + .iter() + .flat_map(|contributor| contributor.tools(&root_context)) + .collect::>(); + let tools_without_memories = registry + .mcp_tool_contributors() + .iter() + .flat_map(|contributor| contributor.tools(&memories_disabled_context)) + .collect::>(); + + println!("prompt fragments: {}", prompt_fragments.len()); + println!("mcp tools: {}", tools.len()); + println!( + "mcp tools when use_memories=false: {}", + tools_without_memories.len() + ); +} + + +mod ctx { + use codex_git_attribution::GitAttributionContext; + use codex_memories::MemoriesContext; + use codex_multi_agent_v2::{MultiAgentV2Context, UsageHintAudience}; + + #[derive(Clone)] + pub struct RuntimeContext { + // Ideally this should be at the config layer instead + pub commit_attribution: Option, + pub memory_tool_enabled: bool, + pub use_memories: bool, + pub multi_agent_v2_enabled: bool, + pub multi_agent_v2_usage_hint_audience: UsageHintAudience, + pub root_agent_usage_hint_text: Option, + pub subagent_usage_hint_text: Option, + } + + impl GitAttributionContext for RuntimeContext { + fn commit_attribution(&self) -> Option<&str> { + self.commit_attribution.as_deref() + } + } + + impl MemoriesContext for RuntimeContext { + fn memory_tool_enabled(&self) -> bool { + self.memory_tool_enabled + } + + fn use_memories(&self) -> bool { + self.use_memories + } + } + + impl MultiAgentV2Context for RuntimeContext { + fn multi_agent_v2_enabled(&self) -> bool { + self.multi_agent_v2_enabled + } + + fn usage_hint_audience(&self) -> UsageHintAudience { + self.multi_agent_v2_usage_hint_audience + } + + fn root_agent_usage_hint_text(&self) -> Option<&str> { + self.root_agent_usage_hint_text.as_deref() + } + + fn subagent_usage_hint_text(&self) -> Option<&str> { + self.subagent_usage_hint_text.as_deref() + } + } +} \ No newline at end of file diff --git a/codex-rs/ext/extension-api/src/contributors.rs b/codex-rs/ext/extension-api/src/contributors.rs new file mode 100644 index 0000000000..94d4cd18e9 --- /dev/null +++ b/codex-rs/ext/extension-api/src/contributors.rs @@ -0,0 +1,36 @@ +//! Runtime contribution ports exposed by the extension API. +//! +//! Extensions can install several independent contribution types. This module +//! is the small public façade over those ports: callers can import the common +//! vocabulary from one place, while each contribution family keeps its own +//! supporting types nearby. + +mod prompt; + +pub use prompt::PromptFragment; +pub use prompt::PromptSlot; + +use rmcp::model::Tool; + +/// Extension contribution that adds prompt fragments during prompt assembly. +/// +/// Implementations should inspect only their feature-owned slice of the +/// current runtime context and describe the prompt content exposed for that +/// invocation. The host remains responsible for ordering fragments and +/// assembling prompt items. +pub trait PromptContributor: Send + Sync { + fn contribute(&self, context: &C) -> Vec; +} + +/// Extension contribution that exposes MCP tool definitions owned by a feature. +/// +/// Implementations should inspect only their feature-owned slice of the +/// current runtime context and return the tools exposed for that invocation. +/// The host remains responsible for mounting those tools and routing +/// execution. +/// +/// This is intentionally MCP-shaped for now because the more general tool +/// abstraction has not been extracted yet. +pub trait McpToolContributor: Send + Sync { + fn tools(&self, context: &C) -> Vec; +} diff --git a/codex-rs/ext/extension-api/src/contributors/prompt.rs b/codex-rs/ext/extension-api/src/contributors/prompt.rs new file mode 100644 index 0000000000..010d91a80a --- /dev/null +++ b/codex-rs/ext/extension-api/src/contributors/prompt.rs @@ -0,0 +1,50 @@ +/// Prompt region targeted by a contributed fragment. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum PromptSlot { + DeveloperPolicy, + DeveloperCapabilities, + ContextualUser, + SeparateDeveloper, +} + +/// One text fragment contributed to prompt assembly by an extension. (wiring not done yet on purpose) +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PromptFragment { + slot: PromptSlot, + text: String, +} + +impl PromptFragment { + /// Creates a prompt fragment for the given slot. + pub fn new(slot: PromptSlot, text: impl Into) -> Self { + Self { + slot, + text: text.into(), + } + } + + /// Creates a developer-policy prompt fragment. + pub fn developer_policy(text: impl Into) -> Self { + Self::new(PromptSlot::DeveloperPolicy, text) + } + + /// Creates a developer-capabilities prompt fragment. + pub fn developer_capability(text: impl Into) -> Self { + Self::new(PromptSlot::DeveloperCapabilities, text) + } + + /// Creates a separate top-level developer prompt fragment. + pub fn separate_developer(text: impl Into) -> Self { + Self::new(PromptSlot::SeparateDeveloper, text) + } + + /// Returns the target prompt slot. + pub fn slot(&self) -> PromptSlot { + self.slot + } + + /// Returns the model-visible text. + pub fn text(&self) -> &str { + &self.text + } +} diff --git a/codex-rs/ext/extension-api/src/extension.rs b/codex-rs/ext/extension-api/src/extension.rs new file mode 100644 index 0000000000..c38f22e9cb --- /dev/null +++ b/codex-rs/ext/extension-api/src/extension.rs @@ -0,0 +1,12 @@ +use std::sync::Arc; + +use crate::ExtensionRegistryBuilder; + +/// First-party extension that can install one or more typed runtime contributions. +/// +/// Implementations should use [`Self::install`] only to register the concrete +/// providers they own. +pub trait CodexExtension: Send + Sync { + /// Registers this extension's concrete typed contributions. + fn install(self: Arc, registry: &mut ExtensionRegistryBuilder); +} diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs new file mode 100644 index 0000000000..ad28cc876e --- /dev/null +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -0,0 +1,20 @@ +//! Semantic ports and typed assembly for first-party Codex extensions. +//! +//! This crate owns the small stable assembly layer: host-independent semantic +//! ports plus the registry used to collect their implementations. Port +//! contracts should live here only once they can be expressed without pulling +//! `codex-core` internals across the boundary. + +#![forbid(unsafe_code)] + +mod contributors; +mod extension; +mod registry; + +pub use contributors::McpToolContributor; +pub use contributors::PromptContributor; +pub use contributors::PromptFragment; +pub use contributors::PromptSlot; +pub use extension::CodexExtension; +pub use registry::ExtensionRegistry; +pub use registry::ExtensionRegistryBuilder; diff --git a/codex-rs/ext/extension-api/src/registry.rs b/codex-rs/ext/extension-api/src/registry.rs new file mode 100644 index 0000000000..84a4511742 --- /dev/null +++ b/codex-rs/ext/extension-api/src/registry.rs @@ -0,0 +1,81 @@ +use std::sync::Arc; + +use crate::CodexExtension; +use crate::McpToolContributor; +use crate::PromptContributor; + +/// Mutable registry used while extensions install their typed contributions. +pub struct ExtensionRegistryBuilder { + mcp_tool_contributors: Vec>>, + prompt_contributors: Vec>>, +} + +impl Default for ExtensionRegistryBuilder { + fn default() -> Self { + Self { + mcp_tool_contributors: Vec::new(), + prompt_contributors: Vec::new(), + } + } +} + +impl ExtensionRegistryBuilder { + /// Creates an empty registry builder. + pub fn new() -> Self { + Self::default() + } + + /// Installs one extension and returns the builder. + #[must_use] + pub fn with_extension(mut self, extension: Arc) -> Self + where + E: CodexExtension + 'static, + { + self.install_extension(extension); + self + } + + /// Installs one extension into the registry under construction. + pub fn install_extension(&mut self, extension: Arc) + where + E: CodexExtension + 'static, + { + extension.install(self); + } + + /// Registers one MCP tool contributor. + pub fn mcp_tool_contributor(&mut self, contributor: Arc>) { + self.mcp_tool_contributors.push(contributor); + } + + /// Registers one prompt contributor. + pub fn prompt_contributor(&mut self, contributor: Arc>) { + self.prompt_contributors.push(contributor); + } + + /// Finishes construction and returns the immutable registry. + pub fn build(self) -> ExtensionRegistry { + ExtensionRegistry { + mcp_tool_contributors: self.mcp_tool_contributors, + prompt_contributors: self.prompt_contributors, + } + } +} + +/// Immutable typed registry produced after extensions are installed. +pub struct ExtensionRegistry { + mcp_tool_contributors: Vec>>, + prompt_contributors: Vec>>, +} + +impl ExtensionRegistry { + /// Returns the registered MCP tool contributors. + pub fn mcp_tool_contributors(&self) -> &[Arc>] { + &self.mcp_tool_contributors + } + + /// Returns the registered prompt contributors. + pub fn prompt_contributors(&self) -> &[Arc>] { + &self.prompt_contributors + } +} diff --git a/codex-rs/ext/git-attribution/BUILD.bazel b/codex-rs/ext/git-attribution/BUILD.bazel new file mode 100644 index 0000000000..0cb1ab5764 --- /dev/null +++ b/codex-rs/ext/git-attribution/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "git-attribution", + crate_name = "codex_git_attribution", +) diff --git a/codex-rs/ext/git-attribution/Cargo.toml b/codex-rs/ext/git-attribution/Cargo.toml new file mode 100644 index 0000000000..46bdbbec0d --- /dev/null +++ b/codex-rs/ext/git-attribution/Cargo.toml @@ -0,0 +1,15 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-git-attribution" +version.workspace = true + +[lib] +name = "codex_git_attribution" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +codex-extension-api = { workspace = true } diff --git a/codex-rs/ext/git-attribution/src/lib.rs b/codex-rs/ext/git-attribution/src/lib.rs new file mode 100644 index 0000000000..a8b2ddbc10 --- /dev/null +++ b/codex-rs/ext/git-attribution/src/lib.rs @@ -0,0 +1,79 @@ +//! Git-attribution prompt contribution packaged as a Codex extension. + +#![forbid(unsafe_code)] + +use std::sync::Arc; + +use codex_extension_api::CodexExtension; +use codex_extension_api::ExtensionRegistryBuilder; +use codex_extension_api::PromptContributor; +use codex_extension_api::PromptFragment; + +const DEFAULT_ATTRIBUTION_VALUE: &str = "Codex "; + +/// Runtime facts needed to render the git-attribution prompt contribution. +/// +/// Hosts should return the current effective attribution override for the +/// thread being assembled. `None` selects Codex's default attribution, while a +/// blank string disables the contribution. +pub trait GitAttributionContext { + fn commit_attribution(&self) -> Option<&str>; +} + +/// Prompt-only extension that contributes the configured git-attribution instruction. +#[derive(Clone, Copy, Debug, Default)] +pub struct GitAttributionExtension; + +impl GitAttributionExtension { + /// Creates an extension instance. + pub fn new() -> Self { + Self + } + + /// Returns the model-visible trailer instruction, if attribution is enabled. + pub fn instruction(&self, context: &C) -> Option { + let trailer = build_commit_message_trailer(context.commit_attribution())?; + Some(format!( + "When you write or edit a git commit message, ensure the message ends with this trailer exactly once:\n{trailer}\n\nRules:\n- Keep existing trailers and append this trailer at the end if missing.\n- Do not duplicate this trailer if it already exists.\n- Keep one blank line between the commit body and trailer block." + )) + } +} + +impl PromptContributor for GitAttributionExtension { + fn contribute(&self, context: &C) -> Vec { + self.instruction(context) + .map(PromptFragment::developer_capability) + .into_iter() + .collect() + } +} + +impl CodexExtension for GitAttributionExtension { + fn install(self: Arc, registry: &mut ExtensionRegistryBuilder) { + registry.prompt_contributor(self); + } +} + +/// Creates a shared git-attribution extension instance. +pub fn extension() -> Arc { + Arc::new(GitAttributionExtension::new()) +} + +fn build_commit_message_trailer(config_attribution: Option<&str>) -> Option { + let value = resolve_attribution_value(config_attribution)?; + Some(format!("Co-authored-by: {value}")) +} + +fn resolve_attribution_value(config_attribution: Option<&str>) -> Option { + match config_attribution { + Some(value) => { + let trimmed = value.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + } + None => Some(DEFAULT_ATTRIBUTION_VALUE.to_string()), + } +} diff --git a/codex-rs/ext/memories/BUILD.bazel b/codex-rs/ext/memories/BUILD.bazel new file mode 100644 index 0000000000..a95d54b842 --- /dev/null +++ b/codex-rs/ext/memories/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "memories", + crate_name = "codex_memories", +) diff --git a/codex-rs/ext/memories/Cargo.toml b/codex-rs/ext/memories/Cargo.toml new file mode 100644 index 0000000000..c21b774cb7 --- /dev/null +++ b/codex-rs/ext/memories/Cargo.toml @@ -0,0 +1,22 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-memories" +version.workspace = true + +[lib] +name = "codex_memories" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +codex-extension-api = { workspace = true } +codex-memories-read = { workspace = true } +codex-utils-absolute-path = { workspace = true } +rmcp = { workspace = true, default-features = false, features = [ + "schemars", + "server", +] } +serde_json = { workspace = true } diff --git a/codex-rs/ext/memories/src/lib.rs b/codex-rs/ext/memories/src/lib.rs new file mode 100644 index 0000000000..d18d026f47 --- /dev/null +++ b/codex-rs/ext/memories/src/lib.rs @@ -0,0 +1,103 @@ +//! Memories tools and prompt contribution packaged as a Codex extension. + +#![forbid(unsafe_code)] + +use std::sync::Arc; + +use codex_extension_api::CodexExtension; +use codex_extension_api::ExtensionRegistryBuilder; +use codex_extension_api::McpToolContributor; +use codex_extension_api::PromptContributor; +use codex_extension_api::PromptFragment; +use codex_memories_read::build_memory_tool_developer_instructions; +use codex_utils_absolute_path::AbsolutePathBuf; +use rmcp::model::Tool; +use rmcp::model::ToolAnnotations; +use serde_json::Map; +use serde_json::Value; + +/// Runtime facts needed to decide whether read-memory surfaces are visible. +/// +/// Hosts should expose the current effective values for the thread being +/// assembled. The extension owns the policy that combines those values. +pub trait MemoriesContext { + fn memory_tool_enabled(&self) -> bool; + fn use_memories(&self) -> bool; +} + +/// Extension that contributes memories MCP tools plus their read-path guidance. +#[derive(Clone, Debug)] +pub struct MemoriesExtension { + read_prompt: Option, +} + +impl MemoriesExtension { + /// Creates an extension from a pre-rendered read prompt. + pub fn new(read_prompt: Option) -> Self { + Self { read_prompt } + } + + /// Creates an extension using the live memories read prompt for this Codex home. + pub async fn from_codex_home(codex_home: &AbsolutePathBuf) -> Self { + Self { + read_prompt: build_memory_tool_developer_instructions(codex_home).await, + } + } + + /// Returns the rendered developer instruction for read access, if available. + pub fn read_prompt(&self) -> Option<&str> { + self.read_prompt.as_deref() + } + + fn is_read_surface_enabled(&self, context: &C) -> bool { + context.memory_tool_enabled() && context.use_memories() + } +} + +impl McpToolContributor for MemoriesExtension { + fn tools(&self, context: &C) -> Vec { + if !self.is_read_surface_enabled(context) { + return Vec::new(); + } + + vec![ + simple_tool("list", "List memory entries."), + simple_tool("read", "Read one memory entry."), + simple_tool("search", "Search memory entries."), + ] + } +} + +impl PromptContributor for MemoriesExtension { + fn contribute(&self, context: &C) -> Vec { + if !self.is_read_surface_enabled(context) { + return Vec::new(); + } + + self.read_prompt() + .map(PromptFragment::developer_policy) + .into_iter() + .collect() + } +} + +impl CodexExtension for MemoriesExtension { + fn install(self: Arc, registry: &mut ExtensionRegistryBuilder) { + registry.mcp_tool_contributor(self.clone()); + registry.prompt_contributor(self); + } +} + +/// Creates a shared memories extension using the live memories read prompt. +pub async fn extension(codex_home: &AbsolutePathBuf) -> Arc { + Arc::new(MemoriesExtension::from_codex_home(codex_home).await) +} + +fn simple_tool(name: &'static str, description: &'static str) -> Tool { + Tool::new(name, description, simple_input_schema()) + .annotate(ToolAnnotations::new().read_only(true)) +} + +fn simple_input_schema() -> Map { + Map::from_iter([("type".to_string(), Value::String("object".to_string()))]) +} diff --git a/codex-rs/ext/multi-agent-v2/BUILD.bazel b/codex-rs/ext/multi-agent-v2/BUILD.bazel new file mode 100644 index 0000000000..fddef76304 --- /dev/null +++ b/codex-rs/ext/multi-agent-v2/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "multi-agent-v2", + crate_name = "codex_multi_agent_v2", +) diff --git a/codex-rs/ext/multi-agent-v2/Cargo.toml b/codex-rs/ext/multi-agent-v2/Cargo.toml new file mode 100644 index 0000000000..569a251f41 --- /dev/null +++ b/codex-rs/ext/multi-agent-v2/Cargo.toml @@ -0,0 +1,15 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-multi-agent-v2" +version.workspace = true + +[lib] +name = "codex_multi_agent_v2" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +codex-extension-api = { workspace = true } diff --git a/codex-rs/ext/multi-agent-v2/src/lib.rs b/codex-rs/ext/multi-agent-v2/src/lib.rs new file mode 100644 index 0000000000..c1622b6552 --- /dev/null +++ b/codex-rs/ext/multi-agent-v2/src/lib.rs @@ -0,0 +1,74 @@ +//! MultiAgentV2 usage-hint prompt contribution packaged as a Codex extension. + +#![forbid(unsafe_code)] + +use std::sync::Arc; + +use codex_extension_api::CodexExtension; +use codex_extension_api::ExtensionRegistryBuilder; +use codex_extension_api::PromptContributor; +use codex_extension_api::PromptFragment; + +/// Which kind of agent should receive a MultiAgentV2 usage hint. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UsageHintAudience { + RootAgent, + SpawnedSubagent, + Omitted, +} + +/// Runtime facts needed to render MultiAgentV2 usage-hint contributions. +/// +/// Hosts should return the current effective settings for the thread being +/// assembled. The extension owns the routing policy that chooses which hint, if +/// any, is visible for that invocation. +pub trait MultiAgentV2Context { + fn multi_agent_v2_enabled(&self) -> bool; + fn usage_hint_audience(&self) -> UsageHintAudience; + fn root_agent_usage_hint_text(&self) -> Option<&str>; + fn subagent_usage_hint_text(&self) -> Option<&str>; +} + +/// Prompt-only extension that contributes one MultiAgentV2 usage hint when enabled. +#[derive(Clone, Copy, Debug, Default)] +pub struct MultiAgentV2Extension; + +impl MultiAgentV2Extension { + /// Creates an extension instance. + pub fn new() -> Self { + Self + } + + /// Returns the usage hint text that should be shown for this agent, if any. + pub fn usage_hint_text<'a, C: MultiAgentV2Context>(&self, context: &'a C) -> Option<&'a str> { + if !context.multi_agent_v2_enabled() { + return None; + } + + match context.usage_hint_audience() { + UsageHintAudience::RootAgent => context.root_agent_usage_hint_text(), + UsageHintAudience::SpawnedSubagent => context.subagent_usage_hint_text(), + UsageHintAudience::Omitted => None, + } + } +} + +impl PromptContributor for MultiAgentV2Extension { + fn contribute(&self, context: &C) -> Vec { + self.usage_hint_text(context) + .map(PromptFragment::separate_developer) + .into_iter() + .collect() + } +} + +impl CodexExtension for MultiAgentV2Extension { + fn install(self: Arc, registry: &mut ExtensionRegistryBuilder) { + registry.prompt_contributor(self); + } +} + +/// Creates a shared MultiAgentV2 extension instance. +pub fn extension() -> Arc { + Arc::new(MultiAgentV2Extension::new()) +}