This commit is contained in:
jif-oai
2026-05-07 15:21:24 +01:00
parent 19dc3d202f
commit ee1b490c0e
10 changed files with 111 additions and 81 deletions

View File

@@ -0,0 +1,14 @@
Everything becomes a good contributor design, which contributors do we need?
git attribution Context
memories Context + Tool + Output
guardian Context + Request
goal Tool + Runtime
image generation Tool + Output
skills Context + Turn
personality Context
plugins / apps / connectors Context + Turn
shell snapshot Runtime
web search Tool
AGENTS.md Context (Runtime too only if you want eager refresh/cache behavior)
future sandboxing probably Request + Runtime

View File

@@ -7,6 +7,7 @@
mod prompt;
mod tool;
mod session_lifecycle;
pub use prompt::PromptFragment;
pub use prompt::PromptSlot;
@@ -14,33 +15,41 @@ pub use tool::ToolCallError;
pub use tool::ToolContribution;
pub use tool::ToolHandler;
/// Extension contribution that adds prompt fragments during prompt assembly.
/// Arguably, can become async
pub trait ContextContributor<C>: Send + Sync {
fn contribute(&self, context: &C) -> Vec<PromptFragment>; // TODO use existing fragments ofc
}
/// Extension contribution that exposes native tools owned by a feature.
pub trait ToolContributor<C>: Send + Sync {
/// Returns the native tools visible for the supplied runtime context.
fn tools(&self, context: &C) -> Vec<ToolContribution<C>>;
}
/// Analyze or perform computation on the output. Works only if the post-processing pipeline is ordered
pub type OutputContributionFuture<'a> = std::pin::Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
pub trait OutputContributor<C, O>: Send + Sync {
fn contribute<'a>(
&'a self,
context: &'a C,
output: &'a mut O,
) -> OutputContributionFuture<'a>;
}
// TODO: WIP
/// Extension contribution that can claim approval requests for a runtime context.
///
/// Implementations should make only the routing decision here. The host keeps
/// ownership of executing the chosen review flow and translating its result
/// back into the surrounding runtime.
/// (ideally we can replace it by a session lifecycle thing or a request contributor?)
pub trait ApprovalInterceptorContributor<C>: Send + Sync {
/// Returns whether this contributor should intercept approvals in `context`.
fn intercepts_approvals(&self, context: &C) -> bool;
}
/// 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<C>: Send + Sync {
fn contribute(&self, context: &C) -> Vec<PromptFragment>;
}
/// Extension contribution that exposes native tools 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 adapting calls
/// into its runtime.
pub trait ToolContributor<C>: Send + Sync {
/// Returns the native tools visible for the supplied runtime context.
fn tools(&self, context: &C) -> Vec<ToolContribution<C>>;
}
pub trait SessionLifecycleContributor<S, T>: Send + Sync {
fn on_lifecycle_event(
&self,
event: session_lifecycle::Event<'_, S, T>,
) -> impl Future<Output = ()> + Send;
}

View File

