diff --git a/acp/Cargo.lock b/acp/Cargo.lock index 08e3424d3..83a62ccd4 100644 --- a/acp/Cargo.lock +++ b/acp/Cargo.lock @@ -685,9 +685,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -706,9 +706,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/acp/Cargo.toml b/acp/Cargo.toml index b38ee9a4e..5fc03b56e 100644 --- a/acp/Cargo.toml +++ b/acp/Cargo.toml @@ -23,7 +23,7 @@ name = "iii-acp" path = "src/main.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std", "io-util", "sync", "time", "signal"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/acp/src/handler.rs b/acp/src/handler.rs index ec39c9c98..1217f9c1e 100644 --- a/acp/src/handler.rs +++ b/acp/src/handler.rs @@ -704,6 +704,9 @@ fn register_event_subscriber( function_id: fn_id, config: json!({ "stream_name": AGENT_EVENTS_STREAM }), metadata: None, + // Resolve the trigger's target in this worker's namespace so it matches + // where the function was registered (None => engine default). + namespace: iii.namespace(), }) { Ok(t) => Some(t), Err(e) => { diff --git a/approval-gate/Cargo.lock b/approval-gate/Cargo.lock index effdfce82..54a025dc2 100644 --- a/approval-gate/Cargo.lock +++ b/approval-gate/Cargo.lock @@ -545,12 +545,13 @@ dependencies = [ [[package]] name = "harness" -version = "1.7.0" +version = "1.7.4" dependencies = [ "anyhow", "async-trait", "clap", "globset", + "iii-console-ui", "iii-helpers", "iii-sdk", "jsonschema", @@ -804,11 +805,23 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "iii-console-ui" +version = "0.1.0" +dependencies = [ + "iii-sdk", + "schemars", + "serde", + "serde_json", + "tokio", + "tracing", +] + [[package]] name = "iii-helpers" -version = "0.21.8" +version = "0.22.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bdc7bbc3abfde934a62cdc5d3045adf52914dfc1ed6c20f8af691fc561dc55" +checksum = "0a5559b662390e53fa7fcac83451ae1667306e94c44a97f6c617f1e3023f1dfd" dependencies = [ "futures-util", "opentelemetry", @@ -827,9 +840,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.8" +version = "0.22.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd563a1d2f55f893d9a433b747f0bf9bc426656413b136b6ed3a699f3c757b2" +checksum = "743c738975e139d6c5ee0e0635699308e9f85969da11003bc123712f45b48def" dependencies = [ "async-trait", "futures-util", diff --git a/approval-gate/Cargo.toml b/approval-gate/Cargo.toml index bbe860e01..8277d30f0 100644 --- a/approval-gate/Cargo.toml +++ b/approval-gate/Cargo.toml @@ -15,7 +15,7 @@ name = "approval_gate" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.8" +iii-sdk = "=0.22.0-alpha.2" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/approval-gate/src/configuration.rs b/approval-gate/src/configuration.rs index b753e837b..afcd782f1 100644 --- a/approval-gate/src/configuration.rs +++ b/approval-gate/src/configuration.rs @@ -29,7 +29,25 @@ use crate::config::WorkerConfig; /// whole-snapshot replaces the inner `Arc` under the write lock. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "approval-gate"; +pub const DEFAULT_CONFIG_ID: &str = "approval-gate"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "approval::on-config-change"; const CONFIG_RETRIES: u32 = 3; /// Base backoff between configuration RPC retries; multiplied by the @@ -59,7 +77,7 @@ const FILESYSTEM_ACCESS_WATCH_ON_ERROR: &str = "fail_open"; /// value, so this is safe to call every boot). pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Approval Gate", "description": "Policy and decision surface settings: the deployment \ approval defaults (permission mode for new sessions \ @@ -98,14 +116,14 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { async fn get_config_value(iii: &IIIClient) -> Result { try_get_config_value(iii) .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + .ok_or_else(|| format!("configuration `{config_entry}` not found", config_entry = config_id())) } /// Returns `Ok(None)` when the entry does not exist. The engine's /// missing-entry codes vary in case (`function_not_found`, /// `STATEMENT_NOT_FOUND`, `NOT_FOUND`), so match case-insensitively. async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.to_ascii_uppercase().contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -128,6 +146,7 @@ pub fn bind_hook(iii: &IIIClient) { "on_error": HOOK_ON_ERROR, }), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => tracing::info!( trigger_type = "harness::hook::pre-trigger", @@ -157,6 +176,7 @@ pub fn bind_filesystem_access_watch_hook(iii: &IIIClient) { "on_error": FILESYSTEM_ACCESS_WATCH_ON_ERROR, }), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => tracing::info!( trigger_type = "harness::hook::post-trigger", @@ -258,10 +278,11 @@ pub fn register_config_trigger(iii: &IIIClient, cell: ConfigCell) -> Result<(), trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/approval-gate/src/events.rs b/approval-gate/src/events.rs index a6290a52b..fdc6d094c 100644 --- a/approval-gate/src/events.rs +++ b/approval-gate/src/events.rs @@ -253,15 +253,36 @@ impl Emitter { ) { for binding in set.snapshot() { if binding_matches(&binding.filter, session_id, session_metadata) { - let res = self - .iii - .trigger(TriggerRequest { - function_id: binding.function_id.clone(), - payload: payload.clone(), - action: Some(TriggerAction::Void), - timeout_ms: None, - }) - .await; + let request = TriggerRequest { + function_id: binding.function_id.clone(), + payload: payload.clone(), + action: Some(TriggerAction::Void), + timeout_ms: None, + }; + let route_ns = self.iii.namespace().filter(|_| { + !matches!( + binding.function_id.split("::").next(), + Some( + "state" + | "stream" + | "queue" + | "pubsub" + | "configuration" + | "cron" + | "http" + | "engine" + | "sandbox" + | "log" + | "secret" + | "kv" + | "iii" + ) + ) + }); + let res = match route_ns { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + }; if let Err(e) = res { tracing::warn!( function_id = %binding.function_id, @@ -314,6 +335,7 @@ mod tests { function_id: function_id.into(), config, metadata: None, + namespace: None, } } diff --git a/approval-gate/src/harness.rs b/approval-gate/src/harness.rs index 601a59e21..1c7d0a153 100644 --- a/approval-gate/src/harness.rs +++ b/approval-gate/src/harness.rs @@ -10,13 +10,16 @@ use iii_sdk::IIIClient; use serde_json::{json, Value}; pub async fn function_resolve(iii: &IIIClient, payload: Value) -> Result { - iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: "harness::function::resolve".into(), payload, action: None, timeout_ms: None, - }) - .await + }; + match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } } fn parse_roots(reply: &Value) -> Vec { @@ -36,14 +39,16 @@ fn parse_roots(reply: &Value) -> Vec { /// error on "no grants" — only on transport/RPC failure, e.g. an older /// harness that doesn't implement this control plane yet). pub async fn filesystem_grants(iii: &IIIClient, session_id: &str) -> Result, Error> { - let reply = iii - .trigger(TriggerRequest { - function_id: "harness::filesystem::grants".into(), - payload: json!({ "session_id": session_id }), - action: None, - timeout_ms: None, - }) - .await?; + let request = TriggerRequest { + function_id: "harness::filesystem::grants".into(), + payload: json!({ "session_id": session_id }), + action: None, + timeout_ms: None, + }; + let reply = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await?, + None => iii.trigger(request).await?, + }; Ok(parse_roots(&reply)) } @@ -55,13 +60,15 @@ pub async fn filesystem_grant( session_id: &str, root: &str, ) -> Result, Error> { - let reply = iii - .trigger(TriggerRequest { - function_id: "harness::filesystem::grant".into(), - payload: json!({ "session_id": session_id, "root": root }), - action: None, - timeout_ms: None, - }) - .await?; + let request = TriggerRequest { + function_id: "harness::filesystem::grant".into(), + payload: json!({ "session_id": session_id, "root": root }), + action: None, + timeout_ms: None, + }; + let reply = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await?, + None => iii.trigger(request).await?, + }; Ok(parse_roots(&reply)) } diff --git a/approval-gate/src/main.rs b/approval-gate/src/main.rs index b7670398a..ecff0d3cd 100644 --- a/approval-gate/src/main.rs +++ b/approval-gate/src/main.rs @@ -86,6 +86,7 @@ fn bind_best_effort( function_id: function_id.to_string(), config, metadata: None, + namespace: iii.namespace(), }); match res { Ok(_) => tracing::info!(trigger_type, function_id, "trigger binding requested"), diff --git a/approval-gate/src/session.rs b/approval-gate/src/session.rs index 979ad5b33..ed34a3fea 100644 --- a/approval-gate/src/session.rs +++ b/approval-gate/src/session.rs @@ -6,11 +6,14 @@ use iii_sdk::IIIClient; use serde_json::{json, Value}; pub async fn get(iii: &IIIClient, session_id: &str) -> Result { - iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: "session::get".into(), payload: json!({ "session_id": session_id }), action: None, timeout_ms: None, - }) - .await + }; + match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } } diff --git a/approval-gate/src/testkit/engine.rs b/approval-gate/src/testkit/engine.rs index a14eafb2c..2ea578679 100644 --- a/approval-gate/src/testkit/engine.rs +++ b/approval-gate/src/testkit/engine.rs @@ -459,6 +459,7 @@ pub async fn boot(engine: &Engine, opts: BootOpts) -> TestStack { function_id: "recorder::on_created".to_string(), config: json!({}), metadata: None, + namespace: iii.namespace(), }) .expect("bind pending_created"); iii.register_trigger(RegisterTriggerInput { @@ -466,6 +467,7 @@ pub async fn boot(engine: &Engine, opts: BootOpts) -> TestStack { function_id: "recorder::on_resolved".to_string(), config: json!({}), metadata: None, + namespace: iii.namespace(), }) .expect("bind pending_resolved"); diff --git a/bridge/Cargo.lock b/bridge/Cargo.lock index 323a1b714..c4874ac61 100644 --- a/bridge/Cargo.lock +++ b/bridge/Cargo.lock @@ -660,9 +660,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -681,9 +681,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/bridge/Cargo.toml b/bridge/Cargo.toml index 62f434836..e2570af6a 100644 --- a/bridge/Cargo.toml +++ b/bridge/Cargo.toml @@ -18,7 +18,7 @@ name = "bridge" path = "src/main.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" async-trait = "0.1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } diff --git a/bridge/src/boot.rs b/bridge/src/boot.rs index c45598fa1..9603d1306 100644 --- a/bridge/src/boot.rs +++ b/bridge/src/boot.rs @@ -122,7 +122,7 @@ pub async fn start(iii: Arc, config: BridgeConfig) -> anyhow::Result< /// `bridge.invoke` + `bridge.invoke_async` on the local engine — exact /// function ids and descriptions from the builtin (mod.rs:96-191). /// -/// Both use `new_async_with_bad_request` with the typed [`functions:: +/// Both use `new_async` with the typed [`functions:: /// InvokeInput`]: the SDK auto-extracts a real request schema instead of the /// permissive `AnyValue` a `Value` closure param would emit, while /// [`functions::invoke_bad_request`] keeps owning the `deserialization_error` @@ -133,33 +133,27 @@ pub fn register_bridge_functions(iii: &Arc, remote: RemoteCell) { let caller = caller.clone(); iii.register_function( functions::INVOKE_FN, - RegisterFunction::new_async_with_bad_request( - move |req: functions::InvokeInput| { - let caller = caller.clone(); - async move { - functions::handle_invoke_typed(caller.as_ref(), req) - .await - .map_err(Error::from) - } - }, - functions::invoke_bad_request, - ) - .description("Invoke a function on the remote III instance"), - ); - } - iii.register_function( - functions::INVOKE_ASYNC_FN, - RegisterFunction::new_async_with_bad_request( - move |req: functions::InvokeInput| { + RegisterFunction::new_async(move |req: functions::InvokeInput| { let caller = caller.clone(); async move { - functions::handle_invoke_async_typed(caller.as_ref(), req) + functions::handle_invoke_typed(caller.as_ref(), req) .await .map_err(Error::from) } - }, - functions::invoke_bad_request, - ) + }) + .description("Invoke a function on the remote III instance"), + ); + } + iii.register_function( + functions::INVOKE_ASYNC_FN, + RegisterFunction::new_async(move |req: functions::InvokeInput| { + let caller = caller.clone(); + async move { + functions::handle_invoke_async_typed(caller.as_ref(), req) + .await + .map_err(Error::from) + } + }) .description("Fire-and-forget invoke on the remote III instance"), ); } diff --git a/bridge/src/configuration.rs b/bridge/src/configuration.rs index ed63b1b7c..61a6c0ee5 100644 --- a/bridge/src/configuration.rs +++ b/bridge/src/configuration.rs @@ -73,7 +73,25 @@ pub type ConfigCell = Arc>>; /// Apply lock to serialize overlapping configuration change runs. pub type ApplyLock = Arc>; -pub const CONFIG_ID: &str = "bridge"; +pub const DEFAULT_CONFIG_ID: &str = "bridge"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "bridge::on-config-change"; const CONFIG_RETRIES: u32 = 3; const CONFIG_RETRY_BACKOFF_MS: u64 = 250; @@ -87,7 +105,7 @@ const CONFIG_BUS_TIMEOUT_MS: u64 = 10_000; /// restarts. pub async fn register_config(iii: &IIIClient, seed: Option<&BridgeConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Bridge", "description": "Bridge to a remote iii instance — remote WebSocket URL, functions exposed to the remote engine, and remote functions forwarded locally.", "schema": BridgeConfig::json_schema(), @@ -113,7 +131,7 @@ pub async fn fetch_config(iii: &IIIClient) -> Result { match try_get_config_value(iii).await? { Some(value) if !value.is_null() => BridgeConfig::from_json(&value), _ => { - tracing::info!("no `{CONFIG_ID}` configuration value stored; using built-in default"); + tracing::info!("no `{config_entry}` configuration value stored; using built-in default", config_entry = config_id()); Ok(BridgeConfig::default()) } } @@ -130,7 +148,7 @@ async fn try_get_config_value(iii: &IIIClient) -> Result, String> match trigger_with_retry( iii, "configuration::get", - json!({ "id": CONFIG_ID }), + json!({ "id": config_id() }), CONFIG_BUS_TIMEOUT_MS, ) .await @@ -201,10 +219,11 @@ pub async fn register_config_trigger(iii: &Arc, boot: &BootHandle) -> trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/bridge/tests/e2e_reload.rs b/bridge/tests/e2e_reload.rs index 53f93e01f..780dfeaf1 100644 --- a/bridge/tests/e2e_reload.rs +++ b/bridge/tests/e2e_reload.rs @@ -40,7 +40,7 @@ fn echo_fn() -> RegisterFunction { async fn set_bridge_config(iii: &Arc, value: Value) { iii.trigger(TriggerRequest { function_id: "configuration::set".to_string(), - payload: json!({ "id": configuration::CONFIG_ID, "value": value }), + payload: json!({ "id": configuration::config_id(), "value": value }), action: None, timeout_ms: Some(10_000), }) diff --git a/browser/Cargo.lock b/browser/Cargo.lock index a7b6eae4e..e1861c7da 100644 --- a/browser/Cargo.lock +++ b/browser/Cargo.lock @@ -822,9 +822,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -843,9 +843,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/browser/Cargo.toml b/browser/Cargo.toml index a8818f6cd..ab165f9aa 100644 --- a/browser/Cargo.toml +++ b/browser/Cargo.toml @@ -15,7 +15,7 @@ name = "browser" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" iii-console-ui = { path = "../crates/console-ui" } arc-swap = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time", "process"] } diff --git a/browser/src/configuration.rs b/browser/src/configuration.rs index 7bef16766..bf535c378 100644 --- a/browser/src/configuration.rs +++ b/browser/src/configuration.rs @@ -13,14 +13,32 @@ use serde_json::{json, Value}; use crate::config::{SharedConfig, WorkerConfig}; -pub const CONFIG_ID: &str = "browser"; +pub const DEFAULT_CONFIG_ID: &str = "browser"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "browser::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "browser", "description": "Session limits, buffers, timeouts, viewport, and executable for the browser worker.", "schema": WorkerConfig::json_schema(), @@ -53,7 +71,7 @@ async fn should_seed_default(iii: &IIIClient) -> Result { } async fn try_get_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -90,8 +108,9 @@ pub fn register_config_trigger(iii: &IIIClient, config: SharedConfig) -> Result< iii.register_trigger(RegisterTriggerInput { trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), - config: json!({ "configuration_id": CONFIG_ID, "event_types": ["configuration:updated"] }), + config: json!({ "configuration_id": config_id(), "event_types": ["configuration:updated"] }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/browser/src/events.rs b/browser/src/events.rs index c396150aa..3d6f927d7 100644 --- a/browser/src/events.rs +++ b/browser/src/events.rs @@ -440,6 +440,7 @@ mod tests { function_id: "console::on-event".to_string(), config, metadata: None, + namespace: None, } } @@ -494,6 +495,7 @@ mod tests { function_id: "fn::match".to_string(), config: json!({ "session_id": "b1" }), metadata: None, + namespace: None, }) .unwrap(); set.add(TriggerConfig { @@ -501,6 +503,7 @@ mod tests { function_id: "fn::other".to_string(), config: json!({ "session_id": "b2" }), metadata: None, + namespace: None, }) .unwrap(); diff --git a/claude-code/package.json b/claude-code/package.json index 02ccad33d..14b3a6a22 100644 --- a/claude-code/package.json +++ b/claude-code/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.173", - "iii-sdk": "^0.21.6", + "iii-sdk": "0.22.0-alpha.3", "yaml": "^2.6.0", "zod": "^4.0.0" }, diff --git a/claude-code/pnpm-lock.yaml b/claude-code/pnpm-lock.yaml index 29c788680..1a4c03c91 100644 --- a/claude-code/pnpm-lock.yaml +++ b/claude-code/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^0.3.173 version: 0.3.173(@anthropic-ai/sdk@0.104.1(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) iii-sdk: - specifier: ^0.21.6 - version: 0.21.6 + specifier: 0.22.0-alpha.3 + version: 0.22.0-alpha.3 yaml: specifier: ^2.6.0 version: 2.9.0 @@ -630,8 +630,8 @@ packages: peerDependencies: hono: ^4 - '@iii-dev/helpers@0.21.6': - resolution: {integrity: sha512-UkiIqC5Ob41xc9uYx1SRt4LXLKrzAMzOCq6Kwo4hCak7Qj4Q1tP0GW0nYP7UpgYHsQnzf+2sspb2yD/DVa2ePA==} + '@iii-dev/helpers@0.22.0-alpha.3': + resolution: {integrity: sha512-zwilMTMq2MgVFu/aOEbtrPDpB+AVcdcqKBVAGc4vXv4XT2sK5eAXpdyzPcNgf8bGRKcRGZJ0ZJ8dA8WDVlSlaw==} '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -1178,8 +1178,8 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} - iii-sdk@0.21.6: - resolution: {integrity: sha512-D9NGtcGWdY8tqXRIjvRDqNqKRI2qJcQguSDWuUEPXaYyIHSn1jp3PIVF34cCY6C+f3Bby8fjptxpfzWYQUOg2A==} + iii-sdk@0.22.0-alpha.3: + resolution: {integrity: sha512-S6dH8ssbZsuuh8m6k0gG//D5K11w/wTkUviRMQ0sP3NNQh4ZhxZz0TbGlhROpwCYdJb1Rgl8dAHgeXbhyoIUXg==} import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} @@ -1919,7 +1919,7 @@ snapshots: dependencies: hono: 4.12.25 - '@iii-dev/helpers@0.21.6': + '@iii-dev/helpers@0.22.0-alpha.3': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api-logs': 0.57.2 @@ -2530,9 +2530,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iii-sdk@0.21.6: + iii-sdk@0.22.0-alpha.3: dependencies: - '@iii-dev/helpers': 0.21.6 + '@iii-dev/helpers': 0.22.0-alpha.3 '@opentelemetry/api': 1.9.1 ws: 8.21.0 transitivePeerDependencies: diff --git a/claude-code/src/index.ts b/claude-code/src/index.ts index 95648844b..3624f78dd 100644 --- a/claude-code/src/index.ts +++ b/claude-code/src/index.ts @@ -39,7 +39,13 @@ const bootConfig: Config = { claude_executable: resolveClaudeExecutable(seed.claude_executable), }; -const iii = registerWorker(url, { workerName: 'claude-code' }); +// namespace: explicit III_NAMESPACE so the worker registers under a namespace +// when one is set (the SDK also reads it, passed here for visibility). Absent, +// the engine applies its default namespace. +const iii = registerWorker(url, { + workerName: 'claude-code', + namespace: process.env.III_NAMESPACE, +}); // Best-effort: a configuration-worker hiccup at boot must not stop the worker // from registering claude::*; it falls back to the seed via fetchRuntime. diff --git a/codex/Cargo.lock b/codex/Cargo.lock index 9b00e2047..ce357f675 100644 --- a/codex/Cargo.lock +++ b/codex/Cargo.lock @@ -672,9 +672,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -693,9 +693,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/codex/Cargo.toml b/codex/Cargo.toml index 52298ac37..12c758131 100644 --- a/codex/Cargo.toml +++ b/codex/Cargo.toml @@ -15,8 +15,8 @@ name = "codex" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" -iii-helpers = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" +iii-helpers = "=0.22.0-alpha.3" schemars = { version = "0.8", features = ["uuid1"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "process", "time", "io-util"] } serde = { version = "1", features = ["derive"] } diff --git a/codex/src/configuration.rs b/codex/src/configuration.rs index b617140d1..34bf75951 100644 --- a/codex/src/configuration.rs +++ b/codex/src/configuration.rs @@ -21,7 +21,25 @@ use crate::config::Config; /// the inner `Arc` under the write lock. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "codex"; +pub const DEFAULT_CONFIG_ID: &str = "codex"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "codex::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -31,7 +49,7 @@ const CONFIG_RETRIES: u32 = 3; /// only when no stored value exists yet. pub async fn register_config(iii: &IIIClient, seed: Option<&Config>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Codex", "description": "OpenAI Codex worker: per-turn defaults (model, sandbox mode, approval policy, reasoning effort, working directory), the agent::events / codex::events stream names, the codex CLI path, an optional API base URL, and whether to inject the iii runtime context as developer_instructions.", "schema": Config::json_schema(), @@ -65,7 +83,7 @@ async fn should_seed_default(iii: &IIIClient) -> Result { /// `Ok(None)` when the entry does not exist (`NOT_FOUND`). async fn try_get_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -102,10 +120,11 @@ pub fn register_config_trigger(iii: &IIIClient, cell: ConfigCell) -> Result<(), trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/computer/Cargo.lock b/computer/Cargo.lock index 33d9af7bf..837fe37fc 100644 --- a/computer/Cargo.lock +++ b/computer/Cargo.lock @@ -1101,9 +1101,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.8" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bdc7bbc3abfde934a62cdc5d3045adf52914dfc1ed6c20f8af691fc561dc55" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -1122,9 +1122,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/computer/Cargo.toml b/computer/Cargo.toml index b900c8c50..82a7fd737 100644 --- a/computer/Cargo.toml +++ b/computer/Cargo.toml @@ -15,7 +15,7 @@ name = "computer" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" iii-console-ui = { path = "../crates/console-ui" } arc-swap = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } diff --git a/computer/src/configuration.rs b/computer/src/configuration.rs index 7be70da12..9495ca72c 100644 --- a/computer/src/configuration.rs +++ b/computer/src/configuration.rs @@ -92,6 +92,7 @@ pub fn register_config_trigger(iii: &IIIClient, config: SharedConfig) -> Result< function_id: CONFIG_FN_ID.to_string(), config: json!({ "configuration_id": CONFIG_ID, "event_types": ["configuration:updated"] }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/console/Cargo.lock b/console/Cargo.lock index a35d691f7..23de50a52 100644 --- a/console/Cargo.lock +++ b/console/Cargo.lock @@ -778,9 +778,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -799,9 +799,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "743c738975e139d6c5ee0e0635699308e9f85969da11003bc123712f45b48def" dependencies = [ "async-trait", "futures-util", diff --git a/console/Cargo.toml b/console/Cargo.toml index d091df0d1..774a312cf 100644 --- a/console/Cargo.toml +++ b/console/Cargo.toml @@ -14,7 +14,7 @@ path = "src/main.rs" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.2" # Worker-side injectable console UI (content function + console:script/style # triggers + hot-reload watcher) — the console dogfoods its own protocol to # ship the injectable-UI toggle form. Direct link, never published. diff --git a/console/src/configuration.rs b/console/src/configuration.rs index ab6213ae1..5e1d6a46e 100644 --- a/console/src/configuration.rs +++ b/console/src/configuration.rs @@ -28,7 +28,25 @@ use serde_json::{json, Value}; use crate::ui_assets::UiControl; -pub const CONFIG_ID: &str = "console"; +pub const DEFAULT_CONFIG_ID: &str = "console"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; const CONFIG_RETRY_BACKOFF_MS: u64 = 250; @@ -140,7 +158,7 @@ pub async fn register_console_config(iii: &IIIClient) { }; let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Console", "description": "Console UI preferences — Traces V2 saved views \ (grouping, filters, display settings, hidden functions), \ @@ -153,7 +171,7 @@ pub async fn register_console_config(iii: &IIIClient) { } match trigger_with_retry(iii, "configuration::register", payload).await { - Ok(_) => tracing::info!(id = CONFIG_ID, "console configuration registered"), + Ok(_) => tracing::info!(id = config_id(), "console configuration registered"), Err(e) => tracing::warn!( error = %e, "console configuration registration failed; skipping \ @@ -218,10 +236,11 @@ pub async fn start_injectable_ui_sync(iii: &Arc, control: UiControl) trigger_type: "configuration".to_string(), function_id: CONFIG_CHANGE_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), }) { tracing::warn!( error = %e, @@ -267,7 +286,7 @@ pub struct ConfigChangeRequest {} /// `Ok(None)` when the entry does not exist or holds `null`. async fn existing_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").filter(|v| !v.is_null()).cloned()), Err(e) if e.to_ascii_uppercase().contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), diff --git a/console/src/ui_assets.rs b/console/src/ui_assets.rs index 059d96ccf..fe29507db 100644 --- a/console/src/ui_assets.rs +++ b/console/src/ui_assets.rs @@ -1077,6 +1077,7 @@ mod tests { function_id: function_id.to_string(), config: json!({ "path": path }), metadata: None, + namespace: None, } } @@ -1086,6 +1087,7 @@ mod tests { function_id: function_id.to_string(), config: json!({}), metadata: None, + namespace: None, } } diff --git a/context-manager/Cargo.lock b/context-manager/Cargo.lock index 6c0102cf2..b1d403ab8 100644 --- a/context-manager/Cargo.lock +++ b/context-manager/Cargo.lock @@ -939,9 +939,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -960,9 +960,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/context-manager/Cargo.toml b/context-manager/Cargo.toml index 1d1903e8a..f924e16c9 100644 --- a/context-manager/Cargo.toml +++ b/context-manager/Cargo.toml @@ -15,7 +15,7 @@ name = "context_manager" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/context-manager/src/adapters/cache.rs b/context-manager/src/adapters/cache.rs index 6bbee1f5f..d9ee60780 100644 --- a/context-manager/src/adapters/cache.rs +++ b/context-manager/src/adapters/cache.rs @@ -138,6 +138,7 @@ pub fn register_models_changed_flush(iii: &IIIClient, cache: Arc self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + }; + let resp = match budget_result { Ok(resp) => resp, Err(err) if is_unroutable(&err) => { // Rolling upgrades may briefly run a context-manager newer than // the router. Preserve the former, conservative behavior until // `models::budget` is registered instead of dropping all the way // to the 8k fallback model. - let response = self - .iii - .trigger(TriggerRequest { - function_id: "router::models::get".to_string(), - payload, - action: None, - timeout_ms: Some(MODELS_GET_TIMEOUT_MS), - }) - .await - .map_err(|e| e.to_string())?; + let request = TriggerRequest { + function_id: "router::models::get".to_string(), + payload, + action: None, + timeout_ms: Some(MODELS_GET_TIMEOUT_MS), + }; + let response = match self.iii.namespace() { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + } + .map_err(|e| e.to_string())?; if response.is_null() { return Ok(None); } @@ -188,15 +190,16 @@ impl Summarizer for RouterSummarizer { // Read the outer budget from the live snapshot so a // summarizer_timeout_ms change takes effect without a restart. let timeout_ms = self.config.read().await.summarizer_timeout_ms; - let trigger_result = self - .iii - .trigger(TriggerRequest { - function_id: "router::chat".to_string(), - payload, - action: None, - timeout_ms: Some(timeout_ms), - }) - .await; + let request = TriggerRequest { + function_id: "router::chat".to_string(), + payload, + action: None, + timeout_ms: Some(timeout_ms), + }; + let trigger_result = match self.iii.namespace() { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + }; // The terminal event precedes the trigger response; give the // socket a short drain window, then stop reading either way. diff --git a/context-manager/src/configuration.rs b/context-manager/src/configuration.rs index 120fff985..dc1377e93 100644 --- a/context-manager/src/configuration.rs +++ b/context-manager/src/configuration.rs @@ -36,7 +36,25 @@ use crate::ports::{LeaseCell, LeaseStore}; /// whole-snapshot replaces the inner `Arc` under the write lock. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "context-manager"; +pub const DEFAULT_CONFIG_ID: &str = "context-manager"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "context::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -51,7 +69,7 @@ const CONFIG_RETRY_BACKOFF_MS: u64 = 250; /// value, so this is safe to call every boot). pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Context Manager", "description": "Model-ready context assembly tuning: token-budget reserves, \ function-result prune thresholds, compaction tail size, the \ @@ -89,14 +107,14 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { async fn get_config_value(iii: &IIIClient) -> Result { try_get_config_value(iii) .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + .ok_or_else(|| format!("configuration `{config_entry}` not found", config_entry = config_id())) } /// Returns `Ok(None)` when the entry does not exist. The engine's /// missing-entry codes vary in case (`function_not_found`, /// `STATEMENT_NOT_FOUND`, `NOT_FOUND`), so match case-insensitively. async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.to_ascii_uppercase().contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -162,10 +180,11 @@ pub fn register_config_trigger( trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/crates/console-ui/Cargo.toml b/crates/console-ui/Cargo.toml index e20ece1f7..3cf8a2c55 100644 --- a/crates/console-ui/Cargo.toml +++ b/crates/console-ui/Cargo.toml @@ -14,10 +14,10 @@ publish = false [dependencies] # Range, not an exact pin: workers on this repo's SDK line carry their own -# exact pins (0.21.6 for state, 0.21.8 for the harness) and cargo must be able +# exact pins (alpha.2 for the harness, alpha.3 for most workers) and cargo must be able # to unify them with this crate. Path-linking this crate must never be what # drags a worker onto a different SDK build, nor what blocks one. -iii-sdk = ">=0.21.6, <0.22" +iii-sdk = ">=0.22.0-alpha.2, <0.22.0-alpha.4" tokio = { version = "1", features = ["rt", "sync", "time", "fs"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/console-ui/src/lib.rs b/crates/console-ui/src/lib.rs index 27831c43a..24f30cbd5 100644 --- a/crates/console-ui/src/lib.rs +++ b/crates/console-ui/src/lib.rs @@ -386,6 +386,7 @@ fn register_asset_trigger( function_id: function_id.to_string(), config: serde_json::json!({ "path": path }), metadata: None, + namespace: iii.namespace(), }) } diff --git a/cron/Cargo.lock b/cron/Cargo.lock index 0ec2911fe..7be03a7d5 100644 --- a/cron/Cargo.lock +++ b/cron/Cargo.lock @@ -773,9 +773,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -794,9 +794,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/cron/Cargo.toml b/cron/Cargo.toml index 0eef504c7..44a56bc65 100644 --- a/cron/Cargo.toml +++ b/cron/Cargo.toml @@ -18,7 +18,7 @@ name = "cron" path = "src/main.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" async-trait = "0.1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } diff --git a/cron/src/configuration.rs b/cron/src/configuration.rs index 16efd1353..1bcf16cd9 100644 --- a/cron/src/configuration.rs +++ b/cron/src/configuration.rs @@ -13,7 +13,25 @@ use crate::config::CronConfig; use crate::locks; use crate::scheduler::Scheduler; -pub const CONFIG_ID: &str = "cron"; +pub const DEFAULT_CONFIG_ID: &str = "cron"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "cron::on-config-change"; const CONFIG_RETRIES: u32 = 3; const CONFIG_RETRY_BACKOFF_MS: u64 = 250; @@ -21,7 +39,7 @@ const CONFIG_BUS_TIMEOUT_MS: u64 = 10_000; pub async fn register_config(iii: &IIIClient, seed: Option<&CronConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Cron", "description": "Cron scheduler settings - lock backend for multi-instance mutual exclusion (local or redis).", "schema": CronConfig::json_schema(), @@ -44,7 +62,7 @@ pub async fn fetch_config(iii: &IIIClient) -> Result { match try_get_config_value(iii).await? { Some(value) if !value.is_null() => CronConfig::from_json(&value), _ => { - tracing::info!("no `{CONFIG_ID}` configuration value stored; using built-in default"); + tracing::info!("no `{config_entry}` configuration value stored; using built-in default", config_entry = config_id()); Ok(CronConfig::default()) } } @@ -61,7 +79,7 @@ async fn try_get_config_value(iii: &IIIClient) -> Result, String> match trigger_with_retry( iii, "configuration::get", - json!({ "id": CONFIG_ID }), + json!({ "id": config_id() }), CONFIG_BUS_TIMEOUT_MS, ) .await @@ -92,10 +110,11 @@ pub fn register_config_trigger(iii: &Arc, parts: BootParts) -> Result trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/cron/src/trigger.rs b/cron/src/trigger.rs index 2ad0d9cfb..9c999a948 100644 --- a/cron/src/trigger.rs +++ b/cron/src/trigger.rs @@ -101,6 +101,7 @@ mod tests { function_id: "backend".to_string(), config: cfg, metadata: None, + namespace: None, } } diff --git a/cron/tests/e2e_schedule.rs b/cron/tests/e2e_schedule.rs index a4758f10a..b6b0f2bff 100644 --- a/cron/tests/e2e_schedule.rs +++ b/cron/tests/e2e_schedule.rs @@ -46,6 +46,7 @@ async fn cron_trigger_fires_bound_function_with_parity_payload() { function_id: "e2e::cron-backend".to_string(), config: serde_json::json!({"expression": "*/1 * * * * *"}), metadata: None, + namespace: iii.namespace(), }) .expect("trigger registration"); diff --git a/database/Cargo.lock b/database/Cargo.lock index 4f8dcfeae..2ae5470bf 100644 --- a/database/Cargo.lock +++ b/database/Cargo.lock @@ -1282,9 +1282,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "0a5559b662390e53fa7fcac83451ae1667306e94c44a97f6c617f1e3023f1dfd" dependencies = [ "futures-util", "opentelemetry", @@ -1303,9 +1303,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "743c738975e139d6c5ee0e0635699308e9f85969da11003bc123712f45b48def" dependencies = [ "async-trait", "futures-util", diff --git a/database/Cargo.toml b/database/Cargo.toml index 397ae1f01..d32342d3f 100644 --- a/database/Cargo.toml +++ b/database/Cargo.toml @@ -14,8 +14,8 @@ path = "src/main.rs" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" -iii-helpers = "=0.21.6" +iii-sdk = "=0.22.0-alpha.2" +iii-helpers = "=0.22.0-alpha.2" iii-console-ui = { path = "../crates/console-ui" } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } diff --git a/database/src/configuration.rs b/database/src/configuration.rs index 2f49d8870..7f73f0a9c 100644 --- a/database/src/configuration.rs +++ b/database/src/configuration.rs @@ -11,7 +11,25 @@ use serde_json::{json, Value}; use std::collections::HashMap; use std::time::Duration; -pub const CONFIG_ID: &str = "database"; +pub const DEFAULT_CONFIG_ID: &str = "database"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "database::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -21,7 +39,7 @@ const CONFIG_RETRIES: u32 = 3; /// built-in defaults are seeded only when no stored value exists yet. pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Database", "description": "Connection pools for PostgreSQL, MySQL, and SQLite.", "schema": WorkerConfig::json_schema(), @@ -56,12 +74,12 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { async fn get_config_value(iii: &IIIClient) -> Result { try_get_config_value(iii) .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + .ok_or_else(|| format!("configuration `{config_entry}` not found", config_entry = config_id())) } /// Returns `Ok(None)` when the entry does not exist (`NOT_FOUND`). async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -148,10 +166,11 @@ pub fn register_config_trigger( trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/devin/Cargo.lock b/devin/Cargo.lock index 1498bb769..2942356f0 100644 --- a/devin/Cargo.lock +++ b/devin/Cargo.lock @@ -670,9 +670,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -691,9 +691,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/devin/Cargo.toml b/devin/Cargo.toml index 80b170b20..190c628aa 100644 --- a/devin/Cargo.toml +++ b/devin/Cargo.toml @@ -15,8 +15,8 @@ name = "devin" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" -iii-helpers = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" +iii-helpers = "=0.22.0-alpha.3" schemars = { version = "0.8", features = ["uuid1"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "process", "time", "io-util"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } diff --git a/devin/src/configuration.rs b/devin/src/configuration.rs index 6aed77cd8..5facf319d 100644 --- a/devin/src/configuration.rs +++ b/devin/src/configuration.rs @@ -20,7 +20,25 @@ use crate::config::Config; /// the inner `Arc` under the write lock. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "devin"; +pub const DEFAULT_CONFIG_ID: &str = "devin"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "devin::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -30,7 +48,7 @@ const CONFIG_RETRIES: u32 = 3; /// only when no stored value exists yet. pub async fn register_config(iii: &IIIClient, seed: Option<&Config>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Devin", "description": "Devin worker: the API bearer token and organization id, the Devin REST base URL and request timeout, the agent::events / devin::events stream names, the devin CLI path and extra args, and whether to inject the iii runtime context into a devin::run prompt.", "schema": Config::json_schema(), @@ -64,7 +82,7 @@ async fn should_seed_default(iii: &IIIClient) -> Result { /// `Ok(None)` when the entry does not exist (`NOT_FOUND`). async fn try_get_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -101,10 +119,11 @@ pub fn register_config_trigger(iii: &IIIClient, cell: ConfigCell) -> Result<(), trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/editor/Cargo.lock b/editor/Cargo.lock index 4b8aa3d24..7bb767142 100644 --- a/editor/Cargo.lock +++ b/editor/Cargo.lock @@ -675,9 +675,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -696,9 +696,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index af231f8b6..05745cd75 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -17,11 +17,11 @@ path = "src/lib.rs" # Lockstep with every other worker and with `crates/console-ui`, which this # binary links: two iii-sdk versions in one process means two incompatible # `IIIClient` types. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" iii-console-ui = { path = "../crates/console-ui" } # Shared observability: the observer fires inside a harness turn, so its span # has to hang off that turn rather than dangle as its own trace root. -iii-helpers = "=0.21.6" +iii-helpers = "=0.22.0-alpha.3" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/editor/src/configuration.rs b/editor/src/configuration.rs index 7b4d2954c..021fe86bc 100644 --- a/editor/src/configuration.rs +++ b/editor/src/configuration.rs @@ -28,7 +28,25 @@ use crate::config::WorkerConfig; /// write lock. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "editor"; +pub const DEFAULT_CONFIG_ID: &str = "editor"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "editor::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -55,7 +73,7 @@ pub fn cell(cfg: WorkerConfig) -> ConfigCell { /// this on every boot never overwrites an operator's edit. pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Editor", "description": "Editor workspace limits: diff size and context, file-finder and \ content-search caps, the largest file an open will pull back, and \ @@ -75,9 +93,12 @@ pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Re /// Read the live configuration (env-expanded by the configuration worker — /// `from_json` does NOT re-expand). pub async fn fetch_config(iii: &IIIClient) -> Result { - let value = try_get_config_value(iii) - .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found"))?; + let value = try_get_config_value(iii).await?.ok_or_else(|| { + format!( + "configuration `{config_entry}` not found", + config_entry = config_id() + ) + })?; if value.is_null() { tracing::info!("no stored configuration; using built-in defaults"); return Ok(WorkerConfig::default()); @@ -96,7 +117,7 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { /// `Ok(None)` when the entry does not exist. The engine's missing-entry codes /// vary in case (`function_not_found`, `NOT_FOUND`), so match case-insensitively. async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.to_ascii_uppercase().contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -277,10 +298,11 @@ pub async fn register_config_trigger( trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => return Ok(()), Err(e) => { diff --git a/editor/src/observe.rs b/editor/src/observe.rs index 134cc126b..2e31907d5 100644 --- a/editor/src/observe.rs +++ b/editor/src/observe.rs @@ -230,6 +230,7 @@ pub fn bind(iii: &Arc, cfg: &ConfigCell, bus: &Arc, emitter: Cha "on_error": HOOK_ON_ERROR, }), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => tracing::info!(function_id = HOOK_FN_ID, "file-change observer bound"), Err(e) => tracing::warn!( diff --git a/email/Cargo.lock b/email/Cargo.lock index 1c85cf138..278700a76 100644 --- a/email/Cargo.lock +++ b/email/Cargo.lock @@ -1204,9 +1204,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -1225,9 +1225,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/email/Cargo.toml b/email/Cargo.toml index 0f7e56581..5b8164f30 100644 --- a/email/Cargo.toml +++ b/email/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time", "io-util", "net"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/email/src/handlers/send.rs b/email/src/handlers/send.rs index 426a8e2dd..e0ec392c2 100644 --- a/email/src/handlers/send.rs +++ b/email/src/handlers/send.rs @@ -94,14 +94,16 @@ pub fn register(iii: &Arc, cfg: &Arc) { } } - let cred = iii - .trigger(TriggerRequest { - function_id: "auth::get_token".to_string(), - payload: json!({ "provider": format!("email::{}", req.account) }), - action: None, - timeout_ms: Some(5_000), - }) - .await + let request = TriggerRequest { + function_id: "auth::get_token".to_string(), + payload: json!({ "provider": format!("email::{}", req.account) }), + action: None, + timeout_ms: Some(5_000), + }; + let cred = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } .map_err(|e| { Error::Handler( json!({ diff --git a/email/src/provider/imap/connection.rs b/email/src/provider/imap/connection.rs index 22be9b948..f5b385672 100644 --- a/email/src/provider/imap/connection.rs +++ b/email/src/provider/imap/connection.rs @@ -192,20 +192,22 @@ async fn fetch_credential(account: &str) -> Result { "internal: iii handle not initialized for credential fetch", ) })?; - let cred = iii - .trigger(TriggerRequest { - function_id: "auth::get_token".to_string(), - payload: json!({ "provider": format!("email::{}", account) }), - action: None, - timeout_ms: Some(5_000), - }) - .await - .map_err(|e| { - handler_err( - "E606", - &format!("auth::get_token failed for `email::{account}`: {e}"), - ) - })?; + let request = TriggerRequest { + function_id: "auth::get_token".to_string(), + payload: json!({ "provider": format!("email::{}", account) }), + action: None, + timeout_ms: Some(5_000), + }; + let cred = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } + .map_err(|e| { + handler_err( + "E606", + &format!("auth::get_token failed for `email::{account}`: {e}"), + ) + })?; if cred.is_null() { return Err(handler_err( "E607", diff --git a/email/src/triggers/dispatcher.rs b/email/src/triggers/dispatcher.rs index 00d4d4edb..5923d7bed 100644 --- a/email/src/triggers/dispatcher.rs +++ b/email/src/triggers/dispatcher.rs @@ -53,7 +53,31 @@ impl EventDispatcher for EngineDispatcher { timeout_ms: Some(sub.handler_timeout_ms), }; let bound = Duration::from_millis(sub.handler_timeout_ms.saturating_add(1_000)); - match timeout(bound, self.iii.trigger(req)).await { + let route_ns = self.iii.namespace().filter(|_| { + !matches!( + sub.function_id.split("::").next(), + Some( + "state" + | "stream" + | "queue" + | "pubsub" + | "configuration" + | "cron" + | "http" + | "engine" + | "sandbox" + | "log" + | "secret" + | "kv" + | "iii" + ) + ) + }); + let result = match route_ns { + Some(ns) => timeout(bound, self.iii.trigger(req.namespace(ns))).await, + None => timeout(bound, self.iii.trigger(req)).await, + }; + match result { Ok(Ok(_)) => {} Ok(Err(e)) => { tracing::warn!( diff --git a/eval/Cargo.lock b/eval/Cargo.lock index 4be2ec8f8..d47b0457b 100644 --- a/eval/Cargo.lock +++ b/eval/Cargo.lock @@ -532,7 +532,7 @@ dependencies = [ [[package]] name = "harness" -version = "1.6.7" +version = "1.7.4" dependencies = [ "anyhow", "async-trait", @@ -791,9 +791,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.8" +version = "0.22.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bdc7bbc3abfde934a62cdc5d3045adf52914dfc1ed6c20f8af691fc561dc55" +checksum = "0a5559b662390e53fa7fcac83451ae1667306e94c44a97f6c617f1e3023f1dfd" dependencies = [ "futures-util", "opentelemetry", @@ -812,9 +812,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.8" +version = "0.22.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd563a1d2f55f893d9a433b747f0bf9bc426656413b136b6ed3a699f3c757b2" +checksum = "743c738975e139d6c5ee0e0635699308e9f85969da11003bc123712f45b48def" dependencies = [ "async-trait", "futures-util", diff --git a/eval/Cargo.toml b/eval/Cargo.toml index 388ecf0fb..f8414841b 100644 --- a/eval/Cargo.toml +++ b/eval/Cargo.toml @@ -16,8 +16,8 @@ path = "src/lib.rs" [dependencies] harness = { path = "../harness" } -iii-sdk = "=0.21.8" -iii-helpers = "=0.21.8" +iii-sdk = "=0.22.0-alpha.2" +iii-helpers = "=0.22.0-alpha.2" iii-console-ui = { path = "../crates/console-ui" } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } diff --git a/eval/src/main.rs b/eval/src/main.rs index e16597e6c..c4b85e263 100644 --- a/eval/src/main.rs +++ b/eval/src/main.rs @@ -111,6 +111,7 @@ fn bind_triggers(iii: &Arc) -> Vec { function_id: function_id.into(), config, metadata: None, + namespace: iii.namespace(), }) { Ok(handle) => handles.push(handle), Err(error) => tracing::warn!( diff --git a/fp/Cargo.lock b/fp/Cargo.lock index e7e6f7b16..f4ef66405 100644 --- a/fp/Cargo.lock +++ b/fp/Cargo.lock @@ -645,9 +645,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -666,9 +666,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/fp/Cargo.toml b/fp/Cargo.toml index e50178275..9da3e1c93 100644 --- a/fp/Cargo.toml +++ b/fp/Cargo.toml @@ -17,7 +17,7 @@ name = "fp" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" # Must stay on the same schemars major as iii-sdk so the derived schemas line up. schemars = "0.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal"] } diff --git a/fp/src/guidance.rs b/fp/src/guidance.rs index 4d6a10976..091b1b58b 100644 --- a/fp/src/guidance.rs +++ b/fp/src/guidance.rs @@ -170,6 +170,7 @@ fn bind(iii: &IIIClient, trigger_type: &str, function_id: &str, config: Value) - function_id: function_id.to_string(), config, metadata: None, + namespace: iii.namespace(), }) { Ok(handle) => { tracing::info!(trigger_type, function_id, "trigger binding requested"); diff --git a/github/Cargo.lock b/github/Cargo.lock index 73c8535ae..4dfe8571c 100644 --- a/github/Cargo.lock +++ b/github/Cargo.lock @@ -680,9 +680,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -701,9 +701,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/github/Cargo.toml b/github/Cargo.toml index ccc22797d..0dbdd3a9b 100644 --- a/github/Cargo.toml +++ b/github/Cargo.toml @@ -17,7 +17,7 @@ name = "github" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" iii-console-ui = { path = "../crates/console-ui" } # Must stay on the same schemars major as iii-sdk so the derived schemas line up. schemars = "0.8" diff --git a/github/src/configuration.rs b/github/src/configuration.rs index ab1f1a379..8f5cb76c3 100644 --- a/github/src/configuration.rs +++ b/github/src/configuration.rs @@ -20,7 +20,25 @@ use crate::config::Config; /// the inner `Arc` under the write lock. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "github"; +pub const DEFAULT_CONFIG_ID: &str = "github"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "github::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -30,7 +48,7 @@ const CONFIG_RETRIES: u32 = 3; /// only when no stored value exists yet. pub async fn register_config(iii: &IIIClient, seed: Option<&Config>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "GitHub", "description": "GitHub worker: the gh binary path, the GH_TOKEN credential injected into child gh processes, and the timeout / output-capture limits applied to every gh invocation.", "schema": Config::json_schema(), @@ -64,7 +82,7 @@ async fn should_seed_default(iii: &IIIClient) -> Result { /// `Ok(None)` when the entry does not exist (`NOT_FOUND`). async fn try_get_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -101,10 +119,11 @@ pub fn register_config_trigger(iii: &IIIClient, cell: ConfigCell) -> Result<(), trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/grok/Cargo.lock b/grok/Cargo.lock index c0bcbe627..3bf074467 100644 --- a/grok/Cargo.lock +++ b/grok/Cargo.lock @@ -671,9 +671,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -692,9 +692,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/grok/Cargo.toml b/grok/Cargo.toml index 685d2dff4..88ce3b2f1 100644 --- a/grok/Cargo.toml +++ b/grok/Cargo.toml @@ -15,8 +15,8 @@ name = "grok" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" -iii-helpers = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" +iii-helpers = "=0.22.0-alpha.3" schemars = { version = "0.8", features = ["uuid1"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "process", "time", "io-util"] } serde = { version = "1", features = ["derive"] } diff --git a/grok/src/configuration.rs b/grok/src/configuration.rs index c8e13d7c7..e280d235e 100644 --- a/grok/src/configuration.rs +++ b/grok/src/configuration.rs @@ -21,7 +21,25 @@ use crate::config::Config; /// the inner `Arc` under the write lock. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "grok"; +pub const DEFAULT_CONFIG_ID: &str = "grok"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "grok::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -31,7 +49,7 @@ const CONFIG_RETRIES: u32 = 3; /// only when no stored value exists yet. pub async fn register_config(iii: &IIIClient, seed: Option<&Config>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Grok", "description": "xAI Grok CLI worker: per-turn defaults (model, working directory, auto-approve), the agent::events / grok::events stream names, the grok CLI path, and whether to inject the iii runtime context into the prompt.", "schema": Config::json_schema(), @@ -65,7 +83,7 @@ async fn should_seed_default(iii: &IIIClient) -> Result { /// `Ok(None)` when the entry does not exist (`NOT_FOUND`). async fn try_get_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -102,10 +120,11 @@ pub fn register_config_trigger(iii: &IIIClient, cell: ConfigCell) -> Result<(), trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/harness/Cargo.lock b/harness/Cargo.lock index dca943141..cbf984688 100644 --- a/harness/Cargo.lock +++ b/harness/Cargo.lock @@ -509,8 +509,8 @@ dependencies = [ "clap", "globset", "iii-console-ui", - "iii-helpers", - "iii-sdk", + "iii-helpers 0.22.0-alpha.2", + "iii-sdk 0.22.0-alpha.2", "jsonschema", "schemars", "serde", @@ -531,7 +531,7 @@ dependencies = [ "anyhow", "clap", "harness", - "iii-sdk", + "iii-sdk 0.21.8", "schemars", "serde", "serde_json", @@ -547,7 +547,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "iii-sdk", + "iii-sdk 0.21.8", "jsonschema", "nix", "regex", @@ -808,7 +808,7 @@ dependencies = [ name = "iii-console-ui" version = "0.1.0" dependencies = [ - "iii-sdk", + "iii-sdk 0.22.0-alpha.2", "schemars", "serde", "serde_json", @@ -837,6 +837,27 @@ dependencies = [ "uuid", ] +[[package]] +name = "iii-helpers" +version = "0.22.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5559b662390e53fa7fcac83451ae1667306e94c44a97f6c617f1e3023f1dfd" +dependencies = [ + "futures-util", + "opentelemetry", + "opentelemetry-http", + "opentelemetry_sdk", + "reqwest", + "schemars", + "serde", + "serde_json", + "sysinfo", + "tokio", + "tokio-tungstenite", + "tracing", + "uuid", +] + [[package]] name = "iii-sdk" version = "0.21.8" @@ -846,7 +867,28 @@ dependencies = [ "async-trait", "futures-util", "hostname", - "iii-helpers", + "iii-helpers 0.21.8", + "reqwest", + "schemars", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-tungstenite", + "tracing", + "uuid", +] + +[[package]] +name = "iii-sdk" +version = "0.22.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743c738975e139d6c5ee0e0635699308e9f85969da11003bc123712f45b48def" +dependencies = [ + "async-trait", + "futures-util", + "hostname", + "iii-helpers 0.22.0-alpha.2", "reqwest", "schemars", "serde", diff --git a/harness/Cargo.toml b/harness/Cargo.toml index 963ae34f3..bf23cc090 100644 --- a/harness/Cargo.toml +++ b/harness/Cargo.toml @@ -19,12 +19,12 @@ path = "src/lib.rs" [dependencies] # 0.21.8 adds the reconnect reattach handshake. Older SDKs can lose this # worker's registrations when the engine reloads another worker. -iii-sdk = "=0.21.8" +iii-sdk = "=0.22.0-alpha.2" # Re-exports the same `opentelemetry` the SDK uses, so we can carry the active # OTel context across `tokio::spawn` boundaries (shared `Context` global). # 0.21.5 adds the live span-start push (`LiveSpanStartProcessor`), so the # console renders `harness::turn step` while it runs instead of on close. -iii-helpers = "=0.21.8" +iii-helpers = "=0.22.0-alpha.2" # Worker-side injectable console UI (content function + console:script/style # triggers + hot-reload watcher) — direct link, never published. iii-console-ui = { path = "../crates/console-ui" } diff --git a/harness/src/clients/context.rs b/harness/src/clients/context.rs index f4a8e00cd..c12db6933 100644 --- a/harness/src/clients/context.rs +++ b/harness/src/clients/context.rs @@ -132,15 +132,16 @@ impl ContextClient { payload["system_prompt"] = json!(sp); } - let resp = self - .iii - .trigger(TriggerRequest { - function_id: "context::assemble".into(), - payload, - action: None, - timeout_ms: Some(self.timeout_ms), - }) - .await; + let request = TriggerRequest { + function_id: "context::assemble".into(), + payload, + action: None, + timeout_ms: Some(self.timeout_ms), + }; + let resp = match self.iii.namespace() { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + }; match resp { Ok(v) => serde_json::from_value::(v) @@ -167,15 +168,16 @@ impl ContextClient { payload["system_prompt"] = json!(sp); } - let resp = self - .iii - .trigger(TriggerRequest { - function_id: "context::count-tokens".into(), - payload, - action: None, - timeout_ms: Some(self.timeout_ms), - }) - .await; + let request = TriggerRequest { + function_id: "context::count-tokens".into(), + payload, + action: None, + timeout_ms: Some(self.timeout_ms), + }; + let resp = match self.iii.namespace() { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + }; match resp { Ok(v) => serde_json::from_value::(v) diff --git a/harness/src/clients/engine.rs b/harness/src/clients/engine.rs index 095c85014..afa9b1dd4 100644 --- a/harness/src/clients/engine.rs +++ b/harness/src/clients/engine.rs @@ -68,12 +68,18 @@ impl EngineClient { // stale when the engine restarts while this worker is idle and would // falsely interrupt the first slow call made afterwards. let baseline = engine_epoch_ms(&self.iii).await; - let call = self.iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: function_id.to_string(), payload, action: None, timeout_ms: Some(self.timeout_ms), - }); + }; + let request: iii_sdk::protocol::TriggerRequestWithMetadata = + match crate::trigger::route_namespace(&self.iii, function_id) { + Some(namespace) => request.namespace(namespace), + None => request.into(), + }; + let call = self.iii.trigger(request); let result = match baseline { Some(baseline) => { tokio::select! { diff --git a/harness/src/clients/router.rs b/harness/src/clients/router.rs index 8f311c1df..7b59899cb 100644 --- a/harness/src/clients/router.rs +++ b/harness/src/clients/router.rs @@ -169,13 +169,16 @@ impl RouterClient { let parent_cx = iii_helpers::observability::opentelemetry::Context::current(); let mut trigger = tokio::spawn( async move { - iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: "router::chat".into(), payload, action: None, timeout_ms: Some(timeout_ms), - }) - .await + }; + match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } } .with_context(parent_cx), ); @@ -415,15 +418,16 @@ impl RouterClient { /// Abort an in-flight stream by `request_id` (best-effort). pub async fn abort(&self, request_id: &str) -> bool { - let res = self - .iii - .trigger(TriggerRequest { - function_id: "router::abort".into(), - payload: json!({ "request_id": request_id }), - action: None, - timeout_ms: Some(self.timeout_ms), - }) - .await; + let request = TriggerRequest { + function_id: "router::abort".into(), + payload: json!({ "request_id": request_id }), + action: None, + timeout_ms: Some(self.timeout_ms), + }; + let res = match self.iii.namespace() { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + }; match res { Ok(v) => v.get("aborted").and_then(Value::as_bool).unwrap_or(false), Err(e) => { @@ -442,16 +446,17 @@ impl RouterClient { if let Some(p) = provider { payload["provider"] = json!(p); } - let resp = self - .iii - .trigger(TriggerRequest { - function_id: "router::system_prompt::get".into(), - payload, - action: None, - timeout_ms: Some(self.timeout_ms), - }) - .await - .ok()?; + let request = TriggerRequest { + function_id: "router::system_prompt::get".into(), + payload, + action: None, + timeout_ms: Some(self.timeout_ms), + }; + let resp = match self.iii.namespace() { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + } + .ok()?; resp.get("system_prompt") .and_then(Value::as_str) .filter(|s| !s.is_empty()) @@ -507,16 +512,17 @@ impl RouterClient { if let Some(p) = provider { payload["provider"] = json!(p); } - let resp = self - .iii - .trigger(TriggerRequest { - function_id: "router::models::get".into(), - payload, - action: None, - timeout_ms: Some(self.timeout_ms), - }) - .await - .ok()?; + let request = TriggerRequest { + function_id: "router::models::get".into(), + payload, + action: None, + timeout_ms: Some(self.timeout_ms), + }; + let resp = match self.iii.namespace() { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + } + .ok()?; if resp.is_null() { return None; } @@ -530,15 +536,16 @@ impl RouterClient { /// Whether `model` supports a capability (false when the router is absent /// or the model is unknown — the caller falls back). pub async fn models_supports(&self, provider: &str, id: &str, capability: &str) -> bool { - let resp = self - .iii - .trigger(TriggerRequest { - function_id: "router::models::supports".into(), - payload: json!({ "provider": provider, "id": id, "capability": capability }), - action: None, - timeout_ms: Some(self.timeout_ms), - }) - .await; + let request = TriggerRequest { + function_id: "router::models::supports".into(), + payload: json!({ "provider": provider, "id": id, "capability": capability }), + action: None, + timeout_ms: Some(self.timeout_ms), + }; + let resp = match self.iii.namespace() { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + }; match resp { Ok(v) => v.get("supported").and_then(Value::as_bool).unwrap_or(false), Err(_) => false, diff --git a/harness/src/clients/session.rs b/harness/src/clients/session.rs index d160262b6..67cf935fb 100644 --- a/harness/src/clients/session.rs +++ b/harness/src/clients/session.rs @@ -70,15 +70,17 @@ impl SessionClient { } async fn call(&self, function_id: &str, payload: Value) -> Result { - self.iii - .trigger(TriggerRequest { - function_id: function_id.to_string(), - payload, - action: None, - timeout_ms: Some(self.timeout_ms), - }) - .await - .map_err(|e| HarnessError::Dependency(format!("{function_id}: {e}"))) + let request = TriggerRequest { + function_id: function_id.to_string(), + payload, + action: None, + timeout_ms: Some(self.timeout_ms), + }; + let res = match self.iii.namespace() { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + }; + res.map_err(|e| HarnessError::Dependency(format!("{function_id}: {e}"))) } /// Best-effort session title from `session::get` — `None` when the diff --git a/harness/src/conditions.rs b/harness/src/conditions.rs index 2046661df..4c28e268f 100644 --- a/harness/src/conditions.rs +++ b/harness/src/conditions.rs @@ -124,15 +124,18 @@ pub async fn evaluate(deps: &Deps, binding: &Binding, event: Value) -> Result request.namespace(namespace), + None => request.into(), + }; + let raw = deps.iii.trigger(request).await; let decision = match raw { Ok(v) => parse_decision(&v), diff --git a/harness/src/configuration.rs b/harness/src/configuration.rs index 198212234..23868f2d1 100644 --- a/harness/src/configuration.rs +++ b/harness/src/configuration.rs @@ -33,7 +33,25 @@ use crate::subscriptions::ON_SESSION_DELETED_ID; /// Hot-swappable config snapshot shared with every handler. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "harness"; +pub const DEFAULT_CONFIG_ID: &str = "harness"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "harness::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -46,7 +64,7 @@ pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Re discard_legacy_harness_config_if_needed(iii).await?; let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Harness", "description": "Durable turn-loop settings: default turn cap and pending-call timeout, \ sub-agent depth/fan-out budgets, output-contract validation retries, the \ @@ -83,14 +101,17 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { } async fn get_config_value(iii: &IIIClient) -> Result { - try_get_config_value(iii) - .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + try_get_config_value(iii).await?.ok_or_else(|| { + format!( + "configuration `{config_entry}` not found", + config_entry = config_id() + ) + }) } /// Returns `Ok(None)` when the entry does not exist (codes vary in case). async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.to_ascii_uppercase().contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -145,7 +166,7 @@ async fn discard_legacy_harness_config_if_needed(iii: &IIIClient) -> Result<(), iii, "configuration::register", json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Harness", "description": "Legacy harness configuration placeholder (discarding pre-Rust entry).", "schema": { @@ -160,7 +181,7 @@ async fn discard_legacy_harness_config_if_needed(iii: &IIIClient) -> Result<(), iii, "configuration::set", json!({ - "id": CONFIG_ID, + "id": config_id(), "value": WorkerConfig::default().to_json(), }), ) @@ -198,6 +219,7 @@ fn bind(iii: &IIIClient, trigger_type: &str, function_id: &str, config: Value) - function_id: function_id.to_string(), config, metadata: None, + namespace: iii.namespace(), }) { Ok(handle) => { tracing::info!(trigger_type, function_id, "trigger binding requested"); @@ -286,10 +308,11 @@ pub fn register_config_trigger( trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/harness/src/discovery.rs b/harness/src/discovery.rs index 5bd6e9d95..7159e585f 100644 --- a/harness/src/discovery.rs +++ b/harness/src/discovery.rs @@ -270,6 +270,7 @@ pub fn register_functions_trigger(iii: &Arc, cell: FunctionsCell, tim function_id: FUNCTIONS_FN_ID.to_string(), config: json!({}), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => tracing::info!( trigger_type = FUNCTIONS_TRIGGER_TYPE, diff --git a/harness/src/events.rs b/harness/src/events.rs index f90e53b03..503eb57a6 100644 --- a/harness/src/events.rs +++ b/harness/src/events.rs @@ -435,9 +435,15 @@ impl TurnEvents { action: Some(TriggerAction::Void), timeout_ms: None, }; - let res = match binding.metadata.clone() { - Some(m) => self.iii.trigger(request.metadata(m)).await, - None => self.iii.trigger(request).await, + // A dynamic target may be an engine builtin (a hook wired to + // `state::set`) or a sibling worker's function — route to the + // worker's namespace only when it is NOT a builtin. + let route_ns = crate::trigger::route_namespace(&self.iii, &binding.function_id); + let res = match (binding.metadata.clone(), route_ns) { + (Some(m), Some(ns)) => self.iii.trigger(request.metadata(m).namespace(ns)).await, + (Some(m), None) => self.iii.trigger(request.metadata(m)).await, + (None, Some(ns)) => self.iii.trigger(request.namespace(ns)).await, + (None, None) => self.iii.trigger(request).await, }; if let Err(e) = res { tracing::warn!(trigger_type, function_id = %binding.function_id, error = %e, "turn-event fan-out failed"); @@ -456,9 +462,15 @@ async fn deliver_ready(iii: &IIIClient, binding: &Binding) { action: Some(TriggerAction::Void), timeout_ms: None, }; - let result = match &binding.metadata { - Some(metadata) => iii.trigger(request.metadata(metadata.clone())).await, - None => iii.trigger(request).await, + let route_ns = crate::trigger::route_namespace(iii, &binding.function_id); + let result = match (&binding.metadata, route_ns) { + (Some(metadata), Some(namespace)) => { + iii.trigger(request.metadata(metadata.clone()).namespace(namespace)) + .await + } + (Some(metadata), None) => iii.trigger(request.metadata(metadata.clone())).await, + (None, Some(namespace)) => iii.trigger(request.namespace(namespace)).await, + (None, None) => iii.trigger(request).await, }; if let Err(error) = result { tracing::warn!( diff --git a/harness/src/functions/subscribe.rs b/harness/src/functions/subscribe.rs index b68b32c7c..0d2c6b134 100644 --- a/harness/src/functions/subscribe.rs +++ b/harness/src/functions/subscribe.rs @@ -951,6 +951,7 @@ async fn register_post_turn_hook( function_id: function_id.clone(), config, metadata: None, + namespace: deps.iii.namespace(), }) .map_err(|e| { HarnessError::InvalidRequest(format!("post-turn hook registration failed: {e}")) diff --git a/harness/src/functions/trigger_deliver.rs b/harness/src/functions/trigger_deliver.rs index 78baafbf2..2cd8ba09a 100644 --- a/harness/src/functions/trigger_deliver.rs +++ b/harness/src/functions/trigger_deliver.rs @@ -470,14 +470,20 @@ async fn call_target( // happen?" becomes unanswerable from the timeline. The dispatch timeout // bounds the wait. let timeout_ms = deps.cfg().await.dispatch_timeout_ms; + let request = TriggerRequest { + function_id: target.to_string(), + payload: payload.clone(), + action: None, + timeout_ms: Some(timeout_ms), + }; + let request: iii_sdk::protocol::TriggerRequestWithMetadata = + match crate::trigger::route_namespace(&deps.iii, target) { + Some(namespace) => request.namespace(namespace), + None => request.into(), + }; let outcome = deps .iii - .trigger(TriggerRequest { - function_id: target.to_string(), - payload: payload.clone(), - action: None, - timeout_ms: Some(timeout_ms), - }) + .trigger(request) .await .map(|_| ()) .map_err(|e| e.to_string()); diff --git a/harness/src/hooks/mod.rs b/harness/src/hooks/mod.rs index 674c18297..c391b371e 100644 --- a/harness/src/hooks/mod.rs +++ b/harness/src/hooks/mod.rs @@ -372,6 +372,7 @@ mod tests { function_id: function_id.into(), config: body, metadata: None, + namespace: None, } } diff --git a/harness/src/hooks/runner.rs b/harness/src/hooks/runner.rs index a1a795010..31a6f87fb 100644 --- a/harness/src/hooks/runner.rs +++ b/harness/src/hooks/runner.rs @@ -351,15 +351,20 @@ impl HookRegistry { payload: Value, ) -> Result { let baggage = [("iii.tag.kind", "harness.hook"), ("iii.hook.point", point)]; - let fut = iii_helpers::observability::run_with_baggage( - &baggage, - self.iii.trigger(TriggerRequest { - function_id: binding.function_id.clone(), - payload, - action: None, - timeout_ms: Some(binding.timeout_ms), - }), - ); + let request = TriggerRequest { + function_id: binding.function_id.clone(), + payload, + action: None, + timeout_ms: Some(binding.timeout_ms), + }; + // A hook target is dynamic: engine builtins stay in the default + // namespace; sibling-worker functions follow this worker's namespace. + let request: iii_sdk::protocol::TriggerRequestWithMetadata = + match crate::trigger::route_namespace(&self.iii, &binding.function_id) { + Some(namespace) => request.namespace(namespace), + None => request.into(), + }; + let fut = iii_helpers::observability::run_with_baggage(&baggage, self.iii.trigger(request)); let bounded = tokio::time::timeout( Duration::from_millis(binding.timeout_ms.saturating_add(1_000)), fut, diff --git a/harness/src/trigger.rs b/harness/src/trigger.rs index 527cf141a..d1e252476 100644 --- a/harness/src/trigger.rs +++ b/harness/src/trigger.rs @@ -7,6 +7,7 @@ //! P1 covers the glob policy + target + normalisation. The `pre_trigger` / //! `post_trigger` hook chain and `pending` deferral layer on in later phases. +use iii_sdk::IIIClient; use serde_json::{json, Value}; use crate::clients::EngineClient; @@ -21,6 +22,35 @@ pub struct ResultData { pub details: Value, } +/// Route worker-to-worker calls through this worker's namespace while keeping +/// engine builtins in `default`. +pub(crate) fn route_namespace(iii: &IIIClient, function_id: &str) -> Option { + (!is_engine_builtin(function_id)) + .then(|| iii.namespace()) + .flatten() +} + +fn is_engine_builtin(function_id: &str) -> bool { + matches!( + function_id.split("::").next(), + Some( + "state" + | "stream" + | "queue" + | "pubsub" + | "configuration" + | "cron" + | "http" + | "engine" + | "sandbox" + | "log" + | "secret" + | "kv" + | "iii" + ) + ) +} + /// The outcome of triggering one call. pub enum TriggerResult { /// A settled result (success, policy denial, or target error). @@ -272,6 +302,21 @@ mod tests { })) } + #[test] + fn namespace_routing_keeps_builtins_in_default() { + for function_id in [ + "state::get", + "engine::functions::list", + "queue::push", + "configuration::get", + ] { + assert!(is_engine_builtin(function_id), "{function_id}"); + } + for function_id in ["router::chat", "session::get", "approval::evaluate"] { + assert!(!is_engine_builtin(function_id), "{function_id}"); + } + } + #[test] fn discovery_reports_the_intercepted_registration_contract() { // The raw engine row for register_trigger would claim `function_id` diff --git a/harness/src/turn_loop.rs b/harness/src/turn_loop.rs index 123ec9fe6..041d2fa2c 100644 --- a/harness/src/turn_loop.rs +++ b/harness/src/turn_loop.rs @@ -142,17 +142,19 @@ pub async fn enqueue_step( } let mut last_error = String::new(); for attempt in 1..=ENQUEUE_ATTEMPTS { - match iii - .trigger(TriggerRequest { - function_id: "harness::turn".to_string(), - payload: payload.clone(), - action: Some(TriggerAction::Enqueue { - queue: TURN_QUEUE.to_string(), - }), - timeout_ms: None, - }) - .await - { + let request = TriggerRequest { + function_id: "harness::turn".to_string(), + payload: payload.clone(), + action: Some(TriggerAction::Enqueue { + queue: TURN_QUEUE.to_string(), + }), + timeout_ms: None, + }; + let res = match iii.namespace() { + Some(namespace) => iii.trigger(request.namespace(namespace)).await, + None => iii.trigger(request).await, + }; + match res { Ok(_) => return Ok(()), Err(e) => { last_error = e.to_string(); diff --git a/hermes/pyproject.toml b/hermes/pyproject.toml index efe921d5f..048a08e9d 100644 --- a/hermes/pyproject.toml +++ b/hermes/pyproject.toml @@ -10,8 +10,8 @@ authors = [{ name = "iii" }] license = "Apache-2.0" requires-python = ">=3.11" dependencies = [ - "iii-sdk==0.21.6", - "iii-helpers==0.21.6", + "iii-sdk==0.22.0a3", + "iii-helpers==0.22.0a3", "opentelemetry-api", "opentelemetry-sdk", "pyyaml", diff --git a/hermes/src/main.py b/hermes/src/main.py index 0799b9f54..a7be27f7b 100644 --- a/hermes/src/main.py +++ b/hermes/src/main.py @@ -129,9 +129,15 @@ def _print_connected(state: str, url: str) -> None: def main() -> None: cfg = load_config() url = os.environ.get("III_URL") or cfg["engine_url"] + # namespace: register under III_NAMESPACE when set (the SDK also resolves the + # env; passed explicitly for visibility). Absent -> the engine's default. iii = register_worker( address=url, - options=InitOptions(worker_name="hermes", otel={"enabled": True, "service_name": "hermes"}), + options=InitOptions( + worker_name="hermes", + namespace=os.environ.get("III_NAMESPACE"), + otel={"enabled": True, "service_name": "hermes"}, + ), ) logging.basicConfig(level=logging.INFO) logger = logging.getLogger("hermes") diff --git a/http/Cargo.lock b/http/Cargo.lock index 3592fe84b..4c16795d7 100644 --- a/http/Cargo.lock +++ b/http/Cargo.lock @@ -795,9 +795,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -847,9 +847,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/http/Cargo.toml b/http/Cargo.toml index 2ef6722ba..6a3353460 100644 --- a/http/Cargo.toml +++ b/http/Cargo.toml @@ -18,12 +18,12 @@ name = "http" path = "src/main.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" # Direct dep for W3C trace-context propagation helpers # (`observability::extract_context` + the re-exported `opentelemetry` crate). # Pinned to the version `iii-sdk` already resolves so the `opentelemetry` types # line up across both crates. -iii-helpers = "=0.21.6" +iii-helpers = "=0.22.0-alpha.3" # W3C trace-context parent propagation on `tracing::Span` (`OpenTelemetrySpanExt`). # 0.32 pairs with opentelemetry 0.31 (same as the engine). tracing-opentelemetry = "0.32" diff --git a/http/src/configuration.rs b/http/src/configuration.rs index 0250d6fff..d6d50a0df 100644 --- a/http/src/configuration.rs +++ b/http/src/configuration.rs @@ -60,7 +60,25 @@ pub type ConfigCell = Arc>>; /// (`engine/src/workers/rest_api/api_core.rs`). pub type ApplyLock = Arc>; -pub const CONFIG_ID: &str = "http"; +pub const DEFAULT_CONFIG_ID: &str = "http"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "http::on-config-change"; const CONFIG_RETRIES: u32 = 3; const CONFIG_RETRY_BACKOFF_MS: u64 = 250; @@ -78,7 +96,7 @@ pub fn new_cell(config: RestApiConfig) -> ConfigCell { /// only when nothing is stored yet, so runtime edits survive restarts. pub async fn register_config(iii: &IIIClient, seed: Option<&RestApiConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "HTTP", "description": "HTTP server settings — host/port binding, CORS, request timeout, concurrency limit, and global middleware.", "schema": RestApiConfig::json_schema(), @@ -104,7 +122,7 @@ pub async fn fetch_config(iii: &IIIClient) -> Result { match try_get_config_value(iii).await? { Some(value) if !value.is_null() => RestApiConfig::from_json(&value), _ => { - tracing::info!("no `{CONFIG_ID}` configuration value stored; using built-in default"); + tracing::info!("no `{config_entry}` configuration value stored; using built-in default", config_entry = config_id()); Ok(RestApiConfig::default()) } } @@ -121,7 +139,7 @@ async fn try_get_config_value(iii: &IIIClient) -> Result, String> match trigger_with_retry( iii, "configuration::get", - json!({ "id": CONFIG_ID }), + json!({ "id": config_id() }), CONFIG_BUS_TIMEOUT_MS, ) .await @@ -186,10 +204,11 @@ pub fn register_config_trigger( trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/http/src/trigger.rs b/http/src/trigger.rs index e4bd15d67..8a4ffe27c 100644 --- a/http/src/trigger.rs +++ b/http/src/trigger.rs @@ -412,6 +412,7 @@ mod tests { function_id: function_id.to_string(), config, metadata: None, + namespace: None, } } diff --git a/http/tests/common/backend.rs b/http/tests/common/backend.rs index 4371ad629..05f257cfb 100644 --- a/http/tests/common/backend.rs +++ b/http/tests/common/backend.rs @@ -43,6 +43,7 @@ pub async fn register_echo_backend(iii: &Arc, api_path: &str, http_me function_id, config: json!({ "api_path": api_path, "http_method": http_method }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger"); } @@ -89,6 +90,7 @@ pub async fn register_trace_capturing_backend( function_id, config: json!({ "api_path": api_path, "http_method": http_method }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger"); @@ -121,6 +123,7 @@ pub async fn register_removable_echo_backend( function_id, config: json!({ "api_path": api_path, "http_method": http_method }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger") } @@ -163,6 +166,7 @@ pub async fn register_echo_backend_with_middleware( "middleware_function_ids": middleware_function_ids, }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger with middleware"); } @@ -250,6 +254,7 @@ pub async fn register_echo_backend_with_condition( "condition_function_id": condition_function_id, }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger with condition"); } @@ -301,6 +306,7 @@ pub async fn register_slow_backend( function_id, config: json!({ "api_path": api_path, "http_method": http_method }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger"); } @@ -333,6 +339,7 @@ pub async fn register_sleep_backend( function_id, config: json!({ "api_path": api_path, "http_method": http_method }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger"); } @@ -360,6 +367,7 @@ pub async fn register_erroring_backend(iii: &Arc, api_path: &str, htt function_id, config: json!({ "api_path": api_path, "http_method": http_method }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger"); } diff --git a/http/tests/e2e_condition.rs b/http/tests/e2e_condition.rs index 5dce52ce4..24ffaea61 100644 --- a/http/tests/e2e_condition.rs +++ b/http/tests/e2e_condition.rs @@ -58,6 +58,7 @@ async fn register_counting_backend_with_condition( "condition_function_id": condition_function_id, }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger with condition"); } diff --git a/http/tests/e2e_reload.rs b/http/tests/e2e_reload.rs index 7995b1e0a..91ef48376 100644 --- a/http/tests/e2e_reload.rs +++ b/http/tests/e2e_reload.rs @@ -86,7 +86,7 @@ async fn global_middleware_added_via_config_set_blocks_without_restart() { }); iii.trigger(TriggerRequest { function_id: "configuration::set".to_string(), - payload: json!({ "id": configuration::CONFIG_ID, "value": new_value }), + payload: json!({ "id": configuration::config_id(), "value": new_value }), action: None, timeout_ms: Some(10_000), }) @@ -166,7 +166,7 @@ async fn cors_restricted_via_config_set_applies_without_restart() { }); iii.trigger(TriggerRequest { function_id: "configuration::set".to_string(), - payload: json!({ "id": configuration::CONFIG_ID, "value": new_value }), + payload: json!({ "id": configuration::config_id(), "value": new_value }), action: None, timeout_ms: Some(10_000), }) @@ -319,7 +319,7 @@ async fn host_port_change_rebinds_listener_without_restart() { let new_value = json!({ "host": "127.0.0.1", "port": p2 }); iii.trigger(TriggerRequest { function_id: "configuration::set".to_string(), - payload: json!({ "id": configuration::CONFIG_ID, "value": new_value }), + payload: json!({ "id": configuration::config_id(), "value": new_value }), action: None, timeout_ms: Some(10_000), }) diff --git a/http/tests/e2e_request_body.rs b/http/tests/e2e_request_body.rs index 314b8755b..93cb08427 100644 --- a/http/tests/e2e_request_body.rs +++ b/http/tests/e2e_request_body.rs @@ -80,6 +80,7 @@ async fn register_request_body_echo_backend( function_id, config: json!({ "api_path": api_path, "http_method": http_method }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger for request-body echo backend"); } diff --git a/http/tests/e2e_streaming.rs b/http/tests/e2e_streaming.rs index b56e00457..3f227f5e5 100644 --- a/http/tests/e2e_streaming.rs +++ b/http/tests/e2e_streaming.rs @@ -50,6 +50,7 @@ async fn register_stream_backend(iii: &Arc, api_path: &str, http_meth function_id, config: json!({ "api_path": api_path, "http_method": http_method }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger for stream backend"); } @@ -94,6 +95,7 @@ async fn register_late_stream_backend(iii: &Arc, api_path: &str, http function_id, config: json!({ "api_path": api_path, "http_method": http_method }), metadata: None, + namespace: iii.namespace(), }) .expect("register http trigger for late stream backend"); } diff --git a/iii-directory/Cargo.lock b/iii-directory/Cargo.lock index 022375f00..4f1a0f221 100644 --- a/iii-directory/Cargo.lock +++ b/iii-directory/Cargo.lock @@ -1090,9 +1090,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -1111,9 +1111,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "743c738975e139d6c5ee0e0635699308e9f85969da11003bc123712f45b48def" dependencies = [ "async-trait", "futures-util", diff --git a/iii-directory/Cargo.toml b/iii-directory/Cargo.toml index 0f5580e53..ee7b51755 100644 --- a/iii-directory/Cargo.toml +++ b/iii-directory/Cargo.toml @@ -15,7 +15,7 @@ name = "iii_directory" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.2" arc-swap = "1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time", "fs", "process"] } serde = { version = "1", features = ["derive"] } diff --git a/iii-directory/src/configuration.rs b/iii-directory/src/configuration.rs index 302947284..9d0f762f6 100644 --- a/iii-directory/src/configuration.rs +++ b/iii-directory/src/configuration.rs @@ -25,7 +25,25 @@ use crate::config::{SharedConfig, SkillsConfig, Topology}; use crate::functions::registry::RegistryCache; use crate::functions::skills::RegisteredWorkersCache; -pub const CONFIG_ID: &str = "iii-directory"; +pub const DEFAULT_CONFIG_ID: &str = "iii-directory"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "directory::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -71,7 +89,7 @@ impl SharedState { /// Otherwise, built-in defaults are seeded only when no stored value exists. pub async fn register_config(iii: &IIIClient, seed: Option<&SkillsConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "iii-directory", "description": "Skills/prompts folders, workers-registry URL, download timeouts, \ and skill-visibility filters for the iii-directory worker.", @@ -108,12 +126,12 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { async fn get_config_value(iii: &IIIClient) -> Result { try_get_config_value(iii) .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + .ok_or_else(|| format!("configuration `{config_entry}` not found", config_entry = config_id())) } /// Returns `Ok(None)` when the entry does not exist (`NOT_FOUND`). async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -169,10 +187,11 @@ pub fn register_config_trigger(iii: &IIIClient, state: SharedState) -> Result<() trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/iii-directory/src/functions/skills.rs b/iii-directory/src/functions/skills.rs index 3f7e8bcff..8ea8fa794 100644 --- a/iii-directory/src/functions/skills.rs +++ b/iii-directory/src/functions/skills.rs @@ -300,14 +300,16 @@ impl RegisteredWorkersCache { /// Fetch `worker::list` from the engine WITHOUT holding the lock, /// then store the result (or fall back to the stale set on error). async fn fetch_and_store(&self, iii: &IIIClient) -> Option> { - let result = iii - .trigger(TriggerRequest { - function_id: "worker::list".to_string(), - payload: json!({}), - action: None, - timeout_ms: Some(5_000), - }) - .await; + let request = TriggerRequest { + function_id: "worker::list".to_string(), + payload: json!({}), + action: None, + timeout_ms: Some(5_000), + }; + let result = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + }; // Re-acquire the lock and store or fall back. let mut lock = self.inner.lock().await; diff --git a/iii-directory/src/main.rs b/iii-directory/src/main.rs index 53885a513..39c7b58e1 100644 --- a/iii-directory/src/main.rs +++ b/iii-directory/src/main.rs @@ -256,6 +256,7 @@ fn setup_auto_download( "stages": ["done"] }), metadata: None, + namespace: iii_sub.namespace(), }); match result { Ok(_) => { @@ -356,14 +357,16 @@ async fn reconcile_one( async fn fetch_worker_list_with_retry(iii: &IIIClient) -> Option> { const MAX_ATTEMPTS: u32 = 6; for attempt in 1..=MAX_ATTEMPTS { - let result = iii - .trigger(TriggerRequest { - function_id: "worker::list".to_string(), - payload: json!({}), - action: None, - timeout_ms: Some(10_000), - }) - .await; + let request = TriggerRequest { + function_id: "worker::list".to_string(), + payload: json!({}), + action: None, + timeout_ms: Some(10_000), + }; + let result = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + }; match result { Ok(val) => { diff --git a/iii-directory/src/trigger_types.rs b/iii-directory/src/trigger_types.rs index ee9b6b437..36a40c0e5 100644 --- a/iii-directory/src/trigger_types.rs +++ b/iii-directory/src/trigger_types.rs @@ -87,14 +87,36 @@ pub async fn dispatch(iii: &IIIClient, subscribers: &SubscriberSet, payload: Val for function_id in targets { let fid = function_id.clone(); let payload_copy = payload.clone(); - let res = iii - .trigger(TriggerRequest { - function_id: fid, - payload: payload_copy, - action: Some(TriggerAction::Void), - timeout_ms: None, - }) - .await; + let request = TriggerRequest { + function_id: fid, + payload: payload_copy, + action: Some(TriggerAction::Void), + timeout_ms: None, + }; + let route_ns = iii.namespace().filter(|_| { + !matches!( + function_id.split("::").next(), + Some( + "state" + | "stream" + | "queue" + | "pubsub" + | "configuration" + | "cron" + | "http" + | "engine" + | "sandbox" + | "log" + | "secret" + | "kv" + | "iii" + ) + ) + }); + let res = match route_ns { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + }; if let Err(e) = res { tracing::warn!( function_id = %function_id, @@ -180,6 +202,7 @@ mod tests { function_id: function_id.to_string(), config: json!({}), metadata: None, + namespace: None, } } diff --git a/image-resize/Cargo.lock b/image-resize/Cargo.lock index 27add8049..5ca0a78ab 100644 --- a/image-resize/Cargo.lock +++ b/image-resize/Cargo.lock @@ -697,9 +697,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -718,9 +718,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/image-resize/Cargo.toml b/image-resize/Cargo.toml index 064699189..fe00144c4 100644 --- a/image-resize/Cargo.toml +++ b/image-resize/Cargo.toml @@ -11,7 +11,7 @@ name = "image-resize" path = "src/main.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] } kamadak-exif = "0.6" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal"] } diff --git a/image-resize/src/main.rs b/image-resize/src/main.rs index e7e9608b4..8b61affab 100644 --- a/image-resize/src/main.rs +++ b/image-resize/src/main.rs @@ -17,6 +17,11 @@ struct Cli { /// WebSocket URL of the III engine (port 49134 = engine main WS, not StreamModule 3112) #[arg(long, env = "III_URL", default_value = "ws://127.0.0.1:49134")] url: String, + + /// Namespace to register under. Absent uses the engine's default namespace. + /// Falls back to the III_NAMESPACE env var. + #[arg(long, env = "III_NAMESPACE")] + namespace: Option, } #[tokio::main] @@ -49,9 +54,15 @@ async fn main() -> Result<()> { let config = Arc::new(resize_config); - tracing::info!(url = %cli.url, "connecting to III engine"); + tracing::info!(url = %cli.url, namespace = ?cli.namespace, "connecting to III engine"); - let iii = register_worker(&cli.url, InitOptions::default()); + let iii = register_worker( + &cli.url, + InitOptions { + namespace: cli.namespace.clone(), + ..Default::default() + }, + ); let resize_handler = handler::build_handler(cli.url.clone(), config); diff --git a/llm-router/Cargo.lock b/llm-router/Cargo.lock index 0b02bbeeb..61d7ef359 100644 --- a/llm-router/Cargo.lock +++ b/llm-router/Cargo.lock @@ -892,9 +892,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -913,9 +913,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/llm-router/Cargo.toml b/llm-router/Cargo.toml index 290ac0e3e..596f1554f 100644 --- a/llm-router/Cargo.toml +++ b/llm-router/Cargo.toml @@ -18,13 +18,13 @@ path = "src/main.rs" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" iii-console-ui = { path = "../crates/console-ui", optional = true } # Re-exports the same `opentelemetry` the SDK uses, so we can carry the active # OTel context across `tokio::spawn` boundaries (shared `Context` global). # 0.21.5 adds the live span-start push, so `router::chat` renders in the # console while it streams instead of on close. -iii-helpers = "=0.21.6" +iii-helpers = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/llm-router/src/provider_scaffold/router_client.rs b/llm-router/src/provider_scaffold/router_client.rs index c098a5222..d6eb71323 100644 --- a/llm-router/src/provider_scaffold/router_client.rs +++ b/llm-router/src/provider_scaffold/router_client.rs @@ -10,13 +10,18 @@ use iii_sdk::IIIClient; use serde_json::{json, Value}; pub async fn call(iii: &IIIClient, function_id: &str, payload: Value) -> Result { - iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: function_id.into(), payload, action: None, timeout_ms: Some(15_000), - }) - .await + }; + // Route the cross-worker call to this worker's namespace so it reaches the + // router registered in the same namespace (None => the engine's default). + match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } } /// `router::provider::resolve` — credential + effective settings. diff --git a/llm-router/src/register.rs b/llm-router/src/register.rs index 2d731fb08..13261dc68 100644 --- a/llm-router/src/register.rs +++ b/llm-router/src/register.rs @@ -35,7 +35,6 @@ use crate::registry::resolve::{make_provider_resolve, make_update_credential}; use crate::registry::store::RegistryStore; use crate::surface; use crate::triggers::RouterEvents; -use crate::types::errors::invalid_request_from_serde; use crate::types::router::ConfigChangedEvent; /// `metadata.internal = true` keeps a registration out of the default @@ -93,30 +92,24 @@ pub async fn register_router(iii: IIIClient) -> Result { let (iii_for_chat, pipeline) = (iii.clone(), pipeline.clone()); iii.register_function( surface::CHAT_ID, - RegisterFunction::new_async_with_bad_request( - move |input: ChatFnInput| { - let (iii, pipeline) = (iii_for_chat.clone(), pipeline.clone()); - async move { - let sink = open_sink(&iii, &input.writer_ref).await?; - let result = pipeline.run(input.call, sink.clone()).await; - sink.close(); // the handler owns closing the caller's channel - result - } - }, - invalid_request_from_serde, - ) + RegisterFunction::new_async(move |input: ChatFnInput| { + let (iii, pipeline) = (iii_for_chat.clone(), pipeline.clone()); + async move { + let sink = open_sink(&iii, &input.writer_ref).await?; + let result = pipeline.run(input.call, sink.clone()).await; + sink.close(); // the handler owns closing the caller's channel + result + } + }) .description(surface::CHAT_DESC) .metadata(internal_meta()), ); } iii.register_function( surface::COMPLETE_ID, - RegisterFunction::new_async_with_bad_request( - make_complete(iii.clone(), pipeline.clone()), - invalid_request_from_serde, - ) - .description(surface::COMPLETE_DESC) - .metadata(internal_meta()), + RegisterFunction::new_async(make_complete(iii.clone(), pipeline.clone())) + .description(surface::COMPLETE_DESC) + .metadata(internal_meta()), ); iii.register_function( surface::ABORT_ID, @@ -192,16 +185,13 @@ pub async fn register_router(iii: IIIClient) -> Result { ); iii.register_function( surface::PROVIDER_REGISTER_ID, - RegisterFunction::new_async_with_bad_request( - make_provider_register( - iii.clone(), - registry.clone(), - catalog.clone(), - entry_lock.clone(), - events.clone(), - ), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_provider_register( + iii.clone(), + registry.clone(), + catalog.clone(), + entry_lock.clone(), + events.clone(), + )) .description(surface::PROVIDER_REGISTER_DESC) .metadata(internal_meta()), ); @@ -224,10 +214,11 @@ pub async fn register_router(iii: IIIClient) -> Result { ); iii.register_function( surface::MODELS_RECONCILE_ID, - RegisterFunction::new_async_with_bad_request( - make_models_reconcile(registry.clone(), catalog.clone(), events.clone()), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_models_reconcile( + registry.clone(), + catalog.clone(), + events.clone(), + )) .description(surface::MODELS_RECONCILE_DESC) .metadata(internal_meta()), ); @@ -266,6 +257,7 @@ pub async fn register_router(iii: IIIClient) -> Result { function_id: surface::ON_CONFIG_CHANGED_ID.into(), config: json!({ "configuration_id": "llm-router", "event_types": ["configuration:updated"] }), metadata: None, + namespace: iii.namespace(), })?; // Close the boot race between the initial fetch and trigger binding by diff --git a/llm-router/src/triggers.rs b/llm-router/src/triggers.rs index eead0cbc6..a7fc8a39d 100644 --- a/llm-router/src/triggers.rs +++ b/llm-router/src/triggers.rs @@ -224,12 +224,14 @@ mod tests { function_id: "probe::on_ready".into(), config: Value::Object(serde_json::Map::new()), metadata: None, + namespace: None, }); set.insert(TriggerConfig { id: "sub-2".into(), function_id: "other::recv".into(), config: Value::Object(serde_json::Map::new()), metadata: None, + namespace: None, }); let mut fns = set.function_ids(); fns.sort(); diff --git a/llm-router/tests/integration.rs b/llm-router/tests/integration.rs index ed8a010b1..80a0edc8c 100644 --- a/llm-router/tests/integration.rs +++ b/llm-router/tests/integration.rs @@ -1050,6 +1050,7 @@ async fn models_changed_event_reaches_a_trigger_subscriber() { function_id: "probe::on_models_changed".into(), config: json!({}), metadata: None, + namespace: probe.namespace(), }) .expect("router::models::changed trigger registered"); diff --git a/lsp/Cargo.lock b/lsp/Cargo.lock index 5d0e697c6..b77a4ebcb 100644 --- a/lsp/Cargo.lock +++ b/lsp/Cargo.lock @@ -481,9 +481,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -712,9 +712,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -733,9 +733,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", @@ -754,12 +754,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -794,13 +794,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -2053,9 +2052,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -2066,9 +2065,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -2076,9 +2075,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2086,9 +2085,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -2099,9 +2098,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -2142,9 +2141,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/lsp/Cargo.toml b/lsp/Cargo.toml index 311749d7a..162071048 100644 --- a/lsp/Cargo.toml +++ b/lsp/Cargo.toml @@ -13,7 +13,7 @@ name = "lsp" path = "src/main.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" tower-lsp-server = "0.23" tree-sitter = "0.24" tree-sitter-typescript = "0.23" diff --git a/mcp/Cargo.lock b/mcp/Cargo.lock index 0915f38d7..146953d90 100644 --- a/mcp/Cargo.lock +++ b/mcp/Cargo.lock @@ -887,9 +887,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -908,9 +908,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/mcp/Cargo.toml b/mcp/Cargo.toml index 920e93971..5a434d45a 100644 --- a/mcp/Cargo.toml +++ b/mcp/Cargo.toml @@ -15,8 +15,8 @@ name = "iii_mcp" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" -iii-helpers = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" +iii-helpers = "=0.22.0-alpha.3" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/mcp/src/main.rs b/mcp/src/main.rs index a3ab6c556..ac063ee48 100644 --- a/mcp/src/main.rs +++ b/mcp/src/main.rs @@ -87,6 +87,7 @@ async fn main() -> Result<()> { "http_method": "POST", }), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => tracing::info!( function_id = functions::FUNCTION_ID, diff --git a/mcp/tests/common/engine.rs b/mcp/tests/common/engine.rs index 90cb323f5..8bba2317d 100644 --- a/mcp/tests/common/engine.rs +++ b/mcp/tests/common/engine.rs @@ -106,6 +106,7 @@ async fn register_bridge(iii: &Arc) { "http_method": "POST", }), metadata: None, + namespace: iii.namespace(), }) { eprintln!( "[warn] mcp BDD harness failed to register HTTP trigger at {api_path}: {e}; \ diff --git a/memory-consolidate/Cargo.lock b/memory-consolidate/Cargo.lock index f4517b366..0978a1438 100644 --- a/memory-consolidate/Cargo.lock +++ b/memory-consolidate/Cargo.lock @@ -642,9 +642,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.3" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6455e06a9357696d8aa7cf7ba6d6b30ce8803036d85ea53a51de76178b867ec" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/memory-consolidate/Cargo.toml b/memory-consolidate/Cargo.toml index 6e7f72554..5f4554a4b 100644 --- a/memory-consolidate/Cargo.toml +++ b/memory-consolidate/Cargo.toml @@ -15,7 +15,7 @@ name = "memory_consolidate" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.3" +iii-sdk = "=0.22.0-alpha.3" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/memory-consolidate/src/configuration.rs b/memory-consolidate/src/configuration.rs index d3046ccf1..9f1f2747b 100644 --- a/memory-consolidate/src/configuration.rs +++ b/memory-consolidate/src/configuration.rs @@ -13,7 +13,25 @@ use serde_json::{json, Value}; use crate::config::WorkerConfig; use crate::functions::ConfigCell; -pub const CONFIG_ID: &str = "memory-consolidate"; +pub const DEFAULT_CONFIG_ID: &str = "memory-consolidate"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "memory-consolidate::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -21,7 +39,7 @@ const CONFIG_RETRY_BACKOFF_MS: u64 = 250; pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Memory Consolidate", "description": "Scheduled memory hygiene: dedup interval, dry-run mode, bank allowlist, \ and the per-pass supersede cap.", @@ -56,14 +74,14 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { async fn get_config_value(iii: &IIIClient) -> Result { try_get_config_value(iii) .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + .ok_or_else(|| format!("configuration `{config_entry}` not found", config_entry = config_id())) } /// `Ok(None)` = the entry does not exist. A successful reply MUST carry /// `value` — a malformed response must not seed defaults over an intended /// configuration. async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => resp .get("value") .cloned() @@ -113,10 +131,11 @@ pub fn register_config_trigger(iii: &IIIClient, cell: ConfigCell) -> Result<(), trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/memory-consolidate/src/consolidate.rs b/memory-consolidate/src/consolidate.rs index 0f9c6cb40..2ff068a02 100644 --- a/memory-consolidate/src/consolidate.rs +++ b/memory-consolidate/src/consolidate.rs @@ -171,13 +171,16 @@ fn group_by(rows: &[MemoryRow], key: fn(&str) -> String, report_only: bool) -> V } async fn call(iii: &IIIClient, function_id: &str, payload: Value) -> Result { - iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: function_id.to_string(), payload, action: None, timeout_ms: Some(CALL_TIMEOUT_MS), - }) - .await + }; + match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } .map_err(|e| format!("{function_id}: {e}")) } diff --git a/memory-consolidate/src/llm.rs b/memory-consolidate/src/llm.rs index 6900ad619..82ecd63e9 100644 --- a/memory-consolidate/src/llm.rs +++ b/memory-consolidate/src/llm.rs @@ -146,15 +146,17 @@ async fn resolve_model(iii: &IIIClient, configured: &str) -> Result iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } + .map_err(|e| format!("router::models::list: {e}"))?; reply .get("models") .and_then(Value::as_array) @@ -172,24 +174,26 @@ pub async fn judge( prompt: String, ) -> Result { let model = resolve_model(iii, configured_model).await?; - let reply = iii - .trigger(TriggerRequest { - function_id: "router::complete".into(), - payload: json!({ - "model": model, - "system_prompt": JUDGE_SYSTEM, - "messages": [{ - "role": "user", - "content": [{ "type": "text", "text": prompt }], - "timestamp": now_ms() as i64, - }], - "max_output_tokens": 1_024, - }), - action: None, - timeout_ms: Some(LLM_TIMEOUT_MS), - }) - .await - .map_err(|e| format!("router::complete: {e}"))?; + let request = TriggerRequest { + function_id: "router::complete".into(), + payload: json!({ + "model": model, + "system_prompt": JUDGE_SYSTEM, + "messages": [{ + "role": "user", + "content": [{ "type": "text", "text": prompt }], + "timestamp": now_ms() as i64, + }], + "max_output_tokens": 1_024, + }), + action: None, + timeout_ms: Some(LLM_TIMEOUT_MS), + }; + let reply = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } + .map_err(|e| format!("router::complete: {e}"))?; let text = reply .pointer("/message/content") .and_then(Value::as_array) diff --git a/memory-consolidate/src/main.rs b/memory-consolidate/src/main.rs index 4851524c9..58d7b8efa 100644 --- a/memory-consolidate/src/main.rs +++ b/memory-consolidate/src/main.rs @@ -88,6 +88,7 @@ fn bind_schedule(iii: Arc, deps: Arc) { function_id: functions::TICK_ID.to_string(), config: serde_json::json!({ "expression": CRON_EXPRESSION }), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => { tracing::info!(expression = CRON_EXPRESSION, "cron heartbeat bound"); @@ -200,6 +201,7 @@ async fn main() -> Result<()> { function_id: spec.function_id.to_string(), config: serde_json::json!({ "api_path": api_path, "http_method": "POST" }), metadata: None, + namespace: iii.namespace(), }) { tracing::debug!(error = %e, function_id = spec.function_id, "http trigger registration failed"); } diff --git a/memory/Cargo.lock b/memory/Cargo.lock index c0e2a65b6..819b7e410 100644 --- a/memory/Cargo.lock +++ b/memory/Cargo.lock @@ -660,9 +660,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.8" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bdc7bbc3abfde934a62cdc5d3045adf52914dfc1ed6c20f8af691fc561dc55" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -681,9 +681,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/memory/Cargo.toml b/memory/Cargo.toml index dc9703ac2..374b5a793 100644 --- a/memory/Cargo.toml +++ b/memory/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" # Bumped from =0.21.3 to match the `iii-console-ui` crate's exact pin: both are # `=` requirements so they must resolve to a single version, and the injectable # UI helper must never drag a worker onto a different SDK build. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" # Worker-side injectable console UI (content function + console:script/style # triggers + hot-reload watcher) — direct path link, never published. iii-console-ui = { path = "../crates/console-ui" } diff --git a/memory/src/configuration.rs b/memory/src/configuration.rs index c1ed99fe0..2e45d8c13 100644 --- a/memory/src/configuration.rs +++ b/memory/src/configuration.rs @@ -23,7 +23,25 @@ pub type ConfigCell = Arc>>; /// Hot-swappable store handle (rebuilt on a `data_dir` change). pub type StoreCell = Arc>>; -pub const CONFIG_ID: &str = "memory"; +pub const DEFAULT_CONFIG_ID: &str = "memory"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "memory::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -35,7 +53,7 @@ const CONFIG_RETRY_BACKOFF_MS: u64 = 250; /// stored value exists yet. pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Memory", "description": "Cross-session agent memory: bank data directory, default bank, \ per-turn injection toggles and budgets, and the post-turn \ @@ -73,13 +91,13 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { async fn get_config_value(iii: &IIIClient) -> Result { try_get_config_value(iii) .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + .ok_or_else(|| format!("configuration `{config_entry}` not found", config_entry = config_id())) } /// Returns `Ok(None)` when the entry does not exist. The engine's /// missing-entry codes vary in case, so match case-insensitively. async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { // A successful reply MUST carry `value`; treating its absence as // "no entry" would let a malformed response seed defaults over an // intended configuration. @@ -147,10 +165,11 @@ pub fn register_config_trigger( trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/memory/src/embed_client.rs b/memory/src/embed_client.rs index 4f5c40294..1765537af 100644 --- a/memory/src/embed_client.rs +++ b/memory/src/embed_client.rs @@ -31,15 +31,17 @@ pub async fn embed_texts( if !model.trim().is_empty() { payload["model"] = json!(model); } - let reply = iii - .trigger(TriggerRequest { - function_id: "router::embed".into(), - payload, - action: None, - timeout_ms: Some(timeout_ms), - }) - .await - .ok()?; + let request = TriggerRequest { + function_id: "router::embed".into(), + payload, + action: None, + timeout_ms: Some(timeout_ms), + }; + let reply = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } + .ok()?; let embeddings: Vec> = serde_json::from_value(reply.get("embeddings").cloned().unwrap_or(Value::Null)).ok()?; if embeddings.len() == texts.len() { diff --git a/memory/src/events.rs b/memory/src/events.rs index b4532ca2c..7c7cccf9d 100644 --- a/memory/src/events.rs +++ b/memory/src/events.rs @@ -206,15 +206,36 @@ impl Emitter { if !matches { continue; } - let res = self - .iii - .trigger(TriggerRequest { - function_id: binding.function_id.clone(), - payload: payload.clone(), - action: Some(TriggerAction::Void), - timeout_ms: None, - }) - .await; + let request = TriggerRequest { + function_id: binding.function_id.clone(), + payload: payload.clone(), + action: Some(TriggerAction::Void), + timeout_ms: None, + }; + let route_ns = self.iii.namespace().filter(|_| { + !matches!( + binding.function_id.split("::").next(), + Some( + "state" + | "stream" + | "queue" + | "pubsub" + | "configuration" + | "cron" + | "http" + | "engine" + | "sandbox" + | "log" + | "secret" + | "kv" + | "iii" + ) + ) + }); + let res = match route_ns { + Some(ns) => self.iii.trigger(request.namespace(ns)).await, + None => self.iii.trigger(request).await, + }; if let Err(e) = res { tracing::warn!(function_id = %binding.function_id, error = %e, "void fan-out failed"); } diff --git a/memory/src/extract.rs b/memory/src/extract.rs index f74b778d7..7effcebc7 100644 --- a/memory/src/extract.rs +++ b/memory/src/extract.rs @@ -107,14 +107,16 @@ fn merge_learned(existing: &str, additions: &[String]) -> (String, usize) { /// `None` — injecting or writing the default bank when the session's real /// bank is unknown would leak across contexts. pub async fn resolve_bank(iii: &IIIClient, session_id: &str, default_bank: &str) -> Option { - let reply = iii - .trigger(TriggerRequest { - function_id: "session::get".into(), - payload: json!({ "session_id": session_id }), - action: None, - timeout_ms: Some(5_000), - }) - .await; + let request = TriggerRequest { + function_id: "session::get".into(), + payload: json!({ "session_id": session_id }), + action: None, + timeout_ms: Some(5_000), + }; + let reply = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + }; match reply { Ok(v) => { let from_meta = v @@ -163,25 +165,26 @@ pub async fn try_run(deps: &Deps, session_id: &str) -> Result<(), String> { } let model = resolve_model(&deps.iii, &cfg.extraction_model).await?; - let reply = deps - .iii - .trigger(TriggerRequest { - function_id: "router::complete".into(), - payload: json!({ - "model": model, - "system_prompt": EXTRACT_SYSTEM, - "messages": [{ - "role": "user", - "content": [{ "type": "text", "text": transcript }], - "timestamp": now_ms() as i64, - }], - "max_output_tokens": 1024, - }), - action: None, - timeout_ms: Some(cfg.extraction_timeout_ms), - }) - .await - .map_err(|e| format!("router::complete: {e}"))?; + let request = TriggerRequest { + function_id: "router::complete".into(), + payload: json!({ + "model": model, + "system_prompt": EXTRACT_SYSTEM, + "messages": [{ + "role": "user", + "content": [{ "type": "text", "text": transcript }], + "timestamp": now_ms() as i64, + }], + "max_output_tokens": 1024, + }), + action: None, + timeout_ms: Some(cfg.extraction_timeout_ms), + }; + let reply = match deps.iii.namespace() { + Some(ns) => deps.iii.trigger(request.namespace(ns)).await, + None => deps.iii.trigger(request).await, + } + .map_err(|e| format!("router::complete: {e}"))?; let text = assistant_text(&reply); let items = parse_items(&text)?; @@ -407,15 +410,17 @@ async fn fetch_transcript_since( if let Some(c) = &cursor { payload["cursor"] = json!(c); } - let reply = iii - .trigger(TriggerRequest { - function_id: "session::messages".into(), - payload, - action: None, - timeout_ms: Some(10_000), - }) - .await - .map_err(|e| format!("session::messages: {e}"))?; + let request = TriggerRequest { + function_id: "session::messages".into(), + payload, + action: None, + timeout_ms: Some(10_000), + }; + let reply = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } + .map_err(|e| format!("session::messages: {e}"))?; if let Some(items) = reply.get("messages").and_then(Value::as_array) { for item in items { let entry_id = item @@ -507,15 +512,17 @@ async fn resolve_model(iii: &IIIClient, configured: &str) -> Result iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } + .map_err(|e| format!("router::models::list: {e}"))?; let first = reply .get("models") .and_then(Value::as_array) diff --git a/memory/src/functions/ops.rs b/memory/src/functions/ops.rs index 298d45bc4..929a7c61a 100644 --- a/memory/src/functions/ops.rs +++ b/memory/src/functions/ops.rs @@ -117,15 +117,16 @@ async fn roundtrip_check(deps: &Deps) -> DoctorCheck { } async fn sibling_check(deps: &Deps, name: &str, function_id: &str, why: &str) -> DoctorCheck { - let res = deps - .iii - .trigger(TriggerRequest { - function_id: function_id.into(), - payload: json!({}), - action: None, - timeout_ms: Some(3_000), - }) - .await; + let request = TriggerRequest { + function_id: function_id.into(), + payload: json!({}), + action: None, + timeout_ms: Some(3_000), + }; + let res = match deps.iii.namespace() { + Some(ns) => deps.iii.trigger(request.namespace(ns)).await, + None => deps.iii.trigger(request).await, + }; match res { Ok(_) => DoctorCheck { name: name.into(), diff --git a/memory/src/main.rs b/memory/src/main.rs index b33a9fb17..5d08e5392 100644 --- a/memory/src/main.rs +++ b/memory/src/main.rs @@ -80,6 +80,7 @@ fn bind_best_effort( function_id: function_id.to_string(), config, metadata: None, + namespace: iii.namespace(), }); match res { Ok(_) => tracing::info!(trigger_type, function_id, "trigger binding requested"), @@ -241,6 +242,7 @@ async fn main() -> Result<()> { function_id: spec.function_id.to_string(), config: json!({ "api_path": api_path, "http_method": "POST" }), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => tracing::debug!( function_id = spec.function_id, diff --git a/opencode/package.json b/opencode/package.json index 6c225ab73..8e596f2df 100644 --- a/opencode/package.json +++ b/opencode/package.json @@ -23,7 +23,7 @@ "opencode-worker": "./dist/index.js" }, "dependencies": { - "iii-sdk": "^0.21.6", + "iii-sdk": "0.22.0-alpha.3", "yaml": "^2.6.0", "zod": "^4.0.0" }, diff --git a/opencode/pnpm-lock.yaml b/opencode/pnpm-lock.yaml index 580ee5b98..4c8823a5b 100644 --- a/opencode/pnpm-lock.yaml +++ b/opencode/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: iii-sdk: - specifier: ^0.21.6 - version: 0.21.6 + specifier: 0.22.0-alpha.3 + version: 0.22.0-alpha.3 yaml: specifier: ^2.6.0 version: 2.9.0 @@ -560,8 +560,8 @@ packages: cpu: [x64] os: [win32] - '@iii-dev/helpers@0.21.6': - resolution: {integrity: sha512-UkiIqC5Ob41xc9uYx1SRt4LXLKrzAMzOCq6Kwo4hCak7Qj4Q1tP0GW0nYP7UpgYHsQnzf+2sspb2yD/DVa2ePA==} + '@iii-dev/helpers@0.22.0-alpha.3': + resolution: {integrity: sha512-zwilMTMq2MgVFu/aOEbtrPDpB+AVcdcqKBVAGc4vXv4XT2sK5eAXpdyzPcNgf8bGRKcRGZJ0ZJ8dA8WDVlSlaw==} '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -936,8 +936,8 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} - iii-sdk@0.21.6: - resolution: {integrity: sha512-D9NGtcGWdY8tqXRIjvRDqNqKRI2qJcQguSDWuUEPXaYyIHSn1jp3PIVF34cCY6C+f3Bby8fjptxpfzWYQUOg2A==} + iii-sdk@0.22.0-alpha.3: + resolution: {integrity: sha512-S6dH8ssbZsuuh8m6k0gG//D5K11w/wTkUviRMQ0sP3NNQh4ZhxZz0TbGlhROpwCYdJb1Rgl8dAHgeXbhyoIUXg==} import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} @@ -1440,7 +1440,7 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@iii-dev/helpers@0.21.6': + '@iii-dev/helpers@0.22.0-alpha.3': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api-logs': 0.57.2 @@ -1839,9 +1839,9 @@ snapshots: dependencies: function-bind: 1.1.2 - iii-sdk@0.21.6: + iii-sdk@0.22.0-alpha.3: dependencies: - '@iii-dev/helpers': 0.21.6 + '@iii-dev/helpers': 0.22.0-alpha.3 '@opentelemetry/api': 1.9.1 ws: 8.21.0 transitivePeerDependencies: diff --git a/opencode/src/index.ts b/opencode/src/index.ts index c3bb9eae3..5b283240d 100644 --- a/opencode/src/index.ts +++ b/opencode/src/index.ts @@ -37,7 +37,12 @@ const bootConfig: Config = { opencode_executable: resolveOpencodeExecutable(seed.opencode_executable), }; -const iii = registerWorker(url, { workerName: 'opencode' }); +// namespace: register under III_NAMESPACE when set (the SDK also reads it, +// passed here for visibility). Absent, the engine's default namespace is used. +const iii = registerWorker(url, { + workerName: 'opencode', + namespace: process.env.III_NAMESPACE, +}); try { await registerOpencodeConfig(iii, bootConfig); diff --git a/pdf/Cargo.lock b/pdf/Cargo.lock index 452574ad2..df2283b40 100644 --- a/pdf/Cargo.lock +++ b/pdf/Cargo.lock @@ -889,9 +889,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.8" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84bdc7bbc3abfde934a62cdc5d3045adf52914dfc1ed6c20f8af691fc561dc55" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -910,9 +910,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/pdf/Cargo.toml b/pdf/Cargo.toml index 2a2e6d61f..25aa5d145 100644 --- a/pdf/Cargo.toml +++ b/pdf/Cargo.toml @@ -18,7 +18,7 @@ name = "pdf" path = "src/main.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" # Worker-side injectable console UI (content function + console:script/style # triggers + hot-reload watcher) — direct link, never published. iii-console-ui = { path = "../crates/console-ui" } diff --git a/pdf/src/configuration.rs b/pdf/src/configuration.rs index ef6cc088d..711bd5afe 100644 --- a/pdf/src/configuration.rs +++ b/pdf/src/configuration.rs @@ -138,6 +138,7 @@ pub fn register_config_trigger(iii: &IIIClient, cell: ConfigCell) -> Result<(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/pi/package.json b/pi/package.json index 689c01195..468f7a2bc 100644 --- a/pi/package.json +++ b/pi/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@earendil-works/pi-coding-agent": "^0.79.0", - "iii-sdk": "^0.21.6", + "iii-sdk": "0.22.0-alpha.3", "yaml": "^2.6.0", "zod": "^4.0.0" }, diff --git a/pi/pnpm-lock.yaml b/pi/pnpm-lock.yaml index 067568fb6..e53a8d163 100644 --- a/pi/pnpm-lock.yaml +++ b/pi/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^0.79.0 version: 0.79.10(ws@8.21.0)(zod@4.4.3) iii-sdk: - specifier: ^0.21.6 - version: 0.21.6 + specifier: 0.22.0-alpha.3 + version: 0.22.0-alpha.3 yaml: specifier: ^2.6.0 version: 2.9.0 @@ -704,8 +704,8 @@ packages: '@modelcontextprotocol/sdk': optional: true - '@iii-dev/helpers@0.21.6': - resolution: {integrity: sha512-UkiIqC5Ob41xc9uYx1SRt4LXLKrzAMzOCq6Kwo4hCak7Qj4Q1tP0GW0nYP7UpgYHsQnzf+2sspb2yD/DVa2ePA==} + '@iii-dev/helpers@0.22.0-alpha.3': + resolution: {integrity: sha512-zwilMTMq2MgVFu/aOEbtrPDpB+AVcdcqKBVAGc4vXv4XT2sK5eAXpdyzPcNgf8bGRKcRGZJ0ZJ8dA8WDVlSlaw==} '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -1301,8 +1301,8 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - iii-sdk@0.21.6: - resolution: {integrity: sha512-D9NGtcGWdY8tqXRIjvRDqNqKRI2qJcQguSDWuUEPXaYyIHSn1jp3PIVF34cCY6C+f3Bby8fjptxpfzWYQUOg2A==} + iii-sdk@0.22.0-alpha.3: + resolution: {integrity: sha512-S6dH8ssbZsuuh8m6k0gG//D5K11w/wTkUviRMQ0sP3NNQh4ZhxZz0TbGlhROpwCYdJb1Rgl8dAHgeXbhyoIUXg==} import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} @@ -2246,7 +2246,7 @@ snapshots: - supports-color - utf-8-validate - '@iii-dev/helpers@0.21.6': + '@iii-dev/helpers@0.22.0-alpha.3': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api-logs': 0.57.2 @@ -2865,9 +2865,9 @@ snapshots: ignore@7.0.5: {} - iii-sdk@0.21.6: + iii-sdk@0.22.0-alpha.3: dependencies: - '@iii-dev/helpers': 0.21.6 + '@iii-dev/helpers': 0.22.0-alpha.3 '@opentelemetry/api': 1.9.1 ws: 8.21.0 transitivePeerDependencies: diff --git a/pi/src/index.ts b/pi/src/index.ts index 172a02301..22af501ce 100644 --- a/pi/src/index.ts +++ b/pi/src/index.ts @@ -32,7 +32,12 @@ const url = values.url : (process.env.III_URL ?? process.env.III_ENGINE_URL ?? seed.engine_url); const bootConfig: Config = { ...seed, engine_url: url }; -const iii = registerWorker(url, { workerName: 'pi' }); +// namespace: register under III_NAMESPACE when set (the SDK also reads it, +// passed here for visibility). Absent, the engine's default namespace is used. +const iii = registerWorker(url, { + workerName: 'pi', + namespace: process.env.III_NAMESPACE, +}); // Best-effort: a configuration-worker hiccup at boot must not stop the worker // from registering pi::*; it falls back to the seed via fetchRuntime. diff --git a/provider-anthropic/Cargo.lock b/provider-anthropic/Cargo.lock index 685c58e30..7f01e3879 100644 --- a/provider-anthropic/Cargo.lock +++ b/provider-anthropic/Cargo.lock @@ -669,9 +669,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -690,9 +690,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/provider-anthropic/Cargo.toml b/provider-anthropic/Cargo.toml index ccd753eb2..8ec12b90b 100644 --- a/provider-anthropic/Cargo.toml +++ b/provider-anthropic/Cargo.toml @@ -22,7 +22,7 @@ path = "src/lib.rs" llm-router = { path = "../llm-router", default-features = false } # Must match llm-router's pin (one iii-sdk per graph). 0.21.5 brings the # live span-start push: `provider::anthropic::stream` renders while streaming. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/provider-anthropic/src/register.rs b/provider-anthropic/src/register.rs index f67f3355a..5bef39dc9 100644 --- a/provider-anthropic/src/register.rs +++ b/provider-anthropic/src/register.rs @@ -2,7 +2,6 @@ //! declare-with-backoff loop (spec § Registration lifecycle). use crate::config::{DEFAULT_API_URL, DEFAULT_MAX_TOKENS}; use crate::discovery::{make_refresh_models, refresh_models}; -use crate::errors::invalid_request_from_serde; use crate::stream_fn::make_stream; use crate::surface; use crate::{router_client, state, PROVIDER_ID}; @@ -145,21 +144,20 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { iii.register_function( surface::STREAM_ID, - RegisterFunction::new_async_with_bad_request( - make_stream(iii.clone(), http.clone(), cache.clone(), aborts.clone()), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_stream( + iii.clone(), + http.clone(), + cache.clone(), + aborts.clone(), + )) .description(surface::STREAM_DESC) .metadata(json!({ "internal": true })), ); iii.register_function( surface::ABORT_ID, - RegisterFunction::new_async_with_bad_request( - make_abort(aborts), - invalid_request_from_serde, - ) - .description(surface::ABORT_DESC) - .metadata(json!({ "internal": true })), + RegisterFunction::new_async(make_abort(aborts)) + .description(surface::ABORT_DESC) + .metadata(json!({ "internal": true })), ); iii.register_function( surface::REFRESH_MODELS_ID, @@ -207,6 +205,7 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { function_id: surface::ON_ROUTER_READY_ID.into(), config: json!({}), metadata: None, + namespace: iii.namespace(), }); // Boot declare, off the boot path (a missing router must not block boot). diff --git a/provider-claude-code/Cargo.lock b/provider-claude-code/Cargo.lock index 9f188732d..f5fc34b92 100644 --- a/provider-claude-code/Cargo.lock +++ b/provider-claude-code/Cargo.lock @@ -2,6 +2,20 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -61,6 +75,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "async-trait" version = "0.1.91" @@ -78,12 +98,33 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.13.1" @@ -99,6 +140,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -111,6 +163,15 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.3.0" @@ -190,6 +251,21 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -224,6 +300,31 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + [[package]] name = "crypto-common" version = "0.1.7" @@ -234,12 +335,93 @@ dependencies = [ "typenum", ] +[[package]] +name = "daachorse" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f55d7153ba3b507595872a3874803f07a8a81d1e888abed8e5db7da0597d6e2" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dary_heap" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe" +dependencies = [ + "serde", +] + [[package]] name = "data-encoding" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.119", +] + [[package]] name = "digest" version = "0.10.7" @@ -267,6 +449,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + [[package]] name = "errno" version = "0.3.14" @@ -277,12 +465,35 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" + +[[package]] +name = "fancy-regex" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -527,7 +738,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -626,6 +837,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -649,9 +866,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -670,9 +887,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", @@ -701,6 +918,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -738,7 +964,7 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "llm-router" -version = "1.4.1" +version = "1.4.2" dependencies = [ "async-trait", "clap", @@ -746,11 +972,14 @@ dependencies = [ "iii-helpers", "iii-sdk", "regex", + "reqwest", "schemars", "serde", "serde_json", "sha2", "thiserror", + "tiktoken-rs", + "tokenizers", "tokio", "tracing", "tracing-subscriber", @@ -769,6 +998,22 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "macro_rules_attribute" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ae8f6d608c795738406608304d30a2dfbdc8e58e44f7ba43236da5208ded3c" +dependencies = [ + "macro_rules_attribute-proc_macro", + "pastey", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c" + [[package]] name = "matchers" version = "0.2.0" @@ -784,6 +1029,12 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.2.2" @@ -795,6 +1046,38 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "monostate" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -894,6 +1177,18 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -961,7 +1256,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "socket2", "thiserror", @@ -982,7 +1277,7 @@ dependencies = [ "rand 0.10.2", "rand_pcg", "ring", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", @@ -1082,6 +1377,37 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-cond" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" +dependencies = [ + "either", + "itertools", + "rayon", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.13.1" @@ -1117,7 +1443,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "futures-util", @@ -1166,6 +1492,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -1422,12 +1754,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom", + "serde", + "unicode-segmentation", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -1525,6 +1875,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiktoken-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4a168cfc1d8ed65bf17a6ee0843ad9a68f863c63c0fb2fa7eab67838782ee" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bstr", + "fancy-regex", + "lazy_static", + "regex", + "rustc-hash 1.1.0", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -1550,6 +1915,39 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokenizers" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44e5bea67576e04b6ff8564c5d9e09c2ef0cf476502245f2f120e497769d3112" +dependencies = [ + "ahash", + "compact_str", + "daachorse", + "dary_heap", + "derive_builder", + "esaxx-rs", + "fancy-regex", + "getrandom 0.3.4", + "itertools", + "log", + "macro_rules_attribute", + "monostate", + "paste", + "rand 0.9.5", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + [[package]] name = "tokio" version = "1.53.1" @@ -1770,6 +2168,27 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization-alignments" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" +dependencies = [ + "smallvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/provider-claude-code/Cargo.toml b/provider-claude-code/Cargo.toml index 272cfa307..24f57f630 100644 --- a/provider-claude-code/Cargo.toml +++ b/provider-claude-code/Cargo.toml @@ -19,7 +19,7 @@ path = "src/lib.rs" llm-router = { path = "../llm-router", default-features = false } # Must match llm-router's pin (one iii-sdk per graph). 0.21.5 brings the # live span-start push: `provider::claude-code::stream` renders while streaming. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/provider-claude-code/src/register.rs b/provider-claude-code/src/register.rs index 599b083af..0a54f3074 100644 --- a/provider-claude-code/src/register.rs +++ b/provider-claude-code/src/register.rs @@ -2,7 +2,6 @@ //! declare-with-backoff loop (spec § Registration lifecycle). use crate::config::{DEFAULT_API_URL, DEFAULT_MAX_TOKENS}; use crate::discovery::{make_refresh_models, refresh_models, refresh_models_periodically}; -use crate::errors::invalid_request_from_serde; use crate::stream_fn::make_stream; use crate::surface; use crate::{auth, router_client, state, PROVIDER_ID}; @@ -151,21 +150,20 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { iii.register_function( surface::STREAM_ID, - RegisterFunction::new_async_with_bad_request( - make_stream(iii.clone(), http.clone(), cache.clone(), aborts.clone()), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_stream( + iii.clone(), + http.clone(), + cache.clone(), + aborts.clone(), + )) .description(surface::STREAM_DESC) .metadata(json!({ "internal": true })), ); iii.register_function( surface::ABORT_ID, - RegisterFunction::new_async_with_bad_request( - make_abort(aborts), - invalid_request_from_serde, - ) - .description(surface::ABORT_DESC) - .metadata(json!({ "internal": true })), + RegisterFunction::new_async(make_abort(aborts)) + .description(surface::ABORT_DESC) + .metadata(json!({ "internal": true })), ); iii.register_function( surface::REFRESH_MODELS_ID, @@ -198,6 +196,7 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { function_id: surface::ON_ROUTER_READY_ID.into(), config: json!({}), metadata: None, + namespace: iii.namespace(), }); // Boot declare, off the boot path (a missing router must not block boot). diff --git a/provider-deepseek/Cargo.lock b/provider-deepseek/Cargo.lock index 85db11567..4c3397931 100644 --- a/provider-deepseek/Cargo.lock +++ b/provider-deepseek/Cargo.lock @@ -866,9 +866,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -887,9 +887,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/provider-deepseek/Cargo.toml b/provider-deepseek/Cargo.toml index 72fb6cf0a..ba76f2766 100644 --- a/provider-deepseek/Cargo.toml +++ b/provider-deepseek/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" [dependencies] llm-router = { path = "../llm-router", default-features = false } # Must match llm-router's pin (one iii-sdk per graph). -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/provider-deepseek/src/register.rs b/provider-deepseek/src/register.rs index f1d02c97f..d8584135c 100644 --- a/provider-deepseek/src/register.rs +++ b/provider-deepseek/src/register.rs @@ -2,7 +2,6 @@ //! declare-with-backoff loop (spec § Registration lifecycle). use crate::config::{DEFAULT_API_URL, DEFAULT_MAX_TOKENS}; use crate::discovery::{make_refresh_models, refresh_models}; -use crate::errors::invalid_request_from_serde; use crate::stream_fn::make_stream; use crate::surface; use crate::{router_client, state, PROVIDER_ID}; @@ -149,21 +148,20 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { iii.register_function( surface::STREAM_ID, - RegisterFunction::new_async_with_bad_request( - make_stream(iii.clone(), http.clone(), cache.clone(), aborts.clone()), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_stream( + iii.clone(), + http.clone(), + cache.clone(), + aborts.clone(), + )) .description(surface::STREAM_DESC) .metadata(json!({ "internal": true })), ); iii.register_function( surface::ABORT_ID, - RegisterFunction::new_async_with_bad_request( - make_abort(aborts), - invalid_request_from_serde, - ) - .description(surface::ABORT_DESC) - .metadata(json!({ "internal": true })), + RegisterFunction::new_async(make_abort(aborts)) + .description(surface::ABORT_DESC) + .metadata(json!({ "internal": true })), ); iii.register_function( surface::REFRESH_MODELS_ID, @@ -205,6 +203,7 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { function_id: surface::ON_ROUTER_READY_ID.into(), config: json!({}), metadata: None, + namespace: iii.namespace(), }); // Boot declare, off the boot path (a missing router must not block boot). diff --git a/provider-kimi/Cargo.lock b/provider-kimi/Cargo.lock index a340069c1..1984db024 100644 --- a/provider-kimi/Cargo.lock +++ b/provider-kimi/Cargo.lock @@ -845,9 +845,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -866,9 +866,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", @@ -943,7 +943,7 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "llm-router" -version = "1.4.1" +version = "1.4.2" dependencies = [ "async-trait", "clap", diff --git a/provider-kimi/Cargo.toml b/provider-kimi/Cargo.toml index 5bec29b79..82b884afc 100644 --- a/provider-kimi/Cargo.toml +++ b/provider-kimi/Cargo.toml @@ -18,7 +18,7 @@ path = "src/lib.rs" [dependencies] llm-router = { path = "../llm-router", default-features = false } # Must match llm-router's pin so the provider and router share one IIIClient type. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/provider-kimi/src/register.rs b/provider-kimi/src/register.rs index 938de6dff..9c58bdcd7 100644 --- a/provider-kimi/src/register.rs +++ b/provider-kimi/src/register.rs @@ -2,7 +2,6 @@ //! declare-with-backoff loop (spec § Registration lifecycle). use crate::config::{DEFAULT_API_URL, DEFAULT_MAX_TOKENS}; use crate::discovery::{make_refresh_models, refresh_models}; -use crate::errors::invalid_request_from_serde; use crate::stream_fn::make_stream; use crate::surface; use crate::{router_client, state, PROVIDER_ID}; @@ -124,22 +123,16 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { iii.register_function( surface::STREAM_ID, - RegisterFunction::new_async_with_bad_request( - make_stream(iii.clone(), http.clone(), aborts.clone()), - invalid_request_from_serde, - ) - .description(surface::STREAM_DESC), + RegisterFunction::new_async(make_stream(iii.clone(), http.clone(), aborts.clone())) + .description(surface::STREAM_DESC), ); iii.register_function( surface::ABORT_ID, - RegisterFunction::new_async_with_bad_request( - make_abort(aborts), - invalid_request_from_serde, - ) - .description(surface::ABORT_DESC) - // Control-plane callback (router::abort fan-out) — hide from the - // agent-facing catalog like the other providers' abort functions. - .metadata(serde_json::json!({ "internal": true })), + RegisterFunction::new_async(make_abort(aborts)) + .description(surface::ABORT_DESC) + // Control-plane callback (router::abort fan-out) — hide from the + // agent-facing catalog like the other providers' abort functions. + .metadata(serde_json::json!({ "internal": true })), ); iii.register_function( surface::REFRESH_MODELS_ID, @@ -180,6 +173,7 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { function_id: surface::ON_ROUTER_READY_ID.into(), config: json!({}), metadata: None, + namespace: iii.namespace(), }); // Boot declare, off the boot path (a missing router must not block boot). diff --git a/provider-kimi/src/router_client.rs b/provider-kimi/src/router_client.rs index 6cd49b7ae..610e2a92d 100644 --- a/provider-kimi/src/router_client.rs +++ b/provider-kimi/src/router_client.rs @@ -9,13 +9,18 @@ use llm_router::types::router::ProviderResolveResponse; use serde_json::{json, Value}; async fn call(iii: &IIIClient, function_id: &str, payload: Value) -> Result { - iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: function_id.into(), payload, action: None, timeout_ms: Some(15_000), - }) - .await + }; + // Route the cross-worker call to this worker's namespace so it reaches the + // router registered in the same namespace (None => the engine's default). + match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await, + None => iii.trigger(request).await, + } } /// `router::provider::resolve` — credential + effective settings. diff --git a/provider-llamacpp/Cargo.lock b/provider-llamacpp/Cargo.lock index f3e2bae9c..94dabf799 100644 --- a/provider-llamacpp/Cargo.lock +++ b/provider-llamacpp/Cargo.lock @@ -866,9 +866,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -887,9 +887,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", @@ -964,7 +964,7 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "llm-router" -version = "1.4.1" +version = "1.4.2" dependencies = [ "async-trait", "clap", diff --git a/provider-llamacpp/Cargo.toml b/provider-llamacpp/Cargo.toml index c6295de0e..185bd0fe8 100644 --- a/provider-llamacpp/Cargo.toml +++ b/provider-llamacpp/Cargo.toml @@ -19,7 +19,7 @@ path = "src/lib.rs" llm-router = { path = "../llm-router", default-features = false } # Must match llm-router's pin (one iii-sdk per graph). 0.21.5 brings the # live span-start push: `provider::llamacpp::stream` renders while streaming. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/provider-llamacpp/src/register.rs b/provider-llamacpp/src/register.rs index 4c55d7ffd..7f18fc3d2 100644 --- a/provider-llamacpp/src/register.rs +++ b/provider-llamacpp/src/register.rs @@ -2,7 +2,6 @@ //! declare-with-backoff loop (spec § Registration lifecycle). use crate::config::{DEFAULT_API_URL, DEFAULT_MAX_TOKENS}; use crate::discovery::{make_refresh_models, refresh_models}; -use crate::errors::invalid_request_from_serde; use crate::stream_fn::make_stream; use crate::surface; use crate::{router_client, state, PROVIDER_ID}; @@ -134,20 +133,19 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { iii.register_function( surface::STREAM_ID, - RegisterFunction::new_async_with_bad_request( - make_stream(iii.clone(), http.clone(), cache.clone(), aborts.clone()), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_stream( + iii.clone(), + http.clone(), + cache.clone(), + aborts.clone(), + )) .description(surface::STREAM_DESC), ); iii.register_function( surface::ABORT_ID, - RegisterFunction::new_async_with_bad_request( - make_abort(aborts), - invalid_request_from_serde, - ) - .description(surface::ABORT_DESC) - .metadata(json!({ "internal": true })), + RegisterFunction::new_async(make_abort(aborts)) + .description(surface::ABORT_DESC) + .metadata(json!({ "internal": true })), ); iii.register_function( surface::REFRESH_MODELS_ID, @@ -208,6 +206,7 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { function_id: surface::ON_ROUTER_READY_ID.into(), config: json!({}), metadata: None, + namespace: iii.namespace(), }); // Boot declare, off the boot path (a missing router must not block boot). diff --git a/provider-openai-codex/Cargo.lock b/provider-openai-codex/Cargo.lock index e1b8481d6..6eceb1f31 100644 --- a/provider-openai-codex/Cargo.lock +++ b/provider-openai-codex/Cargo.lock @@ -706,9 +706,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -727,9 +727,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/provider-openai-codex/Cargo.toml b/provider-openai-codex/Cargo.toml index 7d84d7a9b..28d7f6c93 100644 --- a/provider-openai-codex/Cargo.toml +++ b/provider-openai-codex/Cargo.toml @@ -19,7 +19,7 @@ path = "src/lib.rs" llm-router = { path = "../llm-router", default-features = false } # Must match llm-router's pin (one iii-sdk per graph). 0.21.5 brings the # live span-start push: `provider::openai-codex::stream` renders while streaming. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/provider-openai-codex/src/register.rs b/provider-openai-codex/src/register.rs index f486d130b..d39b5a961 100644 --- a/provider-openai-codex/src/register.rs +++ b/provider-openai-codex/src/register.rs @@ -4,7 +4,6 @@ use crate::config::{DEFAULT_API_URL, DEFAULT_MAX_TOKENS}; use crate::discovery::{ make_refresh_models, refresh_models, refresh_models_periodically, CatalogRefreshState, }; -use crate::errors::invalid_request_from_serde; use crate::stream_fn::make_stream; use crate::surface; use crate::{auth, router_client, state, PROVIDER_ID}; @@ -147,21 +146,20 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { iii.register_function( surface::STREAM_ID, - RegisterFunction::new_async_with_bad_request( - make_stream(iii.clone(), http.clone(), cache.clone(), aborts.clone()), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_stream( + iii.clone(), + http.clone(), + cache.clone(), + aborts.clone(), + )) .description(surface::STREAM_DESC) .metadata(json!({ "internal": true })), ); iii.register_function( surface::ABORT_ID, - RegisterFunction::new_async_with_bad_request( - make_abort(aborts), - invalid_request_from_serde, - ) - .description(surface::ABORT_DESC) - .metadata(json!({ "internal": true })), + RegisterFunction::new_async(make_abort(aborts)) + .description(surface::ABORT_DESC) + .metadata(json!({ "internal": true })), ); iii.register_function( surface::REFRESH_MODELS_ID, @@ -210,6 +208,7 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { function_id: surface::ON_ROUTER_READY_ID.into(), config: json!({}), metadata: None, + namespace: iii.namespace(), }); tokio::spawn(declare_and_refresh( diff --git a/provider-openai/Cargo.lock b/provider-openai/Cargo.lock index 1422c0338..0a6cda57f 100644 --- a/provider-openai/Cargo.lock +++ b/provider-openai/Cargo.lock @@ -706,9 +706,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -727,9 +727,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/provider-openai/Cargo.toml b/provider-openai/Cargo.toml index 5521a5f6c..e0132ddac 100644 --- a/provider-openai/Cargo.toml +++ b/provider-openai/Cargo.toml @@ -22,7 +22,7 @@ path = "src/lib.rs" llm-router = { path = "../llm-router", default-features = false } # Must match llm-router's pin (one iii-sdk per graph). 0.21.5 brings the # live span-start push: `provider::openai::stream` renders while streaming. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/provider-openai/src/register.rs b/provider-openai/src/register.rs index e989dbd65..d8175b0ca 100644 --- a/provider-openai/src/register.rs +++ b/provider-openai/src/register.rs @@ -2,7 +2,6 @@ //! declare-with-backoff loop (spec § Registration lifecycle). use crate::config::{DEFAULT_API_URL, DEFAULT_MAX_TOKENS}; use crate::discovery::{make_refresh_models, refresh_models}; -use crate::errors::invalid_request_from_serde; use crate::stream_fn::make_stream; use crate::surface; use crate::{router_client, state, PROVIDER_ID}; @@ -143,21 +142,20 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { iii.register_function( surface::STREAM_ID, - RegisterFunction::new_async_with_bad_request( - make_stream(iii.clone(), http.clone(), cache.clone(), aborts.clone()), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_stream( + iii.clone(), + http.clone(), + cache.clone(), + aborts.clone(), + )) .description(surface::STREAM_DESC) .metadata(json!({ "internal": true })), ); iii.register_function( surface::ABORT_ID, - RegisterFunction::new_async_with_bad_request( - make_abort(aborts), - invalid_request_from_serde, - ) - .description(surface::ABORT_DESC) - .metadata(json!({ "internal": true })), + RegisterFunction::new_async(make_abort(aborts)) + .description(surface::ABORT_DESC) + .metadata(json!({ "internal": true })), ); iii.register_function( surface::REFRESH_MODELS_ID, @@ -213,6 +211,7 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { function_id: surface::ON_ROUTER_READY_ID.into(), config: json!({}), metadata: None, + namespace: iii.namespace(), }); // Boot declare, off the boot path (a missing router must not block boot). diff --git a/provider-xai/Cargo.lock b/provider-xai/Cargo.lock index 9a15590e3..27d0d956a 100644 --- a/provider-xai/Cargo.lock +++ b/provider-xai/Cargo.lock @@ -880,9 +880,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -901,9 +901,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", @@ -996,7 +996,7 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "llm-router" -version = "1.4.1" +version = "1.4.2" dependencies = [ "async-trait", "clap", diff --git a/provider-xai/Cargo.toml b/provider-xai/Cargo.toml index b3d1473c7..0e3bab2af 100644 --- a/provider-xai/Cargo.toml +++ b/provider-xai/Cargo.toml @@ -22,7 +22,7 @@ path = "src/lib.rs" llm-router = { path = "../llm-router", default-features = false } # Must match llm-router's pin (one iii-sdk per graph). 0.21.5 brings the # live span-start push: `provider::xai::stream` renders while streaming. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/provider-xai/src/configuration.rs b/provider-xai/src/configuration.rs index 609a484b0..b92142091 100644 --- a/provider-xai/src/configuration.rs +++ b/provider-xai/src/configuration.rs @@ -18,7 +18,25 @@ use crate::config::WorkerConfig; /// Hot-swappable config snapshot shared with the stream handler. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "provider-xai"; +pub const DEFAULT_CONFIG_ID: &str = "provider-xai"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "provider::xai::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -31,7 +49,7 @@ pub fn new_cell() -> ConfigCell { /// when no stored value exists yet. pub async fn register_config(iii: &IIIClient) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "xAI Provider", "description": "xAI provider behaviour: enable Agent Tools (live X / web search via the /v1/responses API) and pick which server-side tools to offer. Credentials and endpoints are managed by the llm-router configuration entry, not here.", "schema": WorkerConfig::json_schema(), @@ -59,7 +77,7 @@ async fn should_seed_default(iii: &IIIClient) -> Result { /// `Ok(None)` when the entry does not exist (`NOT_FOUND`). async fn try_get_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -97,10 +115,11 @@ pub fn register_config_trigger(iii: &IIIClient, cell: ConfigCell) -> Result<(), trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/provider-xai/src/register.rs b/provider-xai/src/register.rs index 74741590a..126ddd326 100644 --- a/provider-xai/src/register.rs +++ b/provider-xai/src/register.rs @@ -2,7 +2,6 @@ //! declare-with-backoff loop (spec § Registration lifecycle). use crate::config::{DEFAULT_API_URL, DEFAULT_MAX_TOKENS}; use crate::discovery::{make_refresh_models, refresh_models}; -use crate::errors::invalid_request_from_serde; use crate::stream_fn::make_stream; use crate::surface; use crate::{router_client, state, PROVIDER_ID}; @@ -176,27 +175,21 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { iii.register_function( surface::STREAM_ID, - RegisterFunction::new_async_with_bad_request( - make_stream( - iii.clone(), - http.clone(), - cell.clone(), - cache.clone(), - aborts.clone(), - ), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_stream( + iii.clone(), + http.clone(), + cell.clone(), + cache.clone(), + aborts.clone(), + )) .description(surface::STREAM_DESC) .metadata(json!({ "internal": true })), ); iii.register_function( surface::ABORT_ID, - RegisterFunction::new_async_with_bad_request( - make_abort(aborts), - invalid_request_from_serde, - ) - .description(surface::ABORT_DESC) - .metadata(json!({ "internal": true })), + RegisterFunction::new_async(make_abort(aborts)) + .description(surface::ABORT_DESC) + .metadata(json!({ "internal": true })), ); iii.register_function( surface::REFRESH_MODELS_ID, @@ -242,6 +235,7 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { function_id: surface::ON_ROUTER_READY_ID.into(), config: json!({}), metadata: None, + namespace: iii.namespace(), }); // Boot declare, off the boot path (a missing router must not block boot). diff --git a/provider-zai/Cargo.lock b/provider-zai/Cargo.lock index e6bb227c0..a31fa2867 100644 --- a/provider-zai/Cargo.lock +++ b/provider-zai/Cargo.lock @@ -880,9 +880,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -901,9 +901,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/provider-zai/Cargo.toml b/provider-zai/Cargo.toml index e196e55ef..8714448f4 100644 --- a/provider-zai/Cargo.toml +++ b/provider-zai/Cargo.toml @@ -19,7 +19,7 @@ path = "src/lib.rs" llm-router = { path = "../llm-router", default-features = false } # Must match llm-router's pin (one iii-sdk per graph). 0.21.5 brings the # live span-start push: `provider::zai::stream` renders while streaming. -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" serde = { version = "1", features = ["derive"] } serde_json = "1" # Must stay on the same schemars major as iii-sdk so the derived diff --git a/provider-zai/src/register.rs b/provider-zai/src/register.rs index 5a61a5eea..9897da202 100644 --- a/provider-zai/src/register.rs +++ b/provider-zai/src/register.rs @@ -2,7 +2,6 @@ //! declare-with-backoff loop (spec § Registration lifecycle). use crate::config::{DEFAULT_API_URL, DEFAULT_MAX_TOKENS}; use crate::discovery::{make_refresh_models, refresh_models}; -use crate::errors::invalid_request_from_serde; use crate::stream_fn::make_stream; use crate::surface; use crate::{router_client, state, PROVIDER_ID}; @@ -146,21 +145,20 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { iii.register_function( surface::STREAM_ID, - RegisterFunction::new_async_with_bad_request( - make_stream(iii.clone(), http.clone(), cache.clone(), aborts.clone()), - invalid_request_from_serde, - ) + RegisterFunction::new_async(make_stream( + iii.clone(), + http.clone(), + cache.clone(), + aborts.clone(), + )) .description(surface::STREAM_DESC) .metadata(json!({ "internal": true })), ); iii.register_function( surface::ABORT_ID, - RegisterFunction::new_async_with_bad_request( - make_abort(aborts), - invalid_request_from_serde, - ) - .description(surface::ABORT_DESC) - .metadata(json!({ "internal": true })), + RegisterFunction::new_async(make_abort(aborts)) + .description(surface::ABORT_DESC) + .metadata(json!({ "internal": true })), ); iii.register_function( surface::REFRESH_MODELS_ID, @@ -201,6 +199,7 @@ pub async fn register_provider(iii: IIIClient) -> Result<(), Error> { function_id: surface::ON_ROUTER_READY_ID.into(), config: json!({}), metadata: None, + namespace: iii.namespace(), }); // Boot declare, off the boot path (a missing router must not block boot). diff --git a/pubsub/Cargo.lock b/pubsub/Cargo.lock index d02b9eac5..e0bf93771 100644 --- a/pubsub/Cargo.lock +++ b/pubsub/Cargo.lock @@ -696,9 +696,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -737,9 +737,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/pubsub/Cargo.toml b/pubsub/Cargo.toml index 9d7a5f578..406bc9f41 100644 --- a/pubsub/Cargo.toml +++ b/pubsub/Cargo.toml @@ -18,7 +18,7 @@ name = "pubsub" path = "src/main.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" async-trait = "0.1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } diff --git a/pubsub/src/configuration.rs b/pubsub/src/configuration.rs index 605dae2f0..38c2e74a5 100644 --- a/pubsub/src/configuration.rs +++ b/pubsub/src/configuration.rs @@ -24,7 +24,25 @@ use crate::boot::{ApplyLock, ConfigCell}; use crate::config::PubSubConfig; use crate::hub::Hub; -pub const CONFIG_ID: &str = "pubsub"; +pub const DEFAULT_CONFIG_ID: &str = "pubsub"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "pubsub::on-config-change"; const CONFIG_RETRIES: u32 = 3; const CONFIG_RETRY_BACKOFF_MS: u64 = 250; @@ -38,7 +56,7 @@ const CONFIG_BUS_TIMEOUT_MS: u64 = 10_000; /// included only when nothing is stored yet, so runtime edits survive restarts. pub async fn register_config(iii: &IIIClient, seed: Option<&PubSubConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "PubSub", "description": "PubSub worker settings — the pub/sub backend adapter (`local` in-process broadcast, or `redis` for cross-instance delivery). The adapter hot-swaps at runtime: a change rebuilds the backend and re-subscribes live subscriptions onto it.", "schema": PubSubConfig::json_schema(), @@ -64,7 +82,7 @@ pub async fn fetch_config(iii: &IIIClient) -> Result { match try_get_config_value(iii).await? { Some(value) if !value.is_null() => PubSubConfig::from_json(&value), _ => { - tracing::info!("no `{CONFIG_ID}` configuration value stored; using built-in default"); + tracing::info!("no `{config_entry}` configuration value stored; using built-in default", config_entry = config_id()); Ok(PubSubConfig::default()) } } @@ -81,7 +99,7 @@ async fn try_get_config_value(iii: &IIIClient) -> Result, String> match trigger_with_retry( iii, "configuration::get", - json!({ "id": CONFIG_ID }), + json!({ "id": config_id() }), CONFIG_BUS_TIMEOUT_MS, ) .await @@ -136,10 +154,11 @@ pub fn register_config_trigger( trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/pubsub/src/trigger.rs b/pubsub/src/trigger.rs index 49c3c21fd..93a7c9399 100644 --- a/pubsub/src/trigger.rs +++ b/pubsub/src/trigger.rs @@ -84,6 +84,7 @@ mod tests { function_id: "test::listener".to_string(), config: cfg, metadata: None, + namespace: None, } } diff --git a/pubsub/tests/e2e_pubsub.rs b/pubsub/tests/e2e_pubsub.rs index 5f015e8a0..bafebdcdf 100644 --- a/pubsub/tests/e2e_pubsub.rs +++ b/pubsub/tests/e2e_pubsub.rs @@ -47,6 +47,7 @@ async fn publish_fans_out_raw_data_to_subscribe_triggers() { function_id: fn_id.to_string(), config: json!({"topic": "e2e.orders"}), metadata: None, + namespace: iii.namespace(), }) .expect("trigger registration"); } diff --git a/queue/Cargo.lock b/queue/Cargo.lock index e020ec87f..23102a28e 100644 --- a/queue/Cargo.lock +++ b/queue/Cargo.lock @@ -1217,9 +1217,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -1263,9 +1263,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/queue/Cargo.toml b/queue/Cargo.toml index 718fe9865..7e584071b 100644 --- a/queue/Cargo.toml +++ b/queue/Cargo.toml @@ -27,8 +27,8 @@ rabbitmq = ["dep:lapin"] test-adapters = [] [dependencies] -iii-sdk = "=0.21.6" -iii-helpers = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" +iii-helpers = "=0.22.0-alpha.3" async-trait = "0.1" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } diff --git a/queue/src/configuration.rs b/queue/src/configuration.rs index c93d10ec3..434da3342 100644 --- a/queue/src/configuration.rs +++ b/queue/src/configuration.rs @@ -15,7 +15,25 @@ use crate::trigger::QueueTriggerHandler; #[cfg(test)] use crate::{adapter::SwappableAdapter, boot, trigger::Invoker}; -pub const CONFIG_ID: &str = "queue"; +pub const DEFAULT_CONFIG_ID: &str = "queue"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} pub const CONFIG_FN_ID: &str = "queue::on-config-change"; const CONFIG_RETRIES: u32 = 3; const CONFIG_RETRY_BACKOFF_MS: u64 = 250; @@ -27,7 +45,7 @@ pub fn new_cell(config: QueueConfig) -> ConfigCell { pub async fn register_config(iii: &IIIClient, seed: Option<&QueueConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Queue", "description": "Durable queue worker settings: transport persistence and named function queues.", "schema": QueueConfig::json_schema(), @@ -54,7 +72,8 @@ pub async fn fetch_config(iii: &IIIClient) -> Result { Some(value) if !value.is_null() => QueueConfig::from_json(&value).map(|c| c.normalized()), _ => { tracing::info!( - "no `{CONFIG_ID}` configuration value stored; using durable packaged default" + "no `{config_entry}` configuration value stored; using durable packaged default", + config_entry = config_id() ); Ok(QueueConfig::packaged_default()) } @@ -69,7 +88,7 @@ pub async fn persist_config(iii: &IIIClient, config: &QueueConfig) -> Result<(), trigger_with_retry( iii, "configuration::set", - json!({ "id": CONFIG_ID, "value": config.to_json() }), + json!({ "id": config_id(), "value": config.to_json() }), CONFIG_BUS_TIMEOUT_MS, ) .await?; @@ -98,10 +117,11 @@ pub fn register_config_trigger( trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } @@ -155,7 +175,7 @@ async fn try_get_config_value(iii: &IIIClient) -> Result, String> match trigger_with_retry( iii, "configuration::get", - json!({ "id": CONFIG_ID }), + json!({ "id": config_id() }), CONFIG_BUS_TIMEOUT_MS, ) .await diff --git a/queue/src/trigger.rs b/queue/src/trigger.rs index 2c3ad9c18..697314c90 100644 --- a/queue/src/trigger.rs +++ b/queue/src/trigger.rs @@ -545,6 +545,7 @@ mod tests { function_id: function_id.to_string(), config, metadata: None, + namespace: None, } } diff --git a/queue/tests/e2e_durability.rs b/queue/tests/e2e_durability.rs index 63560ba05..f42af2edc 100644 --- a/queue/tests/e2e_durability.rs +++ b/queue/tests/e2e_durability.rs @@ -87,6 +87,7 @@ fn register_subscriber_with_config( function_id: function_id.to_string(), config, metadata: None, + namespace: iii.namespace(), }) .expect("register durable subscriber trigger"); } diff --git a/rbac-proxy/Cargo.lock b/rbac-proxy/Cargo.lock index 7b06d7363..b770a0f6e 100644 --- a/rbac-proxy/Cargo.lock +++ b/rbac-proxy/Cargo.lock @@ -692,9 +692,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -713,9 +713,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/rbac-proxy/Cargo.toml b/rbac-proxy/Cargo.toml index c7c0b9700..a37084107 100644 --- a/rbac-proxy/Cargo.toml +++ b/rbac-proxy/Cargo.toml @@ -15,7 +15,7 @@ name = "rbac_proxy" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "net", "io-util", "time"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/rbac-proxy/src/configuration.rs b/rbac-proxy/src/configuration.rs index 9ae76e60e..a102f52b2 100644 --- a/rbac-proxy/src/configuration.rs +++ b/rbac-proxy/src/configuration.rs @@ -33,7 +33,25 @@ use crate::server::ServerHandle; /// reload whole-snapshot replaces the inner `Arc` under the write lock. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "rbac-proxy"; +pub const DEFAULT_CONFIG_ID: &str = "rbac-proxy"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "rbac-proxy::on-config-change"; const FUNCTIONS_AVAILABLE_FN_ID: &str = "rbac-proxy::on-functions-available"; const CONFIG_RETRIES: u32 = 3; @@ -50,7 +68,7 @@ pub async fn register_config( seed: Option<&WorkerConfig>, ) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "RBAC Proxy", "description": "Boundary-proxy settings: the public RBAC port, the trusted upstream engine URL, \ the RBAC contract (auth function, expose filters, registration hooks), middleware, \ @@ -101,13 +119,13 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { async fn get_config_value(iii: &IIIClient) -> Result { try_get_config_value(iii) .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + .ok_or_else(|| format!("configuration `{config_entry}` not found", config_entry = config_id())) } /// `Ok(None)` when the entry does not exist. Engine missing-entry codes vary in /// case, so match case-insensitively. async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.to_ascii_uppercase().contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -167,10 +185,11 @@ pub fn register_config_trigger( trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } @@ -248,6 +267,7 @@ pub fn bind_catalog_refresh(iii: &Arc, catalog: Arc) { function_id: FUNCTIONS_AVAILABLE_FN_ID.to_string(), config: json!({}), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => tracing::info!("bound catalog-refresh trigger (engine::functions-available)"), Err(e) => { diff --git a/sandbox-code-runner/Cargo.lock b/sandbox-code-runner/Cargo.lock index 887e3bfce..17a4cce71 100644 --- a/sandbox-code-runner/Cargo.lock +++ b/sandbox-code-runner/Cargo.lock @@ -679,9 +679,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -700,9 +700,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/sandbox-code-runner/Cargo.toml b/sandbox-code-runner/Cargo.toml index a2a780ead..eba8fe31e 100644 --- a/sandbox-code-runner/Cargo.toml +++ b/sandbox-code-runner/Cargo.toml @@ -14,8 +14,8 @@ path = "src/main.rs" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" -iii-helpers = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" +iii-helpers = "=0.22.0-alpha.3" # Worker-side injectable console UI (content function + console:script/style # triggers + hot-reload watcher) — direct link, never published. iii-console-ui = { path = "../crates/console-ui" } diff --git a/sandbox-code-runner/src/functions/mod.rs b/sandbox-code-runner/src/functions/mod.rs index 37e4cff58..321e7d0ac 100644 --- a/sandbox-code-runner/src/functions/mod.rs +++ b/sandbox-code-runner/src/functions/mod.rs @@ -144,6 +144,7 @@ pub fn setup_harness_hooks(iii: &Arc) { function_id: inject_guidance::GUIDANCE_HOOK_ID.to_string(), config: serde_json::json!({ "on_error": "fail_open" }), metadata: None, + namespace: iii.namespace(), }) { Ok(_) => tracing::info!( "sandbox-code-runner pre-generate hook bound (guidance injection active)" diff --git a/scrapling/pyproject.toml b/scrapling/pyproject.toml index 5f89bb57f..aafb89fcb 100644 --- a/scrapling/pyproject.toml +++ b/scrapling/pyproject.toml @@ -13,7 +13,8 @@ authors = [{ name = "iii" }] license = "Apache-2.0" requires-python = ">=3.11" dependencies = [ - "iii-sdk==0.21.6", + "iii-sdk==0.22.0a3", + "iii-helpers==0.22.0a3", # transitive iii-sdk dep; pinned so uv allows the pre-release "scrapling[fetchers]==0.4.9", "markdownify", # scrapling's HTML→Markdown backend (only in its [shell]/[ai] extras) "pillow", # screenshot downscale/tiling so images fit model input limits diff --git a/scrapling/src/main.py b/scrapling/src/main.py index ff6227a4c..ddc9b1c4f 100644 --- a/scrapling/src/main.py +++ b/scrapling/src/main.py @@ -61,7 +61,15 @@ def main() -> None: idle_timeout=cfg.get("session_idle_timeout_s") or None, ) - iii = register_worker(address=url, options=InitOptions(worker_name="scrapling")) + # namespace: register under III_NAMESPACE when set (the SDK also resolves the + # env; passed explicitly for visibility). Absent -> the engine's default. + iii = register_worker( + address=url, + options=InitOptions( + worker_name="scrapling", + namespace=os.environ.get("III_NAMESPACE"), + ), + ) handlers = create_handlers(load_config, iii=iii) for spec in FUNCTIONS: diff --git a/session-manager/Cargo.lock b/session-manager/Cargo.lock index cde75e50a..c4c445dfb 100644 --- a/session-manager/Cargo.lock +++ b/session-manager/Cargo.lock @@ -914,9 +914,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -935,9 +935,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/session-manager/Cargo.toml b/session-manager/Cargo.toml index 4702d23ee..fc52a9709 100644 --- a/session-manager/Cargo.toml +++ b/session-manager/Cargo.toml @@ -15,10 +15,10 @@ name = "session_manager" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" # `run_with_baggage` for call-site span hiding (`iii.tag.hidden` on the # session-event fan-out — see src/events.rs `IiiDeliverer`). -iii-helpers = "=0.21.6" +iii-helpers = "=0.22.0-alpha.3" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/session-manager/src/configuration.rs b/session-manager/src/configuration.rs index a3defb874..06864550b 100644 --- a/session-manager/src/configuration.rs +++ b/session-manager/src/configuration.rs @@ -42,7 +42,25 @@ use crate::runtime::{build_runtime, SessionBuildContext, SessionRuntime}; /// replaces the inner `Arc` under the write lock. pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "session-manager"; +pub const DEFAULT_CONFIG_ID: &str = "session-manager"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "session::on-config-change"; const CONFIG_STATUS_FN_ID: &str = "session::config-status"; const CONFIG_TIMEOUT_MS: u64 = 5_000; @@ -138,7 +156,7 @@ impl ReloadStatus { /// this is safe to call every boot). pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Session Manager", "description": "Durable conversation store settings: the storage adapter (fs / bridge) \ and its config, plus the default and maximum page sizes for \ @@ -176,14 +194,14 @@ async fn should_seed_default_value(iii: &IIIClient) -> Result { async fn get_config_value(iii: &IIIClient) -> Result { try_get_config_value(iii) .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + .ok_or_else(|| format!("configuration `{config_entry}` not found", config_entry = config_id())) } /// Returns `Ok(None)` when the entry does not exist. The engine's /// missing-entry codes vary in case (`function_not_found`, /// `STATEMENT_NOT_FOUND`, `NOT_FOUND`), so match case-insensitively. async fn try_get_config_value(iii: &IIIClient) -> Result, String> { - match trigger_with_retry(iii, "configuration::get", json!({ "id": CONFIG_ID })).await { + match trigger_with_retry(iii, "configuration::get", json!({ "id": config_id() })).await { Ok(resp) => Ok(resp.get("value").cloned()), Err(e) if e.to_ascii_uppercase().contains("NOT_FOUND") => Ok(None), Err(e) => Err(e), @@ -359,10 +377,11 @@ pub fn register_config_trigger(iii: &IIIClient, state: AppState) -> Result<(), E trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/session-manager/src/events.rs b/session-manager/src/events.rs index d502aaa62..db89dbcd5 100644 --- a/session-manager/src/events.rs +++ b/session-manager/src/events.rs @@ -992,6 +992,7 @@ pub fn attach_bridge_relay(remote: &Arc, local_emitter: Arc) function_id: relay_id.clone(), config: serde_json::json!({}), metadata: None, + namespace: remote.namespace(), }) { Ok(_) => tracing::info!(relay = %relay_id, "attached to the main instance's event feed"), Err(e) => tracing::warn!( @@ -1284,6 +1285,7 @@ mod tests { function_id: "ui::recv".into(), config: json!({}), metadata: None, + namespace: None, }) .unwrap(); @@ -1312,6 +1314,7 @@ mod tests { function_id: "ui::recv".into(), config: json!({ "session_id": "s_1", "roles": ["assistant"] }), metadata: None, + namespace: None, }) .unwrap(); assert_eq!(set.len(), 1); @@ -1321,6 +1324,7 @@ mod tests { function_id: "ui::recv".into(), config: json!({ "bogus": true }), metadata: None, + namespace: None, }); assert!(err.is_err()); assert_eq!(set.len(), 1); diff --git a/session-manager/src/resync.rs b/session-manager/src/resync.rs index c0c50b0fd..2f2ab4590 100644 --- a/session-manager/src/resync.rs +++ b/session-manager/src/resync.rs @@ -200,6 +200,7 @@ mod tests { function_id: "test::recv".to_string(), config: json!({}), metadata: None, + namespace: None, }) .unwrap(); } diff --git a/session-manager/tests/steps/binding_steps.rs b/session-manager/tests/steps/binding_steps.rs index 50f7a338b..8564dea8f 100644 --- a/session-manager/tests/steps/binding_steps.rs +++ b/session-manager/tests/steps/binding_steps.rs @@ -31,6 +31,7 @@ fn add_binding( function_id: function_id.to_string(), config, metadata: None, + namespace: None, }) } diff --git a/session-manager/tests/steps/bridge_steps.rs b/session-manager/tests/steps/bridge_steps.rs index 37d70b556..d3bddaa2e 100644 --- a/session-manager/tests/steps/bridge_steps.rs +++ b/session-manager/tests/steps/bridge_steps.rs @@ -72,6 +72,7 @@ async fn bridge_local_binding( function_id, config, metadata: None, + namespace: None, }) .expect("bridge-local binding config must be valid"); } diff --git a/session-manager/tests/steps/engine_steps.rs b/session-manager/tests/steps/engine_steps.rs index 7aacf66d3..a13d19d27 100644 --- a/session-manager/tests/steps/engine_steps.rs +++ b/session-manager/tests/steps/engine_steps.rs @@ -176,6 +176,7 @@ async fn engine_subscriber( function_id: function_id.clone(), config, metadata: None, + namespace: iii.namespace(), }) .expect("register engine trigger"); // And a beat for the registration to round-trip to our handler. diff --git a/shell/Cargo.lock b/shell/Cargo.lock index 3f760bf09..bd65c6437 100644 --- a/shell/Cargo.lock +++ b/shell/Cargo.lock @@ -905,9 +905,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "5090e8ba42874e09f8fa82419a0ef9f150eae4301eb64272cd0a184b548812fc" dependencies = [ "futures-util", "opentelemetry", @@ -926,9 +926,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/shell/Cargo.toml b/shell/Cargo.toml index 22035c884..c8f662a20 100644 --- a/shell/Cargo.toml +++ b/shell/Cargo.toml @@ -11,8 +11,8 @@ name = "shell" path = "src/main.rs" [dependencies] -iii-sdk = "=0.21.6" -iii-helpers = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" +iii-helpers = "=0.22.0-alpha.3" iii-console-ui = { path = "../crates/console-ui" } schemars = { version = "0.8", features = ["uuid1"] } libc = "0.2" diff --git a/shell/src/configuration.rs b/shell/src/configuration.rs index 1cf7868b8..88de54e9b 100644 --- a/shell/src/configuration.rs +++ b/shell/src/configuration.rs @@ -17,7 +17,25 @@ use crate::config::ShellConfig; use crate::fs::host::{HostFsBackend, HostFsConfig, IiiChannelMaker}; use crate::fs::FsBackend; -pub const CONFIG_ID: &str = "shell"; +pub const DEFAULT_CONFIG_ID: &str = "shell"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "shell::on-config-change"; const CONFIG_TIMEOUT_MS: u64 = 5_000; const CONFIG_RETRIES: u32 = 3; @@ -180,7 +198,7 @@ pub fn build_runtime(cfg: &ShellConfig, iii: &IIIClient) -> Result) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Shell", "description": "Command denylist, timeout & output caps, and the fs jail.", "schema": ShellConfig::json_schema(), @@ -258,14 +276,14 @@ fn parse_fetched_value(value: Value) -> Result { async fn get_config_value(iii: &IIIClient) -> Result { try_get_config_value(iii, false) .await? - .ok_or_else(|| format!("configuration `{CONFIG_ID}` not found")) + .ok_or_else(|| format!("configuration `{config_entry}` not found", config_entry = config_id())) } async fn try_get_config_value(iii: &IIIClient, raw: bool) -> Result, String> { match trigger_with_retry( iii, "configuration::get", - json!({ "id": CONFIG_ID, "raw": raw }), + json!({ "id": config_id(), "raw": raw }), ) .await { @@ -360,10 +378,11 @@ pub fn register_config_trigger(iii: &IIIClient, state: AppState) -> Result<(), E trigger_type: "configuration".to_string(), function_id: CONFIG_FN_ID.to_string(), config: json!({ - "configuration_id": CONFIG_ID, + "configuration_id": config_id(), "event_types": ["configuration:updated"], }), metadata: None, + namespace: iii.namespace(), })?; Ok(()) } diff --git a/slack/Cargo.lock b/slack/Cargo.lock index d03da3e72..3776dd224 100644 --- a/slack/Cargo.lock +++ b/slack/Cargo.lock @@ -657,9 +657,9 @@ dependencies = [ [[package]] name = "iii-helpers" -version = "0.21.6" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d84d5c149ae4404365a79feca28aa66f6a7dbed56423b4b8c4e2421e0b5add" +checksum = "d990b866c3ef708290e1160f5640721130a8ebc28afa65b387551ee358f0a616" dependencies = [ "futures-util", "opentelemetry", @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "iii-sdk" -version = "0.21.6" +version = "0.22.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dd060fddcc9153b0dd07c038a14cf172ce15ce1d4edb98155563ed55b2caba" +checksum = "89e2b076a2227fea8175522eb184c3623af102841a641ec2ff90a8936215d982" dependencies = [ "async-trait", "futures-util", diff --git a/slack/Cargo.toml b/slack/Cargo.toml index f7f1d0816..bfdcf8428 100644 --- a/slack/Cargo.toml +++ b/slack/Cargo.toml @@ -15,7 +15,7 @@ name = "slack" path = "src/lib.rs" [dependencies] -iii-sdk = "=0.21.6" +iii-sdk = "=0.22.0-alpha.3" tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/slack/src/clients/approval.rs b/slack/src/clients/approval.rs index 1e861d6af..8647ca5a9 100644 --- a/slack/src/clients/approval.rs +++ b/slack/src/clients/approval.rs @@ -12,7 +12,7 @@ pub async fn resolve( allow: bool, timeout_ms: u64, ) -> Result<(), Error> { - iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: "approval::resolve".into(), payload: json!({ "session_id": session_id, @@ -21,8 +21,11 @@ pub async fn resolve( }), action: None, timeout_ms: Some(timeout_ms), - }) - .await?; + }; + match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await?, + None => iii.trigger(request).await?, + }; Ok(()) } @@ -32,12 +35,15 @@ pub async fn approve_always( function_id: &str, timeout_ms: u64, ) -> Result<(), Error> { - iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: "approval::approve-always".into(), payload: json!({ "session_id": session_id, "function_id": function_id }), action: None, timeout_ms: Some(timeout_ms), - }) - .await?; + }; + match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await?, + None => iii.trigger(request).await?, + }; Ok(()) } diff --git a/slack/src/clients/harness.rs b/slack/src/clients/harness.rs index 9ef0ee943..614ce7704 100644 --- a/slack/src/clients/harness.rs +++ b/slack/src/clients/harness.rs @@ -50,25 +50,30 @@ pub async fn send( req: SendRequest, timeout_ms: u64, ) -> Result { - let value = iii - .trigger(TriggerRequest { - function_id: "harness::send".into(), - payload: serde_json::to_value(&req).unwrap_or(Value::Null), - action: None, - timeout_ms: Some(timeout_ms), - }) - .await?; + let request = TriggerRequest { + function_id: "harness::send".into(), + payload: serde_json::to_value(&req).unwrap_or(Value::Null), + action: None, + timeout_ms: Some(timeout_ms), + }; + let value = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await?, + None => iii.trigger(request).await?, + }; serde_json::from_value(value).map_err(|e| Error::Handler(format!("harness::send: {e}"))) } pub async fn stop(iii: &IIIClient, session_id: &str, timeout_ms: u64) -> Result<(), Error> { - iii.trigger(TriggerRequest { + let request = TriggerRequest { function_id: "harness::stop".into(), payload: json!({ "session_id": session_id }), action: None, timeout_ms: Some(timeout_ms), - }) - .await?; + }; + match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await?, + None => iii.trigger(request).await?, + }; Ok(()) } diff --git a/slack/src/clients/router.rs b/slack/src/clients/router.rs index 0789251e9..970124a81 100644 --- a/slack/src/clients/router.rs +++ b/slack/src/clients/router.rs @@ -21,14 +21,16 @@ struct ModelsListResponse { } pub async fn list_models(iii: &IIIClient, timeout_ms: u64) -> Result, Error> { - let value = iii - .trigger(TriggerRequest { - function_id: "router::models::list".into(), - payload: json!({ "capability": "tools" }), - action: None, - timeout_ms: Some(timeout_ms), - }) - .await?; + let request = TriggerRequest { + function_id: "router::models::list".into(), + payload: json!({ "capability": "tools" }), + action: None, + timeout_ms: Some(timeout_ms), + }; + let value = match iii.namespace() { + Some(ns) => iii.trigger(request.namespace(ns)).await?, + None => iii.trigger(request).await?, + }; let resp: ModelsListResponse = serde_json::from_value(value) .map_err(|e| Error::Handler(format!("router::models::list: {e}")))?; Ok(resp.models) diff --git a/slack/src/configuration.rs b/slack/src/configuration.rs index 7ba69373d..4d2d355d8 100644 --- a/slack/src/configuration.rs +++ b/slack/src/configuration.rs @@ -16,7 +16,25 @@ use crate::ingress; pub type ConfigCell = Arc>>; -pub const CONFIG_ID: &str = "slack"; +pub const DEFAULT_CONFIG_ID: &str = "slack"; + +/// The configuration entry this worker owns. +/// +/// `III_CONFIG_NAME` when a supervisor set it, else the built-in name. A worker +/// that hardcodes its id turns that id into a global scarce name: two instances +/// share one entry and take turns overwriting it, and each write wakes both. +/// Being told which entry is its own is what lets them differ. +pub fn config_id() -> &'static str { + static ID: std::sync::OnceLock = std::sync::OnceLock::new(); + ID.get_or_init(|| { + std::env::var("III_CONFIG_NAME") + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_CONFIG_ID.to_string()) + }) + .as_str() +} const CONFIG_FN_ID: &str = "slack::on-config-change"; const CONFIG_RETRIES: u32 = 3; const CONFIG_RETRY_BACKOFF_MS: u64 = 250; @@ -28,7 +46,7 @@ fn config_rpc_timeout_ms(seed: Option<&WorkerConfig>) -> u64 { pub async fn register_config(iii: &IIIClient, seed: Option<&WorkerConfig>) -> Result<(), String> { let mut payload = json!({ - "id": CONFIG_ID, + "id": config_id(), "name": "Slack", "description": "Slack worker: bot/user tokens, optional channel/team scoping, and RPC timeout. Configurable here or via env (${SLACK_BOT_TOKEN}).", "schema": WorkerConfig::json_schema(), @@ -76,7 +94,7 @@ async fn try_get_config_value(iii: &IIIClient, timeout_ms: u64) -> Result