extension: add initial typed extension API (#21736)

## Why

`codex-core` still owns a growing amount of product-specific behavior.
This PR starts the extraction path by introducing a small, typed
first-party extension seam: features can install the contribution
families they actually own, while the host keeps lifecycle and state
ownership instead of pushing a broad service locator into the API.

See the `examples/` for illustration

## Known limitations
* Tool contract definition will be shared with core
* Fragments must be extracted
* Missing some contributors
This commit is contained in:
jif-oai authored and GitHub committed 2026-05-11 11:06:24 +02:00
1 parent 2abdeb34d5
commit d2c3ebac1f
14 files changed
+648

No files matched your search

@@ -0,0 +1,50 @@
// All this file should be replaced by the existing fragment implementation ofc
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PromptSlot {
DeveloperPolicy,
DeveloperCapabilities,
ContextualUser,
SeparateDeveloper,
}
#[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<String>) -> Self {
Self {
slot,
text: text.into(),
}
}
/// Creates a developer-policy prompt fragment.
pub fn developer_policy(text: impl Into<String>) -> Self {
Self::new(PromptSlot::DeveloperPolicy, text)
}
/// Creates a developer-capabilities prompt fragment.
pub fn developer_capability(text: impl Into<String>) -> Self {
Self::new(PromptSlot::DeveloperCapabilities, text)
}
/// Creates a separate top-level developer prompt fragment.
pub fn separate_developer(text: impl Into<String>) -> 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
}
}
@@ -0,0 +1,68 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use codex_tools::ResponsesApiTool;
use serde_json::Value;
use thiserror::Error;
// TMP while we don't have the fully extracted tools
#[derive(Clone)]
pub struct ToolContribution {
spec: ResponsesApiTool,
handler: Arc<dyn ToolHandler>,
supports_parallel_tool_calls: bool,
}
impl ToolContribution {
pub fn new(spec: ResponsesApiTool, handler: Arc<dyn ToolHandler>) -> Self {
Self {
spec,
handler,
supports_parallel_tool_calls: false,
}
}
#[must_use]
pub fn allow_parallel_calls(mut self) -> Self {
self.supports_parallel_tool_calls = true;
self
}
pub fn spec(&self) -> &ResponsesApiTool {
&self.spec
}
pub fn supports_parallel_tool_calls(&self) -> bool {
self.supports_parallel_tool_calls
}
pub fn handler(&self) -> Arc<dyn ToolHandler> {
Arc::clone(&self.handler)
}
}
//////// Just to make it compile ////////////////////////////////
pub trait ToolHandler: Send + Sync {
/// Handles one JSON-encoded invocation for this tool.
fn handle<'a>(
&'a self,
arguments: Value,
) -> Pin<Box<dyn Future<Output = Result<Value, ToolCallError>> + Send + 'a>>;
}
/// Error returned by a contributed native tool handler.
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[error("{message}")]
pub struct ToolCallError {
message: String,
}
impl ToolCallError {
/// Creates a contributed-tool error with the supplied model-visible text.
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}