diff --git a/.github/workflows/proto-check.yml b/.github/workflows/proto-check.yml index c7e24af..25de234 100644 --- a/.github/workflows/proto-check.yml +++ b/.github/workflows/proto-check.yml @@ -7,12 +7,16 @@ on: - 'proto/**' - 'python/amplifier_core/_grpc_gen/**' - 'crates/amplifier-core/src/generated/**' + - 'scripts/generate_grpc_stubs.py' + - '.github/workflows/proto-check.yml' pull_request: branches: [main] paths: - 'proto/**' - 'python/amplifier_core/_grpc_gen/**' - 'crates/amplifier-core/src/generated/**' + - 'scripts/generate_grpc_stubs.py' + - '.github/workflows/proto-check.yml' jobs: proto-sync: @@ -32,32 +36,18 @@ jobs: python-version: '3.12' - name: Install grpcio-tools - run: pip install grpcio-tools + run: pip install grpcio-tools==1.78.0 - name: Regenerate Python stubs run: | - mkdir -p /tmp/proto_check - python -m grpc_tools.protoc \ - -Iproto \ - --python_out=/tmp/proto_check \ - --grpc_python_out=/tmp/proto_check \ - proto/amplifier_module.proto + python scripts/generate_grpc_stubs.py --output /tmp/proto_check - name: Compare with committed stubs run: | - # Check that key message classes exist in regenerated output - python -c " - import importlib.util, sys - spec = importlib.util.spec_from_file_location('pb2', '/tmp/proto_check/amplifier_module_pb2.py') - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - required = ['ChatRequest', 'ChatResponse', 'ToolResult', 'HookResult', 'ModuleInfo'] - missing = [t for t in required if not hasattr(mod, t)] - if missing: - print(f'FAIL: Missing types in regenerated stubs: {missing}') - sys.exit(1) - print('PASS: Regenerated stubs contain all required types') - " + diff -u python/amplifier_core/_grpc_gen/amplifier_module_pb2.py \ + /tmp/proto_check/amplifier_module_pb2.py + diff -u python/amplifier_core/_grpc_gen/amplifier_module_pb2_grpc.py \ + /tmp/proto_check/amplifier_module_pb2_grpc.py rust-build: name: Rust build and test diff --git a/README.md b/README.md index a9c65f8..3f76052 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,14 @@ maturin develop uv run maturin develop ``` +To regenerate the committed Python gRPC stubs, install the pinned generator +and run the canonical command: + +```bash +python -m pip install grpcio-tools==1.78.0 +python scripts/generate_grpc_stubs.py +``` + See [docs/RUST_CORE_TESTING.md](docs/RUST_CORE_TESTING.md) for the full development setup guide. **Build dependencies**: Rust 1.70+, maturin diff --git a/bindings/node/index.d.ts b/bindings/node/index.d.ts index b06fa5d..12f6e39 100644 --- a/bindings/node/index.d.ts +++ b/bindings/node/index.d.ts @@ -31,6 +31,14 @@ export const enum UserMessageLevel { Warning = 'Warning', Error = 'Error' } +export interface JsContextInjection { + content: string + role: ContextInjectionRole + ephemeral: boolean + appendToLastToolResult: boolean + hookName: string + event: string +} export interface JsHookResult { action: HookAction reason?: string @@ -44,6 +52,7 @@ export interface JsHookResult { approvalPrompt?: string approvalTimeout?: number approvalDefault?: ApprovalDefault + contextInjections?: Array } /** Structured error object returned to JS with a typed `code` property. */ export interface JsAmplifierError { diff --git a/bindings/node/src/hook_result.rs b/bindings/node/src/hook_result.rs index f0a528d..4c94bde 100644 --- a/bindings/node/src/hook_result.rs +++ b/bindings/node/src/hook_result.rs @@ -2,10 +2,20 @@ // Structs — exported as TypeScript interfaces via #[napi(object)] // --------------------------------------------------------------------------- -use amplifier_core::models::HookResult; +use amplifier_core::models::{ContextInjection, HookResult}; use crate::enums::{ApprovalDefault, ContextInjectionRole, HookAction, UserMessageLevel}; +#[napi(object)] +pub struct JsContextInjection { + pub content: String, + pub role: ContextInjectionRole, + pub ephemeral: bool, + pub append_to_last_tool_result: bool, + pub hook_name: String, + pub event: String, +} + #[napi(object)] pub struct JsHookResult { pub action: HookAction, @@ -20,6 +30,7 @@ pub struct JsHookResult { pub approval_prompt: Option, pub approval_timeout: Option, pub approval_default: Option, + pub context_injections: Option>, } // --------------------------------------------------------------------------- @@ -40,5 +51,23 @@ pub(crate) fn hook_result_to_js(result: HookResult) -> JsHookResult { approval_prompt: result.approval_prompt, approval_timeout: Some(result.approval_timeout), approval_default: Some(result.approval_default.into()), + context_injections: Some( + result + .context_injections + .into_iter() + .map(context_injection_to_js) + .collect(), + ), + } +} + +fn context_injection_to_js(injection: ContextInjection) -> JsContextInjection { + JsContextInjection { + content: injection.content, + role: injection.role.into(), + ephemeral: injection.ephemeral, + append_to_last_tool_result: injection.append_to_last_tool_result, + hook_name: injection.hook_name, + event: injection.event, } } diff --git a/bindings/node/src/lib.rs b/bindings/node/src/lib.rs index 3e4f5f2..9f5b196 100644 --- a/bindings/node/src/lib.rs +++ b/bindings/node/src/lib.rs @@ -33,7 +33,7 @@ pub use enums::{ ApprovalDefault, ContextInjectionRole, HookAction, SessionState, UserMessageLevel, }; pub use errors::{amplifier_error_to_js, JsAmplifierError}; -pub use hook_result::JsHookResult; +pub use hook_result::{JsContextInjection, JsHookResult}; pub use hooks::JsHookRegistry; pub use module_resolver::{load_wasm_from_path, resolve_module, JsModuleManifest}; pub use session::JsAmplifierSession; diff --git a/bindings/python/src/coordinator/hook_dispatch.rs b/bindings/python/src/coordinator/hook_dispatch.rs index ffa8b40..e5baba6 100644 --- a/bindings/python/src/coordinator/hook_dispatch.rs +++ b/bindings/python/src/coordinator/hook_dispatch.rs @@ -34,6 +34,16 @@ fn truncate_for_log(s: &str, max_chars: usize) -> String { } } +struct PreparedInjection { + content: String, + role: String, + ephemeral: bool, + append_to_last_tool_result: bool, + hook_name: String, + event: String, + tokens: usize, +} + #[pymethods] impl PyCoordinator { /// Process a HookResult and route actions to appropriate subsystems. @@ -118,6 +128,10 @@ impl PyCoordinator { let approval_timeout: f64 = result.getattr("approval_timeout")?.extract()?; let approval_default: String = result.getattr("approval_default")?.extract()?; + if action == "inject_context" { + return self.process_context_injections(py, result, event, hook_name); + } + // Read coordinator config let size_limit: Option = { let val = self.get_injection_size_limit(py)?; @@ -505,6 +519,296 @@ impl PyCoordinator { } impl PyCoordinator { + fn process_context_injections<'py>( + &mut self, + py: Python<'py>, + result: Bound<'py, PyAny>, + event: String, + hook_name: &str, + ) -> PyResult> { + let scalar_content: Option = { + let value = result.getattr("context_injection")?; + (!value.is_none()).then(|| value.extract()).transpose()? + }; + let scalar_role: String = result.getattr("context_injection_role")?.extract()?; + let scalar_ephemeral: bool = result.getattr("ephemeral")?.extract()?; + let scalar_append: bool = result.getattr("append_to_last_tool_result")?.extract()?; + let suppress_output: bool = result.getattr("suppress_output")?.extract()?; + let user_message: Option = { + let value = result.getattr("user_message")?; + (!value.is_none()).then(|| value.extract()).transpose()? + }; + let user_message_level: String = result.getattr("user_message_level")?.extract()?; + let user_message_source: Option = { + let value = result.getattr("user_message_source")?; + (!value.is_none()).then(|| value.extract()).transpose()? + }; + + let size_limit: Option = { + let value = self.get_injection_size_limit(py)?; + (!value.bind(py).is_none()) + .then(|| value.extract(py)) + .transpose()? + }; + let budget: Option = { + let value = self.get_injection_budget_per_turn(py)?; + (!value.bind(py).is_none()) + .then(|| value.extract(py)) + .transpose()? + }; + + let items = result.getattr("context_injections")?; + let mut raw_items = Vec::new(); + for item in items.try_iter()? { + let item = item?; + raw_items.push(( + item.getattr("content")?.extract::()?, + item.getattr("role")?.extract::()?, + item.getattr("ephemeral")?.extract::()?, + item.getattr("append_to_last_tool_result")? + .extract::()?, + item.getattr("hook_name")?.extract::()?, + item.getattr("event")?.extract::()?, + )); + } + + // Scalar-only results predate the structured carrier. Normalize them + // here for direct callers that have not passed through HookRegistry. + if raw_items.is_empty() { + if let Some(content) = scalar_content { + raw_items.push(( + content, + scalar_role, + scalar_ephemeral, + scalar_append, + hook_name.to_string(), + event.clone(), + )); + } + } + + const CHARS_PER_TOKEN_ESTIMATE: usize = 4; + let sanitize = py + .import("amplifier_core.models")? + .getattr("_sanitize_for_llm")?; + let mut prepared = Vec::new(); + for (content, role, ephemeral, append, item_hook_name, item_event) in raw_items { + let sanitized_content: String = sanitize + .call1((content,))? + .extract() + .map_err(|error| { + log::error!( + "SECURITY: Sanitization unavailable for hook '{}' — rejecting injection: {error}", + item_hook_name + ); + PyValueError::new_err( + "Context injection rejected: content sanitization function unavailable", + ) + })?; + if sanitized_content.is_empty() { + continue; + } + let char_count = sanitized_content.chars().count(); + if let Some(limit) = size_limit { + if char_count > limit { + log::error!( + "Hook injection too large: {} (chars={}, limit={})", + item_hook_name, + char_count, + limit + ); + return Err(PyValueError::new_err(format!( + "Context injection exceeds {} characters", + limit + ))); + } + } + prepared.push(PreparedInjection { + content: sanitized_content, + role, + ephemeral, + append_to_last_tool_result: append, + hook_name: item_hook_name, + event: item_event, + tokens: char_count / CHARS_PER_TOKEN_ESTIMATE, + }); + } + + // Every item is validated before the budget state is charged or any + // context write begins. Budget is still advisory, as it was for the + // legacy scalar path, but applies to the total item sequence. + let mut running_total = self.current_turn_injections; + for item in &prepared { + if let Some(limit) = budget { + if running_total + item.tokens > limit { + log::warn!( + "Warning: Hook injection budget exceeded \ + (hook={}, current={}, attempted={}, budget={})", + item.hook_name, + running_total, + item.tokens, + limit + ); + } + } + running_total += item.tokens; + } + self.current_turn_injections = running_total; + + let context_obj: Py = { + let mount_points = self.mount_points.bind(py); + match mount_points.get_item("context")? { + Some(context) if !context.is_none() => context.unbind(), + _ => py.None(), + } + }; + let has_context = { + let context = context_obj.bind(py); + !context.is_none() && context.hasattr("add_message")? + }; + + let now = if has_context && prepared.iter().any(|item| !item.ephemeral) { + let datetime = py.import("datetime")?; + let utc = datetime.getattr("timezone")?.getattr("utc")?; + Some( + datetime + .getattr("datetime")? + .call_method1("now", (utc,))? + .call_method0("isoformat")? + .unbind(), + ) + } else { + None + }; + + let mut messages = Vec::new(); + let mut residual = Vec::new(); + for item in prepared { + log::info!( + "Hook context injection \ + (hook={}, event={}, chars={}, role={}, tokens={}, ephemeral={})", + item.hook_name, + item.event, + item.content.chars().count(), + item.role, + item.tokens, + item.ephemeral + ); + if item.ephemeral { + residual.push(item); + continue; + } + if has_context { + let metadata = PyDict::new(py); + metadata.set_item("source", "hook")?; + metadata.set_item("hook_name", &item.hook_name)?; + metadata.set_item("event", &item.event)?; + metadata.set_item("timestamp", now.as_ref().unwrap().bind(py))?; + metadata.set_item("requested_role", &item.role)?; + metadata.set_item("ephemeral", false)?; + metadata.set_item("persisted", true)?; + + let message = PyDict::new(py); + message.set_item("role", &item.role)?; + message.set_item("content", &item.content)?; + message.set_item("metadata", metadata)?; + messages.push(message.into_any().unbind()); + } + } + + if let Some(message) = user_message.filter(|message| !message.is_empty()) { + let source = user_message_source.as_deref().unwrap_or(hook_name); + let display = self.display_system_obj.bind(py); + if display.is_none() { + log::debug!( + "Hook message ({}): {} (hook={})", + user_message_level, + truncate_for_log(&message, 200), + source + ); + } else if let Err(error) = display.call_method( + "show_message", + (&message, &user_message_level, format!("hook:{source}")), + None, + ) { + log::error!("Error calling display_system: {error}"); + } + } + if suppress_output { + log::debug!("Hook '{}' requested output suppression", hook_name); + } + + let result_py = result.unbind(); + let hook_result_cls = py + .import("amplifier_core.models")? + .getattr("HookResult")? + .unbind(); + wrap_future_as_coroutine( + py, + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let bridge = PyContextManagerBridge { + py_obj: context_obj, + }; + for message in messages { + bridge.add_message(message).await?; + } + Self::make_residual_hook_result(&hook_result_cls, &result_py, &residual) + }), + ) + } + + fn make_residual_hook_result( + hook_result_cls: &Py, + original: &Py, + residual: &[PreparedInjection], + ) -> PyResult> { + Python::try_attach(|py| -> PyResult> { + let values = original.call_method0(py, "model_dump")?; + let updates = PyDict::new(py); + let injections = pyo3::types::PyList::empty(py); + for item in residual { + let injection = PyDict::new(py); + injection.set_item("content", &item.content)?; + injection.set_item("role", &item.role)?; + injection.set_item("ephemeral", item.ephemeral)?; + injection.set_item( + "append_to_last_tool_result", + item.append_to_last_tool_result, + )?; + injection.set_item("hook_name", &item.hook_name)?; + injection.set_item("event", &item.event)?; + injections.append(injection)?; + } + updates.set_item("context_injections", injections)?; + if residual.is_empty() { + updates.set_item("action", "continue")?; + updates.set_item("context_injection", py.None())?; + updates.set_item("context_injection_role", "system")?; + updates.set_item("ephemeral", false)?; + updates.set_item("append_to_last_tool_result", false)?; + } else { + updates.set_item("action", "inject_context")?; + updates.set_item( + "context_injection", + residual + .iter() + .map(|item| item.content.as_str()) + .collect::>() + .join("\n\n"), + )?; + updates.set_item("context_injection_role", &residual[0].role)?; + updates.set_item("ephemeral", true)?; + updates.set_item( + "append_to_last_tool_result", + residual.iter().any(|item| item.append_to_last_tool_result), + )?; + } + values.call_method1(py, "update", (updates,))?; + hook_result_cls.call_method1(py, "model_validate", (values,)) + }) + .ok_or_else(|| PyRuntimeError::new_err("Failed to create residual HookResult"))? + } + /// Construct a HookResult Python object with the given action and optional reason. /// /// Centralises the `Python::try_attach` + `PyDict` + `hook_result_cls.call` pattern diff --git a/crates/amplifier-core/src/bridges/grpc_hook.rs b/crates/amplifier-core/src/bridges/grpc_hook.rs index 5a7fa7d..0a1f8cb 100644 --- a/crates/amplifier-core/src/bridges/grpc_hook.rs +++ b/crates/amplifier-core/src/bridges/grpc_hook.rs @@ -194,6 +194,35 @@ impl GrpcHookBridge { } }; + let context_injections = proto + .context_injections + .into_iter() + .map(|injection| { + let role = match amplifier_module::ContextInjectionRole::try_from(injection.role) { + Ok(amplifier_module::ContextInjectionRole::System) => { + models::ContextInjectionRole::System + } + Ok(amplifier_module::ContextInjectionRole::User) => { + models::ContextInjectionRole::User + } + Ok(amplifier_module::ContextInjectionRole::Assistant) => { + models::ContextInjectionRole::Assistant + } + Ok(amplifier_module::ContextInjectionRole::Unspecified) | Err(_) => { + models::ContextInjectionRole::System + } + }; + models::ContextInjection { + content: injection.content, + role, + ephemeral: injection.ephemeral, + append_to_last_tool_result: injection.append_to_last_tool_result, + hook_name: injection.hook_name, + event: injection.event, + } + }) + .collect(); + let approval_prompt = if proto.approval_prompt.is_empty() { None } else { @@ -267,6 +296,7 @@ impl GrpcHookBridge { user_message_level, user_message_source, append_to_last_tool_result: proto.append_to_last_tool_result, + context_injections, extensions: HashMap::new(), } } @@ -333,6 +363,7 @@ mod tests { user_message_level: 0, user_message_source: String::new(), append_to_last_tool_result: false, + context_injections: vec![], } } diff --git a/crates/amplifier-core/src/generated/amplifier.module.rs b/crates/amplifier-core/src/generated/amplifier.module.rs index d510a11..b280ae7 100644 --- a/crates/amplifier-core/src/generated/amplifier.module.rs +++ b/crates/amplifier-core/src/generated/amplifier.module.rs @@ -405,6 +405,21 @@ pub struct ToolResult { pub error_json: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, ::prost::Message)] +pub struct ContextInjection { + #[prost(string, tag = "1")] + pub content: ::prost::alloc::string::String, + #[prost(enumeration = "ContextInjectionRole", tag = "2")] + pub role: i32, + #[prost(bool, tag = "3")] + pub ephemeral: bool, + #[prost(bool, tag = "4")] + pub append_to_last_tool_result: bool, + #[prost(string, tag = "5")] + pub hook_name: ::prost::alloc::string::String, + #[prost(string, tag = "6")] + pub event: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct HookResult { #[prost(enumeration = "HookAction", tag = "1")] pub action: i32, @@ -437,6 +452,8 @@ pub struct HookResult { pub user_message_source: ::prost::alloc::string::String, #[prost(bool, tag = "15")] pub append_to_last_tool_result: bool, + #[prost(message, repeated, tag = "16")] + pub context_injections: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] pub struct ModelInfo { diff --git a/crates/amplifier-core/src/generated/conversions.rs b/crates/amplifier-core/src/generated/conversions.rs index 39b06cf..fdc8ae3 100644 --- a/crates/amplifier-core/src/generated/conversions.rs +++ b/crates/amplifier-core/src/generated/conversions.rs @@ -602,6 +602,27 @@ pub fn native_hook_result_to_proto( .map(|d| to_json_or_warn(d, "HookResult data")) .unwrap_or_default(); + let context_injections = result + .context_injections + .iter() + .map(|injection| amplifier_module::ContextInjection { + content: injection.content.clone(), + role: match injection.role { + ContextInjectionRole::System => { + amplifier_module::ContextInjectionRole::System as i32 + } + ContextInjectionRole::User => amplifier_module::ContextInjectionRole::User as i32, + ContextInjectionRole::Assistant => { + amplifier_module::ContextInjectionRole::Assistant as i32 + } + }, + ephemeral: injection.ephemeral, + append_to_last_tool_result: injection.append_to_last_tool_result, + hook_name: injection.hook_name.clone(), + event: injection.event.clone(), + }) + .collect(); + amplifier_module::HookResult { action, data_json, @@ -618,6 +639,7 @@ pub fn native_hook_result_to_proto( user_message_level, user_message_source: result.user_message_source.clone().unwrap_or_default(), append_to_last_tool_result: result.append_to_last_tool_result, + context_injections, } } @@ -2289,7 +2311,8 @@ mod tests { fn hook_result_roundtrip_via_bridge_reverse() { use crate::bridges::grpc_hook::GrpcHookBridge; use crate::models::{ - ApprovalDefault, ContextInjectionRole, HookAction, HookResult, UserMessageLevel, + ApprovalDefault, ContextInjection, ContextInjectionRole, HookAction, HookResult, + UserMessageLevel, }; let original = HookResult { @@ -2308,6 +2331,14 @@ mod tests { user_message_level: UserMessageLevel::Warning, user_message_source: Some("approval-hook".to_string()), append_to_last_tool_result: false, + context_injections: vec![ContextInjection { + content: "ordered injection".to_string(), + role: ContextInjectionRole::Assistant, + ephemeral: true, + append_to_last_tool_result: true, + hook_name: "registered-hook".to_string(), + event: "tool:post".to_string(), + }], extensions: HashMap::new(), }; @@ -2334,6 +2365,7 @@ mod tests { restored.append_to_last_tool_result, original.append_to_last_tool_result ); + assert_eq!(restored.context_injections, original.context_injections); } #[test] diff --git a/crates/amplifier-core/src/generated/equivalence_tests.rs b/crates/amplifier-core/src/generated/equivalence_tests.rs index 87b4775..9a844fa 100644 --- a/crates/amplifier-core/src/generated/equivalence_tests.rs +++ b/crates/amplifier-core/src/generated/equivalence_tests.rs @@ -59,7 +59,7 @@ mod tests { } #[test] - fn proto_hook_result_has_all_15_fields() { + fn proto_hook_result_has_all_16_fields() { let result = HookResult { action: HookAction::Modify as i32, data_json: r#"{"modified": true}"#.into(), @@ -76,6 +76,7 @@ mod tests { user_message_level: UserMessageLevel::Warning as i32, user_message_source: "content-filter".into(), append_to_last_tool_result: true, + context_injections: vec![], }; assert_eq!(result.action, HookAction::Modify as i32); assert_eq!(result.data_json, r#"{"modified": true}"#); diff --git a/crates/amplifier-core/src/hooks.rs b/crates/amplifier-core/src/hooks.rs index d10b113..3f115ff 100644 --- a/crates/amplifier-core/src/hooks.rs +++ b/crates/amplifier-core/src/hooks.rs @@ -31,7 +31,7 @@ use std::time::Duration; use serde_json::Value; -use crate::models::{HookAction, HookResult}; +use crate::models::{ContextInjection, HookAction, HookResult}; use crate::traits::HookHandler; // --------------------------------------------------------------------------- @@ -209,7 +209,7 @@ impl HookRegistry { let mut inject_context_results: Vec = Vec::new(); for (handler, name) in &entries { - let result = match handler.handle(event, current_data.clone()).await { + let mut result = match handler.handle(event, current_data.clone()).await { Ok(r) => r, Err(e) => { // Error in handler -- log and continue (matches Python behaviour). @@ -222,6 +222,8 @@ impl HookRegistry { } }; + normalize_inject_context_result(&mut result, name, event); + // Deny short-circuits immediately if result.action == HookAction::Deny { return result; @@ -241,7 +243,7 @@ impl HookRegistry { } // Collect inject_context for merging at end - if result.action == HookAction::InjectContext && result.context_injection.is_some() { + if result.action == HookAction::InjectContext && !result.context_injections.is_empty() { inject_context_results.push(result.clone()); } @@ -402,58 +404,111 @@ fn value_to_map(value: &Value) -> HashMap { /// Merge multiple inject_context HookResults into a single result. /// -/// Combines injections with `"\n\n"` separator. `context_injection_role` and -/// `suppress_output` are taken from the first result (these are display/role -/// settings where "first wins" is a reasonable, harmless default). `ephemeral` -/// and `append_to_last_tool_result` instead use OR semantics across ALL -/// results, not just the first: -/// -/// `ephemeral` in particular MUST be the logical OR of every contributing -/// result, not `first.ephemeral`. The combined `context_injection` string is -/// the concatenation of every hook's content -- if even one of those hooks -/// marked its own contribution `ephemeral: true` (regenerated per turn), the -/// resulting combined string is regenerated per turn too, byte-for-byte -/// identical only when ALL contributors are stable. Taking only the first -/// result's `ephemeral` flag meant a single non-ephemeral hook running before -/// an ephemeral one (pure registration-order luck) would silently downgrade -/// the merged result to `ephemeral: false` -- causing every downstream -/// consumer that trusts `Message.metadata["ephemeral"]` (e.g. the Anthropic -/// provider's conversation-region prompt-cache breakpoint placement) to lose -/// the ephemeral signal for the ENTIRE combined injection, not just the -/// stable part. See amplifier_module_provider_anthropic's -/// `_count_trailing_ephemeral_messages` / `_apply_conversation_cache_control`. +/// Keeps the ordered list lossless and projects the legacy scalar fields from +/// every item: bodies are joined with `"\n\n"`, the first role wins, and both +/// boolean flags use OR semantics. fn merge_inject_context_results(results: &[HookResult]) -> HookResult { if results.is_empty() { return HookResult::default(); } - if results.len() == 1 { - return results[0].clone(); + let context_injections: Vec = results + .iter() + .flat_map(|result| { + if result.context_injections.is_empty() { + result + .context_injection + .as_ref() + .map(|content| { + vec![ContextInjection { + content: content.clone(), + role: result.context_injection_role.clone(), + ephemeral: result.ephemeral, + append_to_last_tool_result: result.append_to_last_tool_result, + hook_name: "unknown".to_string(), + event: String::new(), + }] + }) + .unwrap_or_default() + } else { + result.context_injections.clone() + } + }) + .collect(); + + if context_injections.is_empty() { + return HookResult::default(); } - // Combine all injections - let combined_content: String = results + let combined_content = context_injections .iter() - .filter_map(|r| r.context_injection.as_deref()) + .map(|injection| injection.content.as_str()) .collect::>() .join("\n\n"); - - // role/suppress_output: "first wins" -- harmless display-only settings. let first = &results[0]; + let first_injection = &context_injections[0]; + + // A single handler may return multiple structured injections. This is + // normalization, not aggregation: retain the handler's other result-level + // fields (including UI and approval metadata) while updating only the + // legacy injection projection. + if results.len() == 1 { + let mut result = first.clone(); + result.action = HookAction::InjectContext; + result.context_injection = Some(combined_content); + result.context_injection_role = first_injection.role.clone(); + result.ephemeral = context_injections + .iter() + .any(|injection| injection.ephemeral); + result.append_to_last_tool_result = context_injections + .iter() + .any(|injection| injection.append_to_last_tool_result); + result.context_injections = context_injections; + return result; + } HookResult { action: HookAction::InjectContext, context_injection: Some(combined_content), - context_injection_role: first.context_injection_role.clone(), - // OR semantics: ANY contributing result marking itself ephemeral - // makes the whole merged injection ephemeral (see doc comment above). - ephemeral: results.iter().any(|r| r.ephemeral), + context_injection_role: first_injection.role.clone(), + ephemeral: context_injections + .iter() + .any(|injection| injection.ephemeral), suppress_output: first.suppress_output, - append_to_last_tool_result: results.iter().any(|r| r.append_to_last_tool_result), + append_to_last_tool_result: context_injections + .iter() + .any(|injection| injection.append_to_last_tool_result), + context_injections, ..Default::default() } } +/// Bind injection provenance at the trusted registration boundary and promote +/// legacy scalar injections into the structured carrier. +fn normalize_inject_context_result(result: &mut HookResult, handler_name: &str, event: &str) { + if result.action != HookAction::InjectContext { + return; + } + + if result.context_injections.is_empty() { + if let Some(content) = result.context_injection.clone() { + result.context_injections.push(ContextInjection { + content, + role: result.context_injection_role.clone(), + ephemeral: result.ephemeral, + append_to_last_tool_result: result.append_to_last_tool_result, + hook_name: handler_name.to_string(), + event: event.to_string(), + }); + } + } + + for injection in &mut result.context_injections { + injection.hook_name = handler_name.to_string(); + injection.event = event.to_string(); + } +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -830,6 +885,91 @@ mod tests { assert!(injection.contains("second injection")); } + #[tokio::test] + async fn single_structured_injection_result_preserves_non_injection_fields() { + use crate::models::{ApprovalDefault, ContextInjectionRole, UserMessageLevel}; + + let registry = HookRegistry::new(); + let original_data = HashMap::from([("keep".to_string(), serde_json::json!(true))]); + let original_extensions = + HashMap::from([("future_field".to_string(), serde_json::json!("preserved"))]); + let handler = Arc::new(SimpleHandler(HookResult { + action: HookAction::InjectContext, + data: Some(original_data.clone()), + reason: Some("keep reason".into()), + // The scalar fields are intentionally stale: the list is canonical. + context_injection: Some("stale scalar".into()), + context_injection_role: ContextInjectionRole::System, + ephemeral: false, + approval_prompt: Some("keep approval".into()), + approval_options: Some(vec!["allow".into(), "deny".into()]), + approval_timeout: 42.0, + approval_default: ApprovalDefault::Allow, + suppress_output: true, + user_message: Some("Context injected".into()), + user_message_level: UserMessageLevel::Warning, + user_message_source: Some("status-context".into()), + append_to_last_tool_result: false, + context_injections: vec![ + ContextInjection { + content: "first".into(), + role: ContextInjectionRole::User, + ephemeral: false, + append_to_last_tool_result: false, + hook_name: "untrusted".into(), + event: "untrusted".into(), + }, + ContextInjection { + content: "second".into(), + role: ContextInjectionRole::Assistant, + ephemeral: true, + append_to_last_tool_result: true, + hook_name: "untrusted".into(), + event: "untrusted".into(), + }, + ], + extensions: original_extensions.clone(), + })); + let _ = registry.register( + "test:structured", + handler, + 0, + Some("structured-hook".into()), + ); + + let result = registry + .emit("test:structured", serde_json::json!({})) + .await; + + assert_eq!(result.action, HookAction::InjectContext); + assert_eq!(result.context_injection.as_deref(), Some("first\n\nsecond")); + assert_eq!(result.context_injection_role, ContextInjectionRole::User); + assert!(result.ephemeral); + assert!(result.append_to_last_tool_result); + assert_eq!(result.context_injections.len(), 2); + assert_eq!(result.context_injections[0].hook_name, "structured-hook"); + assert_eq!(result.context_injections[0].event, "test:structured"); + assert_eq!(result.context_injections[1].hook_name, "structured-hook"); + assert_eq!(result.context_injections[1].event, "test:structured"); + assert_eq!(result.data, Some(original_data)); + assert_eq!(result.reason.as_deref(), Some("keep reason")); + assert_eq!(result.approval_prompt.as_deref(), Some("keep approval")); + assert_eq!( + result.approval_options, + Some(vec!["allow".into(), "deny".into()]) + ); + assert_eq!(result.approval_timeout, 42.0); + assert_eq!(result.approval_default, ApprovalDefault::Allow); + assert!(result.suppress_output); + assert_eq!(result.user_message.as_deref(), Some("Context injected")); + assert_eq!(result.user_message_level, UserMessageLevel::Warning); + assert_eq!( + result.user_message_source.as_deref(), + Some("status-context") + ); + assert_eq!(result.extensions, original_extensions); + } + // --------------------------------------------------------------- // Unregister // --------------------------------------------------------------- @@ -1257,6 +1397,39 @@ mod tests { ); } + #[tokio::test] + async fn registered_mixed_legacy_injections_preserve_item_lifetime() { + let registry = HookRegistry::new(); + let durable = Arc::new(SimpleHandler(HookResult { + action: HookAction::InjectContext, + context_injection: Some("durable".into()), + context_injection_role: crate::models::ContextInjectionRole::Assistant, + ephemeral: false, + ..Default::default() + })); + let temporary = Arc::new(SimpleHandler(HookResult { + action: HookAction::InjectContext, + context_injection: Some("temporary".into()), + context_injection_role: crate::models::ContextInjectionRole::User, + ephemeral: true, + ..Default::default() + })); + + let _ = registry.register("test:mixed", durable, 0, Some("durable-hook".into())); + let _ = registry.register("test:mixed", temporary, 10, Some("temporary-hook".into())); + + let result = registry.emit("test:mixed", serde_json::json!({})).await; + assert_eq!(result.context_injections.len(), 2); + assert_eq!(result.context_injections[0].content, "durable"); + assert!(!result.context_injections[0].ephemeral); + assert_eq!(result.context_injections[0].hook_name, "durable-hook"); + assert_eq!(result.context_injections[0].event, "test:mixed"); + assert_eq!(result.context_injections[1].content, "temporary"); + assert!(result.context_injections[1].ephemeral); + assert_eq!(result.context_injections[1].hook_name, "temporary-hook"); + assert_eq!(result.context_injections[1].event, "test:mixed"); + } + /// Verify that the `log` crate is available and usable from amplifier-core. /// This ensures Rust code can emit structured log messages that will be /// routed through Python's logging module via pyo3-log in the bindings. diff --git a/crates/amplifier-core/src/lib.rs b/crates/amplifier-core/src/lib.rs index 3390987..3891f64 100644 --- a/crates/amplifier-core/src/lib.rs +++ b/crates/amplifier-core/src/lib.rs @@ -51,8 +51,8 @@ pub use errors::{AmplifierError, ContextError, HookError, ProviderError, Session // Core data models pub use models::{ ApprovalDefault, ApprovalRequest, ApprovalResponse, ConfigField, ConfigFieldType, - ContextInjectionRole, HookAction, HookResult, ModelInfo, ModuleInfo, ModuleType, ProviderInfo, - SessionState, SessionStatus, ToolResult, UserMessageLevel, + ContextInjection, ContextInjectionRole, HookAction, HookResult, ModelInfo, ModuleInfo, + ModuleType, ProviderInfo, SessionState, SessionStatus, ToolResult, UserMessageLevel, }; // Chat protocol models diff --git a/crates/amplifier-core/src/models.rs b/crates/amplifier-core/src/models.rs index 266c62e..6c3c50d 100644 --- a/crates/amplifier-core/src/models.rs +++ b/crates/amplifier-core/src/models.rs @@ -106,6 +106,40 @@ pub enum SessionState { // Structs // --------------------------------------------------------------------------- +/// One independently processed context injection from a hook. +/// +/// The registry binds `hook_name` and `event` from the registered handler that +/// emitted this item. Consumers must not rely on handler-provided provenance. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ContextInjection { + /// Text to inject into the agent's conversation context. + pub content: String, + + /// Role for the injected message in conversation. + #[serde(default)] + pub role: ContextInjectionRole, + + /// Whether this injection is request-only rather than durable history. + #[serde(default)] + pub ephemeral: bool, + + /// Carrier for post-tool-result placement; interpreted by the loop layer. + #[serde(default)] + pub append_to_last_tool_result: bool, + + /// Registered handler name, bound by the hook registry. + #[serde(default = "default_unknown_hook_name")] + pub hook_name: String, + + /// Event that emitted the injection, bound by the hook registry. + #[serde(default)] + pub event: String, +} + +fn default_unknown_hook_name() -> String { + "unknown".to_string() +} + /// Result from hook execution with enhanced capabilities. /// /// Hooks can observe, block, modify operations, inject context to the agent, @@ -187,6 +221,13 @@ pub struct HookResult { #[serde(default)] pub append_to_last_tool_result: bool, + /// Ordered, lossless context injection items. + /// + /// The legacy scalar context fields remain for backwards compatibility and + /// are projected from this list by the hook registry. + #[serde(default)] + pub context_injections: Vec, + /// Extension fields for forward-compatibility. /// Captures any unknown JSON keys during deserialization. #[serde(flatten)] @@ -215,6 +256,7 @@ impl Default for HookResult { user_message_level: UserMessageLevel::default(), user_message_source: None, append_to_last_tool_result: false, + context_injections: Vec::new(), extensions: HashMap::new(), } } @@ -560,6 +602,23 @@ mod tests { assert!(result.context_injection.is_none()); assert_eq!(result.context_injection_role, ContextInjectionRole::System); assert!(!result.ephemeral); + assert!(result.context_injections.is_empty()); + } + + #[test] + fn context_injection_serialization_roundtrip() { + let injection = ContextInjection { + content: "temporary reminder".into(), + role: ContextInjectionRole::User, + ephemeral: true, + append_to_last_tool_result: true, + hook_name: "registered-hook".into(), + event: "tool:post".into(), + }; + + let json = serde_json::to_string(&injection).unwrap(); + let restored: ContextInjection = serde_json::from_str(&json).unwrap(); + assert_eq!(restored, injection); } #[test] diff --git a/proto/amplifier_module.proto b/proto/amplifier_module.proto index e12f988..cd47b85 100644 --- a/proto/amplifier_module.proto +++ b/proto/amplifier_module.proto @@ -389,6 +389,15 @@ message ToolResult { string error_json = 3; } +message ContextInjection { + string content = 1; + ContextInjectionRole role = 2; + bool ephemeral = 3; + bool append_to_last_tool_result = 4; + string hook_name = 5; + string event = 6; +} + message HookResult { HookAction action = 1; string data_json = 2; @@ -406,6 +415,7 @@ message HookResult { UserMessageLevel user_message_level = 13; string user_message_source = 14; bool append_to_last_tool_result = 15; + repeated ContextInjection context_injections = 16; } message ModelInfo { diff --git a/python/amplifier_core/__init__.py b/python/amplifier_core/__init__.py index b4e014c..ca80776 100644 --- a/python/amplifier_core/__init__.py +++ b/python/amplifier_core/__init__.py @@ -74,6 +74,7 @@ from .message_models import ToolSpec from .message_models import Usage from .models import ConfigField +from .models import ContextInjection from .models import HookResult from .models import ModelInfo from .models import ModuleInfo @@ -114,6 +115,7 @@ "HookRegistry", "ToolCall", "ToolResult", + "ContextInjection", "HookResult", "ConfigField", "ModelInfo", diff --git a/python/amplifier_core/_grpc_gen/__init__.py b/python/amplifier_core/_grpc_gen/__init__.py index 4b20486..8e32a4e 100644 --- a/python/amplifier_core/_grpc_gen/__init__.py +++ b/python/amplifier_core/_grpc_gen/__init__.py @@ -1,8 +1,5 @@ """Generated gRPC stubs for amplifier_module.proto. Do not edit these files directly. Regenerate with: - python -m grpc_tools.protoc -I proto \ - --python_out=python/amplifier_core/_grpc_gen \ - --grpc_python_out=python/amplifier_core/_grpc_gen \ - proto/amplifier_module.proto + python scripts/generate_grpc_stubs.py """ diff --git a/python/amplifier_core/_grpc_gen/amplifier_module_pb2.py b/python/amplifier_core/_grpc_gen/amplifier_module_pb2.py index 6f4d751..f1ac9dd 100644 --- a/python/amplifier_core/_grpc_gen/amplifier_module_pb2.py +++ b/python/amplifier_core/_grpc_gen/amplifier_module_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x61mplifier_module.proto\x12\x10\x61mplifier.module\"\x07\n\x05\x45mpty\"F\n\x08ToolSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x17\n\x0fparameters_json\x18\x03 \x01(\t\"9\n\x12ToolExecuteRequest\x12\r\n\x05input\x18\x01 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x02 \x01(\t\"[\n\x13ToolExecuteResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0e\n\x06output\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\xd6\x01\n\nModuleInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x31\n\x0bmodule_type\x18\x04 \x01(\x0e\x32\x1c.amplifier.module.ModuleType\x12\x13\n\x0bmount_point\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x1a\n\x12\x63onfig_schema_json\x18\x07 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x08 \x03(\t\x12\x0e\n\x06\x61uthor\x18\t \x01(\t\"\x8c\x01\n\x0cMountRequest\x12:\n\x06\x63onfig\x18\x01 \x03(\x0b\x32*.amplifier.module.MountRequest.ConfigEntry\x12\x11\n\tmodule_id\x18\x02 \x01(\t\x1a-\n\x0b\x43onfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"_\n\rMountResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12.\n\x06status\x18\x03 \x01(\x0e\x32\x1e.amplifier.module.HealthStatus\"V\n\x13HealthCheckResponse\x12.\n\x06status\x18\x01 \x01(\x0e\x32\x1e.amplifier.module.HealthStatus\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xca\x02\n\x0b\x43onfigField\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x35\n\nfield_type\x18\x03 \x01(\x0e\x32!.amplifier.module.ConfigFieldType\x12\x0e\n\x06prompt\x18\x04 \x01(\t\x12\x0f\n\x07\x65nv_var\x18\x05 \x01(\t\x12\x0f\n\x07\x63hoices\x18\x06 \x03(\t\x12\x10\n\x08required\x18\x07 \x01(\x08\x12\x15\n\rdefault_value\x18\x08 \x01(\t\x12>\n\tshow_when\x18\t \x03(\x0b\x32+.amplifier.module.ConfigField.ShowWhenEntry\x12\x16\n\x0erequires_model\x18\n \x01(\x08\x1a/\n\rShowWhenEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x01\n\rProviderError\x12\x37\n\nerror_type\x18\x01 \x01(\x0e\x32#.amplifier.module.ProviderErrorType\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x15\n\rprovider_name\x18\x03 \x01(\t\x12\r\n\x05model\x18\x04 \x01(\t\x12\x13\n\x0bstatus_code\x18\x05 \x01(\x05\x12\x11\n\tretryable\x18\x06 \x01(\x08\x12\x13\n\x0bretry_after\x18\x07 \x01(\x01\"\x97\x01\n\tToolError\x12\x33\n\nerror_type\x18\x01 \x01(\x0e\x32\x1f.amplifier.module.ToolErrorType\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x11\n\ttool_name\x18\x03 \x01(\t\x12\x0e\n\x06stdout\x18\x04 \x01(\t\x12\x0e\n\x06stderr\x18\x05 \x01(\t\x12\x11\n\texit_code\x18\x06 \x01(\x05\"d\n\tHookError\x12\x33\n\nerror_type\x18\x01 \x01(\x0e\x32\x1f.amplifier.module.HookErrorType\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x11\n\thook_name\x18\x03 \x01(\t\"\xef\x01\n\x0e\x41mplifierError\x12\x39\n\x0eprovider_error\x18\x01 \x01(\x0b\x32\x1f.amplifier.module.ProviderErrorH\x00\x12\x31\n\ntool_error\x18\x02 \x01(\x0b\x32\x1b.amplifier.module.ToolErrorH\x00\x12\x31\n\nhook_error\x18\x03 \x01(\x0b\x32\x1b.amplifier.module.HookErrorH\x00\x12\x17\n\rgeneric_error\x18\x04 \x01(\tH\x00\x12\x1a\n\x10validation_error\x18\x05 \x01(\tH\x00\x42\x07\n\x05\x65rror\"\x19\n\tTextBlock\x12\x0c\n\x04text\x18\x01 \x01(\t\"E\n\rThinkingBlock\x12\x10\n\x08thinking\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\"%\n\x15RedactedThinkingBlock\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\"=\n\rToolCallBlock\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x12\n\ninput_json\x18\x03 \x01(\t\"<\n\x0fToolResultBlock\x12\x14\n\x0ctool_call_id\x18\x01 \x01(\t\x12\x13\n\x0boutput_json\x18\x02 \x01(\t\"C\n\nImageBlock\x12\x12\n\nmedia_type\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x13\n\x0bsource_json\x18\x03 \x01(\t\"2\n\x0eReasoningBlock\x12\x0f\n\x07\x63ontent\x18\x01 \x03(\t\x12\x0f\n\x07summary\x18\x02 \x03(\t\"\xf1\x03\n\x0c\x43ontentBlock\x12\x31\n\ntext_block\x18\x01 \x01(\x0b\x32\x1b.amplifier.module.TextBlockH\x00\x12\x39\n\x0ethinking_block\x18\x02 \x01(\x0b\x32\x1f.amplifier.module.ThinkingBlockH\x00\x12J\n\x17redacted_thinking_block\x18\x03 \x01(\x0b\x32\'.amplifier.module.RedactedThinkingBlockH\x00\x12:\n\x0ftool_call_block\x18\x04 \x01(\x0b\x32\x1f.amplifier.module.ToolCallBlockH\x00\x12>\n\x11tool_result_block\x18\x05 \x01(\x0b\x32!.amplifier.module.ToolResultBlockH\x00\x12\x33\n\x0bimage_block\x18\x06 \x01(\x0b\x32\x1c.amplifier.module.ImageBlockH\x00\x12;\n\x0freasoning_block\x18\x07 \x01(\x0b\x32 .amplifier.module.ReasoningBlockH\x00\x12\x30\n\nvisibility\x18\x08 \x01(\x0e\x32\x1c.amplifier.module.VisibilityB\x07\n\x05\x62lock\"B\n\x10\x43ontentBlockList\x12.\n\x06\x62locks\x18\x01 \x03(\x0b\x32\x1e.amplifier.module.ContentBlock\"\xca\x01\n\x07Message\x12$\n\x04role\x18\x01 \x01(\x0e\x32\x16.amplifier.module.Role\x12\x16\n\x0ctext_content\x18\x02 \x01(\tH\x00\x12;\n\rblock_content\x18\x03 \x01(\x0b\x32\".amplifier.module.ContentBlockListH\x00\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x14\n\x0ctool_call_id\x18\x05 \x01(\t\x12\x15\n\rmetadata_json\x18\x06 \x01(\tB\t\n\x07\x63ontent\"C\n\x0fToolCallMessage\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x16\n\x0e\x61rguments_json\x18\x03 \x01(\t\"K\n\rToolSpecProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x17\n\x0fparameters_json\x18\x03 \x01(\t\"7\n\x10JsonSchemaFormat\x12\x13\n\x0bschema_json\x18\x01 \x01(\t\x12\x0e\n\x06strict\x18\x02 \x01(\x08\"u\n\x0eResponseFormat\x12\x0e\n\x04text\x18\x01 \x01(\x08H\x00\x12\x0e\n\x04json\x18\x02 \x01(\x08H\x00\x12\x39\n\x0bjson_schema\x18\x03 \x01(\x0b\x32\".amplifier.module.JsonSchemaFormatH\x00\x42\x08\n\x06\x66ormat\"\xf7\x01\n\x05Usage\x12\x15\n\rprompt_tokens\x18\x01 \x01(\x05\x12\x19\n\x11\x63ompletion_tokens\x18\x02 \x01(\x05\x12\x14\n\x0ctotal_tokens\x18\x03 \x01(\x05\x12\x1d\n\x10reasoning_tokens\x18\x04 \x01(\x05H\x00\x88\x01\x01\x12\x1e\n\x11\x63\x61\x63he_read_tokens\x18\x05 \x01(\x05H\x01\x88\x01\x01\x12\"\n\x15\x63\x61\x63he_creation_tokens\x18\x06 \x01(\x05H\x02\x88\x01\x01\x42\x13\n\x11_reasoning_tokensB\x14\n\x12_cache_read_tokensB\x18\n\x16_cache_creation_tokens\"@\n\x0b\x44\x65gradation\x12\x11\n\trequested\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tual\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\"\x81\x03\n\x0b\x43hatRequest\x12+\n\x08messages\x18\x01 \x03(\x0b\x32\x19.amplifier.module.Message\x12.\n\x05tools\x18\x02 \x03(\x0b\x32\x1f.amplifier.module.ToolSpecProto\x12\x39\n\x0fresponse_format\x18\x03 \x01(\x0b\x32 .amplifier.module.ResponseFormat\x12\x13\n\x0btemperature\x18\x04 \x01(\x01\x12\r\n\x05top_p\x18\x05 \x01(\x01\x12\x19\n\x11max_output_tokens\x18\x06 \x01(\x05\x12\x17\n\x0f\x63onversation_id\x18\x07 \x01(\t\x12\x0e\n\x06stream\x18\x08 \x01(\x08\x12\x15\n\rmetadata_json\x18\t \x01(\t\x12\r\n\x05model\x18\n \x01(\t\x12\x13\n\x0btool_choice\x18\x0b \x01(\t\x12\x0c\n\x04stop\x18\x0c \x03(\t\x12\x18\n\x10reasoning_effort\x18\r \x01(\t\x12\x0f\n\x07timeout\x18\x0e \x01(\x01\"\x98\x02\n\x0c\x43hatResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x35\n\ntool_calls\x18\x02 \x03(\x0b\x32!.amplifier.module.ToolCallMessage\x12&\n\x05usage\x18\x03 \x01(\x0b\x32\x17.amplifier.module.Usage\x12\x32\n\x0b\x64\x65gradation\x18\x04 \x01(\x0b\x32\x1d.amplifier.module.Degradation\x12\x15\n\rfinish_reason\x18\x05 \x01(\t\x12\x15\n\rmetadata_json\x18\x06 \x01(\t\x12\x36\n\x0e\x63ontent_blocks\x18\x07 \x03(\x0b\x32\x1e.amplifier.module.ContentBlock\"F\n\nToolResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x13\n\x0boutput_json\x18\x02 \x01(\t\x12\x12\n\nerror_json\x18\x03 \x01(\t\"\xa7\x04\n\nHookResult\x12,\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x1c.amplifier.module.HookAction\x12\x11\n\tdata_json\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x19\n\x11\x63ontext_injection\x18\x04 \x01(\t\x12\x46\n\x16\x63ontext_injection_role\x18\x05 \x01(\x0e\x32&.amplifier.module.ContextInjectionRole\x12\x11\n\tephemeral\x18\x06 \x01(\x08\x12\x17\n\x0f\x61pproval_prompt\x18\x07 \x01(\t\x12\x18\n\x10\x61pproval_options\x18\x08 \x03(\t\x12\x1d\n\x10\x61pproval_timeout\x18\t \x01(\x01H\x00\x88\x01\x01\x12;\n\x10\x61pproval_default\x18\n \x01(\x0e\x32!.amplifier.module.ApprovalDefault\x12\x17\n\x0fsuppress_output\x18\x0b \x01(\x08\x12\x14\n\x0cuser_message\x18\x0c \x01(\t\x12>\n\x12user_message_level\x18\r \x01(\x0e\x32\".amplifier.module.UserMessageLevel\x12\x1b\n\x13user_message_source\x18\x0e \x01(\t\x12\"\n\x1a\x61ppend_to_last_tool_result\x18\x0f \x01(\x08\x42\x13\n\x11_approval_timeout\"\x8d\x01\n\tModelInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontext_window\x18\x03 \x01(\x05\x12\x19\n\x11max_output_tokens\x18\x04 \x01(\x05\x12\x14\n\x0c\x63\x61pabilities\x18\x05 \x03(\t\x12\x15\n\rdefaults_json\x18\x06 \x01(\t\"\xb0\x01\n\x0cProviderInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x1b\n\x13\x63redential_env_vars\x18\x03 \x03(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x03(\t\x12\x15\n\rdefaults_json\x18\x05 \x01(\t\x12\x34\n\rconfig_fields\x18\x06 \x03(\x0b\x32\x1d.amplifier.module.ConfigField\"\x80\x01\n\x0f\x41pprovalRequest\x12\x11\n\ttool_name\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\t\x12\x14\n\x0c\x64\x65tails_json\x18\x03 \x01(\t\x12\x12\n\nrisk_level\x18\x04 \x01(\t\x12\x14\n\x07timeout\x18\x05 \x01(\x01H\x00\x88\x01\x01\x42\n\n\x08_timeout\"F\n\x10\x41pprovalResponse\x12\x10\n\x08\x61pproved\x18\x01 \x01(\x08\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x10\n\x08remember\x18\x03 \x01(\x08\"A\n\x12ListModelsResponse\x12+\n\x06models\x18\x01 \x03(\x0b\x32\x1b.amplifier.module.ModelInfo\"O\n\x16ParseToolCallsResponse\x12\x35\n\ntool_calls\x18\x01 \x03(\x0b\x32!.amplifier.module.ToolCallMessage\"@\n\x1aOrchestratorExecuteRequest\x12\x0e\n\x06prompt\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\">\n\x1bOrchestratorExecuteResponse\x12\x10\n\x08response\x18\x01 \x01(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"?\n\x11\x41\x64\x64MessageRequest\x12*\n\x07message\x18\x01 \x01(\x0b\x32\x19.amplifier.module.Message\"B\n\x13GetMessagesResponse\x12+\n\x08messages\x18\x01 \x03(\x0b\x32\x19.amplifier.module.Message\"J\n\x1bGetMessagesForRequestParams\x12\x14\n\x0ctoken_budget\x18\x01 \x01(\x05\x12\x15\n\rprovider_name\x18\x02 \x01(\t\"A\n\x12SetMessagesRequest\x12+\n\x08messages\x18\x01 \x03(\x0b\x32\x19.amplifier.module.Message\"5\n\x11HookHandleRequest\x12\r\n\x05\x65vent\x18\x01 \x01(\t\x12\x11\n\tdata_json\x18\x02 \x01(\t\".\n\x17GetSubscriptionsRequest\x12\x13\n\x0b\x63onfig_json\x18\x01 \x01(\t\"V\n\x18GetSubscriptionsResponse\x12:\n\rsubscriptions\x18\x01 \x03(\x0b\x32#.amplifier.module.EventSubscription\"B\n\x11\x45ventSubscription\x12\r\n\x05\x65vent\x18\x01 \x01(\t\x12\x10\n\x08priority\x18\x02 \x01(\x05\x12\x0c\n\x04name\x18\x03 \x01(\t\"d\n\x1b\x43ompleteWithProviderRequest\x12\x15\n\rprovider_name\x18\x01 \x01(\t\x12.\n\x07request\x18\x02 \x01(\x0b\x32\x1d.amplifier.module.ChatRequest\";\n\x12\x45xecuteToolRequest\x12\x11\n\ttool_name\x18\x01 \x01(\t\x12\x12\n\ninput_json\x18\x02 \x01(\t\"3\n\x0f\x45mitHookRequest\x12\r\n\x05\x65vent\x18\x01 \x01(\t\x12\x11\n\tdata_json\x18\x02 \x01(\t\"V\n\x19\x45mitHookAndCollectRequest\x12\r\n\x05\x65vent\x18\x01 \x01(\t\x12\x11\n\tdata_json\x18\x02 \x01(\t\x12\x17\n\x0ftimeout_seconds\x18\x03 \x01(\x01\"4\n\x1a\x45mitHookAndCollectResponse\x12\x16\n\x0eresponses_json\x18\x01 \x03(\t\"(\n\x12GetMessagesRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\"Y\n\x17KernelAddMessageRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12*\n\x07message\x18\x02 \x01(\x0b\x32\x19.amplifier.module.Message\"a\n\x17GetMountedModuleRequest\x12\x13\n\x0bmodule_name\x18\x01 \x01(\t\x12\x31\n\x0bmodule_type\x18\x02 \x01(\x0e\x32\x1c.amplifier.module.ModuleType\"U\n\x18GetMountedModuleResponse\x12\r\n\x05\x66ound\x18\x01 \x01(\x08\x12*\n\x04info\x18\x02 \x01(\x0b\x32\x1c.amplifier.module.ModuleInfo\"=\n\x19RegisterCapabilityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\nvalue_json\x18\x02 \x01(\t\"$\n\x14GetCapabilityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\":\n\x15GetCapabilityResponse\x12\r\n\x05\x66ound\x18\x01 \x01(\x08\x12\x12\n\nvalue_json\x18\x02 \x01(\t*\xbc\x01\n\nModuleType\x12\x1b\n\x17MODULE_TYPE_UNSPECIFIED\x10\x00\x12\x18\n\x14MODULE_TYPE_PROVIDER\x10\x01\x12\x14\n\x10MODULE_TYPE_TOOL\x10\x02\x12\x14\n\x10MODULE_TYPE_HOOK\x10\x03\x12\x16\n\x12MODULE_TYPE_MEMORY\x10\x04\x12\x19\n\x15MODULE_TYPE_GUARDRAIL\x10\x05\x12\x18\n\x14MODULE_TYPE_APPROVAL\x10\x06*\x82\x01\n\x0cHealthStatus\x12\x1d\n\x19HEALTH_STATUS_UNSPECIFIED\x10\x00\x12\x19\n\x15HEALTH_STATUS_SERVING\x10\x01\x12\x1d\n\x19HEALTH_STATUS_NOT_SERVING\x10\x02\x12\x19\n\x15HEALTH_STATUS_UNKNOWN\x10\x03*\xad\x01\n\x0f\x43onfigFieldType\x12!\n\x1d\x43ONFIG_FIELD_TYPE_UNSPECIFIED\x10\x00\x12\x1c\n\x18\x43ONFIG_FIELD_TYPE_STRING\x10\x01\x12\x1c\n\x18\x43ONFIG_FIELD_TYPE_NUMBER\x10\x02\x12\x1d\n\x19\x43ONFIG_FIELD_TYPE_BOOLEAN\x10\x03\x12\x1c\n\x18\x43ONFIG_FIELD_TYPE_SECRET\x10\x04*\xd8\x02\n\x11ProviderErrorType\x12#\n\x1fPROVIDER_ERROR_TYPE_UNSPECIFIED\x10\x00\x12\x1c\n\x18PROVIDER_ERROR_TYPE_AUTH\x10\x01\x12\"\n\x1ePROVIDER_ERROR_TYPE_RATE_LIMIT\x10\x02\x12&\n\"PROVIDER_ERROR_TYPE_CONTEXT_LENGTH\x10\x03\x12\'\n#PROVIDER_ERROR_TYPE_INVALID_REQUEST\x10\x04\x12&\n\"PROVIDER_ERROR_TYPE_CONTENT_FILTER\x10\x05\x12#\n\x1fPROVIDER_ERROR_TYPE_UNAVAILABLE\x10\x06\x12\x1f\n\x1bPROVIDER_ERROR_TYPE_TIMEOUT\x10\x07\x12\x1d\n\x19PROVIDER_ERROR_TYPE_OTHER\x10\x08*\x8c\x01\n\rToolErrorType\x12\x1f\n\x1bTOOL_ERROR_TYPE_UNSPECIFIED\x10\x00\x12\x1d\n\x19TOOL_ERROR_TYPE_EXECUTION\x10\x01\x12\x1e\n\x1aTOOL_ERROR_TYPE_VALIDATION\x10\x02\x12\x1b\n\x17TOOL_ERROR_TYPE_TIMEOUT\x10\x03*\x8c\x01\n\rHookErrorType\x12\x1f\n\x1bHOOK_ERROR_TYPE_UNSPECIFIED\x10\x00\x12\x1d\n\x19HOOK_ERROR_TYPE_EXECUTION\x10\x01\x12\x1e\n\x1aHOOK_ERROR_TYPE_VALIDATION\x10\x02\x12\x1b\n\x17HOOK_ERROR_TYPE_TIMEOUT\x10\x03*\x86\x01\n\x04Role\x12\x14\n\x10ROLE_UNSPECIFIED\x10\x00\x12\x0f\n\x0bROLE_SYSTEM\x10\x01\x12\r\n\tROLE_USER\x10\x02\x12\x12\n\x0eROLE_ASSISTANT\x10\x03\x12\r\n\tROLE_TOOL\x10\x04\x12\x11\n\rROLE_FUNCTION\x10\x05\x12\x12\n\x0eROLE_DEVELOPER\x10\x06*o\n\nVisibility\x12\x1a\n\x16VISIBILITY_UNSPECIFIED\x10\x00\x12\x12\n\x0eVISIBILITY_ALL\x10\x01\x12\x17\n\x13VISIBILITY_LLM_ONLY\x10\x02\x12\x18\n\x14VISIBILITY_USER_ONLY\x10\x03*\xab\x01\n\nHookAction\x12\x1b\n\x17HOOK_ACTION_UNSPECIFIED\x10\x00\x12\x18\n\x14HOOK_ACTION_CONTINUE\x10\x01\x12\x16\n\x12HOOK_ACTION_MODIFY\x10\x02\x12\x14\n\x10HOOK_ACTION_DENY\x10\x03\x12\x1e\n\x1aHOOK_ACTION_INJECT_CONTEXT\x10\x04\x12\x18\n\x14HOOK_ACTION_ASK_USER\x10\x05*\xa8\x01\n\x14\x43ontextInjectionRole\x12&\n\"CONTEXT_INJECTION_ROLE_UNSPECIFIED\x10\x00\x12!\n\x1d\x43ONTEXT_INJECTION_ROLE_SYSTEM\x10\x01\x12\x1f\n\x1b\x43ONTEXT_INJECTION_ROLE_USER\x10\x02\x12$\n CONTEXT_INJECTION_ROLE_ASSISTANT\x10\x03*l\n\x0f\x41pprovalDefault\x12 \n\x1c\x41PPROVAL_DEFAULT_UNSPECIFIED\x10\x00\x12\x1c\n\x18\x41PPROVAL_DEFAULT_APPROVE\x10\x01\x12\x19\n\x15\x41PPROVAL_DEFAULT_DENY\x10\x02*\x91\x01\n\x10UserMessageLevel\x12\"\n\x1eUSER_MESSAGE_LEVEL_UNSPECIFIED\x10\x00\x12\x1b\n\x17USER_MESSAGE_LEVEL_INFO\x10\x01\x12\x1e\n\x1aUSER_MESSAGE_LEVEL_WARNING\x10\x02\x12\x1c\n\x18USER_MESSAGE_LEVEL_ERROR\x10\x03\x32\xa5\x01\n\x0bToolService\x12>\n\x07GetSpec\x12\x17.amplifier.module.Empty\x1a\x1a.amplifier.module.ToolSpec\x12V\n\x07\x45xecute\x12$.amplifier.module.ToolExecuteRequest\x1a%.amplifier.module.ToolExecuteResponse2\x9f\x03\n\x0fProviderService\x12\x42\n\x07GetInfo\x12\x17.amplifier.module.Empty\x1a\x1e.amplifier.module.ProviderInfo\x12K\n\nListModels\x12\x17.amplifier.module.Empty\x1a$.amplifier.module.ListModelsResponse\x12I\n\x08\x43omplete\x12\x1d.amplifier.module.ChatRequest\x1a\x1e.amplifier.module.ChatResponse\x12T\n\x11\x43ompleteStreaming\x12\x1d.amplifier.module.ChatRequest\x1a\x1e.amplifier.module.ChatResponse0\x01\x12Z\n\x0eParseToolCalls\x12\x1e.amplifier.module.ChatResponse\x1a(.amplifier.module.ParseToolCallsResponse2}\n\x13OrchestratorService\x12\x66\n\x07\x45xecute\x12,.amplifier.module.OrchestratorExecuteRequest\x1a-.amplifier.module.OrchestratorExecuteResponse2\xa3\x03\n\x0e\x43ontextService\x12J\n\nAddMessage\x12#.amplifier.module.AddMessageRequest\x1a\x17.amplifier.module.Empty\x12M\n\x0bGetMessages\x12\x17.amplifier.module.Empty\x1a%.amplifier.module.GetMessagesResponse\x12m\n\x15GetMessagesForRequest\x12-.amplifier.module.GetMessagesForRequestParams\x1a%.amplifier.module.GetMessagesResponse\x12L\n\x0bSetMessages\x12$.amplifier.module.SetMessagesRequest\x1a\x17.amplifier.module.Empty\x12\x39\n\x05\x43lear\x12\x17.amplifier.module.Empty\x1a\x17.amplifier.module.Empty2\xc5\x01\n\x0bHookService\x12K\n\x06Handle\x12#.amplifier.module.HookHandleRequest\x1a\x1c.amplifier.module.HookResult\x12i\n\x10GetSubscriptions\x12).amplifier.module.GetSubscriptionsRequest\x1a*.amplifier.module.GetSubscriptionsResponse2k\n\x0f\x41pprovalService\x12X\n\x0fRequestApproval\x12!.amplifier.module.ApprovalRequest\x1a\".amplifier.module.ApprovalResponse2\xd0\x07\n\rKernelService\x12\x65\n\x14\x43ompleteWithProvider\x12-.amplifier.module.CompleteWithProviderRequest\x1a\x1e.amplifier.module.ChatResponse\x12p\n\x1d\x43ompleteWithProviderStreaming\x12-.amplifier.module.CompleteWithProviderRequest\x1a\x1e.amplifier.module.ChatResponse0\x01\x12Q\n\x0b\x45xecuteTool\x12$.amplifier.module.ExecuteToolRequest\x1a\x1c.amplifier.module.ToolResult\x12K\n\x08\x45mitHook\x12!.amplifier.module.EmitHookRequest\x1a\x1c.amplifier.module.HookResult\x12o\n\x12\x45mitHookAndCollect\x12+.amplifier.module.EmitHookAndCollectRequest\x1a,.amplifier.module.EmitHookAndCollectResponse\x12Z\n\x0bGetMessages\x12$.amplifier.module.GetMessagesRequest\x1a%.amplifier.module.GetMessagesResponse\x12P\n\nAddMessage\x12).amplifier.module.KernelAddMessageRequest\x1a\x17.amplifier.module.Empty\x12i\n\x10GetMountedModule\x12).amplifier.module.GetMountedModuleRequest\x1a*.amplifier.module.GetMountedModuleResponse\x12Z\n\x12RegisterCapability\x12+.amplifier.module.RegisterCapabilityRequest\x1a\x17.amplifier.module.Empty\x12`\n\rGetCapability\x12&.amplifier.module.GetCapabilityRequest\x1a\'.amplifier.module.GetCapabilityResponse2\xaf\x02\n\x0fModuleLifecycle\x12H\n\x05Mount\x12\x1e.amplifier.module.MountRequest\x1a\x1f.amplifier.module.MountResponse\x12;\n\x07\x43leanup\x12\x17.amplifier.module.Empty\x1a\x17.amplifier.module.Empty\x12M\n\x0bHealthCheck\x12\x17.amplifier.module.Empty\x1a%.amplifier.module.HealthCheckResponse\x12\x46\n\rGetModuleInfo\x12\x17.amplifier.module.Empty\x1a\x1c.amplifier.module.ModuleInfob\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16\x61mplifier_module.proto\x12\x10\x61mplifier.module\"\x07\n\x05\x45mpty\"F\n\x08ToolSpec\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x17\n\x0fparameters_json\x18\x03 \x01(\t\"9\n\x12ToolExecuteRequest\x12\r\n\x05input\x18\x01 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x02 \x01(\t\"[\n\x13ToolExecuteResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x0e\n\x06output\x18\x02 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"\xd6\x01\n\nModuleInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t\x12\x31\n\x0bmodule_type\x18\x04 \x01(\x0e\x32\x1c.amplifier.module.ModuleType\x12\x13\n\x0bmount_point\x18\x05 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x06 \x01(\t\x12\x1a\n\x12\x63onfig_schema_json\x18\x07 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x08 \x03(\t\x12\x0e\n\x06\x61uthor\x18\t \x01(\t\"\x8c\x01\n\x0cMountRequest\x12:\n\x06\x63onfig\x18\x01 \x03(\x0b\x32*.amplifier.module.MountRequest.ConfigEntry\x12\x11\n\tmodule_id\x18\x02 \x01(\t\x1a-\n\x0b\x43onfigEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"_\n\rMountResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\r\n\x05\x65rror\x18\x02 \x01(\t\x12.\n\x06status\x18\x03 \x01(\x0e\x32\x1e.amplifier.module.HealthStatus\"V\n\x13HealthCheckResponse\x12.\n\x06status\x18\x01 \x01(\x0e\x32\x1e.amplifier.module.HealthStatus\x12\x0f\n\x07message\x18\x02 \x01(\t\"\xca\x02\n\x0b\x43onfigField\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x35\n\nfield_type\x18\x03 \x01(\x0e\x32!.amplifier.module.ConfigFieldType\x12\x0e\n\x06prompt\x18\x04 \x01(\t\x12\x0f\n\x07\x65nv_var\x18\x05 \x01(\t\x12\x0f\n\x07\x63hoices\x18\x06 \x03(\t\x12\x10\n\x08required\x18\x07 \x01(\x08\x12\x15\n\rdefault_value\x18\x08 \x01(\t\x12>\n\tshow_when\x18\t \x03(\x0b\x32+.amplifier.module.ConfigField.ShowWhenEntry\x12\x16\n\x0erequires_model\x18\n \x01(\x08\x1a/\n\rShowWhenEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xbc\x01\n\rProviderError\x12\x37\n\nerror_type\x18\x01 \x01(\x0e\x32#.amplifier.module.ProviderErrorType\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x15\n\rprovider_name\x18\x03 \x01(\t\x12\r\n\x05model\x18\x04 \x01(\t\x12\x13\n\x0bstatus_code\x18\x05 \x01(\x05\x12\x11\n\tretryable\x18\x06 \x01(\x08\x12\x13\n\x0bretry_after\x18\x07 \x01(\x01\"\x97\x01\n\tToolError\x12\x33\n\nerror_type\x18\x01 \x01(\x0e\x32\x1f.amplifier.module.ToolErrorType\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x11\n\ttool_name\x18\x03 \x01(\t\x12\x0e\n\x06stdout\x18\x04 \x01(\t\x12\x0e\n\x06stderr\x18\x05 \x01(\t\x12\x11\n\texit_code\x18\x06 \x01(\x05\"d\n\tHookError\x12\x33\n\nerror_type\x18\x01 \x01(\x0e\x32\x1f.amplifier.module.HookErrorType\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x11\n\thook_name\x18\x03 \x01(\t\"\xef\x01\n\x0e\x41mplifierError\x12\x39\n\x0eprovider_error\x18\x01 \x01(\x0b\x32\x1f.amplifier.module.ProviderErrorH\x00\x12\x31\n\ntool_error\x18\x02 \x01(\x0b\x32\x1b.amplifier.module.ToolErrorH\x00\x12\x31\n\nhook_error\x18\x03 \x01(\x0b\x32\x1b.amplifier.module.HookErrorH\x00\x12\x17\n\rgeneric_error\x18\x04 \x01(\tH\x00\x12\x1a\n\x10validation_error\x18\x05 \x01(\tH\x00\x42\x07\n\x05\x65rror\"\x19\n\tTextBlock\x12\x0c\n\x04text\x18\x01 \x01(\t\"E\n\rThinkingBlock\x12\x10\n\x08thinking\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\"%\n\x15RedactedThinkingBlock\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\"=\n\rToolCallBlock\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x12\n\ninput_json\x18\x03 \x01(\t\"<\n\x0fToolResultBlock\x12\x14\n\x0ctool_call_id\x18\x01 \x01(\t\x12\x13\n\x0boutput_json\x18\x02 \x01(\t\"C\n\nImageBlock\x12\x12\n\nmedia_type\x18\x01 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x13\n\x0bsource_json\x18\x03 \x01(\t\"2\n\x0eReasoningBlock\x12\x0f\n\x07\x63ontent\x18\x01 \x03(\t\x12\x0f\n\x07summary\x18\x02 \x03(\t\"\xf1\x03\n\x0c\x43ontentBlock\x12\x31\n\ntext_block\x18\x01 \x01(\x0b\x32\x1b.amplifier.module.TextBlockH\x00\x12\x39\n\x0ethinking_block\x18\x02 \x01(\x0b\x32\x1f.amplifier.module.ThinkingBlockH\x00\x12J\n\x17redacted_thinking_block\x18\x03 \x01(\x0b\x32\'.amplifier.module.RedactedThinkingBlockH\x00\x12:\n\x0ftool_call_block\x18\x04 \x01(\x0b\x32\x1f.amplifier.module.ToolCallBlockH\x00\x12>\n\x11tool_result_block\x18\x05 \x01(\x0b\x32!.amplifier.module.ToolResultBlockH\x00\x12\x33\n\x0bimage_block\x18\x06 \x01(\x0b\x32\x1c.amplifier.module.ImageBlockH\x00\x12;\n\x0freasoning_block\x18\x07 \x01(\x0b\x32 .amplifier.module.ReasoningBlockH\x00\x12\x30\n\nvisibility\x18\x08 \x01(\x0e\x32\x1c.amplifier.module.VisibilityB\x07\n\x05\x62lock\"B\n\x10\x43ontentBlockList\x12.\n\x06\x62locks\x18\x01 \x03(\x0b\x32\x1e.amplifier.module.ContentBlock\"\xca\x01\n\x07Message\x12$\n\x04role\x18\x01 \x01(\x0e\x32\x16.amplifier.module.Role\x12\x16\n\x0ctext_content\x18\x02 \x01(\tH\x00\x12;\n\rblock_content\x18\x03 \x01(\x0b\x32\".amplifier.module.ContentBlockListH\x00\x12\x0c\n\x04name\x18\x04 \x01(\t\x12\x14\n\x0ctool_call_id\x18\x05 \x01(\t\x12\x15\n\rmetadata_json\x18\x06 \x01(\tB\t\n\x07\x63ontent\"C\n\x0fToolCallMessage\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x16\n\x0e\x61rguments_json\x18\x03 \x01(\t\"K\n\rToolSpecProto\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x17\n\x0fparameters_json\x18\x03 \x01(\t\"7\n\x10JsonSchemaFormat\x12\x13\n\x0bschema_json\x18\x01 \x01(\t\x12\x0e\n\x06strict\x18\x02 \x01(\x08\"u\n\x0eResponseFormat\x12\x0e\n\x04text\x18\x01 \x01(\x08H\x00\x12\x0e\n\x04json\x18\x02 \x01(\x08H\x00\x12\x39\n\x0bjson_schema\x18\x03 \x01(\x0b\x32\".amplifier.module.JsonSchemaFormatH\x00\x42\x08\n\x06\x66ormat\"\xf7\x01\n\x05Usage\x12\x15\n\rprompt_tokens\x18\x01 \x01(\x05\x12\x19\n\x11\x63ompletion_tokens\x18\x02 \x01(\x05\x12\x14\n\x0ctotal_tokens\x18\x03 \x01(\x05\x12\x1d\n\x10reasoning_tokens\x18\x04 \x01(\x05H\x00\x88\x01\x01\x12\x1e\n\x11\x63\x61\x63he_read_tokens\x18\x05 \x01(\x05H\x01\x88\x01\x01\x12\"\n\x15\x63\x61\x63he_creation_tokens\x18\x06 \x01(\x05H\x02\x88\x01\x01\x42\x13\n\x11_reasoning_tokensB\x14\n\x12_cache_read_tokensB\x18\n\x16_cache_creation_tokens\"@\n\x0b\x44\x65gradation\x12\x11\n\trequested\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tual\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\"\x81\x03\n\x0b\x43hatRequest\x12+\n\x08messages\x18\x01 \x03(\x0b\x32\x19.amplifier.module.Message\x12.\n\x05tools\x18\x02 \x03(\x0b\x32\x1f.amplifier.module.ToolSpecProto\x12\x39\n\x0fresponse_format\x18\x03 \x01(\x0b\x32 .amplifier.module.ResponseFormat\x12\x13\n\x0btemperature\x18\x04 \x01(\x01\x12\r\n\x05top_p\x18\x05 \x01(\x01\x12\x19\n\x11max_output_tokens\x18\x06 \x01(\x05\x12\x17\n\x0f\x63onversation_id\x18\x07 \x01(\t\x12\x0e\n\x06stream\x18\x08 \x01(\x08\x12\x15\n\rmetadata_json\x18\t \x01(\t\x12\r\n\x05model\x18\n \x01(\t\x12\x13\n\x0btool_choice\x18\x0b \x01(\t\x12\x0c\n\x04stop\x18\x0c \x03(\t\x12\x18\n\x10reasoning_effort\x18\r \x01(\t\x12\x0f\n\x07timeout\x18\x0e \x01(\x01\"\x98\x02\n\x0c\x43hatResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x35\n\ntool_calls\x18\x02 \x03(\x0b\x32!.amplifier.module.ToolCallMessage\x12&\n\x05usage\x18\x03 \x01(\x0b\x32\x17.amplifier.module.Usage\x12\x32\n\x0b\x64\x65gradation\x18\x04 \x01(\x0b\x32\x1d.amplifier.module.Degradation\x12\x15\n\rfinish_reason\x18\x05 \x01(\t\x12\x15\n\rmetadata_json\x18\x06 \x01(\t\x12\x36\n\x0e\x63ontent_blocks\x18\x07 \x03(\x0b\x32\x1e.amplifier.module.ContentBlock\"F\n\nToolResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x13\n\x0boutput_json\x18\x02 \x01(\t\x12\x12\n\nerror_json\x18\x03 \x01(\t\"\xb2\x01\n\x10\x43ontextInjection\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x34\n\x04role\x18\x02 \x01(\x0e\x32&.amplifier.module.ContextInjectionRole\x12\x11\n\tephemeral\x18\x03 \x01(\x08\x12\"\n\x1a\x61ppend_to_last_tool_result\x18\x04 \x01(\x08\x12\x11\n\thook_name\x18\x05 \x01(\t\x12\r\n\x05\x65vent\x18\x06 \x01(\t\"\xe7\x04\n\nHookResult\x12,\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32\x1c.amplifier.module.HookAction\x12\x11\n\tdata_json\x18\x02 \x01(\t\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x19\n\x11\x63ontext_injection\x18\x04 \x01(\t\x12\x46\n\x16\x63ontext_injection_role\x18\x05 \x01(\x0e\x32&.amplifier.module.ContextInjectionRole\x12\x11\n\tephemeral\x18\x06 \x01(\x08\x12\x17\n\x0f\x61pproval_prompt\x18\x07 \x01(\t\x12\x18\n\x10\x61pproval_options\x18\x08 \x03(\t\x12\x1d\n\x10\x61pproval_timeout\x18\t \x01(\x01H\x00\x88\x01\x01\x12;\n\x10\x61pproval_default\x18\n \x01(\x0e\x32!.amplifier.module.ApprovalDefault\x12\x17\n\x0fsuppress_output\x18\x0b \x01(\x08\x12\x14\n\x0cuser_message\x18\x0c \x01(\t\x12>\n\x12user_message_level\x18\r \x01(\x0e\x32\".amplifier.module.UserMessageLevel\x12\x1b\n\x13user_message_source\x18\x0e \x01(\t\x12\"\n\x1a\x61ppend_to_last_tool_result\x18\x0f \x01(\x08\x12>\n\x12\x63ontext_injections\x18\x10 \x03(\x0b\x32\".amplifier.module.ContextInjectionB\x13\n\x11_approval_timeout\"\x8d\x01\n\tModelInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x16\n\x0e\x63ontext_window\x18\x03 \x01(\x05\x12\x19\n\x11max_output_tokens\x18\x04 \x01(\x05\x12\x14\n\x0c\x63\x61pabilities\x18\x05 \x03(\t\x12\x15\n\rdefaults_json\x18\x06 \x01(\t\"\xb0\x01\n\x0cProviderInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x1b\n\x13\x63redential_env_vars\x18\x03 \x03(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x03(\t\x12\x15\n\rdefaults_json\x18\x05 \x01(\t\x12\x34\n\rconfig_fields\x18\x06 \x03(\x0b\x32\x1d.amplifier.module.ConfigField\"\x80\x01\n\x0f\x41pprovalRequest\x12\x11\n\ttool_name\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63tion\x18\x02 \x01(\t\x12\x14\n\x0c\x64\x65tails_json\x18\x03 \x01(\t\x12\x12\n\nrisk_level\x18\x04 \x01(\t\x12\x14\n\x07timeout\x18\x05 \x01(\x01H\x00\x88\x01\x01\x42\n\n\x08_timeout\"F\n\x10\x41pprovalResponse\x12\x10\n\x08\x61pproved\x18\x01 \x01(\x08\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x10\n\x08remember\x18\x03 \x01(\x08\"A\n\x12ListModelsResponse\x12+\n\x06models\x18\x01 \x03(\x0b\x32\x1b.amplifier.module.ModelInfo\"O\n\x16ParseToolCallsResponse\x12\x35\n\ntool_calls\x18\x01 \x03(\x0b\x32!.amplifier.module.ToolCallMessage\"@\n\x1aOrchestratorExecuteRequest\x12\x0e\n\x06prompt\x18\x01 \x01(\t\x12\x12\n\nsession_id\x18\x02 \x01(\t\">\n\x1bOrchestratorExecuteResponse\x12\x10\n\x08response\x18\x01 \x01(\t\x12\r\n\x05\x65rror\x18\x02 \x01(\t\"?\n\x11\x41\x64\x64MessageRequest\x12*\n\x07message\x18\x01 \x01(\x0b\x32\x19.amplifier.module.Message\"B\n\x13GetMessagesResponse\x12+\n\x08messages\x18\x01 \x03(\x0b\x32\x19.amplifier.module.Message\"J\n\x1bGetMessagesForRequestParams\x12\x14\n\x0ctoken_budget\x18\x01 \x01(\x05\x12\x15\n\rprovider_name\x18\x02 \x01(\t\"A\n\x12SetMessagesRequest\x12+\n\x08messages\x18\x01 \x03(\x0b\x32\x19.amplifier.module.Message\"5\n\x11HookHandleRequest\x12\r\n\x05\x65vent\x18\x01 \x01(\t\x12\x11\n\tdata_json\x18\x02 \x01(\t\".\n\x17GetSubscriptionsRequest\x12\x13\n\x0b\x63onfig_json\x18\x01 \x01(\t\"V\n\x18GetSubscriptionsResponse\x12:\n\rsubscriptions\x18\x01 \x03(\x0b\x32#.amplifier.module.EventSubscription\"B\n\x11\x45ventSubscription\x12\r\n\x05\x65vent\x18\x01 \x01(\t\x12\x10\n\x08priority\x18\x02 \x01(\x05\x12\x0c\n\x04name\x18\x03 \x01(\t\"d\n\x1b\x43ompleteWithProviderRequest\x12\x15\n\rprovider_name\x18\x01 \x01(\t\x12.\n\x07request\x18\x02 \x01(\x0b\x32\x1d.amplifier.module.ChatRequest\";\n\x12\x45xecuteToolRequest\x12\x11\n\ttool_name\x18\x01 \x01(\t\x12\x12\n\ninput_json\x18\x02 \x01(\t\"3\n\x0f\x45mitHookRequest\x12\r\n\x05\x65vent\x18\x01 \x01(\t\x12\x11\n\tdata_json\x18\x02 \x01(\t\"V\n\x19\x45mitHookAndCollectRequest\x12\r\n\x05\x65vent\x18\x01 \x01(\t\x12\x11\n\tdata_json\x18\x02 \x01(\t\x12\x17\n\x0ftimeout_seconds\x18\x03 \x01(\x01\"4\n\x1a\x45mitHookAndCollectResponse\x12\x16\n\x0eresponses_json\x18\x01 \x03(\t\"(\n\x12GetMessagesRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\"Y\n\x17KernelAddMessageRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12*\n\x07message\x18\x02 \x01(\x0b\x32\x19.amplifier.module.Message\"a\n\x17GetMountedModuleRequest\x12\x13\n\x0bmodule_name\x18\x01 \x01(\t\x12\x31\n\x0bmodule_type\x18\x02 \x01(\x0e\x32\x1c.amplifier.module.ModuleType\"U\n\x18GetMountedModuleResponse\x12\r\n\x05\x66ound\x18\x01 \x01(\x08\x12*\n\x04info\x18\x02 \x01(\x0b\x32\x1c.amplifier.module.ModuleInfo\"=\n\x19RegisterCapabilityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x12\n\nvalue_json\x18\x02 \x01(\t\"$\n\x14GetCapabilityRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\":\n\x15GetCapabilityResponse\x12\r\n\x05\x66ound\x18\x01 \x01(\x08\x12\x12\n\nvalue_json\x18\x02 \x01(\t*\xbc\x01\n\nModuleType\x12\x1b\n\x17MODULE_TYPE_UNSPECIFIED\x10\x00\x12\x18\n\x14MODULE_TYPE_PROVIDER\x10\x01\x12\x14\n\x10MODULE_TYPE_TOOL\x10\x02\x12\x14\n\x10MODULE_TYPE_HOOK\x10\x03\x12\x16\n\x12MODULE_TYPE_MEMORY\x10\x04\x12\x19\n\x15MODULE_TYPE_GUARDRAIL\x10\x05\x12\x18\n\x14MODULE_TYPE_APPROVAL\x10\x06*\x82\x01\n\x0cHealthStatus\x12\x1d\n\x19HEALTH_STATUS_UNSPECIFIED\x10\x00\x12\x19\n\x15HEALTH_STATUS_SERVING\x10\x01\x12\x1d\n\x19HEALTH_STATUS_NOT_SERVING\x10\x02\x12\x19\n\x15HEALTH_STATUS_UNKNOWN\x10\x03*\xad\x01\n\x0f\x43onfigFieldType\x12!\n\x1d\x43ONFIG_FIELD_TYPE_UNSPECIFIED\x10\x00\x12\x1c\n\x18\x43ONFIG_FIELD_TYPE_STRING\x10\x01\x12\x1c\n\x18\x43ONFIG_FIELD_TYPE_NUMBER\x10\x02\x12\x1d\n\x19\x43ONFIG_FIELD_TYPE_BOOLEAN\x10\x03\x12\x1c\n\x18\x43ONFIG_FIELD_TYPE_SECRET\x10\x04*\xd8\x02\n\x11ProviderErrorType\x12#\n\x1fPROVIDER_ERROR_TYPE_UNSPECIFIED\x10\x00\x12\x1c\n\x18PROVIDER_ERROR_TYPE_AUTH\x10\x01\x12\"\n\x1ePROVIDER_ERROR_TYPE_RATE_LIMIT\x10\x02\x12&\n\"PROVIDER_ERROR_TYPE_CONTEXT_LENGTH\x10\x03\x12\'\n#PROVIDER_ERROR_TYPE_INVALID_REQUEST\x10\x04\x12&\n\"PROVIDER_ERROR_TYPE_CONTENT_FILTER\x10\x05\x12#\n\x1fPROVIDER_ERROR_TYPE_UNAVAILABLE\x10\x06\x12\x1f\n\x1bPROVIDER_ERROR_TYPE_TIMEOUT\x10\x07\x12\x1d\n\x19PROVIDER_ERROR_TYPE_OTHER\x10\x08*\x8c\x01\n\rToolErrorType\x12\x1f\n\x1bTOOL_ERROR_TYPE_UNSPECIFIED\x10\x00\x12\x1d\n\x19TOOL_ERROR_TYPE_EXECUTION\x10\x01\x12\x1e\n\x1aTOOL_ERROR_TYPE_VALIDATION\x10\x02\x12\x1b\n\x17TOOL_ERROR_TYPE_TIMEOUT\x10\x03*\x8c\x01\n\rHookErrorType\x12\x1f\n\x1bHOOK_ERROR_TYPE_UNSPECIFIED\x10\x00\x12\x1d\n\x19HOOK_ERROR_TYPE_EXECUTION\x10\x01\x12\x1e\n\x1aHOOK_ERROR_TYPE_VALIDATION\x10\x02\x12\x1b\n\x17HOOK_ERROR_TYPE_TIMEOUT\x10\x03*\x86\x01\n\x04Role\x12\x14\n\x10ROLE_UNSPECIFIED\x10\x00\x12\x0f\n\x0bROLE_SYSTEM\x10\x01\x12\r\n\tROLE_USER\x10\x02\x12\x12\n\x0eROLE_ASSISTANT\x10\x03\x12\r\n\tROLE_TOOL\x10\x04\x12\x11\n\rROLE_FUNCTION\x10\x05\x12\x12\n\x0eROLE_DEVELOPER\x10\x06*o\n\nVisibility\x12\x1a\n\x16VISIBILITY_UNSPECIFIED\x10\x00\x12\x12\n\x0eVISIBILITY_ALL\x10\x01\x12\x17\n\x13VISIBILITY_LLM_ONLY\x10\x02\x12\x18\n\x14VISIBILITY_USER_ONLY\x10\x03*\xab\x01\n\nHookAction\x12\x1b\n\x17HOOK_ACTION_UNSPECIFIED\x10\x00\x12\x18\n\x14HOOK_ACTION_CONTINUE\x10\x01\x12\x16\n\x12HOOK_ACTION_MODIFY\x10\x02\x12\x14\n\x10HOOK_ACTION_DENY\x10\x03\x12\x1e\n\x1aHOOK_ACTION_INJECT_CONTEXT\x10\x04\x12\x18\n\x14HOOK_ACTION_ASK_USER\x10\x05*\xa8\x01\n\x14\x43ontextInjectionRole\x12&\n\"CONTEXT_INJECTION_ROLE_UNSPECIFIED\x10\x00\x12!\n\x1d\x43ONTEXT_INJECTION_ROLE_SYSTEM\x10\x01\x12\x1f\n\x1b\x43ONTEXT_INJECTION_ROLE_USER\x10\x02\x12$\n CONTEXT_INJECTION_ROLE_ASSISTANT\x10\x03*l\n\x0f\x41pprovalDefault\x12 \n\x1c\x41PPROVAL_DEFAULT_UNSPECIFIED\x10\x00\x12\x1c\n\x18\x41PPROVAL_DEFAULT_APPROVE\x10\x01\x12\x19\n\x15\x41PPROVAL_DEFAULT_DENY\x10\x02*\x91\x01\n\x10UserMessageLevel\x12\"\n\x1eUSER_MESSAGE_LEVEL_UNSPECIFIED\x10\x00\x12\x1b\n\x17USER_MESSAGE_LEVEL_INFO\x10\x01\x12\x1e\n\x1aUSER_MESSAGE_LEVEL_WARNING\x10\x02\x12\x1c\n\x18USER_MESSAGE_LEVEL_ERROR\x10\x03\x32\xa5\x01\n\x0bToolService\x12>\n\x07GetSpec\x12\x17.amplifier.module.Empty\x1a\x1a.amplifier.module.ToolSpec\x12V\n\x07\x45xecute\x12$.amplifier.module.ToolExecuteRequest\x1a%.amplifier.module.ToolExecuteResponse2\x9f\x03\n\x0fProviderService\x12\x42\n\x07GetInfo\x12\x17.amplifier.module.Empty\x1a\x1e.amplifier.module.ProviderInfo\x12K\n\nListModels\x12\x17.amplifier.module.Empty\x1a$.amplifier.module.ListModelsResponse\x12I\n\x08\x43omplete\x12\x1d.amplifier.module.ChatRequest\x1a\x1e.amplifier.module.ChatResponse\x12T\n\x11\x43ompleteStreaming\x12\x1d.amplifier.module.ChatRequest\x1a\x1e.amplifier.module.ChatResponse0\x01\x12Z\n\x0eParseToolCalls\x12\x1e.amplifier.module.ChatResponse\x1a(.amplifier.module.ParseToolCallsResponse2}\n\x13OrchestratorService\x12\x66\n\x07\x45xecute\x12,.amplifier.module.OrchestratorExecuteRequest\x1a-.amplifier.module.OrchestratorExecuteResponse2\xa3\x03\n\x0e\x43ontextService\x12J\n\nAddMessage\x12#.amplifier.module.AddMessageRequest\x1a\x17.amplifier.module.Empty\x12M\n\x0bGetMessages\x12\x17.amplifier.module.Empty\x1a%.amplifier.module.GetMessagesResponse\x12m\n\x15GetMessagesForRequest\x12-.amplifier.module.GetMessagesForRequestParams\x1a%.amplifier.module.GetMessagesResponse\x12L\n\x0bSetMessages\x12$.amplifier.module.SetMessagesRequest\x1a\x17.amplifier.module.Empty\x12\x39\n\x05\x43lear\x12\x17.amplifier.module.Empty\x1a\x17.amplifier.module.Empty2\xc5\x01\n\x0bHookService\x12K\n\x06Handle\x12#.amplifier.module.HookHandleRequest\x1a\x1c.amplifier.module.HookResult\x12i\n\x10GetSubscriptions\x12).amplifier.module.GetSubscriptionsRequest\x1a*.amplifier.module.GetSubscriptionsResponse2k\n\x0f\x41pprovalService\x12X\n\x0fRequestApproval\x12!.amplifier.module.ApprovalRequest\x1a\".amplifier.module.ApprovalResponse2\xd0\x07\n\rKernelService\x12\x65\n\x14\x43ompleteWithProvider\x12-.amplifier.module.CompleteWithProviderRequest\x1a\x1e.amplifier.module.ChatResponse\x12p\n\x1d\x43ompleteWithProviderStreaming\x12-.amplifier.module.CompleteWithProviderRequest\x1a\x1e.amplifier.module.ChatResponse0\x01\x12Q\n\x0b\x45xecuteTool\x12$.amplifier.module.ExecuteToolRequest\x1a\x1c.amplifier.module.ToolResult\x12K\n\x08\x45mitHook\x12!.amplifier.module.EmitHookRequest\x1a\x1c.amplifier.module.HookResult\x12o\n\x12\x45mitHookAndCollect\x12+.amplifier.module.EmitHookAndCollectRequest\x1a,.amplifier.module.EmitHookAndCollectResponse\x12Z\n\x0bGetMessages\x12$.amplifier.module.GetMessagesRequest\x1a%.amplifier.module.GetMessagesResponse\x12P\n\nAddMessage\x12).amplifier.module.KernelAddMessageRequest\x1a\x17.amplifier.module.Empty\x12i\n\x10GetMountedModule\x12).amplifier.module.GetMountedModuleRequest\x1a*.amplifier.module.GetMountedModuleResponse\x12Z\n\x12RegisterCapability\x12+.amplifier.module.RegisterCapabilityRequest\x1a\x17.amplifier.module.Empty\x12`\n\rGetCapability\x12&.amplifier.module.GetCapabilityRequest\x1a\'.amplifier.module.GetCapabilityResponse2\xaf\x02\n\x0fModuleLifecycle\x12H\n\x05Mount\x12\x1e.amplifier.module.MountRequest\x1a\x1f.amplifier.module.MountResponse\x12;\n\x07\x43leanup\x12\x17.amplifier.module.Empty\x1a\x17.amplifier.module.Empty\x12M\n\x0bHealthCheck\x12\x17.amplifier.module.Empty\x1a%.amplifier.module.HealthCheckResponse\x12\x46\n\rGetModuleInfo\x12\x17.amplifier.module.Empty\x1a\x1c.amplifier.module.ModuleInfob\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -35,30 +35,30 @@ _globals['_MOUNTREQUEST_CONFIGENTRY']._serialized_options = b'8\001' _globals['_CONFIGFIELD_SHOWWHENENTRY']._loaded_options = None _globals['_CONFIGFIELD_SHOWWHENENTRY']._serialized_options = b'8\001' - _globals['_MODULETYPE']._serialized_start=7113 - _globals['_MODULETYPE']._serialized_end=7301 - _globals['_HEALTHSTATUS']._serialized_start=7304 - _globals['_HEALTHSTATUS']._serialized_end=7434 - _globals['_CONFIGFIELDTYPE']._serialized_start=7437 - _globals['_CONFIGFIELDTYPE']._serialized_end=7610 - _globals['_PROVIDERERRORTYPE']._serialized_start=7613 - _globals['_PROVIDERERRORTYPE']._serialized_end=7957 - _globals['_TOOLERRORTYPE']._serialized_start=7960 - _globals['_TOOLERRORTYPE']._serialized_end=8100 - _globals['_HOOKERRORTYPE']._serialized_start=8103 - _globals['_HOOKERRORTYPE']._serialized_end=8243 - _globals['_ROLE']._serialized_start=8246 - _globals['_ROLE']._serialized_end=8380 - _globals['_VISIBILITY']._serialized_start=8382 - _globals['_VISIBILITY']._serialized_end=8493 - _globals['_HOOKACTION']._serialized_start=8496 - _globals['_HOOKACTION']._serialized_end=8667 - _globals['_CONTEXTINJECTIONROLE']._serialized_start=8670 - _globals['_CONTEXTINJECTIONROLE']._serialized_end=8838 - _globals['_APPROVALDEFAULT']._serialized_start=8840 - _globals['_APPROVALDEFAULT']._serialized_end=8948 - _globals['_USERMESSAGELEVEL']._serialized_start=8951 - _globals['_USERMESSAGELEVEL']._serialized_end=9096 + _globals['_MODULETYPE']._serialized_start=7358 + _globals['_MODULETYPE']._serialized_end=7546 + _globals['_HEALTHSTATUS']._serialized_start=7549 + _globals['_HEALTHSTATUS']._serialized_end=7679 + _globals['_CONFIGFIELDTYPE']._serialized_start=7682 + _globals['_CONFIGFIELDTYPE']._serialized_end=7855 + _globals['_PROVIDERERRORTYPE']._serialized_start=7858 + _globals['_PROVIDERERRORTYPE']._serialized_end=8202 + _globals['_TOOLERRORTYPE']._serialized_start=8205 + _globals['_TOOLERRORTYPE']._serialized_end=8345 + _globals['_HOOKERRORTYPE']._serialized_start=8348 + _globals['_HOOKERRORTYPE']._serialized_end=8488 + _globals['_ROLE']._serialized_start=8491 + _globals['_ROLE']._serialized_end=8625 + _globals['_VISIBILITY']._serialized_start=8627 + _globals['_VISIBILITY']._serialized_end=8738 + _globals['_HOOKACTION']._serialized_start=8741 + _globals['_HOOKACTION']._serialized_end=8912 + _globals['_CONTEXTINJECTIONROLE']._serialized_start=8915 + _globals['_CONTEXTINJECTIONROLE']._serialized_end=9083 + _globals['_APPROVALDEFAULT']._serialized_start=9085 + _globals['_APPROVALDEFAULT']._serialized_end=9193 + _globals['_USERMESSAGELEVEL']._serialized_start=9196 + _globals['_USERMESSAGELEVEL']._serialized_end=9341 _globals['_EMPTY']._serialized_start=44 _globals['_EMPTY']._serialized_end=51 _globals['_TOOLSPEC']._serialized_start=53 @@ -127,78 +127,80 @@ _globals['_CHATRESPONSE']._serialized_end=4307 _globals['_TOOLRESULT']._serialized_start=4309 _globals['_TOOLRESULT']._serialized_end=4379 - _globals['_HOOKRESULT']._serialized_start=4382 - _globals['_HOOKRESULT']._serialized_end=4933 - _globals['_MODELINFO']._serialized_start=4936 - _globals['_MODELINFO']._serialized_end=5077 - _globals['_PROVIDERINFO']._serialized_start=5080 - _globals['_PROVIDERINFO']._serialized_end=5256 - _globals['_APPROVALREQUEST']._serialized_start=5259 - _globals['_APPROVALREQUEST']._serialized_end=5387 - _globals['_APPROVALRESPONSE']._serialized_start=5389 - _globals['_APPROVALRESPONSE']._serialized_end=5459 - _globals['_LISTMODELSRESPONSE']._serialized_start=5461 - _globals['_LISTMODELSRESPONSE']._serialized_end=5526 - _globals['_PARSETOOLCALLSRESPONSE']._serialized_start=5528 - _globals['_PARSETOOLCALLSRESPONSE']._serialized_end=5607 - _globals['_ORCHESTRATOREXECUTEREQUEST']._serialized_start=5609 - _globals['_ORCHESTRATOREXECUTEREQUEST']._serialized_end=5673 - _globals['_ORCHESTRATOREXECUTERESPONSE']._serialized_start=5675 - _globals['_ORCHESTRATOREXECUTERESPONSE']._serialized_end=5737 - _globals['_ADDMESSAGEREQUEST']._serialized_start=5739 - _globals['_ADDMESSAGEREQUEST']._serialized_end=5802 - _globals['_GETMESSAGESRESPONSE']._serialized_start=5804 - _globals['_GETMESSAGESRESPONSE']._serialized_end=5870 - _globals['_GETMESSAGESFORREQUESTPARAMS']._serialized_start=5872 - _globals['_GETMESSAGESFORREQUESTPARAMS']._serialized_end=5946 - _globals['_SETMESSAGESREQUEST']._serialized_start=5948 - _globals['_SETMESSAGESREQUEST']._serialized_end=6013 - _globals['_HOOKHANDLEREQUEST']._serialized_start=6015 - _globals['_HOOKHANDLEREQUEST']._serialized_end=6068 - _globals['_GETSUBSCRIPTIONSREQUEST']._serialized_start=6070 - _globals['_GETSUBSCRIPTIONSREQUEST']._serialized_end=6116 - _globals['_GETSUBSCRIPTIONSRESPONSE']._serialized_start=6118 - _globals['_GETSUBSCRIPTIONSRESPONSE']._serialized_end=6204 - _globals['_EVENTSUBSCRIPTION']._serialized_start=6206 - _globals['_EVENTSUBSCRIPTION']._serialized_end=6272 - _globals['_COMPLETEWITHPROVIDERREQUEST']._serialized_start=6274 - _globals['_COMPLETEWITHPROVIDERREQUEST']._serialized_end=6374 - _globals['_EXECUTETOOLREQUEST']._serialized_start=6376 - _globals['_EXECUTETOOLREQUEST']._serialized_end=6435 - _globals['_EMITHOOKREQUEST']._serialized_start=6437 - _globals['_EMITHOOKREQUEST']._serialized_end=6488 - _globals['_EMITHOOKANDCOLLECTREQUEST']._serialized_start=6490 - _globals['_EMITHOOKANDCOLLECTREQUEST']._serialized_end=6576 - _globals['_EMITHOOKANDCOLLECTRESPONSE']._serialized_start=6578 - _globals['_EMITHOOKANDCOLLECTRESPONSE']._serialized_end=6630 - _globals['_GETMESSAGESREQUEST']._serialized_start=6632 - _globals['_GETMESSAGESREQUEST']._serialized_end=6672 - _globals['_KERNELADDMESSAGEREQUEST']._serialized_start=6674 - _globals['_KERNELADDMESSAGEREQUEST']._serialized_end=6763 - _globals['_GETMOUNTEDMODULEREQUEST']._serialized_start=6765 - _globals['_GETMOUNTEDMODULEREQUEST']._serialized_end=6862 - _globals['_GETMOUNTEDMODULERESPONSE']._serialized_start=6864 - _globals['_GETMOUNTEDMODULERESPONSE']._serialized_end=6949 - _globals['_REGISTERCAPABILITYREQUEST']._serialized_start=6951 - _globals['_REGISTERCAPABILITYREQUEST']._serialized_end=7012 - _globals['_GETCAPABILITYREQUEST']._serialized_start=7014 - _globals['_GETCAPABILITYREQUEST']._serialized_end=7050 - _globals['_GETCAPABILITYRESPONSE']._serialized_start=7052 - _globals['_GETCAPABILITYRESPONSE']._serialized_end=7110 - _globals['_TOOLSERVICE']._serialized_start=9099 - _globals['_TOOLSERVICE']._serialized_end=9264 - _globals['_PROVIDERSERVICE']._serialized_start=9267 - _globals['_PROVIDERSERVICE']._serialized_end=9682 - _globals['_ORCHESTRATORSERVICE']._serialized_start=9684 - _globals['_ORCHESTRATORSERVICE']._serialized_end=9809 - _globals['_CONTEXTSERVICE']._serialized_start=9812 - _globals['_CONTEXTSERVICE']._serialized_end=10231 - _globals['_HOOKSERVICE']._serialized_start=10234 - _globals['_HOOKSERVICE']._serialized_end=10431 - _globals['_APPROVALSERVICE']._serialized_start=10433 - _globals['_APPROVALSERVICE']._serialized_end=10540 - _globals['_KERNELSERVICE']._serialized_start=10543 - _globals['_KERNELSERVICE']._serialized_end=11519 - _globals['_MODULELIFECYCLE']._serialized_start=11522 - _globals['_MODULELIFECYCLE']._serialized_end=11825 + _globals['_CONTEXTINJECTION']._serialized_start=4382 + _globals['_CONTEXTINJECTION']._serialized_end=4560 + _globals['_HOOKRESULT']._serialized_start=4563 + _globals['_HOOKRESULT']._serialized_end=5178 + _globals['_MODELINFO']._serialized_start=5181 + _globals['_MODELINFO']._serialized_end=5322 + _globals['_PROVIDERINFO']._serialized_start=5325 + _globals['_PROVIDERINFO']._serialized_end=5501 + _globals['_APPROVALREQUEST']._serialized_start=5504 + _globals['_APPROVALREQUEST']._serialized_end=5632 + _globals['_APPROVALRESPONSE']._serialized_start=5634 + _globals['_APPROVALRESPONSE']._serialized_end=5704 + _globals['_LISTMODELSRESPONSE']._serialized_start=5706 + _globals['_LISTMODELSRESPONSE']._serialized_end=5771 + _globals['_PARSETOOLCALLSRESPONSE']._serialized_start=5773 + _globals['_PARSETOOLCALLSRESPONSE']._serialized_end=5852 + _globals['_ORCHESTRATOREXECUTEREQUEST']._serialized_start=5854 + _globals['_ORCHESTRATOREXECUTEREQUEST']._serialized_end=5918 + _globals['_ORCHESTRATOREXECUTERESPONSE']._serialized_start=5920 + _globals['_ORCHESTRATOREXECUTERESPONSE']._serialized_end=5982 + _globals['_ADDMESSAGEREQUEST']._serialized_start=5984 + _globals['_ADDMESSAGEREQUEST']._serialized_end=6047 + _globals['_GETMESSAGESRESPONSE']._serialized_start=6049 + _globals['_GETMESSAGESRESPONSE']._serialized_end=6115 + _globals['_GETMESSAGESFORREQUESTPARAMS']._serialized_start=6117 + _globals['_GETMESSAGESFORREQUESTPARAMS']._serialized_end=6191 + _globals['_SETMESSAGESREQUEST']._serialized_start=6193 + _globals['_SETMESSAGESREQUEST']._serialized_end=6258 + _globals['_HOOKHANDLEREQUEST']._serialized_start=6260 + _globals['_HOOKHANDLEREQUEST']._serialized_end=6313 + _globals['_GETSUBSCRIPTIONSREQUEST']._serialized_start=6315 + _globals['_GETSUBSCRIPTIONSREQUEST']._serialized_end=6361 + _globals['_GETSUBSCRIPTIONSRESPONSE']._serialized_start=6363 + _globals['_GETSUBSCRIPTIONSRESPONSE']._serialized_end=6449 + _globals['_EVENTSUBSCRIPTION']._serialized_start=6451 + _globals['_EVENTSUBSCRIPTION']._serialized_end=6517 + _globals['_COMPLETEWITHPROVIDERREQUEST']._serialized_start=6519 + _globals['_COMPLETEWITHPROVIDERREQUEST']._serialized_end=6619 + _globals['_EXECUTETOOLREQUEST']._serialized_start=6621 + _globals['_EXECUTETOOLREQUEST']._serialized_end=6680 + _globals['_EMITHOOKREQUEST']._serialized_start=6682 + _globals['_EMITHOOKREQUEST']._serialized_end=6733 + _globals['_EMITHOOKANDCOLLECTREQUEST']._serialized_start=6735 + _globals['_EMITHOOKANDCOLLECTREQUEST']._serialized_end=6821 + _globals['_EMITHOOKANDCOLLECTRESPONSE']._serialized_start=6823 + _globals['_EMITHOOKANDCOLLECTRESPONSE']._serialized_end=6875 + _globals['_GETMESSAGESREQUEST']._serialized_start=6877 + _globals['_GETMESSAGESREQUEST']._serialized_end=6917 + _globals['_KERNELADDMESSAGEREQUEST']._serialized_start=6919 + _globals['_KERNELADDMESSAGEREQUEST']._serialized_end=7008 + _globals['_GETMOUNTEDMODULEREQUEST']._serialized_start=7010 + _globals['_GETMOUNTEDMODULEREQUEST']._serialized_end=7107 + _globals['_GETMOUNTEDMODULERESPONSE']._serialized_start=7109 + _globals['_GETMOUNTEDMODULERESPONSE']._serialized_end=7194 + _globals['_REGISTERCAPABILITYREQUEST']._serialized_start=7196 + _globals['_REGISTERCAPABILITYREQUEST']._serialized_end=7257 + _globals['_GETCAPABILITYREQUEST']._serialized_start=7259 + _globals['_GETCAPABILITYREQUEST']._serialized_end=7295 + _globals['_GETCAPABILITYRESPONSE']._serialized_start=7297 + _globals['_GETCAPABILITYRESPONSE']._serialized_end=7355 + _globals['_TOOLSERVICE']._serialized_start=9344 + _globals['_TOOLSERVICE']._serialized_end=9509 + _globals['_PROVIDERSERVICE']._serialized_start=9512 + _globals['_PROVIDERSERVICE']._serialized_end=9927 + _globals['_ORCHESTRATORSERVICE']._serialized_start=9929 + _globals['_ORCHESTRATORSERVICE']._serialized_end=10054 + _globals['_CONTEXTSERVICE']._serialized_start=10057 + _globals['_CONTEXTSERVICE']._serialized_end=10476 + _globals['_HOOKSERVICE']._serialized_start=10479 + _globals['_HOOKSERVICE']._serialized_end=10676 + _globals['_APPROVALSERVICE']._serialized_start=10678 + _globals['_APPROVALSERVICE']._serialized_end=10785 + _globals['_KERNELSERVICE']._serialized_start=10788 + _globals['_KERNELSERVICE']._serialized_end=11764 + _globals['_MODULELIFECYCLE']._serialized_start=11767 + _globals['_MODULELIFECYCLE']._serialized_end=12070 # @@protoc_insertion_point(module_scope) diff --git a/python/amplifier_core/hooks.py b/python/amplifier_core/hooks.py index b8c8b79..c6e7492 100644 --- a/python/amplifier_core/hooks.py +++ b/python/amplifier_core/hooks.py @@ -1,5 +1,5 @@ """Thin alias: re-exports RustHookRegistry (as HookRegistry) and HookResult.""" from ._engine import RustHookRegistry as HookRegistry -from .models import HookResult +from .models import ContextInjection, HookResult -__all__ = ["HookRegistry", "HookResult"] +__all__ = ["HookRegistry", "ContextInjection", "HookResult"] diff --git a/python/amplifier_core/loader_grpc.py b/python/amplifier_core/loader_grpc.py index 41b0c3c..4759336 100644 --- a/python/amplifier_core/loader_grpc.py +++ b/python/amplifier_core/loader_grpc.py @@ -195,8 +195,7 @@ async def load_grpc_module( except ImportError: raise ImportError( "gRPC proto stubs not generated. Run: " - "python -m grpc_tools.protoc -I proto --python_out=python/amplifier_core/_grpc_gen " - "--grpc_python_out=python/amplifier_core/_grpc_gen proto/amplifier_module.proto" + "python scripts/generate_grpc_stubs.py" ) stub = amplifier_module_pb2_grpc.ToolServiceStub(channel) diff --git a/python/amplifier_core/models.py b/python/amplifier_core/models.py index 2bf3330..be568f1 100644 --- a/python/amplifier_core/models.py +++ b/python/amplifier_core/models.py @@ -124,6 +124,17 @@ def get_serialized_output(self) -> str: return "Success" +class ContextInjection(BaseModel): + """One ordered context injection emitted by a hook.""" + + content: str + role: Literal["system", "user", "assistant"] = "system" + ephemeral: bool = False + append_to_last_tool_result: bool = False + hook_name: str = "unknown" + event: str = "" + + class HookResult(BaseModel): """ Result from hook execution with enhanced capabilities. @@ -323,6 +334,13 @@ class HookResult(BaseModel): "Only applicable when action='inject_context' and ephemeral=True." ), ) + context_injections: list[ContextInjection] = Field( + default_factory=list, + description=( + "Ordered, lossless hook context injections. The hook registry projects " + "the legacy scalar context fields from these items for compatibility." + ), + ) class ModelInfo(BaseModel): diff --git a/scripts/generate_grpc_stubs.py b/scripts/generate_grpc_stubs.py new file mode 100644 index 0000000..11ad49e --- /dev/null +++ b/scripts/generate_grpc_stubs.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Generate package-safe Python gRPC stubs from the canonical proto.""" + +from __future__ import annotations + +import argparse +from importlib.metadata import version +import subprocess +import sys +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +PROTO_PATH = PROJECT_ROOT / "proto" / "amplifier_module.proto" +RAW_IMPORT = "import amplifier_module_pb2 as amplifier__module__pb2" +PACKAGE_IMPORT = "from . import amplifier_module_pb2 as amplifier__module__pb2" +GRPCIO_TOOLS_VERSION = "1.78.0" + + +def main() -> None: + if version("grpcio-tools") != GRPCIO_TOOLS_VERSION: + raise RuntimeError( + f"grpcio-tools=={GRPCIO_TOOLS_VERSION} is required; install it with " + f"`python -m pip install grpcio-tools=={GRPCIO_TOOLS_VERSION}`." + ) + parser = argparse.ArgumentParser( + description="Generate package-safe Python stubs for amplifier_module.proto." + ) + parser.add_argument( + "--output", + type=Path, + default=Path("python/amplifier_core/_grpc_gen"), + help="Output directory, relative to the repository root by default.", + ) + args = parser.parse_args() + output = args.output if args.output.is_absolute() else PROJECT_ROOT / args.output + output.mkdir(parents=True, exist_ok=True) + + subprocess.run( + [ + sys.executable, + "-m", + "grpc_tools.protoc", + "-Iproto", + f"--python_out={output}", + f"--grpc_python_out={output}", + str(PROTO_PATH.relative_to(PROJECT_ROOT)), + ], + cwd=PROJECT_ROOT, + check=True, + ) + + grpc_stub = output / "amplifier_module_pb2_grpc.py" + generated = grpc_stub.read_text() + if RAW_IMPORT not in generated: + raise RuntimeError( + f"Expected generated import not found in {grpc_stub}: {RAW_IMPORT!r}" + ) + grpc_stub.write_text(generated.replace(RAW_IMPORT, PACKAGE_IMPORT, 1)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_generated_equivalence.py b/tests/test_generated_equivalence.py index 18e643e..42ff9f4 100644 --- a/tests/test_generated_equivalence.py +++ b/tests/test_generated_equivalence.py @@ -32,7 +32,7 @@ def test_proto_tool_result_field_count(self): class TestHookResultEquivalence: - """Proto HookResult has all 15 fields matching native HookResult.""" + """Proto HookResult has all 16 fields matching native HookResult.""" def test_proto_hook_result_has_all_fields(self): hr = pb2.HookResult() @@ -52,15 +52,37 @@ def test_proto_hook_result_has_all_fields(self): "user_message_level", "user_message_source", "append_to_last_tool_result", + "context_injections", ] - for field in expected_fields: - assert hasattr(hr, field), f"HookResult missing field: {field}" + assert [field.name for field in hr.DESCRIPTOR.fields] == expected_fields def test_proto_hook_result_field_count(self): - """HookResult must have exactly 15 fields.""" + """HookResult must have exactly 16 append-only fields.""" hr = pb2.HookResult() - fields = [f.name for f in hr.DESCRIPTOR.fields] - assert len(fields) == 15, f"Expected 15 fields, got {len(fields)}: {fields}" + expected_numbers = { + "action": 1, + "data_json": 2, + "reason": 3, + "context_injection": 4, + "context_injection_role": 5, + "ephemeral": 6, + "approval_prompt": 7, + "approval_options": 8, + "approval_timeout": 9, + "approval_default": 10, + "suppress_output": 11, + "user_message": 12, + "user_message_level": 13, + "user_message_source": 14, + "append_to_last_tool_result": 15, + "context_injections": 16, + } + assert {field.name: field.number for field in hr.DESCRIPTOR.fields} == expected_numbers + + injections = hr.DESCRIPTOR.fields_by_name["context_injections"] + assert injections.is_repeated + assert injections.type == injections.TYPE_MESSAGE + assert injections.message_type.full_name == pb2.ContextInjection.DESCRIPTOR.full_name class TestHookActionEnumEquivalence: diff --git a/tests/test_hooks_alias.py b/tests/test_hooks_alias.py index 29449f5..1133a5a 100644 --- a/tests/test_hooks_alias.py +++ b/tests/test_hooks_alias.py @@ -3,7 +3,8 @@ """ import amplifier_core.hooks as hooks_module -from amplifier_core.hooks import HookRegistry, HookResult +from amplifier_core.hooks import ContextInjection, HookRegistry, HookResult +from amplifier_core.models import ContextInjection as ModelContextInjection def test_import_hook_registry(): @@ -37,7 +38,8 @@ def test_hook_registry_instantiation(): def test_all_exports(): - """__all__ contains exactly HookRegistry and HookResult.""" + """__all__ exposes every public hook result type.""" all_exports = getattr(hooks_module, "__all__", None) assert all_exports is not None, "hooks module should define __all__" - assert set(all_exports) == {"HookRegistry", "HookResult"} + assert set(all_exports) == {"HookRegistry", "ContextInjection", "HookResult"} + assert ContextInjection is ModelContextInjection diff --git a/tests/test_hooks_request_id.py b/tests/test_hooks_request_id.py index 89657a2..c85885f 100644 --- a/tests/test_hooks_request_id.py +++ b/tests/test_hooks_request_id.py @@ -209,45 +209,77 @@ async def test_emit_carries_request_id_onto_the_error_path(): async def test_concurrent_calls_get_distinct_ids_and_pair_correctly(): """The case FIFO gets wrong. - Interleaving is forced to reproduce the measured trace (agent request, - summarizer request, summarizer response, agent response) where positional - pairing charges each response to the other caller. + Both request callbacks enter and suspend before either can resume. This + deterministically proves concurrent in-flight calls without relying on + task scheduling order. """ registry = HookRegistry() seen = [] - for event in ("llm:request", "llm:response"): - registry.register(event, _recorder(seen), name=f"cap-{event}") + request_ids_before_await = {} + request_ids_after_await = {} + callback_trace = [] + all_request_callbacks_entered = asyncio.Event() + release_request_callbacks = asyncio.Event() + + async def capture(event, data): + payload = dict(data) + caller = payload["caller"] + if event == "llm:request": + request_ids_before_await[caller] = payload[REQUEST_ID_FIELD] + callback_trace.append(("request-start", caller)) + if len(request_ids_before_await) == 2: + all_request_callbacks_entered.set() + await release_request_callbacks.wait() + request_ids_after_await[caller] = payload[REQUEST_ID_FIELD] + callback_trace.append(("request-end", caller)) + seen.append((event, payload)) + return HookResult(action="continue") - summarizer_requested = asyncio.Event() - summarizer_responded = asyncio.Event() + for event in ("llm:request", "llm:response"): + registry.register(event, capture, name=f"cap-{event}") async def agent_call(): await registry.emit("llm:request", {"caller": "agent"}) - await summarizer_responded.wait() await registry.emit("llm:response", {"caller": "agent"}) async def summarizer_call(): - await summarizer_requested.wait() await registry.emit("llm:request", {"caller": "summarizer"}) await registry.emit("llm:response", {"caller": "summarizer"}) - summarizer_responded.set() async def run(): task_agent = asyncio.create_task(agent_call()) task_summarizer = asyncio.create_task(summarizer_call()) - await asyncio.sleep(0) - summarizer_requested.set() - await asyncio.gather(task_agent, task_summarizer) + try: + await asyncio.wait_for(all_request_callbacks_entered.wait(), timeout=10) + except TimeoutError: + release_request_callbacks.set() + await asyncio.gather(task_agent, task_summarizer, return_exceptions=True) + pytest.fail( + "Concurrent emits did not enter both request callbacks before either resumed" + ) + release_request_callbacks.set() + try: + await asyncio.wait_for( + asyncio.gather(task_agent, task_summarizer), + timeout=10, + ) + except TimeoutError: + pytest.fail("Concurrent emits did not complete after callbacks were released") await run() - order = [(event, payload["caller"]) for event, payload in seen] - assert order == [ - ("llm:request", "agent"), - ("llm:request", "summarizer"), - ("llm:response", "summarizer"), - ("llm:response", "agent"), - ], "expected the interleaving that defeats positional pairing" + assert {caller for phase, caller in callback_trace if phase == "request-start"} == { + "agent", + "summarizer", + } + first_end = next( + index + for index, (phase, _) in enumerate(callback_trace) + if phase == "request-end" + ) + assert all(phase == "request-start" for phase, _ in callback_trace[:first_end]) + assert set(request_ids_before_await) == {"agent", "summarizer"} + assert request_ids_before_await == request_ids_after_await by_caller = {} for event, payload in seen: @@ -260,13 +292,6 @@ async def run(): assert summarizer["llm:request"] == summarizer["llm:response"] assert agent["llm:request"] != summarizer["llm:request"] - # And the positional pairing this replaces would have crossed them: - # FIFO joins the first request to the first response, which here belong - # to different callers. - requests = [p[REQUEST_ID_FIELD] for e, p in seen if e == "llm:request"] - responses = [p[REQUEST_ID_FIELD] for e, p in seen if e == "llm:response"] - assert requests[0] != responses[0], "expected FIFO to mis-pair this trace" - @pytest.mark.asyncio async def test_many_concurrent_calls_all_pair_correctly(): diff --git a/tests/test_injection_items.py b/tests/test_injection_items.py new file mode 100644 index 0000000..604d629 --- /dev/null +++ b/tests/test_injection_items.py @@ -0,0 +1,20 @@ +"""Regression coverage for lossless mixed hook context injections.""" + +from amplifier_core.models import ContextInjection, HookResult + + +def test_mixed_durable_and_ephemeral_injections_remain_distinct() -> None: + result = HookResult( + action="inject_context", + context_injections=[ + ContextInjection(content="durable", role="assistant", ephemeral=False), + ContextInjection(content="temporary", role="user", ephemeral=True), + ], + ) + + assert [item.content for item in result.context_injections] == [ + "durable", + "temporary", + ] + assert [item.ephemeral for item in result.context_injections] == [False, True] + assert [item.role for item in result.context_injections] == ["assistant", "user"] \ No newline at end of file diff --git a/tests/test_proto_compilation.py b/tests/test_proto_compilation.py index 9a0084e..db8bfdc 100644 --- a/tests/test_proto_compilation.py +++ b/tests/test_proto_compilation.py @@ -4,6 +4,16 @@ service messages, all 8 gRPC service stubs, and enum value assignments. """ +import shutil +import subprocess +import sys +from pathlib import Path + + +GRPC_GEN_DIR = ( + Path(__file__).parent.parent / "python" / "amplifier_core" / "_grpc_gen" +) + class TestProtoCompilation: """Verify proto-generated modules import cleanly and expose expected symbols.""" @@ -16,6 +26,26 @@ def test_pb2_grpc_module_imports(self): """The pb2_grpc module imports without error.""" from amplifier_core._grpc_gen import amplifier_module_pb2_grpc # noqa: F401 + def test_pb2_grpc_imports_as_a_package(self, tmp_path): + """The stub resolves its sibling pb2 module without a flat module path.""" + package_root = tmp_path / "isolated_package" + package_root.mkdir() + (package_root / "__init__.py").touch() + shutil.copytree(GRPC_GEN_DIR, package_root / "_grpc_gen") + + result = subprocess.run( + [ + sys.executable, + "-c", + "from isolated_package._grpc_gen import amplifier_module_pb2_grpc", + ], + cwd=tmp_path, + capture_output=True, + text=True, + ) + + assert result.returncode == 0, result.stderr + def test_tool_service_messages_exist(self): """Backward-compat: original ToolService messages are present.""" from amplifier_core._grpc_gen import amplifier_module_pb2 as pb2