mirror of
https://github.com/openai/codex.git
synced 2026-11-02 16:20:35 +00:00
feat: track plugins mcps/apps and add plugin info to user_instructions (#13433)
### first half of changes, followed by #13510 Track plugin capabilities as derived summaries on `PluginLoadOutcome` for enabled plugins with at least one skill/app/mcp. Also add `Plugins` section to `user_instructions` injected on session start. These introduce the plugins concept and list enabled plugins, but do NOT currently include paths to enabled plugins or details on what apps/mcps the plugins contain (current plan is to inject this on @-mention). that can be adjusted in a follow up and based on evals. ### tests Added/updated tests, confirmed locally that new `Plugins` section + currently enabled plugins show up in `user_instructions`.
This commit is contained in:
1 parent
be5e8fbd37
commit
d44398905b
7 files changed
+270
-31
No files matched your search
@@ -353,7 +353,7 @@ impl Codex {
|
||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
|
||||
plugins_manager.plugins_for_config(&config);
|
||||
let loaded_plugins = plugins_manager.plugins_for_config(&config);
|
||||
let loaded_skills = skills_manager.skills_for_config(&config);
|
||||
|
||||
for err in &loaded_skills.errors {
|
||||
@@ -390,8 +390,12 @@ impl Codex {
|
||||
|
||||
let allowed_skills_for_implicit_invocation =
|
||||
loaded_skills.allowed_skills_for_implicit_invocation();
|
||||
let user_instructions =
|
||||
get_user_instructions(&config, Some(&allowed_skills_for_implicit_invocation)).await;
|
||||
let user_instructions = get_user_instructions(
|
||||
&config,
|
||||
Some(&allowed_skills_for_implicit_invocation),
|
||||
Some(loaded_plugins.capability_summaries()),
|
||||
)
|
||||
.await;
|
||||
|
||||
let exec_policy = ExecPolicyManager::load(&config.config_layer_stack)
|
||||
.await
|
||||
|
||||
@@ -64,12 +64,66 @@ impl LoadedPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct PluginCapabilitySummary {
|
||||
pub config_name: String,
|
||||
pub display_name: String,
|
||||
pub has_skills: bool,
|
||||
pub mcp_server_names: Vec<String>,
|
||||
pub app_connector_ids: Vec<AppConnectorId>,
|
||||
}
|
||||
|
||||
impl PluginCapabilitySummary {
|
||||
fn from_plugin(plugin: &LoadedPlugin) -> Option<Self> {
|
||||
if !plugin.is_active() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut mcp_server_names: Vec<String> = plugin.mcp_servers.keys().cloned().collect();
|
||||
mcp_server_names.sort_unstable();
|
||||
|
||||
let summary = Self {
|
||||
config_name: plugin.config_name.clone(),
|
||||
display_name: plugin
|
||||
.manifest_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| plugin.config_name.clone()),
|
||||
has_skills: !plugin.skill_roots.is_empty(),
|
||||
mcp_server_names,
|
||||
app_connector_ids: plugin.apps.clone(),
|
||||
};
|
||||
|
||||
(summary.has_skills
|
||||
|| !summary.mcp_server_names.is_empty()
|
||||
|| !summary.app_connector_ids.is_empty())
|
||||
.then_some(summary)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PluginLoadOutcome {
|
||||
pub plugins: Vec<LoadedPlugin>,
|
||||
plugins: Vec<LoadedPlugin>,
|
||||
capability_summaries: Vec<PluginCapabilitySummary>,
|
||||
}
|
||||
|
||||
impl Default for PluginLoadOutcome {
|
||||
fn default() -> Self {
|
||||
Self::from_plugins(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginLoadOutcome {
|
||||
fn from_plugins(plugins: Vec<LoadedPlugin>) -> Self {
|
||||
let capability_summaries = plugins
|
||||
.iter()
|
||||
.filter_map(PluginCapabilitySummary::from_plugin)
|
||||
.collect::<Vec<_>>();
|
||||
Self {
|
||||
plugins,
|
||||
capability_summaries,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn effective_skill_roots(&self) -> Vec<PathBuf> {
|
||||
let mut skill_roots: Vec<PathBuf> = self
|
||||
.plugins
|
||||
@@ -108,6 +162,14 @@ impl PluginLoadOutcome {
|
||||
|
||||
apps
|
||||
}
|
||||
|
||||
pub fn capability_summaries(&self) -> &[PluginCapabilitySummary] {
|
||||
&self.capability_summaries
|
||||
}
|
||||
|
||||
pub fn plugins(&self) -> &[LoadedPlugin] {
|
||||
&self.plugins
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PluginsManager {
|
||||
@@ -317,7 +379,7 @@ pub(crate) fn load_plugins_from_layer_stack(
|
||||
plugins.push(loaded_plugin);
|
||||
}
|
||||
|
||||
PluginLoadOutcome { plugins }
|
||||
PluginLoadOutcome::from_plugins(plugins)
|
||||
}
|
||||
|
||||
pub(crate) fn plugin_namespace_for_skill_path(path: &Path) -> Option<String> {
|
||||
@@ -449,7 +511,8 @@ fn load_apps_from_file(plugin_root: &Path, app_config_path: &Path) -> Vec<AppCon
|
||||
let mut apps: Vec<PluginAppConfig> = parsed.apps.into_values().collect();
|
||||
apps.sort_unstable_by(|left, right| left.id.cmp(&right.id));
|
||||
|
||||
apps.into_iter()
|
||||
let mut connector_ids: Vec<AppConnectorId> = apps
|
||||
.into_iter()
|
||||
.filter_map(|app| {
|
||||
if app.id.trim().is_empty() {
|
||||
warn!(
|
||||
@@ -461,7 +524,9 @@ fn load_apps_from_file(plugin_root: &Path, app_config_path: &Path) -> Vec<AppCon
|
||||
Some(AppConnectorId(app.id))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
connector_ids.dedup();
|
||||
connector_ids
|
||||
}
|
||||
|
||||
fn load_mcp_servers_from_file(plugin_root: &Path, mcp_config_path: &Path) -> PluginMcpDiscovery {
|
||||
@@ -825,6 +890,95 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_index_filters_inactive_and_zero_capability_plugins() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let connector = |id: &str| AppConnectorId(id.to_string());
|
||||
let http_server = |url: &str| McpServerConfig {
|
||||
transport: McpServerTransportConfig::StreamableHttp {
|
||||
url: url.to_string(),
|
||||
bearer_token_env_var: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
},
|
||||
enabled: true,
|
||||
required: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: None,
|
||||
tool_timeout_sec: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
};
|
||||
let plugin = |config_name: &str, dir_name: &str, manifest_name: &str| LoadedPlugin {
|
||||
config_name: config_name.to_string(),
|
||||
manifest_name: Some(manifest_name.to_string()),
|
||||
root: AbsolutePathBuf::try_from(codex_home.path().join(dir_name)).unwrap(),
|
||||
enabled: true,
|
||||
skill_roots: Vec::new(),
|
||||
mcp_servers: HashMap::new(),
|
||||
apps: Vec::new(),
|
||||
error: None,
|
||||
};
|
||||
let summary = |config_name: &str, display_name: &str| PluginCapabilitySummary {
|
||||
config_name: config_name.to_string(),
|
||||
display_name: display_name.to_string(),
|
||||
..PluginCapabilitySummary::default()
|
||||
};
|
||||
let outcome = PluginLoadOutcome::from_plugins(vec![
|
||||
LoadedPlugin {
|
||||
skill_roots: vec![codex_home.path().join("skills-plugin/skills")],
|
||||
..plugin("skills@test", "skills-plugin", "skills-plugin")
|
||||
},
|
||||
LoadedPlugin {
|
||||
mcp_servers: HashMap::from([("alpha".to_string(), http_server("https://alpha"))]),
|
||||
apps: vec![connector("connector_example")],
|
||||
..plugin("alpha@test", "alpha-plugin", "alpha-plugin")
|
||||
},
|
||||
LoadedPlugin {
|
||||
mcp_servers: HashMap::from([("beta".to_string(), http_server("https://beta"))]),
|
||||
apps: vec![connector("connector_example"), connector("connector_gmail")],
|
||||
..plugin("beta@test", "beta-plugin", "beta-plugin")
|
||||
},
|
||||
plugin("empty@test", "empty-plugin", "empty-plugin"),
|
||||
LoadedPlugin {
|
||||
enabled: false,
|
||||
skill_roots: vec![codex_home.path().join("disabled-plugin/skills")],
|
||||
apps: vec![connector("connector_hidden")],
|
||||
..plugin("disabled@test", "disabled-plugin", "disabled-plugin")
|
||||
},
|
||||
LoadedPlugin {
|
||||
apps: vec![connector("connector_broken")],
|
||||
error: Some("failed to load".to_string()),
|
||||
..plugin("broken@test", "broken-plugin", "broken-plugin")
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
outcome.capability_summaries(),
|
||||
&[
|
||||
PluginCapabilitySummary {
|
||||
has_skills: true,
|
||||
..summary("skills@test", "skills-plugin")
|
||||
},
|
||||
PluginCapabilitySummary {
|
||||
mcp_server_names: vec!["alpha".to_string()],
|
||||
app_connector_ids: vec![connector("connector_example")],
|
||||
..summary("alpha@test", "alpha-plugin")
|
||||
},
|
||||
PluginCapabilitySummary {
|
||||
mcp_server_names: vec!["beta".to_string()],
|
||||
app_connector_ids: vec![
|
||||
connector("connector_example"),
|
||||
connector("connector_gmail"),
|
||||
],
|
||||
..summary("beta@test", "beta-plugin")
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_namespace_for_skill_path_uses_manifest_name() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
mod manager;
|
||||
mod manifest;
|
||||
mod marketplace;
|
||||
mod render;
|
||||
mod store;
|
||||
|
||||
pub use manager::AppConnectorId;
|
||||
pub use manager::LoadedPlugin;
|
||||
pub use manager::PluginCapabilitySummary;
|
||||
pub use manager::PluginInstallError;
|
||||
pub use manager::PluginInstallRequest;
|
||||
pub use manager::PluginLoadOutcome;
|
||||
@@ -12,5 +14,6 @@ pub use manager::PluginsManager;
|
||||
pub(crate) use manager::plugin_namespace_for_skill_path;
|
||||
pub(crate) use manifest::load_plugin_manifest;
|
||||
pub(crate) use manifest::plugin_manifest_name;
|
||||
pub(crate) use render::render_plugins_section;
|
||||
pub use store::PluginId;
|
||||
pub use store::PluginInstallResult;
|
||||
@@ -0,0 +1,42 @@
|
||||
use crate::plugins::PluginCapabilitySummary;
|
||||
|
||||
pub(crate) fn render_plugins_section(plugins: &[PluginCapabilitySummary]) -> Option<String> {
|
||||
if plugins.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut lines = vec![
|
||||
"## Plugins".to_string(),
|
||||
"A plugin is a local bundle of skills, MCP servers, and apps. Below is the list of plugins that are enabled and available in this session.".to_string(),
|
||||
"### Available plugins".to_string(),
|
||||
];
|
||||
|
||||
lines.extend(
|
||||
plugins
|
||||
.iter()
|
||||
.map(|plugin| format!("- `{}`", plugin.display_name)),
|
||||
);
|
||||
|
||||
lines.push("### How to use plugins".to_string());
|
||||
lines.push(
|
||||
r###"- Discovery: The list above is the plugins available in this session.
|
||||
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
|
||||
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
|
||||
- Preference: When a relevant plugin is available, prefer using capabilities associated with that plugin over standalone capabilities that provide similar functionality.
|
||||
- Missing/blocked: If the user requests a plugin that is not listed above, or the plugin does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback."###
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
Some(lines.join("\n"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn render_plugins_section_returns_none_for_empty_plugins() {
|
||||
assert_eq!(render_plugins_section(&[]), None);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ use crate::config_loader::default_project_root_markers;
|
||||
use crate::config_loader::merge_toml_values;
|
||||
use crate::config_loader::project_root_markers_from_config;
|
||||
use crate::features::Feature;
|
||||
use crate::plugins::PluginCapabilitySummary;
|
||||
use crate::plugins::render_plugins_section;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::skills::render_skills_section;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
@@ -81,6 +83,7 @@ fn render_js_repl_instructions(config: &Config) -> Option<String> {
|
||||
pub(crate) async fn get_user_instructions(
|
||||
config: &Config,
|
||||
skills: Option<&[SkillMetadata]>,
|
||||
plugins: Option<&[PluginCapabilitySummary]>,
|
||||
) -> Option<String> {
|
||||
let project_docs = read_project_docs(config).await;
|
||||
|
||||
@@ -110,6 +113,13 @@ pub(crate) async fn get_user_instructions(
|
||||
output.push_str(&js_repl_section);
|
||||
}
|
||||
|
||||
if let Some(plugin_section) = plugins.and_then(render_plugins_section) {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
output.push_str(&plugin_section);
|
||||
}
|
||||
|
||||
let skills_section = skills.and_then(render_skills_section);
|
||||
if let Some(skills_section) = skills_section {
|
||||
if !output.is_empty() {
|
||||
@@ -387,7 +397,7 @@ mod tests {
|
||||
async fn no_doc_file_returns_none() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, 4096, None).await, None).await;
|
||||
let res = get_user_instructions(&make_config(&tmp, 4096, None).await, None, None).await;
|
||||
assert!(
|
||||
res.is_none(),
|
||||
"Expected None when AGENTS.md is absent and no system instructions provided"
|
||||
@@ -401,7 +411,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "hello world").unwrap();
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, 4096, None).await, None)
|
||||
let res = get_user_instructions(&make_config(&tmp, 4096, None).await, None, None)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
|
||||
@@ -420,7 +430,7 @@ mod tests {
|
||||
let huge = "A".repeat(LIMIT * 2); // 2 KiB
|
||||
fs::write(tmp.path().join("AGENTS.md"), &huge).unwrap();
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, LIMIT, None).await, None)
|
||||
let res = get_user_instructions(&make_config(&tmp, LIMIT, None).await, None, None)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
|
||||
@@ -452,7 +462,7 @@ mod tests {
|
||||
let mut cfg = make_config(&repo, 4096, None).await;
|
||||
cfg.cwd = nested;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
assert_eq!(res, "root level doc");
|
||||
@@ -464,7 +474,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "something").unwrap();
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, 0, None).await, None).await;
|
||||
let res = get_user_instructions(&make_config(&tmp, 0, None).await, None, None).await;
|
||||
assert!(
|
||||
res.is_none(),
|
||||
"With limit 0 the function should return None"
|
||||
@@ -479,7 +489,7 @@ mod tests {
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("test config should allow js_repl");
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds exactly one image to the outer `js_repl` function output. It accepts a direct image URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\" }))`.\n- Top-level bindings persist across cells. If you hit `SyntaxError: Identifier 'x' has already been declared`, reuse the binding, pick a new name, wrap in `{ ... }` for block scope, or reset the kernel with `js_repl_reset`.\n- Top-level static import declarations (for example `import x from \"pkg\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")` instead.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
@@ -498,7 +508,7 @@ mod tests {
|
||||
.set(features)
|
||||
.expect("test config should allow js_repl tool restrictions");
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds exactly one image to the outer `js_repl` function output. It accepts a direct image URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\" }))`.\n- Top-level bindings persist across cells. If you hit `SyntaxError: Identifier 'x' has already been declared`, reuse the binding, pick a new name, wrap in `{ ... }` for block scope, or reset the kernel with `js_repl_reset`.\n- Top-level static import declarations (for example `import x from \"pkg\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")` instead.\n- Do not call tools directly; use `js_repl` + `codex.tool(...)` for all tool calls, including shell commands.\n- MCP tools (if any) can also be called by name via `codex.tool(...)`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
@@ -517,7 +527,7 @@ mod tests {
|
||||
.set(features)
|
||||
.expect("test config should allow js_repl image detail settings");
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds exactly one image to the outer `js_repl` function output. It accepts a direct image URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\" }))`.\n- When generating or converting images for `view_image` in `js_repl`, prefer JPEG at 85% quality unless lossless quality is strictly required; other formats can be used if the user requests them. This keeps uploads smaller and reduces the chance of hitting image size caps.\n- Top-level bindings persist across cells. If you hit `SyntaxError: Identifier 'x' has already been declared`, reuse the binding, pick a new name, wrap in `{ ... }` for block scope, or reset the kernel with `js_repl_reset`.\n- Top-level static import declarations (for example `import x from \"pkg\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")` instead.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
@@ -533,9 +543,13 @@ mod tests {
|
||||
|
||||
const INSTRUCTIONS: &str = "base instructions";
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await, None)
|
||||
.await
|
||||
.expect("should produce a combined instruction string");
|
||||
let res = get_user_instructions(
|
||||
&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("should produce a combined instruction string");
|
||||
|
||||
let expected = format!("{INSTRUCTIONS}{PROJECT_DOC_SEPARATOR}{}", "proj doc");
|
||||
|
||||
@@ -550,8 +564,12 @@ mod tests {
|
||||
|
||||
const INSTRUCTIONS: &str = "some instructions";
|
||||
|
||||
let res =
|
||||
get_user_instructions(&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await, None).await;
|
||||
let res = get_user_instructions(
|
||||
&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(res, Some(INSTRUCTIONS.to_string()));
|
||||
}
|
||||
@@ -580,7 +598,7 @@ mod tests {
|
||||
let mut cfg = make_config(&repo, 4096, None).await;
|
||||
cfg.cwd = nested;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
assert_eq!(res, "root doc\n\ncrate doc");
|
||||
@@ -609,7 +627,7 @@ mod tests {
|
||||
assert_eq!(discovery[0], expected_parent);
|
||||
assert_eq!(discovery[1], expected_child);
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
assert_eq!(res, "parent doc\n\nchild doc");
|
||||
@@ -624,7 +642,7 @@ mod tests {
|
||||
|
||||
let cfg = make_config(&tmp, 4096, None).await;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("local doc expected");
|
||||
|
||||
@@ -646,7 +664,7 @@ mod tests {
|
||||
|
||||
let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md"]).await;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("fallback doc expected");
|
||||
|
||||
@@ -662,7 +680,7 @@ mod tests {
|
||||
|
||||
let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md", ".example.md"]).await;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("AGENTS.md should win");
|
||||
|
||||
@@ -695,6 +713,7 @@ mod tests {
|
||||
let res = get_user_instructions(
|
||||
&cfg,
|
||||
skills.errors.is_empty().then_some(skills.skills.as_slice()),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
@@ -722,6 +741,7 @@ mod tests {
|
||||
let res = get_user_instructions(
|
||||
&cfg,
|
||||
skills.errors.is_empty().then_some(skills.skills.as_slice()),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
@@ -744,7 +764,7 @@ mod tests {
|
||||
.enable(Feature::Apps)
|
||||
.expect("test config should allow apps");
|
||||
|
||||
let res = get_user_instructions(&cfg, None).await;
|
||||
let res = get_user_instructions(&cfg, None, None).await;
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
@@ -758,7 +778,7 @@ mod tests {
|
||||
.enable(Feature::Apps)
|
||||
.expect("test config should allow apps");
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = get_user_instructions(&cfg, None, None)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
assert_eq!(res, "base doc");
|
||||
|
||||
Reference in new issue
Block a user