mirror of
https://github.com/openai/codex.git
synced 2026-11-08 08:37:30 +00:00
Refresh host skill catalogs through world state (#33425)
## Why Host skills can change after a thread starts. Subsequent turns need the updated catalog without reinjecting an unchanged catalog on every turn. ## What changed - Register the host skill provider in app-server and project its catalog through a dedicated World State section. - Render host skills with the existing metadata budget and usage instructions, including skill-root aliases when available. - Suppress the legacy host catalog and contextual fragment when World State owns the catalog, while preserving host skills for explicit and shadow selection. ## Testing Added an app-server test that installs a host skill at runtime and verifies the catalog updates once and remains stable on the following turn. GitOrigin-RevId: 4cacc6b2258fc3cab8499fbdf9aa6ab0324ec10d
This commit is contained in:
1 parent
2b486b4676
commit
0f2321f244
10 files changed
+366
-23
No files matched your search
@@ -83,7 +83,8 @@ where
|
|||||||
.with_executor_provider(executor_skill_provider)
|
.with_executor_provider(executor_skill_provider)
|
||||||
.with_orchestrator_provider(Arc::new(
|
.with_orchestrator_provider(Arc::new(
|
||||||
codex_skills_extension::OrchestratorSkillProvider::new(),
|
codex_skills_extension::OrchestratorSkillProvider::new(),
|
||||||
));
|
))
|
||||||
|
.with_host_provider(Arc::new(codex_skills_extension::HostSkillProvider::new()));
|
||||||
codex_skills_extension::install_with_providers_and_metrics(
|
codex_skills_extension::install_with_providers_and_metrics(
|
||||||
&mut builder,
|
&mut builder,
|
||||||
skill_providers,
|
skill_providers,
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use app_test_support::TestAppServer;
|
||||||
|
use app_test_support::to_response;
|
||||||
|
use codex_app_server_protocol::JSONRPCResponse;
|
||||||
|
use codex_app_server_protocol::RequestId;
|
||||||
|
use codex_app_server_protocol::SkillsExtraRootsSetParams;
|
||||||
|
use codex_app_server_protocol::SkillsExtraRootsSetResponse;
|
||||||
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
|
use codex_app_server_protocol::ThreadStartResponse;
|
||||||
|
use codex_app_server_protocol::TurnStartParams;
|
||||||
|
use codex_app_server_protocol::UserInput;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
use core_test_support::responses;
|
||||||
|
use core_test_support::skip_if_remote;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
const READ_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
const INITIAL_SKILL_DESCRIPTION: &str = "INITIAL_HOST_SKILL_DESCRIPTION";
|
||||||
|
const RUNTIME_SKILL_DESCRIPTION: &str = "RUNTIME_HOST_SKILL_DESCRIPTION";
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn host_skill_catalog_refreshes_once_when_skills_change() -> Result<()> {
|
||||||
|
skip_if_remote!(
|
||||||
|
Ok(()),
|
||||||
|
"host-local skill changes are not visible to remote executors"
|
||||||
|
);
|
||||||
|
|
||||||
|
let server = responses::start_mock_server().await;
|
||||||
|
let response_mock = responses::mount_sse_sequence(
|
||||||
|
&server,
|
||||||
|
(1..=3)
|
||||||
|
.map(|index| {
|
||||||
|
let response_id = format!("resp-{index}");
|
||||||
|
let message_id = format!("msg-{index}");
|
||||||
|
responses::sse(vec![
|
||||||
|
responses::ev_response_created(&response_id),
|
||||||
|
responses::ev_assistant_message(&message_id, "Done"),
|
||||||
|
responses::ev_completed(&response_id),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let extra_root = TempDir::new()?;
|
||||||
|
let extra_skills_root = extra_root.path().join("skills");
|
||||||
|
write_skill(
|
||||||
|
&extra_skills_root,
|
||||||
|
"initial-host-skill",
|
||||||
|
INITIAL_SKILL_DESCRIPTION,
|
||||||
|
)?;
|
||||||
|
std::fs::write(
|
||||||
|
codex_home.path().join("config.toml"),
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
model = "mock-model"
|
||||||
|
approval_policy = "never"
|
||||||
|
sandbox_mode = "read-only"
|
||||||
|
model_provider = "mock_provider"
|
||||||
|
|
||||||
|
[skills]
|
||||||
|
include_instructions = true
|
||||||
|
|
||||||
|
[skills.bundled]
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
[model_providers.mock_provider]
|
||||||
|
name = "Mock provider for test"
|
||||||
|
base_url = "{}/v1"
|
||||||
|
wire_api = "responses"
|
||||||
|
request_max_retries = 0
|
||||||
|
stream_max_retries = 0
|
||||||
|
"#,
|
||||||
|
server.uri()
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut app_server = TestAppServer::builder()
|
||||||
|
.with_codex_home(codex_home.path())
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
timeout(READ_TIMEOUT, app_server.initialize()).await??;
|
||||||
|
|
||||||
|
let request_id = app_server
|
||||||
|
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||||
|
model: Some("mock-model".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let response: JSONRPCResponse = timeout(
|
||||||
|
READ_TIMEOUT,
|
||||||
|
app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let ThreadStartResponse { thread, .. } = to_response(response)?;
|
||||||
|
|
||||||
|
set_extra_roots(&mut app_server, &extra_skills_root).await?;
|
||||||
|
run_turn(&mut app_server, &thread.id, "Initial catalog").await?;
|
||||||
|
|
||||||
|
write_skill(
|
||||||
|
&extra_skills_root,
|
||||||
|
"runtime-host-skill",
|
||||||
|
RUNTIME_SKILL_DESCRIPTION,
|
||||||
|
)?;
|
||||||
|
set_extra_roots(&mut app_server, &extra_skills_root).await?;
|
||||||
|
run_turn(&mut app_server, &thread.id, "After install").await?;
|
||||||
|
run_turn(&mut app_server, &thread.id, "Unchanged follow-up").await?;
|
||||||
|
|
||||||
|
let requests = response_mock.requests();
|
||||||
|
assert_eq!(3, requests.len());
|
||||||
|
let marker_counts = |marker| {
|
||||||
|
requests
|
||||||
|
.iter()
|
||||||
|
.map(|request| {
|
||||||
|
request
|
||||||
|
.message_input_texts("developer")
|
||||||
|
.iter()
|
||||||
|
.map(|text| text.matches(marker).count())
|
||||||
|
.sum::<usize>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
assert_eq!(vec![1, 2, 2], marker_counts(INITIAL_SKILL_DESCRIPTION));
|
||||||
|
assert_eq!(vec![0, 1, 1], marker_counts(RUNTIME_SKILL_DESCRIPTION));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_skill(root: &std::path::Path, name: &str, description: &str) -> Result<()> {
|
||||||
|
let skill_dir = root.join(name);
|
||||||
|
std::fs::create_dir_all(&skill_dir)?;
|
||||||
|
std::fs::write(
|
||||||
|
skill_dir.join("SKILL.md"),
|
||||||
|
format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n"),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_extra_roots(app_server: &mut TestAppServer, root: &std::path::Path) -> Result<()> {
|
||||||
|
let request_id = app_server
|
||||||
|
.send_skills_extra_roots_set_request(SkillsExtraRootsSetParams {
|
||||||
|
extra_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let response: JSONRPCResponse = timeout(
|
||||||
|
READ_TIMEOUT,
|
||||||
|
app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let _: SkillsExtraRootsSetResponse = to_response(response)?;
|
||||||
|
timeout(
|
||||||
|
READ_TIMEOUT,
|
||||||
|
app_server.read_stream_until_notification_message("skills/changed"),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_turn(app_server: &mut TestAppServer, thread_id: &str, prompt: &str) -> Result<()> {
|
||||||
|
let request_id = app_server
|
||||||
|
.send_turn_start_request(TurnStartParams {
|
||||||
|
thread_id: thread_id.to_string(),
|
||||||
|
input: vec![UserInput::Text {
|
||||||
|
text: prompt.to_string(),
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
timeout(
|
||||||
|
READ_TIMEOUT,
|
||||||
|
app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
timeout(
|
||||||
|
READ_TIMEOUT,
|
||||||
|
app_server.read_stream_until_notification_message("turn/completed"),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ mod experimental_feature_list;
|
|||||||
mod external_agent_config;
|
mod external_agent_config;
|
||||||
mod fs;
|
mod fs;
|
||||||
mod hooks_list;
|
mod hooks_list;
|
||||||
|
mod host_skills;
|
||||||
mod imagegen_extension;
|
mod imagegen_extension;
|
||||||
mod initialize;
|
mod initialize;
|
||||||
mod marketplace_add;
|
mod marketplace_add;
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ pub struct InjectedHostSkillPrompts {
|
|||||||
paths: HashSet<String>,
|
paths: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marks a turn whose skills extension projects the host skill catalog through
|
||||||
|
/// WorldState.
|
||||||
|
///
|
||||||
|
/// Core uses this to keep its legacy thread-start catalog from duplicating the
|
||||||
|
/// extension-owned catalog.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct HostSkillsCatalogInWorldState;
|
||||||
|
|
||||||
impl InjectedHostSkillPrompts {
|
impl InjectedHostSkillPrompts {
|
||||||
pub fn insert_path(&mut self, path: impl Into<String>) {
|
pub fn insert_path(&mut self, path: impl Into<String>) {
|
||||||
let path = path.into();
|
let path = path.into();
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ use codex_analytics::TurnCodexErrorFact;
|
|||||||
use codex_config::types::AuthKeyringBackendKind;
|
use codex_config::types::AuthKeyringBackendKind;
|
||||||
use codex_config::types::OAuthCredentialsStoreMode;
|
use codex_config::types::OAuthCredentialsStoreMode;
|
||||||
use codex_connectors::connector_runtime_context_key;
|
use codex_connectors::connector_runtime_context_key;
|
||||||
|
use codex_core_skills::injection::HostSkillsCatalogInWorldState;
|
||||||
use codex_exec_server::Environment;
|
use codex_exec_server::Environment;
|
||||||
use codex_exec_server::EnvironmentManager;
|
use codex_exec_server::EnvironmentManager;
|
||||||
use codex_extension_api::ExtensionDataInit;
|
use codex_extension_api::ExtensionDataInit;
|
||||||
@@ -3257,6 +3258,10 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if turn_context.config.include_skill_instructions {
|
if turn_context.config.include_skill_instructions {
|
||||||
|
let host_catalog_in_world_state = turn_context
|
||||||
|
.extension_data
|
||||||
|
.get::<HostSkillsCatalogInWorldState>()
|
||||||
|
.is_some();
|
||||||
let available_skills = build_available_skills(
|
let available_skills = build_available_skills(
|
||||||
turn_context.turn_skills.snapshot.outcome(),
|
turn_context.turn_skills.snapshot.outcome(),
|
||||||
default_skill_metadata_budget(turn_context.model_info.context_window),
|
default_skill_metadata_budget(turn_context.model_info.context_window),
|
||||||
@@ -3279,7 +3284,9 @@ impl Session {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
developer_sections.push(skills_instructions.render());
|
if !host_catalog_in_world_state {
|
||||||
|
developer_sections.push(skills_instructions.render());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let loaded_plugins = self
|
let loaded_plugins = self
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use codex_core_skills::HostSkillsSnapshot;
|
use codex_core_skills::HostSkillsSnapshot;
|
||||||
|
use codex_core_skills::default_skill_metadata_budget;
|
||||||
|
use codex_core_skills::injection::HostSkillsCatalogInWorldState;
|
||||||
use codex_core_skills::injection::InjectedHostSkillPrompts;
|
use codex_core_skills::injection::InjectedHostSkillPrompts;
|
||||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||||
use codex_extension_api::ConfigContributor;
|
use codex_extension_api::ConfigContributor;
|
||||||
@@ -38,7 +40,6 @@ use crate::catalog::SkillSourceKind;
|
|||||||
use crate::fragments::SkillInstructions;
|
use crate::fragments::SkillInstructions;
|
||||||
use crate::provider::HostSkillProvider;
|
use crate::provider::HostSkillProvider;
|
||||||
use crate::provider::SkillListQuery;
|
use crate::provider::SkillListQuery;
|
||||||
use crate::provider::SkillProvider;
|
|
||||||
use crate::provider::SkillReadRequest;
|
use crate::provider::SkillReadRequest;
|
||||||
use crate::render::MAX_SKILL_NAME_BYTES;
|
use crate::render::MAX_SKILL_NAME_BYTES;
|
||||||
use crate::render::MAX_SKILL_PATH_BYTES;
|
use crate::render::MAX_SKILL_PATH_BYTES;
|
||||||
@@ -53,6 +54,7 @@ use crate::state::SkillsThreadState;
|
|||||||
use crate::state::SkillsTurnState;
|
use crate::state::SkillsTurnState;
|
||||||
use crate::tools::skill_tools;
|
use crate::tools::skill_tools;
|
||||||
use crate::world_state::executor_skills_world_state_section;
|
use crate::world_state::executor_skills_world_state_section;
|
||||||
|
use crate::world_state::host_skills_world_state_section;
|
||||||
|
|
||||||
struct SkillsExtension<C> {
|
struct SkillsExtension<C> {
|
||||||
providers: SkillProviders,
|
providers: SkillProviders,
|
||||||
@@ -173,15 +175,31 @@ where
|
|||||||
input
|
input
|
||||||
.turn_store
|
.turn_store
|
||||||
.insert(ExecutorSkillsStepState(catalog.clone()));
|
.insert(ExecutorSkillsStepState(catalog.clone()));
|
||||||
let include_usage = input
|
let model_info = input.thread_store.get::<ModelInfo>();
|
||||||
.thread_store
|
let include_usage = model_info
|
||||||
.get::<ModelInfo>()
|
.as_deref()
|
||||||
.is_some_and(|model_info| model_info.include_skills_usage_instructions);
|
.is_some_and(|model_info| model_info.include_skills_usage_instructions);
|
||||||
vec![executor_skills_world_state_section(
|
let mut sections = vec![executor_skills_world_state_section(
|
||||||
&catalog,
|
&catalog,
|
||||||
config.include_instructions,
|
config.include_instructions,
|
||||||
include_usage,
|
include_usage,
|
||||||
)]
|
)];
|
||||||
|
if let Some(host_snapshot) = input.turn_store.get::<HostSkillsSnapshot>()
|
||||||
|
&& self.providers.has_host_provider()
|
||||||
|
{
|
||||||
|
input.turn_store.insert(HostSkillsCatalogInWorldState);
|
||||||
|
sections.push(host_skills_world_state_section(
|
||||||
|
&host_snapshot,
|
||||||
|
config.include_instructions,
|
||||||
|
include_usage,
|
||||||
|
default_skill_metadata_budget(
|
||||||
|
model_info
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|model_info| model_info.context_window),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
sections
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,33 +275,32 @@ where
|
|||||||
|
|
||||||
let config = thread_state.config();
|
let config = thread_state.config();
|
||||||
let host_snapshot = turn_store.get::<HostSkillsSnapshot>();
|
let host_snapshot = turn_store.get::<HostSkillsSnapshot>();
|
||||||
|
let host_catalog_in_world_state =
|
||||||
|
turn_store.get::<HostSkillsCatalogInWorldState>().is_some();
|
||||||
let query = SkillListQuery {
|
let query = SkillListQuery {
|
||||||
turn_id: input.turn_id.clone(),
|
turn_id: input.turn_id.clone(),
|
||||||
executor_roots: Vec::new(),
|
executor_roots: Vec::new(),
|
||||||
host_snapshot: host_snapshot.clone(),
|
host_snapshot: host_snapshot.clone(),
|
||||||
include_host_skills: true,
|
include_host_skills: !host_catalog_in_world_state,
|
||||||
include_bundled_skills: config.bundled_skills_enabled,
|
include_bundled_skills: config.bundled_skills_enabled,
|
||||||
include_orchestrator_skills: thread_state.orchestrator_skills_enabled(),
|
include_orchestrator_skills: thread_state.orchestrator_skills_enabled(),
|
||||||
mcp_resources: session_store.get::<McpResourceClient>(),
|
mcp_resources: session_store.get::<McpResourceClient>(),
|
||||||
};
|
};
|
||||||
let host_query = query.clone();
|
let host_query = query.clone();
|
||||||
let mut catalog = self.list_skills(query, &thread_state).await;
|
let mut catalog = turn_store
|
||||||
if let Some(executor_skills) = turn_store.get::<ExecutorSkillsStepState>() {
|
.get::<ExecutorSkillsStepState>()
|
||||||
catalog.extend(executor_skills.0.clone());
|
.map(|executor_skills| executor_skills.0.clone())
|
||||||
}
|
.unwrap_or_default();
|
||||||
|
catalog.extend(self.list_skills(query, &thread_state).await);
|
||||||
for warning in &catalog.warnings {
|
for warning in &catalog.warnings {
|
||||||
self.emit_warning(&input.turn_id, warning.clone());
|
self.emit_warning(&input.turn_id, warning.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected_entries = collect_explicit_skill_mentions(&input.user_input, &catalog);
|
let selected_entries = collect_explicit_skill_mentions(&input.user_input, &catalog);
|
||||||
let shadow_selection_turn = if config.shadow_selection_enabled {
|
let shadow_selection_turn = if config.shadow_selection_enabled {
|
||||||
// App-server leaves host discovery in core, so its configured providers omit the
|
|
||||||
// host snapshot. Extend only the experiment catalog to keep rendered context intact.
|
|
||||||
let mut shadow_catalog = catalog.clone();
|
let mut shadow_catalog = catalog.clone();
|
||||||
if host_snapshot.is_some()
|
if host_catalog_in_world_state && host_snapshot.is_some() {
|
||||||
&& let Ok(host_catalog) = HostSkillProvider::new().list(host_query).await
|
shadow_catalog.extend(self.providers.list_host_for_turn(host_query).await);
|
||||||
{
|
|
||||||
shadow_catalog.extend(host_catalog);
|
|
||||||
}
|
}
|
||||||
Some(
|
Some(
|
||||||
self.shadow_selection
|
self.shadow_selection
|
||||||
@@ -295,7 +312,9 @@ where
|
|||||||
thread_state
|
thread_state
|
||||||
.replace_shadow_selection_turn(input.turn_id.clone(), shadow_selection_turn);
|
.replace_shadow_selection_turn(input.turn_id.clone(), shadow_selection_turn);
|
||||||
let mut fragments: Vec<Box<dyn ContextualUserFragment + Send>> = Vec::new();
|
let mut fragments: Vec<Box<dyn ContextualUserFragment + Send>> = Vec::new();
|
||||||
if config.include_instructions {
|
if config.include_instructions
|
||||||
|
&& turn_store.get::<HostSkillsCatalogInWorldState>().is_none()
|
||||||
|
{
|
||||||
let mut turn_catalog = catalog.clone();
|
let mut turn_catalog = catalog.clone();
|
||||||
turn_catalog.entries.retain(|entry| {
|
turn_catalog.entries.retain(|entry| {
|
||||||
entry.authority.kind != SkillSourceKind::Executor
|
entry.authority.kind != SkillSourceKind::Executor
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
use codex_core_skills::AvailableSkills;
|
||||||
use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS;
|
use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS;
|
||||||
|
use codex_core_skills::SKILLS_HOW_TO_USE_WITH_ALIASES;
|
||||||
use codex_core_skills::render_available_skills_body;
|
use codex_core_skills::render_available_skills_body;
|
||||||
use codex_extension_api::ContextualUserFragment;
|
use codex_extension_api::ContextualUserFragment;
|
||||||
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
|
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG;
|
||||||
@@ -6,6 +8,7 @@ use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub(crate) struct AvailableSkillsInstructions {
|
pub(crate) struct AvailableSkillsInstructions {
|
||||||
|
skill_root_lines: Vec<String>,
|
||||||
skill_lines: Vec<String>,
|
skill_lines: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +21,30 @@ impl AvailableSkillsInstructions {
|
|||||||
skill_lines.push("### How to use skills".to_string());
|
skill_lines.push("### How to use skills".to_string());
|
||||||
skill_lines.push(SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS.to_string());
|
skill_lines.push(SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS.to_string());
|
||||||
}
|
}
|
||||||
Self { skill_lines }
|
Self {
|
||||||
|
skill_root_lines: Vec::new(),
|
||||||
|
skill_lines,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_available_skills(
|
||||||
|
available: AvailableSkills,
|
||||||
|
include_skills_usage_instructions: bool,
|
||||||
|
) -> Self {
|
||||||
|
let mut skill_lines = available.skill_lines;
|
||||||
|
if include_skills_usage_instructions {
|
||||||
|
skill_lines.push("### How to use skills".to_string());
|
||||||
|
let instructions = if available.skill_root_lines.is_empty() {
|
||||||
|
SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS
|
||||||
|
} else {
|
||||||
|
SKILLS_HOW_TO_USE_WITH_ALIASES
|
||||||
|
};
|
||||||
|
skill_lines.push(instructions.to_string());
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
skill_root_lines: available.skill_root_lines,
|
||||||
|
skill_lines,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +62,7 @@ impl ContextualUserFragment for AvailableSkillsInstructions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn body(&self) -> String {
|
fn body(&self) -> String {
|
||||||
render_available_skills_body(&[], &self.skill_lines)
|
render_available_skills_body(&self.skill_root_lines, &self.skill_lines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,12 @@ impl SkillProviders {
|
|||||||
.any(|source| source.kind == SkillSourceKind::Orchestrator)
|
.any(|source| source.kind == SkillSourceKind::Orchestrator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has_host_provider(&self) -> bool {
|
||||||
|
self.sources
|
||||||
|
.iter()
|
||||||
|
.any(|source| source.kind == SkillSourceKind::Host)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn list_for_turn(&self, query: SkillListQuery) -> SkillCatalog {
|
pub(crate) async fn list_for_turn(&self, query: SkillListQuery) -> SkillCatalog {
|
||||||
self.list_matching(&query, |source| source.should_list(&query))
|
self.list_matching(&query, |source| source.should_list(&query))
|
||||||
.await
|
.await
|
||||||
@@ -140,6 +146,11 @@ impl SkillProviders {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn list_host_for_turn(&self, query: SkillListQuery) -> SkillCatalog {
|
||||||
|
self.list_matching(&query, |source| source.kind == SkillSourceKind::Host)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_matching(
|
async fn list_matching(
|
||||||
&self,
|
&self,
|
||||||
query: &SkillListQuery,
|
query: &SkillListQuery,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
use codex_core_skills::HostSkillsSnapshot;
|
||||||
|
use codex_core_skills::SkillMetadataBudget;
|
||||||
|
use codex_core_skills::build_available_skills;
|
||||||
|
use codex_core_skills::render::SkillRenderSideEffects;
|
||||||
use codex_extension_api::ContextualUserFragment;
|
use codex_extension_api::ContextualUserFragment;
|
||||||
use codex_extension_api::PreviousWorldStateSection;
|
use codex_extension_api::PreviousWorldStateSection;
|
||||||
use codex_extension_api::RenderedWorldStateFragment;
|
use codex_extension_api::RenderedWorldStateFragment;
|
||||||
@@ -7,12 +11,19 @@ use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::catalog::SkillCatalog;
|
use crate::catalog::SkillCatalog;
|
||||||
|
use crate::fragments::AvailableSkillsInstructions;
|
||||||
use crate::render::available_skills_fragment;
|
use crate::render::available_skills_fragment;
|
||||||
|
|
||||||
pub(crate) const SKILLS_WORLD_STATE_ID: &str = "skills";
|
pub(crate) const SKILLS_WORLD_STATE_ID: &str = "skills";
|
||||||
|
pub(crate) const HOST_SKILLS_WORLD_STATE_ID: &str = "host_skills";
|
||||||
|
const MAX_HOST_SKILLS_METADATA_CHARS: usize = 8_000;
|
||||||
|
const MAX_HOST_SKILLS_METADATA_TOKENS: usize = 4_000;
|
||||||
const NO_EXECUTOR_SKILLS_BODY: &str =
|
const NO_EXECUTOR_SKILLS_BODY: &str =
|
||||||
"\n## Skills update\nNo selected-environment skills are currently available.\n";
|
"\n## Skills update\nNo selected-environment skills are currently available.\n";
|
||||||
const HIDDEN_EXECUTOR_SKILLS_BODY: &str = "\n## Skills update\nSelected-environment skills are not listed automatically. Explicit skill mentions can still be resolved when available.\n";
|
const HIDDEN_EXECUTOR_SKILLS_BODY: &str = "\n## Skills update\nSelected-environment skills are not listed automatically. Explicit skill mentions can still be resolved when available.\n";
|
||||||
|
const NO_HOST_SKILLS_BODY: &str =
|
||||||
|
"\n## Host skills update\nNo host skills are currently available.\n";
|
||||||
|
const HIDDEN_HOST_SKILLS_BODY: &str = "\n## Host skills update\nHost skills are not listed automatically. Explicit skill mentions can still be resolved when available.\n";
|
||||||
|
|
||||||
pub(crate) fn executor_skills_world_state_section(
|
pub(crate) fn executor_skills_world_state_section(
|
||||||
catalog: &SkillCatalog,
|
catalog: &SkillCatalog,
|
||||||
@@ -70,3 +81,73 @@ pub(crate) fn executor_skills_world_state_section(
|
|||||||
None => contribution,
|
None => contribution,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn host_skills_world_state_section(
|
||||||
|
host_snapshot: &HostSkillsSnapshot,
|
||||||
|
include_instructions: bool,
|
||||||
|
include_skills_usage_instructions: bool,
|
||||||
|
metadata_budget: SkillMetadataBudget,
|
||||||
|
) -> WorldStateSectionContribution {
|
||||||
|
let outcome = host_snapshot.outcome();
|
||||||
|
let metadata_budget = match metadata_budget {
|
||||||
|
SkillMetadataBudget::Tokens(limit) => {
|
||||||
|
SkillMetadataBudget::Tokens(limit.min(MAX_HOST_SKILLS_METADATA_TOKENS))
|
||||||
|
}
|
||||||
|
SkillMetadataBudget::Characters(limit) => {
|
||||||
|
SkillMetadataBudget::Characters(limit.min(MAX_HOST_SKILLS_METADATA_CHARS))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let available = if include_instructions {
|
||||||
|
build_available_skills(outcome, metadata_budget, SkillRenderSideEffects::None)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let body = available.map(|available| {
|
||||||
|
AvailableSkillsInstructions::from_available_skills(
|
||||||
|
available,
|
||||||
|
include_skills_usage_instructions,
|
||||||
|
)
|
||||||
|
.body()
|
||||||
|
});
|
||||||
|
let snapshot = json!({
|
||||||
|
"body": body,
|
||||||
|
"includeInstructions": include_instructions,
|
||||||
|
});
|
||||||
|
let retained_fragment = body
|
||||||
|
.as_ref()
|
||||||
|
.map(|body| format!("{SKILLS_INSTRUCTIONS_OPEN_TAG}{body}{SKILLS_INSTRUCTIONS_CLOSE_TAG}"));
|
||||||
|
|
||||||
|
let contribution =
|
||||||
|
WorldStateSectionContribution::new(HOST_SKILLS_WORLD_STATE_ID, snapshot, move |previous| {
|
||||||
|
let previous_is_absent = matches!(&previous, PreviousWorldStateSection::Absent);
|
||||||
|
if let PreviousWorldStateSection::Known(previous) = &previous {
|
||||||
|
let previous_body = previous.get("body").and_then(serde_json::Value::as_str);
|
||||||
|
let previous_include_instructions = previous
|
||||||
|
.get("includeInstructions")
|
||||||
|
.and_then(serde_json::Value::as_bool);
|
||||||
|
if previous_body == body.as_deref()
|
||||||
|
&& previous_include_instructions == Some(include_instructions)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = match body.as_deref() {
|
||||||
|
Some(body) => body,
|
||||||
|
None if previous_is_absent => return None,
|
||||||
|
None if !include_instructions => HIDDEN_HOST_SKILLS_BODY,
|
||||||
|
None => NO_HOST_SKILLS_BODY,
|
||||||
|
};
|
||||||
|
Some(RenderedWorldStateFragment::new(
|
||||||
|
"developer",
|
||||||
|
(SKILLS_INSTRUCTIONS_OPEN_TAG, SKILLS_INSTRUCTIONS_CLOSE_TAG),
|
||||||
|
body,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
match retained_fragment {
|
||||||
|
Some(fragment) => contribution.with_retained_fragment_matcher(move |role, text| {
|
||||||
|
role == "developer" && text.contains(&fragment)
|
||||||
|
}),
|
||||||
|
None => contribution,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ use codex_protocol::protocol::SessionSource;
|
|||||||
use codex_protocol::protocol::SkillScope;
|
use codex_protocol::protocol::SkillScope;
|
||||||
use codex_protocol::protocol::TruncationPolicy;
|
use codex_protocol::protocol::TruncationPolicy;
|
||||||
use codex_protocol::user_input::UserInput;
|
use codex_protocol::user_input::UserInput;
|
||||||
|
use codex_skills_extension::HostSkillProvider;
|
||||||
use codex_skills_extension::SkillProviders;
|
use codex_skills_extension::SkillProviders;
|
||||||
use codex_skills_extension::SkillsExtensionConfig;
|
use codex_skills_extension::SkillsExtensionConfig;
|
||||||
use codex_skills_extension::catalog::SkillAuthority;
|
use codex_skills_extension::catalog::SkillAuthority;
|
||||||
@@ -226,7 +227,9 @@ async fn shadow_selection_uses_host_snapshot_and_excludes_executor_candidates()
|
|||||||
let mut builder = ExtensionRegistryBuilder::<()>::new();
|
let mut builder = ExtensionRegistryBuilder::<()>::new();
|
||||||
install_with_providers_and_metrics(
|
install_with_providers_and_metrics(
|
||||||
&mut builder,
|
&mut builder,
|
||||||
SkillProviders::new().with_executor_provider(Arc::new(ExecutorProvider)),
|
SkillProviders::new()
|
||||||
|
.with_executor_provider(Arc::new(ExecutorProvider))
|
||||||
|
.with_host_provider(Arc::new(HostSkillProvider::new())),
|
||||||
Some(metrics.clone()),
|
Some(metrics.clone()),
|
||||||
|_| SkillsExtensionConfig {
|
|_| SkillsExtensionConfig {
|
||||||
include_instructions: true,
|
include_instructions: true,
|
||||||
|
|||||||
Reference in new issue
Block a user