@@ -0,0 +1,6 @@
pub enum Event<'a, S, T> {
SessionStarted(&'a S),
TurnStarted(&'a T),
TurnFinished { turn: &'a T, outcome: String }, // Make outcome better?!
SessionStopping(&'a S),
}

View File

@@ -12,7 +12,7 @@ mod extension;
mod registry;
pub use contributors::ApprovalInterceptorContributor;
pub use contributors::PromptContributor;
pub use contributors::ContextContributor;
pub use contributors::PromptFragment;
pub use contributors::PromptSlot;
pub use contributors::ToolCallError;

View File

@@ -2,21 +2,21 @@ use std::sync::Arc;
use crate::ApprovalInterceptorContributor;
use crate::CodexExtension;
use crate::PromptContributor;
use crate::ContextContributor;
use crate::ToolContributor;
/// Mutable registry used while extensions install their typed contributions.
pub struct ExtensionRegistryBuilder<C> {
approval_interceptor_contributors: Vec<Arc<dyn ApprovalInterceptorContributor<C>>>,
prompt_contributors: Vec<Arc<dyn PromptContributor<C>>>,
context_contributors: Vec<Arc<dyn ContextContributor<C>>>,
tool_contributors: Vec<Arc<dyn ToolContributor<C>>>,
approval_interceptor_contributors: Vec<Arc<dyn ApprovalInterceptorContributor<C>>>,
}
impl<C> Default for ExtensionRegistryBuilder<C> {
fn default() -> Self {
Self {
approval_interceptor_contributors: Vec::new(),
prompt_contributors: Vec::new(),
context_contributors: Vec::new(),
tool_contributors: Vec::new(),
}
}
@@ -55,8 +55,8 @@ impl<C> ExtensionRegistryBuilder<C> {
}
/// Registers one prompt contributor.
pub fn prompt_contributor(&mut self, contributor: Arc<dyn PromptContributor<C>>) {
self.prompt_contributors.push(contributor);
pub fn prompt_contributor(&mut self, contributor: Arc<dyn ContextContributor<C>>) {
self.context_contributors.push(contributor);
}
/// Registers one native tool contributor.
@@ -68,7 +68,7 @@ impl<C> ExtensionRegistryBuilder<C> {
pub fn build(self) -> ExtensionRegistry<C> {
ExtensionRegistry {
approval_interceptor_contributors: self.approval_interceptor_contributors,
prompt_contributors: self.prompt_contributors,
prompt_contributors: self.context_contributors,
tool_contributors: self.tool_contributors,
}
}
@@ -77,7 +77,7 @@ impl<C> ExtensionRegistryBuilder<C> {
/// Immutable typed registry produced after extensions are installed.
pub struct ExtensionRegistry<C> {
approval_interceptor_contributors: Vec<Arc<dyn ApprovalInterceptorContributor<C>>>,
prompt_contributors: Vec<Arc<dyn PromptContributor<C>>>,
prompt_contributors: Vec<Arc<dyn ContextContributor<C>>>,
tool_contributors: Vec<Arc<dyn ToolContributor<C>>>,
}
@@ -90,7 +90,7 @@ impl<C> ExtensionRegistry<C> {
}
/// Returns the registered prompt contributors.
pub fn prompt_contributors(&self) -> &[Arc<dyn PromptContributor<C>>] {
pub fn prompt_contributors(&self) -> &[Arc<dyn ContextContributor<C>>] {
&self.prompt_contributors
}

View File

@@ -5,8 +5,8 @@
use std::sync::Arc;
use codex_extension_api::CodexExtension;
use codex_extension_api::ContextContributor;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::PromptContributor;
use codex_extension_api::PromptFragment;
const DEFAULT_ATTRIBUTION_VALUE: &str = "Codex <noreply@openai.com>";
@@ -39,7 +39,7 @@ impl GitAttributionExtension {
}
}
impl<C: GitAttributionContext> PromptContributor<C> for GitAttributionExtension {
impl<C: GitAttributionContext> ContextContributor<C> for GitAttributionExtension {
fn contribute(&self, context: &C) -> Vec<PromptFragment> {
self.instruction(context)
.map(PromptFragment::developer_capability)

View File

@@ -6,8 +6,8 @@ use std::sync::Arc;
use codex_extension_api::ApprovalInterceptorContributor;
use codex_extension_api::CodexExtension;
use codex_extension_api::ContextContributor;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::PromptContributor;
use codex_extension_api::PromptFragment;
/// Runtime facts needed to expose Guardian surfaces.
@@ -53,7 +53,7 @@ impl<C: GuardianContext> ApprovalInterceptorContributor<C> for GuardianExtension
}
}
impl<C: GuardianContext> PromptContributor<C> for GuardianExtension {
impl<C: GuardianContext> ContextContributor<C> for GuardianExtension {
fn contribute(&self, context: &C) -> Vec<PromptFragment> {
self.policy_prompt(context)
.map(PromptFragment::separate_developer)

View File

@@ -0,0 +1,8 @@
/// 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;
}

View File

@@ -2,14 +2,16 @@
#![forbid(unsafe_code)]
pub mod ctx;
mod list_tool;
use std::path::PathBuf;
use std::sync::Arc;
use crate::ctx::MemoriesContext;
use codex_extension_api::CodexExtension;
use codex_extension_api::ContextContributor;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::PromptContributor;
use codex_extension_api::PromptFragment;
use codex_extension_api::ToolContribution;
use codex_extension_api::ToolContributor;
@@ -18,15 +20,6 @@ use codex_memories_read::memory_root;
use codex_utils_absolute_path::AbsolutePathBuf;
use list_tool::ListMemoriesTool;
/// 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 read surfaces.
#[derive(Clone, Debug)]
pub struct MemoriesExtension {
@@ -34,6 +27,36 @@ pub struct MemoriesExtension {
list_tool: Arc<ListMemoriesTool>,
}
impl<C: MemoriesContext + Send + Sync + 'static> ToolContributor<C> for MemoriesExtension {
fn tools(&self, context: &C) -> Vec<ToolContribution<C>> {
if !self.is_read_surface_enabled(context) {
return Vec::new();
}
vec![self.list_tool.contribution()]
}
}
impl<C: MemoriesContext> ContextContributor<C> for MemoriesExtension {
fn contribute(&self, context: &C) -> Vec<PromptFragment> {
if !self.is_read_surface_enabled(context) {
return Vec::new();
}
self.read_prompt()
.map(PromptFragment::developer_policy)
.into_iter()
.collect()
}
}
impl<C: MemoriesContext + Send + Sync + 'static> CodexExtension<C> for MemoriesExtension {
fn install(self: Arc<Self>, registry: &mut ExtensionRegistryBuilder<C>) {
registry.tool_contributor(self.clone());
registry.prompt_contributor(self);
}
}
impl MemoriesExtension {
fn new(read_prompt: Option<String>, memories_root: impl Into<PathBuf>) -> Self {
Self {
@@ -70,37 +93,7 @@ impl MemoriesExtension {
}
}
impl<C: MemoriesContext + Send + Sync + 'static> ToolContributor<C> for MemoriesExtension {
fn tools(&self, context: &C) -> Vec<ToolContribution<C>> {
if !self.is_read_surface_enabled(context) {
return Vec::new();
}
vec![self.list_tool.contribution()]
}
}
impl<C: MemoriesContext> PromptContributor<C> for MemoriesExtension {
fn contribute(&self, context: &C) -> Vec<PromptFragment> {
if !self.is_read_surface_enabled(context) {
return Vec::new();
}
self.read_prompt()
.map(PromptFragment::developer_policy)
.into_iter()
.collect()
}
}
impl<C: MemoriesContext + Send + Sync + 'static> CodexExtension<C> for MemoriesExtension {
fn install(self: Arc<Self>, registry: &mut ExtensionRegistryBuilder<C>) {
registry.tool_contributor(self.clone());
registry.prompt_contributor(self);
}
}
/// Creates a shared memories extension using the live memories read prompt.
/// todo Other builder pattern...
pub async fn extension(codex_home: &AbsolutePathBuf) -> Arc<MemoriesExtension> {
Arc::new(MemoriesExtension::from_codex_home(codex_home).await)
}

View File

@@ -5,8 +5,8 @@
use std::sync::Arc;
use codex_extension_api::CodexExtension;
use codex_extension_api::ContextContributor;
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.
@@ -53,7 +53,7 @@ impl MultiAgentV2Extension {
}
}
impl<C: MultiAgentV2Context> PromptContributor<C> for MultiAgentV2Extension {
impl<C: MultiAgentV2Context> ContextContributor<C> for MultiAgentV2Extension {
fn contribute(&self, context: &C) -> Vec<PromptFragment> {
self.usage_hint_text(context)
.map(PromptFragment::separate_developer)